@lincyaw/web 1.0.0-rc.0 → 1.0.0-rc.1

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 (58) hide show
  1. package/README.md +33 -4
  2. package/package.json +20 -19
  3. package/src/app/base.css +117 -0
  4. package/src/app/block-playback.ts +81 -0
  5. package/src/app/block-report.ts +104 -0
  6. package/src/app/document-modules.ts +30 -0
  7. package/src/app/env.d.ts +12 -0
  8. package/src/app/main.ts +198 -0
  9. package/src/app/page-app.ts +166 -0
  10. package/src/app/router.ts +109 -0
  11. package/src/app/server.ts +32 -0
  12. package/src/app/site/home.tsx +117 -0
  13. package/src/app/site/layout.tsx +78 -0
  14. package/src/app/site/search.tsx +22 -0
  15. package/src/app/site.css +1416 -0
  16. package/src/client/document.css +172 -0
  17. package/src/client/host.ts +22 -94
  18. package/src/client/preview-frame.ts +152 -0
  19. package/src/client/runtime.ts +159 -0
  20. package/src/domain/app-config.ts +89 -0
  21. package/src/domain/config.ts +17 -9
  22. package/src/domain/dev-updates.ts +12 -0
  23. package/src/domain/index.ts +17 -7
  24. package/src/domain/landing.ts +83 -0
  25. package/src/domain/links.ts +23 -18
  26. package/src/domain/messages.ts +74 -63
  27. package/src/domain/pages.ts +12 -39
  28. package/src/domain/themes.ts +59 -0
  29. package/src/domain/workspace-home.ts +12 -0
  30. package/src/node/client-modules.ts +92 -0
  31. package/src/node/document-bundle.ts +227 -0
  32. package/src/node/landing-template.ts +34 -0
  33. package/src/node/landing.ts +82 -0
  34. package/src/node/offline.ts +34 -11
  35. package/src/node/publish.ts +83 -15
  36. package/src/node/renderer.ts +78 -117
  37. package/src/node/site-assets.ts +17 -0
  38. package/src/node/site-build.ts +297 -0
  39. package/src/node/site-files.ts +87 -0
  40. package/src/node/site-model.ts +333 -0
  41. package/src/node/site-page.ts +82 -0
  42. package/src/node/site-server.ts +817 -0
  43. package/src/node/slides.ts +135 -0
  44. package/src/node/viewer-app.ts +504 -0
  45. package/src/node/viewer-build.ts +38 -0
  46. package/custom-nav-controls.vue +0 -17
  47. package/src/client/env.d.ts +0 -1
  48. package/src/client/home.ts +0 -182
  49. package/src/client/index.ts +0 -4
  50. package/src/client/page.ts +0 -38
  51. package/src/client/preview.ts +0 -71
  52. package/src/client/style.css +0 -427
  53. package/src/client/theme.ts +0 -322
  54. package/src/node/addons.ts +0 -76
  55. package/src/node/site.ts +0 -349
  56. package/src/node/sites.ts +0 -531
  57. package/src/node/vite.ts +0 -166
  58. package/src/node/vitepress.ts +0 -72
@@ -1,17 +1,22 @@
1
+ import { createHash } from 'node:crypto'
1
2
  import { statSync } from 'node:fs'
2
3
  import { join } from 'node:path'
3
- import { irBlocks, mapHtmlUrls, publishedUrl, withProp } from '@lincyaw/kernel'
4
- import type { DocumentLink, IRBlock, IRDocument, LocalAsset, MdastNode, RenderContext, SourcePos } from '@lincyaw/kernel'
5
- import { copyPublished } from '@lincyaw/kernel/node'
4
+ import { irBlocks, mapHtmlUrls, NON_FILE_URL, publishedUrl, withPublishedAssets } from '@lincyaw/kernel'
5
+ import type { DocumentLink, IRBlock, IRDocument, IRSection, LocalAsset, MdastNode, RenderContext, SourcePos } from '@lincyaw/kernel'
6
+ import { copyPublished, fileVersion } from '@lincyaw/kernel/node'
6
7
  import katex from 'katex'
7
8
 
8
9
  export const MEDIA = 'media'
9
10
 
11
+ /** The IR revision of an IR written as JSON. */
12
+ export function irRevisionOfJson(json: string): string {
13
+ return createHash('sha256').update(json).digest('hex').slice(0, 16)
14
+ }
15
+
10
16
  const LINKED = new Set(['image', 'link', 'definition', 'card'])
