Skip to content

Commit 516f446

Browse files
committed
fixup! ✨(frontend) move html option to downloads section
1 parent 33616c0 commit 516f446

File tree

3 files changed

+146
-53
lines changed

3 files changed

+146
-53
lines changed
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { deriveMediaFilename } from '../utils';
2+
3+
describe('deriveMediaFilename', () => {
4+
test('uses last URL segment when src is a valid URL', () => {
5+
const result = deriveMediaFilename({
6+
src: 'https://example.com/path/video.mp4',
7+
index: 0,
8+
blob: new Blob([], { type: 'video/mp4' }),
9+
});
10+
expect(result).toBe('1-video.mp4');
11+
});
12+
13+
test('handles URLs with query/hash and keeps the last segment', () => {
14+
const result = deriveMediaFilename({
15+
src: 'https://site.com/assets/file.name.svg?x=1#test',
16+
index: 0,
17+
blob: new Blob([], { type: 'image/svg+xml' }),
18+
});
19+
expect(result).toBe('1-file.name.svg');
20+
});
21+
22+
test('handles relative URLs using last segment', () => {
23+
const result = deriveMediaFilename({
24+
src: 'not a valid url',
25+
index: 0,
26+
blob: new Blob([], { type: 'image/png' }),
27+
});
28+
// "not a valid url" becomes a relative URL, so we get the last segment
29+
expect(result).toBe('1-not%20a%20valid%20url.png');
30+
});
31+
32+
test('data URLs always use media-{index+1}', () => {
33+
const result = deriveMediaFilename({
34+
src: 'data:image/png;base64,xxx',
35+
index: 0,
36+
blob: new Blob([], { type: 'image/png' }),
37+
});
38+
expect(result).toBe('media-1.png');
39+
});
40+
41+
test('adds extension from MIME when baseName has no extension', () => {
42+
const result = deriveMediaFilename({
43+
src: 'https://a.com/abc',
44+
index: 0,
45+
blob: new Blob([], { type: 'image/webp' }),
46+
});
47+
expect(result).toBe('1-abc.webp');
48+
});
49+
50+
test('does not override extension if baseName already contains one', () => {
51+
const result = deriveMediaFilename({
52+
src: 'https://a.com/image.png',
53+
index: 0,
54+
blob: new Blob([], { type: 'image/jpeg' }),
55+
});
56+
expect(result).toBe('1-image.png');
57+
});
58+
59+
test('handles complex MIME types (e.g., audio/mpeg)', () => {
60+
const result = deriveMediaFilename({
61+
src: 'https://a.com/song',
62+
index: 1,
63+
blob: new Blob([], { type: 'audio/mpeg' }),
64+
});
65+
expect(result).toBe('2-song.mpeg');
66+
});
67+
});

src/frontend/apps/impress/src/features/docs/doc-export/components/ModalExport.tsx

Lines changed: 6 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
2727
import { docxDocsSchemaMappings } from '../mappingDocx';
2828
import { odtDocsSchemaMappings } from '../mappingODT';
2929
import { pdfDocsSchemaMappings } from '../mappingPDF';
30-
import { downloadFile, escapeHtml } from '../utils';
30+
import { deriveMediaFilename, downloadFile, escapeHtml } from '../utils';
3131

