@uniweb/build 0.1.2 → 0.1.4
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/README.md +189 -11
- package/package.json +24 -4
- package/src/dev/index.js +9 -0
- package/src/dev/plugin.js +206 -0
- package/src/images.js +5 -3
- package/src/index.js +5 -0
- package/src/prerender.js +310 -0
- package/src/site/advanced-processors.js +393 -0
- package/src/site/asset-processor.js +281 -0
- package/src/site/assets.js +247 -0
- package/src/site/content-collector.js +344 -0
- package/src/site/index.js +32 -0
- package/src/site/plugin.js +497 -0
- package/src/vite-foundation-plugin.js +7 -3
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Processor
|
|
3
|
+
*
|
|
4
|
+
* Processes site assets (images) during build:
|
|
5
|
+
* - Converts images to WebP for optimization
|
|
6
|
+
* - Generates content-hashed filenames for cache busting
|
|
7
|
+
* - Caches processed assets to avoid redundant work
|
|
8
|
+
*
|
|
9
|
+
* @module @uniweb/build/site
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { readFile, writeFile, mkdir, stat, copyFile } from 'node:fs/promises'
|
|
13
|
+
import { existsSync } from 'node:fs'
|
|
14
|
+
import { join, basename, extname, dirname } from 'node:path'
|
|
15
|
+
import { createHash } from 'node:crypto'
|
|
16
|
+
import sharp from 'sharp'
|
|
17
|
+
|
|
18
|
+
// Image formats that can be converted to WebP
|
|
19
|
+
const CONVERTIBLE_FORMATS = ['.png', '.jpg', '.jpeg', '.gif']
|
|
20
|
+
|
|
21
|
+
// Image formats to pass through without conversion
|
|
22
|
+
const PASSTHROUGH_FORMATS = ['.svg', '.webp', '.avif', '.ico']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate a content hash for a file
|
|
26
|
+
*/
|
|
27
|
+
async function getFileHash(filePath) {
|
|
28
|
+
const content = await readFile(filePath)
|
|
29
|
+
return createHash('md5').update(content).digest('hex').slice(0, 8)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert an image to WebP format
|
|
34
|
+
*
|
|
35
|
+
* @param {Buffer} input - Input image buffer
|
|
36
|
+
* @param {Object} options - Conversion options
|
|
37
|
+
* @returns {Promise<Buffer>} WebP buffer
|
|
38
|
+
*/
|
|
39
|
+
async function convertToWebp(input, options = {}) {
|
|
40
|
+
const { quality = 80 } = options
|
|
41
|
+
|
|
42
|
+
return sharp(input)
|
|
43
|
+
.webp({ quality })
|
|
44
|
+
.toBuffer()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Process a single asset
|
|
49
|
+
*
|
|
50
|
+
* @param {Object} asset - Asset info from manifest
|
|
51
|
+
* @param {Object} options - Processing options
|
|
52
|
+
* @returns {Promise<Object>} Processed asset info
|
|
53
|
+
*/
|
|
54
|
+
export async function processAsset(asset, options = {}) {
|
|
55
|
+
const {
|
|
56
|
+
outputDir,
|
|
57
|
+
assetsSubdir = 'assets',
|
|
58
|
+
convertToWebp: shouldConvert = true,
|
|
59
|
+
quality = 80
|
|
60
|
+
} = options
|
|
61
|
+
|
|
62
|
+
const { original, resolved, isImage } = asset
|
|
63
|
+
|
|
64
|
+
// Check if source file exists
|
|
65
|
+
if (!existsSync(resolved)) {
|
|
66
|
+
console.warn(`[asset-processor] Source not found: ${resolved}`)
|
|
67
|
+
return {
|
|
68
|
+
original,
|
|
69
|
+
output: original, // Keep original path as fallback
|
|
70
|
+
processed: false,
|
|
71
|
+
error: 'Source not found'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ext = extname(resolved).toLowerCase()
|
|
76
|
+
const name = basename(resolved, ext)
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
// Generate content hash for cache busting
|
|
80
|
+
const hash = await getFileHash(resolved)
|
|
81
|
+
|
|
82
|
+
let outputBuffer
|
|
83
|
+
let outputExt = ext
|
|
84
|
+
let converted = false
|
|
85
|
+
|
|
86
|
+
if (isImage && shouldConvert && CONVERTIBLE_FORMATS.includes(ext)) {
|
|
87
|
+
// Convert to WebP
|
|
88
|
+
const input = await readFile(resolved)
|
|
89
|
+
outputBuffer = await convertToWebp(input, { quality })
|
|
90
|
+
outputExt = '.webp'
|
|
91
|
+
converted = true
|
|
92
|
+
} else if (isImage && PASSTHROUGH_FORMATS.includes(ext)) {
|
|
93
|
+
// Copy as-is for SVG, WebP, etc.
|
|
94
|
+
outputBuffer = await readFile(resolved)
|
|
95
|
+
} else {
|
|
96
|
+
// Non-image or unknown format - copy as-is
|
|
97
|
+
outputBuffer = await readFile(resolved)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Generate output filename with hash
|
|
101
|
+
const outputFilename = `${name}-${hash}${outputExt}`
|
|
102
|
+
const outputPath = join(outputDir, assetsSubdir, outputFilename)
|
|
103
|
+
|
|
104
|
+
// Ensure output directory exists
|
|
105
|
+
await mkdir(dirname(outputPath), { recursive: true })
|
|
106
|
+
|
|
107
|
+
// Write processed file
|
|
108
|
+
await writeFile(outputPath, outputBuffer)
|
|
109
|
+
|
|
110
|
+
// Return the URL path (relative to site root)
|
|
111
|
+
const outputUrl = `/${assetsSubdir}/${outputFilename}`
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
original,
|
|
115
|
+
output: outputUrl,
|
|
116
|
+
resolved,
|
|
117
|
+
outputPath,
|
|
118
|
+
processed: true,
|
|
119
|
+
converted,
|
|
120
|
+
size: outputBuffer.length
|
|
121
|
+
}
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.warn(`[asset-processor] Failed to process ${resolved}:`, error.message)
|
|
124
|
+
return {
|
|
125
|
+
original,
|
|
126
|
+
output: original,
|
|
127
|
+
processed: false,
|
|
128
|
+
error: error.message
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Process all assets in a manifest
|
|
135
|
+
*
|
|
136
|
+
* @param {Object} assetManifest - Asset manifest from content collector
|
|
137
|
+
* @param {Object} options - Processing options
|
|
138
|
+
* @returns {Promise<Object>} Mapping of original paths to output URLs
|
|
139
|
+
*/
|
|
140
|
+
export async function processAssets(assetManifest, options = {}) {
|
|
141
|
+
const pathMapping = {}
|
|
142
|
+
const results = {
|
|
143
|
+
processed: 0,
|
|
144
|
+
converted: 0,
|
|
145
|
+
failed: 0,
|
|
146
|
+
totalSize: 0
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const entries = Object.entries(assetManifest)
|
|
150
|
+
|
|
151
|
+
for (const [originalPath, asset] of entries) {
|
|
152
|
+
const result = await processAsset(asset, options)
|
|
153
|
+
pathMapping[originalPath] = result.output
|
|
154
|
+
|
|
155
|
+
if (result.processed) {
|
|
156
|
+
results.processed++
|
|
157
|
+
results.totalSize += result.size || 0
|
|
158
|
+
if (result.converted) {
|
|
159
|
+
results.converted++
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
results.failed++
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return { pathMapping, results }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Rewrite asset paths in ProseMirror content
|
|
171
|
+
*
|
|
172
|
+
* @param {Object} content - ProseMirror document
|
|
173
|
+
* @param {Object} pathMapping - Map of original paths to new paths
|
|
174
|
+
* @returns {Object} Content with rewritten paths
|
|
175
|
+
*/
|
|
176
|
+
export function rewriteContentPaths(content, pathMapping) {
|
|
177
|
+
if (!content) return content
|
|
178
|
+
|
|
179
|
+
// Deep clone to avoid mutating original
|
|
180
|
+
const result = JSON.parse(JSON.stringify(content))
|
|
181
|
+
|
|
182
|
+
function walk(node) {
|
|
183
|
+
// Rewrite image src
|
|
184
|
+
if (node.type === 'image' && node.attrs?.src) {
|
|
185
|
+
const newPath = pathMapping[node.attrs.src]
|
|
186
|
+
if (newPath) {
|
|
187
|
+
node.attrs.src = newPath
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Recurse into content
|
|
192
|
+
if (node.content && Array.isArray(node.content)) {
|
|
193
|
+
node.content.forEach(walk)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Check marks
|
|
197
|
+
if (node.marks && Array.isArray(node.marks)) {
|
|
198
|
+
node.marks.forEach(mark => {
|
|
199
|
+
if (mark.attrs?.src) {
|
|
200
|
+
const newPath = pathMapping[mark.attrs.src]
|
|
201
|
+
if (newPath) {
|
|
202
|
+
mark.attrs.src = newPath
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
walk(result)
|
|
210
|
+
return result
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Rewrite asset paths in section params (frontmatter)
|
|
215
|
+
*
|
|
216
|
+
* @param {Object} params - Section params
|
|
217
|
+
* @param {Object} pathMapping - Map of original paths to new paths
|
|
218
|
+
* @returns {Object} Params with rewritten paths
|
|
219
|
+
*/
|
|
220
|
+
export function rewriteParamPaths(params, pathMapping) {
|
|
221
|
+
if (!params) return params
|
|
222
|
+
|
|
223
|
+
const result = { ...params }
|
|
224
|
+
const imageFields = ['image', 'background', 'backgroundImage', 'thumbnail', 'poster', 'avatar', 'logo', 'icon']
|
|
225
|
+
|
|
226
|
+
for (const field of imageFields) {
|
|
227
|
+
if (result[field] && pathMapping[result[field]]) {
|
|
228
|
+
result[field] = pathMapping[result[field]]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return result
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Rewrite all asset paths in site content
|
|
237
|
+
*
|
|
238
|
+
* @param {Object} siteContent - Full site content object
|
|
239
|
+
* @param {Object} pathMapping - Map of original paths to new paths
|
|
240
|
+
* @returns {Object} Site content with rewritten paths
|
|
241
|
+
*/
|
|
242
|
+
export function rewriteSiteContentPaths(siteContent, pathMapping) {
|
|
243
|
+
// Deep clone
|
|
244
|
+
const result = JSON.parse(JSON.stringify(siteContent))
|
|
245
|
+
|
|
246
|
+
function processSection(section) {
|
|
247
|
+
if (section.content) {
|
|
248
|
+
section.content = rewriteContentPaths(section.content, pathMapping)
|
|
249
|
+
}
|
|
250
|
+
if (section.params) {
|
|
251
|
+
section.params = rewriteParamPaths(section.params, pathMapping)
|
|
252
|
+
}
|
|
253
|
+
if (section.subsections) {
|
|
254
|
+
section.subsections.forEach(processSection)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function processPage(page) {
|
|
259
|
+
if (page.sections) {
|
|
260
|
+
page.sections.forEach(processSection)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Process all pages
|
|
265
|
+
if (result.pages) {
|
|
266
|
+
result.pages.forEach(processPage)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Process header and footer
|
|
270
|
+
if (result.header) {
|
|
271
|
+
processPage(result.header)
|
|
272
|
+
}
|
|
273
|
+
if (result.footer) {
|
|
274
|
+
processPage(result.footer)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Remove the assets manifest from output (no longer needed at runtime)
|
|
278
|
+
delete result.assets
|
|
279
|
+
|
|
280
|
+
return result
|
|
281
|
+
}
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset Resolution Utilities
|
|
3
|
+
*
|
|
4
|
+
* Resolves asset paths in content to file system locations.
|
|
5
|
+
* Supports both relative paths (./image.png) and absolute paths (/images/hero.png).
|
|
6
|
+
*
|
|
7
|
+
* In content-driven sites, markdown is the "code" - local asset references
|
|
8
|
+
* act as implicit imports and should be processed/optimized during build.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join, dirname, isAbsolute, normalize } from 'node:path'
|
|
12
|
+
import { existsSync } from 'node:fs'
|
|
13
|
+
|
|
14
|
+
// Image extensions we should process
|
|
15
|
+
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp', '.gif', '.svg', '.avif']
|
|
16
|
+
|
|
17
|
+
// Video extensions we can extract posters from
|
|
18
|
+
const VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi', '.mkv']
|
|
19
|
+
|
|
20
|
+
// PDF extension
|
|
21
|
+
const PDF_EXTENSION = '.pdf'
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if a path is an external URL
|
|
25
|
+
*/
|
|
26
|
+
function isExternalUrl(src) {
|
|
27
|
+
return /^(https?:)?\/\//.test(src) || src.startsWith('data:')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if a path is a processable image
|
|
32
|
+
*/
|
|
33
|
+
function isImagePath(src) {
|
|
34
|
+
const ext = src.split('.').pop()?.toLowerCase()
|
|
35
|
+
return IMAGE_EXTENSIONS.some(e => e.slice(1) === ext)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check if a path is a video file
|
|
40
|
+
*/
|
|
41
|
+
function isVideoPath(src) {
|
|
42
|
+
const ext = '.' + (src.split('.').pop()?.toLowerCase() || '')
|
|
43
|
+
return VIDEO_EXTENSIONS.includes(ext)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Check if a path is a PDF file
|
|
48
|
+
*/
|
|
49
|
+
function isPdfPath(src) {
|
|
50
|
+
return src.toLowerCase().endsWith(PDF_EXTENSION)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve an asset path to absolute file system path
|
|
55
|
+
*
|
|
56
|
+
* @param {string} src - Original source path from content
|
|
57
|
+
* @param {string} contextPath - Path of the file containing the reference
|
|
58
|
+
* @param {string} siteRoot - Site root directory
|
|
59
|
+
* @returns {Object} Resolution result
|
|
60
|
+
*/
|
|
61
|
+
export function resolveAssetPath(src, contextPath, siteRoot) {
|
|
62
|
+
// External URLs - don't process
|
|
63
|
+
if (isExternalUrl(src)) {
|
|
64
|
+
return { src, resolved: null, external: true }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Already absolute path on filesystem
|
|
68
|
+
if (isAbsolute(src)) {
|
|
69
|
+
return { src, resolved: src, external: false }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let resolved
|
|
73
|
+
|
|
74
|
+
// Relative paths: ./image.png or ../image.png or just image.png
|
|
75
|
+
if (src.startsWith('./') || src.startsWith('../') || !src.startsWith('/')) {
|
|
76
|
+
const contextDir = dirname(contextPath)
|
|
77
|
+
resolved = normalize(join(contextDir, src))
|
|
78
|
+
}
|
|
79
|
+
// Absolute site paths: /images/hero.png
|
|
80
|
+
else if (src.startsWith('/')) {
|
|
81
|
+
// Check public folder first, then assets folder
|
|
82
|
+
const publicPath = join(siteRoot, 'public', src)
|
|
83
|
+
const assetsPath = join(siteRoot, 'assets', src)
|
|
84
|
+
|
|
85
|
+
if (existsSync(publicPath)) {
|
|
86
|
+
resolved = publicPath
|
|
87
|
+
} else if (existsSync(assetsPath)) {
|
|
88
|
+
resolved = assetsPath
|
|
89
|
+
} else {
|
|
90
|
+
// Default to public folder path even if it doesn't exist yet
|
|
91
|
+
resolved = publicPath
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
src,
|
|
97
|
+
resolved,
|
|
98
|
+
external: false,
|
|
99
|
+
isImage: isImagePath(src),
|
|
100
|
+
isVideo: isVideoPath(src),
|
|
101
|
+
isPdf: isPdfPath(src)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Walk a ProseMirror document and collect all asset references
|
|
107
|
+
*
|
|
108
|
+
* @param {Object} doc - ProseMirror document
|
|
109
|
+
* @param {Function} visitor - Callback for each asset: (node, path, attrName) => void
|
|
110
|
+
* attrName is 'src', 'poster', or 'preview'
|
|
111
|
+
* @param {string} [path=''] - Current path in document (for debugging)
|
|
112
|
+
*/
|
|
113
|
+
export function walkContentAssets(doc, visitor, path = '') {
|
|
114
|
+
if (!doc) return
|
|
115
|
+
|
|
116
|
+
// Check for image nodes
|
|
117
|
+
if (doc.type === 'image' && doc.attrs?.src) {
|
|
118
|
+
visitor(doc, path, 'src')
|
|
119
|
+
|
|
120
|
+
// Also collect explicit poster/preview attributes as assets
|
|
121
|
+
if (doc.attrs.poster && !isExternalUrl(doc.attrs.poster)) {
|
|
122
|
+
visitor({ type: 'image', attrs: { src: doc.attrs.poster } }, path, 'poster')
|
|
123
|
+
}
|
|
124
|
+
if (doc.attrs.preview && !isExternalUrl(doc.attrs.preview)) {
|
|
125
|
+
visitor({ type: 'image', attrs: { src: doc.attrs.preview } }, path, 'preview')
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Recurse into content
|
|
130
|
+
if (doc.content && Array.isArray(doc.content)) {
|
|
131
|
+
doc.content.forEach((child, index) => {
|
|
132
|
+
walkContentAssets(child, visitor, `${path}/content[${index}]`)
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Handle marks (links can have images)
|
|
137
|
+
if (doc.marks && Array.isArray(doc.marks)) {
|
|
138
|
+
doc.marks.forEach((mark, index) => {
|
|
139
|
+
if (mark.attrs?.src) {
|
|
140
|
+
visitor(mark, `${path}/marks[${index}]`, 'src')
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Process all assets in a section's content and frontmatter
|
|
148
|
+
*
|
|
149
|
+
* @param {Object} section - Section object with content and params
|
|
150
|
+
* @param {string} markdownPath - Path to the markdown file
|
|
151
|
+
* @param {string} siteRoot - Site root directory
|
|
152
|
+
* @returns {Object} Asset collection result
|
|
153
|
+
* - assets: Asset manifest mapping original paths to resolved info
|
|
154
|
+
* - hasExplicitPoster: Set of video src paths that have explicit poster attributes
|
|
155
|
+
* - hasExplicitPreview: Set of PDF src paths that have explicit preview attributes
|
|
156
|
+
*/
|
|
157
|
+
export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
158
|
+
const assets = {}
|
|
159
|
+
const hasExplicitPoster = new Set()
|
|
160
|
+
const hasExplicitPreview = new Set()
|
|
161
|
+
|
|
162
|
+
// Track current image node's src when we encounter poster/preview
|
|
163
|
+
let currentImageSrc = null
|
|
164
|
+
|
|
165
|
+
// Collect from ProseMirror content
|
|
166
|
+
if (section.content) {
|
|
167
|
+
walkContentAssets(section.content, (node, path, attrName) => {
|
|
168
|
+
const result = resolveAssetPath(node.attrs.src, markdownPath, siteRoot)
|
|
169
|
+
|
|
170
|
+
if (attrName === 'src') {
|
|
171
|
+
// Main src attribute - track it for potential poster/preview
|
|
172
|
+
currentImageSrc = node.attrs.src
|
|
173
|
+
|
|
174
|
+
// Check if this image has explicit poster/preview
|
|
175
|
+
if (node.attrs.poster) {
|
|
176
|
+
hasExplicitPoster.add(node.attrs.src)
|
|
177
|
+
}
|
|
178
|
+
if (node.attrs.preview) {
|
|
179
|
+
hasExplicitPreview.add(node.attrs.src)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!result.external && result.resolved) {
|
|
184
|
+
assets[node.attrs.src] = {
|
|
185
|
+
original: node.attrs.src,
|
|
186
|
+
resolved: result.resolved,
|
|
187
|
+
isImage: result.isImage,
|
|
188
|
+
isVideo: result.isVideo,
|
|
189
|
+
isPdf: result.isPdf
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Collect from frontmatter params (common media fields)
|
|
196
|
+
const mediaFields = [
|
|
197
|
+
'image', 'background', 'backgroundImage', 'thumbnail',
|
|
198
|
+
'poster', 'avatar', 'logo', 'icon',
|
|
199
|
+
'video', 'videoSrc', 'media', 'file', 'pdf', 'document'
|
|
200
|
+
]
|
|
201
|
+
|
|
202
|
+
for (const field of mediaFields) {
|
|
203
|
+
const value = section.params?.[field]
|
|
204
|
+
if (typeof value === 'string' && value) {
|
|
205
|
+
const result = resolveAssetPath(value, markdownPath, siteRoot)
|
|
206
|
+
if (!result.external && result.resolved) {
|
|
207
|
+
assets[value] = {
|
|
208
|
+
original: value,
|
|
209
|
+
resolved: result.resolved,
|
|
210
|
+
isImage: result.isImage,
|
|
211
|
+
isVideo: result.isVideo,
|
|
212
|
+
isPdf: result.isPdf
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return { assets, hasExplicitPoster, hasExplicitPreview }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Merge multiple asset collection results
|
|
223
|
+
*
|
|
224
|
+
* @param {...Object} collections - Asset collection results from collectSectionAssets
|
|
225
|
+
* @returns {Object} Merged collection with combined assets and sets
|
|
226
|
+
*/
|
|
227
|
+
export function mergeAssetCollections(...collections) {
|
|
228
|
+
const merged = {
|
|
229
|
+
assets: {},
|
|
230
|
+
hasExplicitPoster: new Set(),
|
|
231
|
+
hasExplicitPreview: new Set()
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (const collection of collections) {
|
|
235
|
+
// Handle both old format (plain object) and new format (with sets)
|
|
236
|
+
if (collection.assets) {
|
|
237
|
+
Object.assign(merged.assets, collection.assets)
|
|
238
|
+
collection.hasExplicitPoster?.forEach(p => merged.hasExplicitPoster.add(p))
|
|
239
|
+
collection.hasExplicitPreview?.forEach(p => merged.hasExplicitPreview.add(p))
|
|
240
|
+
} else {
|
|
241
|
+
// Legacy: plain asset manifest
|
|
242
|
+
Object.assign(merged.assets, collection)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
return merged
|
|
247
|
+
}
|