返回 oh-my-ppt
master-service.ts
根目录 / src / main / session / master-service.ts
1 import crypto from 'crypto'
2 import fs from 'fs'
3 import path from 'path'
4 import {
5 MASTER_CSS_FILENAME,
6 MASTER_DIRECTORY,
7 MASTER_HTML_FILENAME,
8 buildDefaultMasterConfig,
9 buildMasterCss,
10 buildMasterElementsHtml,
11 normalizeMasterConfig,
12 parseMasterCss,
13 parseMasterElementsHtml,
14 type SessionMasterConfig
15 } from '@shared/master'
16 import {
17 MASTER_LAYOUTS_FILENAME,
18 buildDefaultSessionLayoutLibrary,
19 normalizeSessionLayoutLibrary,
20 type SessionLayoutLibrary
21 } from '@shared/layout-master'
22
23 export type SessionMasterReadResult = {
24 css: string
25 html: string
26 config: SessionMasterConfig
27 exists: boolean
28 }
29
30 export type SessionLayoutLibraryReadResult = {
31 library: SessionLayoutLibrary
32 exists: boolean
33 }
34
35 const rebaseMasterAssetUrls = (css: string): string =>
36 css
37 .replace(/url\(\s*(["'])\.\/assets\//gi, 'url($1../assets/')
38 .replace(/url\(\s*(["'])\.\/images\//gi, 'url($1../images/')
39
40 const unbaseMasterAssetUrls = (css: string): string =>
41 css
42 .replace(/url\(\s*(["'])\.\.\/assets\//gi, 'url($1./assets/')
43 .replace(/url\(\s*(["'])\.\.\/images\//gi, 'url($1./images/')
44
45 const writeAtomically = async (filePath: string, content: string): Promise<void> => {
46 const tempPath = `${filePath}.${crypto.randomUUID()}.tmp`
47 await fs.promises.mkdir(path.dirname(filePath), { recursive: true })
48 try {
49 await fs.promises.writeFile(tempPath, content, 'utf-8')
50 await fs.promises.rename(tempPath, filePath)
51 } finally {
52 await fs.promises.rm(tempPath, { force: true }).catch(() => undefined)
53 }
54 }
55
56 const readFileIfExists = async (filePath: string): Promise<string | null> => {
57 try {
58 return await fs.promises.readFile(filePath, 'utf-8')
59 } catch (error) {
60 if ((error as NodeJS.ErrnoException).code === 'ENOENT') return null
61 throw error
62 }
63 }
64
65 export const getSessionMasterDirectory = (projectDir: string): string =>
66 path.join(path.resolve(projectDir), MASTER_DIRECTORY)
67
68 export const getSessionMasterPath = (projectDir: string): string =>
69 path.join(getSessionMasterDirectory(projectDir), MASTER_CSS_FILENAME)
70
71 export const getSessionMasterHtmlPath = (projectDir: string): string =>
72 path.join(getSessionMasterDirectory(projectDir), MASTER_HTML_FILENAME)
73
74 export const getSessionMasterLayoutsPath = (projectDir: string): string =>
75 path.join(getSessionMasterDirectory(projectDir), MASTER_LAYOUTS_FILENAME)
76
77 const toResult = (css: string, html: string, exists: boolean): SessionMasterReadResult => {
78 const cssConfig = parseMasterCss(unbaseMasterAssetUrls(css))
79 return {
80 css,
81 html,
82 config: normalizeMasterConfig({
83 ...cssConfig,
84 elements: parseMasterElementsHtml(html)
85 }),
86 exists
87 }
88 }
89
90 export async function readSessionMaster(projectDir: string): Promise<SessionMasterReadResult> {
91 const [canonicalCss, masterHtml] = await Promise.all([
92 readFileIfExists(getSessionMasterPath(projectDir)),
93 readFileIfExists(getSessionMasterHtmlPath(projectDir))
94 ])
95 if (canonicalCss === null) {
96 const config = buildDefaultMasterConfig()
97 return {
98 css: buildMasterCss(config),
99 html: masterHtml || buildMasterElementsHtml(config.elements),
100 config: normalizeMasterConfig({ ...config, elements: parseMasterElementsHtml(masterHtml || '') }),
101 exists: false
102 }
103 }
104 const cssConfig = parseMasterCss(unbaseMasterAssetUrls(canonicalCss))
105 return toResult(canonicalCss, masterHtml || buildMasterElementsHtml(cssConfig.elements), true)
106 }
107
108 export async function readSessionLayoutLibrary(
109 projectDir: string
110 ): Promise<SessionLayoutLibraryReadResult> {
111 const raw = await readFileIfExists(getSessionMasterLayoutsPath(projectDir))
112 if (raw === null) return { library: buildDefaultSessionLayoutLibrary(), exists: false }
113 try {
114 return { library: normalizeSessionLayoutLibrary(JSON.parse(raw)), exists: true }
115 } catch {
116 return { library: buildDefaultSessionLayoutLibrary(), exists: true }
117 }
118 }
119
120 export async function writeSessionLayoutLibrary(
121 projectDir: string,
122 value: unknown
123 ): Promise<SessionLayoutLibraryReadResult> {
124 const library = normalizeSessionLayoutLibrary(value)
125 await writeAtomically(
126 getSessionMasterLayoutsPath(projectDir),
127 `${JSON.stringify(library, null, 2)}\n`
128 )
129 return { library, exists: true }
130 }
131
132 export async function createSessionLayoutLibraryIfMissing(
133 projectDir: string
134 ): Promise<SessionLayoutLibraryReadResult> {
135 const existing = await readFileIfExists(getSessionMasterLayoutsPath(projectDir))
136 if (existing !== null) return readSessionLayoutLibrary(projectDir)
137 return writeSessionLayoutLibrary(projectDir, buildDefaultSessionLayoutLibrary())
138 }
139
140 async function writeSessionMasterFiles(
141 projectDir: string,
142 value: unknown,
143 fontFaceCss = ''
144 ): Promise<SessionMasterReadResult> {
145 const config = normalizeMasterConfig(value)
146 const css = rebaseMasterAssetUrls(buildMasterCss(config, fontFaceCss))
147 const html = buildMasterElementsHtml(config.elements)
148 await Promise.all([
149 writeAtomically(getSessionMasterPath(projectDir), css),
150 writeAtomically(getSessionMasterHtmlPath(projectDir), html)
151 ])
152 return { css, html, config, exists: true }
153 }
154
155 export async function writeSessionMaster(
156 projectDir: string,
157 value: unknown,
158 fontFaceCss = ''
159 ): Promise<SessionMasterReadResult> {
160 return writeSessionMasterFiles(projectDir, value, fontFaceCss)
161 }
162
163 /**
164 * This is only called while creating a fresh session copy, before its first
165 * history baseline. Runtime refresh and ordinary reads deliberately do not
166 * create master files.
167 */
168 export async function createSessionMasterIfMissing(projectDir: string): Promise<SessionMasterReadResult> {
169 const canonicalCssPath = getSessionMasterPath(projectDir)
170 const canonicalHtmlPath = getSessionMasterHtmlPath(projectDir)
171 const [canonicalCss, canonicalHtml] = await Promise.all([
172 readFileIfExists(canonicalCssPath),
173 readFileIfExists(canonicalHtmlPath)
174 ])
175
176 if (canonicalCss !== null && canonicalHtml !== null) {
177 await createSessionLayoutLibraryIfMissing(projectDir)
178 return toResult(canonicalCss, canonicalHtml, true)
179 }
180
181 if (canonicalCss !== null) {
182 const config = parseMasterCss(unbaseMasterAssetUrls(canonicalCss))
183 const html = canonicalHtml || buildMasterElementsHtml(config.elements)
184 if (canonicalHtml === null) await writeAtomically(canonicalHtmlPath, html)
185 await createSessionLayoutLibraryIfMissing(projectDir)
186 return toResult(canonicalCss, html, true)
187 }
188
189 const result = await writeSessionMasterFiles(projectDir, buildDefaultMasterConfig())
190 await createSessionLayoutLibraryIfMissing(projectDir)
191 return result
192 }
193
193 lines TYPESCRIPT