11
17
 
12
18
  interface Asset extends LocalAsset {
13
- size: number
14
- mtimeMs: number
19
+ version: string
15
20
  }
16
21
 
17
22
  const assets = new Map<string, Asset>()
@@ -19,21 +24,22 @@ const assets = new Map<string, Asset>()
19
24
  function localAsset(ctx: RenderContext, source: SourcePos, url: string): Asset | undefined {
20
25
  const key = `${source.file}\0${url}`
21
26
  const known = assets.get(key)
22
- const now = known && statSync(known.file, { throwIfNoEntry: false })
23
- if (known && now && now.size === known.size && now.mtimeMs === known.mtimeMs) return known
27
+ const now = known && statSync(known.file, { bigint: true, throwIfNoEntry: false })
28
+ if (known && now && fileVersion(now) === known.version) return known
24
29
  const found = ctx.localAsset(source, url)
25
- const stat = found && statSync(found.file, { throwIfNoEntry: false })
30
+ const stat = found && statSync(found.file, { bigint: true, throwIfNoEntry: false })
26
31
  if (!found || !stat) {
27
32
  assets.delete(key)
28
33
  return undefined
29
34
  }
30
- const asset = { ...found, size: stat.size, mtimeMs: stat.mtimeMs, directory: found.directory ?? stat.isDirectory() }
35
+ const asset = { ...found, version: fileVersion(stat), directory: found.directory ?? stat.isDirectory() }
31
36
  if (asset.directory) assets.delete(key)
32
37
  else assets.set(key, asset)
33
38
  return asset
34
39
  }
35
40
 
36
41
  const rendered = new Map<string, string>()
42
+ const PUBLICATION_CACHE_BYTES = 16 << 20
37
43
 