3232
enum DocDownloadFormat {
3333
HTML = 'html',
@@ -176,58 +176,11 @@ export const ModalExport = ({ onClose, doc }: ModalExportProps) => {
176176
return;
177177
}
178178

179-
// Derive a readable filename:
180-
// - data: URLs → use a generic "media-N.ext"
181-
// - normal URLs → keep the last path segment as base name.
182-
let baseName = `media-${index + 1}`;
183-
184-
if (!src.startsWith('data:')) {
185-
try {
186-
const url = new URL(src, window.location.origin);
187-
const lastSegment = url.pathname.split('/').pop();
188-
if (lastSegment) {
189-
baseName = `${index + 1}-${lastSegment}`;
190-
}
191-
} catch {
192-
// Ignore invalid URLs, keep default baseName.
193-
}
194-
}
195-
196-
let filename = baseName;
197-
198-
// Ensure the filename has an extension consistent with the blob MIME type.
199-
const mimeType = fetched.type;
200-
if (mimeType && !baseName.includes('.')) {
201-
const slashIndex = mimeType.indexOf('/');
202-
const rawSubtype =
203-
slashIndex !== -1 && slashIndex < mimeType.length - 1
204-
? mimeType.slice(slashIndex + 1)
205-
: '';
206-
207-
let extension = '';
208-
const subtype = rawSubtype.toLowerCase();
209-
210-
if (subtype.includes('svg')) {
211-
extension = 'svg';
212-
} else if (subtype.includes('jpeg') || subtype.includes('pjpeg')) {
213-
extension = 'jpg';
214-
} else if (subtype.includes('png')) {
215-
extension = 'png';
216-
} else if (subtype.includes('gif')) {
217-
extension = 'gif';
218-
} else if (subtype.includes('webp')) {
219-
extension = 'webp';
220-
} else if (subtype.includes('pdf')) {
221-
extension = 'pdf';
222-
} else if (subtype) {
223-
extension = subtype.split('+')[0];
224-
}
225-
226-
if (extension) {
227-
filename = `${baseName}.${extension}`;
228-
}
229-
}
230-
179+
const filename = deriveMediaFilename({
180+
src,
181+
index,
182+
blob: fetched,
183+
});
231184
element.setAttribute('src', filename);
232185
mediaFiles.push({ filename, blob: fetched });
233186
}),

src/frontend/apps/impress/src/features/docs/doc-export/utils.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,3 +188,76 @@ export const escapeHtml = (value: string): string =>
188188
.replace(/>/g, '&gt;')
189189
.replace(/"/g, '&quot;')
190190
.replace(/'/g, '&#39;');
191+
192+
interface MediaFilenameParams {
193+
src: string;
194+
index: number;
195+
blob: Blob;
196+
}
197+
198+
/**
199+
* Derives a stable, readable filename for media exported in the HTML ZIP.
200+
*
201+
* Rules:
202+
* - Default base name is "media-{index+1}".
203+
* - For non data: URLs, we reuse the last path segment when possible (e.g. 1-photo.png).
204+
* - If the base name has no extension, we try to infer one from the blob MIME type.
205+
*/
206+
export const deriveMediaFilename = ({
207+
src,
208+
index,
209+
blob,
210+
}: MediaFilenameParams): string => {
211+
// Default base name
212+
let baseName = `media-${index + 1}`;
213+
214+
// Try to reuse the last path segment for non data URLs.
215+
if (!src.startsWith('data:')) {
216+
try {
217+
const url = new URL(src, window.location.origin);
218+
const lastSegment = url.pathname.split('/').pop();
219+
if (lastSegment) {
220+
baseName = `${index + 1}-${lastSegment}`;
221+
}
222+
} catch {
223+
// Ignore invalid URLs, keep default baseName.
224+
}
225+
}
226+
227+
let filename = baseName;
228+
229+
// Ensure the filename has an extension consistent with the blob MIME type.
230+
const mimeType = blob.type;
231+
if (mimeType && !baseName.includes('.')) {
232+
const slashIndex = mimeType.indexOf('/');
233+
const rawSubtype =
234+
slashIndex !== -1 && slashIndex < mimeType.length - 1
235+
? mimeType.slice(slashIndex + 1)
236+
: '';
237+
238+
let extension = '';
239+
const subtype = rawSubtype.toLowerCase();
240+
241+
if (subtype.includes('svg')) {
242+
extension = 'svg';
243+
} else if (subtype.includes('jpeg') || subtype.includes('pjpeg')) {
244+
extension = 'jpg';
245+
} else if (subtype.includes('png')) {
246+
extension = 'png';
247+
} else if (subtype.includes('gif')) {
248+
extension = 'gif';
249+
} else if (subtype.includes('webp')) {
250+
extension = 'webp';
251+
} else if (subtype.includes('pdf')) {
252+
extension = 'pdf';
253+
} else if (subtype) {
254+
extension = subtype.split('+')[0];
255+
}
256+
257+
if (extension) {
258+
filename = `${baseName}.${extension}`;
259+
}
260+
}
261+
262+
return filename;
263+
};

0 commit comments

Comments
 (0)