38
44
  function renderMath(tex: string, display: boolean): string {
39
45
  const key = `${display ? 'D' : 'I'}${tex}`
@@ -47,9 +53,52 @@ function renderMath(tex: string, display: boolean): string {
47
53
 
48
54
  export type DocumentPage = (link: DocumentLink) => string | undefined
49
55
 
50
- export function publishedDocument(doc: IRDocument, ctx: RenderContext, publicDir: string, base: string, documentPage?: DocumentPage): IRDocument {
56
+ export class PublicationCache {
57
+ readonly #sections = new Map<string, { key: string, section: IRSection, bytes: number }>()
58
+ #bytes = 0
59
+
60
+ lookup(section: IRSection): { key: string, section?: IRSection } {
61
+ const key = JSON.stringify(section)
62
+ const known = this.#sections.get(section.id)
63
+ return { key, ...known?.key === key ? { section: known.section } : {} }
64
+ }
65
+
66
+ store(original: IRSection, key: string, section: IRSection): void {
67
+ const old = this.#sections.get(original.id)
68
+ if (old) this.#bytes -= old.bytes
69
+ this.#sections.delete(original.id)
70
+ // Local files and raw HTML can change independently of the IR. Revisit them.
71
+ const local = (node: MdastNode): boolean => node.type === 'html'
72
+ || (LINKED.has(node.type) && typeof node.url === 'string' && !NON_FILE_URL.test(node.url))
73
+ || Boolean(node.children?.some(local))
74
+ for (const block of irBlocks(original.blocks)) {
75
+ if (block.node.type === 'markdown' && local(block.node.mdast)) return
76
+ if (block.node.type === 'component' && (Object.keys(block.node.assets ?? {}).length || (block.node.fallback && local(block.node.fallback)))) return
77
+ if (block.stepStates?.some(state => Object.keys(state.assets ?? {}).length)) return
78
+ }
79
+ const bytes = (key.length + JSON.stringify(section).length) * 2
80
+ if (bytes > PUBLICATION_CACHE_BYTES) return
81
+ this.#sections.set(original.id, { key, section, bytes })
82
+ this.#bytes += bytes
83
+ while (this.#bytes > PUBLICATION_CACHE_BYTES || this.#sections.size > 2048) {
84
+ const first = this.#sections.keys().next().value!
85
+ this.#bytes -= this.#sections.get(first)!.bytes
86
+ this.#sections.delete(first)
87
+ }
88
+ }
89
+
90
+ prune(ids: Set<string>): void {
91
+ for (const [id, entry] of this.#sections) {
92
+ if (ids.has(id)) continue
93
+ this.#sections.delete(id)
94
+ this.#bytes -= entry.bytes
95
+ }
96
+ }
97
+ }
98
+
99
+ export function publishedDocument(doc: IRDocument, ctx: RenderContext, publicDir: string, base: string, documentPage?: DocumentPage, cache?: PublicationCache): IRDocument {
51
100
  const { deckDir } = ctx
52
- const copy = structuredClone(doc)
101
+ const copy: IRDocument = structuredClone(cache ? { ...doc, sections: [] } : doc)
53
102
  const media = join(publicDir, MEDIA)
54
103
  const relative = (pos: { file: string }) => {
55
104
  pos.file = ctx.portable(pos.file, deckDir) as string
@@ -73,20 +122,39 @@ export function publishedDocument(doc: IRDocument, ctx: RenderContext, publicDir
73
122
  if (node.type === 'markdown') mdast(node.mdast, source)
74
123
  if (node.type === 'component') {
75
124
  if (node.fallback) mdast(node.fallback, source)
76
- for (const [path, url] of Object.entries(node.assets ?? {})) {
77
- const published = publish(url, source)
78
- if (published) node.props = withProp(node.props, path, published)
125
+ for (const state of b.stepStates ?? []) {
126
+ if (!state.parameters) continue
127
+ // Plugins can declare state-only resources. Other components inherit
128
+ // their declared asset paths, resolved against the current parameters.
129
+ const assets = state.assets ?? Object.fromEntries(Object.entries(node.assets ?? {}).flatMap(([path, original]) => {
130
+ const at = (props: Record<string, unknown>) => path.split('.').reduce<unknown>((current, key) => current && typeof current === 'object' ? (current as Record<string, unknown>)[key] : undefined, props)
131
+ const value = at(state.parameters!)
132
+ return typeof value === 'string' ? [[path, value !== at(node.props) ? value : original]] : []
133
+ }))
134
+ state.parameters = withPublishedAssets({ ...node, props: state.parameters, assets }, url => publish(url, source)).props
79
135
  }
136
+ node.props = withPublishedAssets(node, url => publish(url, source)).props
80
137
  }
81
138
  }
82
139
  copy.meta.entry = ctx.portable(copy.meta.entry, deckDir) as string
83
140
  copy.config.presmium = ctx.portable(copy.config.presmium, deckDir) as IRDocument['config']['presmium']
84
- for (const section of copy.sections) {
141
+ for (const original of cache ? doc.sections : copy.sections) {
142
+ const known = cache?.lookup(original)
143
+ if (known?.section) {
144
+ copy.sections.push(known.section)
145
+ continue
146
+ }
147
+ const section = cache ? structuredClone(original) : original
85
148
  relative(section.source)
86
149
  if (section.config) section.config = ctx.portable(section.config, deckDir) as typeof section.config
87
150
  for (const b of irBlocks(section.blocks)) block(b)
88
151
  for (const b of irBlocks(section.blocks)) relative(b.source)
152
+ if (cache) {
153
+ copy.sections.push(section)
154
+ cache.store(original, known!.key, section)
155
+ }
89
156
  }
157
+ cache?.prune(new Set(doc.sections.map(section => section.id)))
90
158
  for (const finding of copy.diagnostics) {
91
159
  if (finding.source) relative(finding.source)
92
160
  }
@@ -1,122 +1,52 @@
1
- import { rmSync } from 'node:fs'
2
1
  import { join, resolve } from 'node:path'
3
- import { irBlocks } from '@lincyaw/kernel'
4
- import type { IRDocument, RenderContext, Renderer } from '@lincyaw/kernel'
5
- import { diffDocuments } from '@lincyaw/viewer/domain'
2
+ import type { IRDocument, RenderContext, RenderDevServer, Renderer, SiteDevServer, SiteDocument, SiteSpec } from '@lincyaw/kernel'
6
3
  import launchEditor from 'launch-editor'
7
- import { offlineEnabled, rendererConfig } from '../domain/index.ts'
8
- import type { WebRendererConfig } from '../domain/index.ts'
9
- import { clientModules, nodeVitePlugins } from './addons.ts'
10
- import type { ClientModules } from './addons.ts'
11
- import { writeServiceWorker } from './offline.ts'
12
- import { publishedDocument, publishedFile } from './publish.ts'
13
- import type { DocumentPage } from './publish.ts'
14
- import { siteFiles, write, writeSite } from './site.ts'
15
- import type { WorkspaceOptions } from './site.ts'
16
- import { buildSite, devSite } from './sites.ts'
17
- import { revisionOf } from './vite.ts'
18
- import { vitepressBuild, vitepressDev } from './vitepress.ts'
4
+ import { rendererConfig } from '../domain/index.ts'
5
+ import { buildSite as buildPages } from './site-build.ts'
6
+ import type { BuiltDocument } from './site-build.ts'
7
+ import { documentContext, documentSourceFiles } from './site-model.ts'
8
+ import { startSiteServer } from './site-server.ts'
9
+ import { viewerAppCache } from './viewer-app.ts'
19
10
 
20
11
  const BUILD_DIR = '.presmium/web/build'
12
+ const LOOPBACK = '127.0.0.1'
21
13
 
22
- function sourceFiles(doc: IRDocument): Set<string> {
23
- const files = new Set<string>()
24
- for (const section of doc.sections) {
25
- files.add(section.source.file)
26
- for (const block of irBlocks(section.blocks)) files.add(block.source.file)
27
- }
28
- return files
29
- }
30
-
31
- export interface DocumentSite {
32
- workspace?: WorkspaceOptions
33
- extra?: ClientModules
34
- documentPage?: DocumentPage
35
- config?: string
36
- }
37
-
38
- function merge(modules: ClientModules, extra?: ClientModules): ClientModules {
39
- if (!extra) return modules
40
- return {
41
- components: new Map([...extra.components, ...modules.components]),
42
- plugins: [...extra.plugins, ...modules.plugins],
43
- viewers: [...extra.viewers, ...modules.viewers],
44
- vite: [...extra.vite, ...modules.vite],
45
- nodeVite: [...extra.nodeVite, ...modules.nodeVite],
46
- }
14
+ /** One document as a site of its own: at the root of its site path, with no home page. */
15
+ function documentSite(doc: IRDocument, ctx: RenderContext): SiteSpec {
16
+ return { title: doc.meta.title, documents: [{ dir: '', title: doc.meta.title, doc, ...ctx.translations ? { translations: ctx.translations } : {} }], config: ctx.config }
47
17
  }
48
18
 
49
- const REMOTE = /^(?:[a-z][a-z\d+.-]*:|\/\/)/i
50
-
51
- function headImage(doc: IRDocument, ctx: RenderContext, options: WebRendererConfig, publicDir: string, document: DocumentSite): string | undefined {
52
- const image = options.shell.head?.image
53
- if (!image || REMOTE.test(image)) return image
54
- const published = publishedFile(ctx, publicDir, options.base, { file: document.config ?? resolve(ctx.deckDir, doc.meta.entry), line: 1 }, image)
55
- return published && options.shell.sitemap ? `${options.shell.sitemap.hostname}${published}` : published
19
+ function documentWorkDir(ctx: RenderContext, dir: string): string {
20
+ return join(ctx.workDir, 'documents', dir ? encodeURIComponent(dir) : '_')
56
21
  }
57
22
 
58
- async function prepare(doc: IRDocument, ctx: RenderContext, workDir: string, document: DocumentSite = {}) {
59
- const options = rendererConfig(ctx)
60
- const files = siteFiles(workDir)
61
- rmSync(files.publicDir, { recursive: true, force: true })
62
- const deck = { deckDir: ctx.deckDir, workspaceRoot: ctx.workspaceRoot }
63
- const nodeVite = await nodeVitePlugins(ctx, doc.config.presmium, Boolean(document.workspace))
64
- const publish = (d: IRDocument) => publishedDocument(d, ctx, files.publicDir, options.base, document.documentPage)
65
- const image = headImage(doc, ctx, options, files.publicDir, document)
66
- const site = (published: IRDocument) => writeSite(workDir, {
67
- doc: published,
68
- deck,
69
- modules: merge(clientModules(ctx, nodeVite), document.extra),
70
- options,
71
- workspace: document.workspace,
72
- translations: (ctx.translations ?? []).map(({ lang, doc: translated }) => ({ lang, doc: publish(translated) })),
73
- image,
74
- ...ctx.preview ? { preview: ctx.preview } : {},
23
+ /** The dev server of one document (`presmium dev`): its pages, or its slides. */
24
+ export async function devDocument(doc: IRDocument, ctx: RenderContext, form: 'web' | 'slides'): Promise<RenderDevServer> {
25
+ let current = doc
26
+ const { port } = rendererConfig(ctx)
27
+ const server = await startSiteServer({
28
+ site: () => documentSite(current, ctx),
29
+ form,
30
+ ctx,
31
+ cacheDir: viewerAppCache(),
32
+ workDir: join(ctx.outDir, 'site'),
33
+ document: async () => current,
34
+ context: () => ctx,
35
+ nodePlugins: () => ctx.nodeVitePlugins ?? [],
36
+ translations: () => (form === 'web' ? ctx.translations ?? [] : []),
37
+ listen: { host: LOOPBACK, port: port ?? 0 },
38
+ openSource(file, line) {
39
+ if (!documentSourceFiles(current).has(file)) return
40
+ launchEditor(`${resolve(ctx.deckDir, file)}:${line}`, (opened, error) => {
41
+ console.error(`[presmium] could not open ${opened} in an editor${error ? `: ${error}` : ''}; set $EDITOR or $LAUNCH_EDITOR`)
42
+ })
43
+ },
75
44
  })
76
- return { options, files, publish, site }
77
- }
78
-
79
- export async function buildDocument(doc: IRDocument, ctx: RenderContext, workDir: string, document: DocumentSite = {}): Promise<void> {
80
- const { options, files, publish, site } = await prepare(doc, ctx, workDir, document)
81
- const published = publish(doc)
82
- site(published)
83
- write(join(files.publicDir, 'ir.json'), JSON.stringify(published))
84
- const code = await vitepressBuild(files.root, ctx.outDir)
85
- if (code !== 0) throw new Error(`vitepress build exited with code ${code}`)
86
- if (offlineEnabled(doc.config.presmium)) writeServiceWorker(ctx.outDir, options.base)
87
- }
88
-
89
- export interface DocumentDevServer {
90
- port: number
91
- update(doc: IRDocument): Promise<void>
92
- close(): Promise<void>
93
- }
94
-
95
- export async function devDocument(doc: IRDocument, ctx: RenderContext, document: DocumentSite = {}, port?: number, listen: { host?: string, signal?: AbortSignal } = {}): Promise<DocumentDevServer> {
96
- const { options, files, publish, site } = await prepare(doc, ctx, ctx.outDir, document)
97
- let current = publish(doc)
98
- let revision = revisionOf(JSON.stringify(current))
99
- site(current)
100
- const server = await vitepressDev(files.root, port ?? options.port, (message) => {
101
- if (message.type !== 'presmium:source' || document.workspace || !sourceFiles(current).has(message.file)) return
102
- launchEditor(`${resolve(ctx.deckDir, message.file)}:${message.line}`, (file, error) => {
103
- console.error(`[presmium] could not open ${file} in an editor${error ? `: ${error}` : ''}; set $EDITOR or $LAUNCH_EDITOR`)
104
- })
105
- }, listen)
106
45
  return {
107
- port: server.port,
108
- async update(next: IRDocument) {
109
- const published = publish(next)
110
- const update = diffDocuments(current, published)
111
- site(published)
112
- if (!update) {
113
- if (ctx.translations?.length) server.send({ type: 'presmium:translations' })
114
- return
115
- }
116
- const base = revision
117
- current = published
118
- revision = revisionOf(JSON.stringify(published))
119
- server.send({ type: 'presmium:update', update, base, revision })
46
+ url: server.url,
47
+ async update(next) {
48
+ current = next
49
+ server.update('', next)
120
50
  },
121
51
  close: () => server.close(),
122
52
  }
@@ -128,20 +58,51 @@ export const renderer: Renderer = {
128
58
  support: () => 'full',
129
59
 
130
60
  async build(doc: IRDocument, ctx: RenderContext) {
131
- await buildDocument(doc, ctx, join(ctx.deckDir, BUILD_DIR))
61
+ const work = join(ctx.deckDir, BUILD_DIR)
62
+ const entry: BuiltDocument = { dir: '', doc, ctx, nodePlugins: ctx.nodeVitePlugins ?? [], translations: ctx.translations ?? [] }
63
+ await buildPages(documentSite(doc, ctx), { ...ctx, workDir: work }, [entry])
132
64
  return { outDir: ctx.outDir, skipped: [] }
133
65
  },
134
66
 
135
- async dev(doc: IRDocument, ctx: RenderContext) {
136
- const server = await devDocument(doc, ctx)
137
- const { base } = rendererConfig(ctx)
67
+ dev: (doc, ctx) => devDocument(doc, ctx, 'web'),
68
+
69
+ async buildSite(site: SiteSpec, ctx: RenderContext) {
70
+ const documents: BuiltDocument[] = site.documents.flatMap((entry) => {
71
+ if (!entry.doc) return []
72
+ const own = documentContext(ctx, site, entry.dir, entry.doc, join(ctx.outDir, entry.dir), documentWorkDir(ctx, entry.dir))
73
+ return [{ dir: entry.dir, doc: entry.doc, ctx: own, nodePlugins: entry.nodeVitePlugins ?? [], translations: entry.translations ?? [] }]
74
+ })
75
+ await buildPages(site, ctx, documents)
76
+ return { outDir: ctx.outDir, skipped: [] }
77
+ },
78
+
79
+ async devSite(site: SiteSpec, ctx: RenderContext, compile: (dir: string) => Promise<IRDocument>): Promise<SiteDevServer> {
80
+ const entryOf = (dir: string) => site.documents.find(entry => entry.dir === dir)
81
+ const host = typeof ctx.config.host === 'string' && ctx.config.host ? ctx.config.host : LOOPBACK
82
+ const server = await startSiteServer({
83
+ site: () => site,
84
+ ctx,
85
+ cacheDir: viewerAppCache(),
86
+ workDir: join(ctx.workDir, 'viewer'),
87
+ document: compile,
88
+ context: (dir, doc) => documentContext(ctx, site, dir, doc, documentWorkDir(ctx, dir), documentWorkDir(ctx, dir)),
89
+ nodePlugins: dir => entryOf(dir)?.nodeVitePlugins ?? [],
90
+ translations: dir => entryOf(dir)?.translations ?? [],
91
+ listen: { host, port: Number(ctx.config.port) || 0 },
92
+ })
138
93
  return {
139
- url: `http://localhost:${server.port}${base}`,
140
- update: next => server.update(next),
94
+ url: server.url,
95
+ async update(dir, doc) {
96
+ const known: SiteDocument = { dir, title: doc.meta.title, ...doc.meta.lang ? { lang: doc.meta.lang } : {}, ...Array.isArray(doc.meta.tags) ? { tags: (doc.meta.tags as unknown[]).filter((t): t is string => typeof t === 'string') } : {} }
97
+ const entry = entryOf(dir)
98
+ if (entry) Object.assign(entry, known)
99
+ else site.documents.push(known)
100
+ server.update(dir, doc)
101
+ },
102
+ target: async () => server.origin,
103
+ app: (dir, wait) => server.app(dir, wait),
104
+ warm: dir => server.warm(dir),
141
105
  close: () => server.close(),
142
106
  }
143
107
  },
144
-
145
- buildSite,
146
- devSite,
147
108
  }
@@ -0,0 +1,17 @@
1
+ import { join, resolve } from 'node:path'
2
+ import { ABSOLUTE_URL, isRecord } from '@lincyaw/kernel'
3
+ import type { RenderContext, SiteSpec } from '@lincyaw/kernel'
4
+ import { publishedFile } from './publish.ts'
5
+ import { isSingle } from './site-model.ts'
6
+
7
+ /** Publish shell assets from the site's configuration owner, once for every document in the site. */
8
+ export function publishedSite(site: SiteSpec, ctx: RenderContext, outDir = ctx.outDir, base = ctx.base): SiteSpec {
9
+ const params = isRecord(site.config.params) ? site.config.params : undefined
10
+ if (!params || typeof params.logo !== 'string') return site
11
+ const logo = params.logo.trim()
12
+ const file = isSingle(site) ? resolve(ctx.deckDir, site.documents[0]!.doc?.meta.entry ?? 'lecture.json') : join(ctx.workspaceRoot, 'presmium.workspace.yaml')
13
+ const source = { file, line: 1 }
14
+ const url = ABSOLUTE_URL.test(logo) ? logo : logo.startsWith('/') ? `${ctx.base}${logo.slice(1)}` : publishedFile(ctx, outDir, base, source, logo)
15
+ const { logo: _source, ...rest } = params
16
+ return { ...site, config: { ...site.config, params: { ...rest, ...url ? { logo: url } : {} } } }
17
+ }