rspace-online/modules/rnotes/converters/file-import.ts

172 lines
4.7 KiB
TypeScript

/**
* Generic file import for rNotes.
*
* Handles direct import of individual files:
* - .md / .txt → parse as markdown/text
* - .html → convert via Turndown
* - .jpg / .png / .webp / .gif → create IMAGE note with stored file
*
* All produce ConvertedNote with sourceRef.source = 'manual'.
*/
import TurndownService from 'turndown';
import { markdownToTiptap, extractPlainTextFromTiptap } from './markdown-tiptap';
import { hashContent } from './index';
import type { ConvertedNote } from './index';
const turndown = new TurndownService({ headingStyle: 'atx', codeBlockStyle: 'fenced' });
/** Dispatch file import by extension / MIME type. */
export function importFile(
filename: string,
data: Uint8Array,
mimeType?: string,
): ConvertedNote {
const ext = filename.substring(filename.lastIndexOf('.')).toLowerCase();
const textContent = () => new TextDecoder().decode(data);
if (ext === '.md' || ext === '.markdown') {
return importMarkdownFile(filename, textContent());
}
if (ext === '.txt') {
return importTextFile(filename, textContent());
}
if (ext === '.html' || ext === '.htm') {
return importHtmlFile(filename, textContent());
}
if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp'].includes(ext)) {
return importImageFile(filename, data, mimeType || guessMime(ext));
}
// Default: treat as text
try {
return importTextFile(filename, textContent());
} catch {
// Binary file — store as FILE note
return importBinaryFile(filename, data, mimeType || 'application/octet-stream');
}
}
/** Import a markdown file. */
export function importMarkdownFile(filename: string, content: string): ConvertedNote {
const title = titleFromFilename(filename);
const tiptapJson = markdownToTiptap(content);
const contentPlain = extractPlainTextFromTiptap(tiptapJson);
return {
title,
content: tiptapJson,
contentPlain,
markdown: content,
tags: [],
sourceRef: {
source: 'manual',
externalId: `file:${filename}`,
lastSyncedAt: Date.now(),
contentHash: hashContent(content),
},
};
}
/** Import a plain text file — wrap as simple note. */
export function importTextFile(filename: string, content: string): ConvertedNote {
const title = titleFromFilename(filename);
const tiptapJson = markdownToTiptap(content);
const contentPlain = content;
return {
title,
content: tiptapJson,
contentPlain,
markdown: content,
tags: [],
sourceRef: {
source: 'manual',
externalId: `file:${filename}`,
lastSyncedAt: Date.now(),
contentHash: hashContent(content),
},
};
}
/** Import an HTML file — convert via Turndown. */
export function importHtmlFile(filename: string, html: string): ConvertedNote {
const title = titleFromFilename(filename);
const markdown = turndown.turndown(html);
const tiptapJson = markdownToTiptap(markdown);
const contentPlain = extractPlainTextFromTiptap(tiptapJson);
return {
title,
content: tiptapJson,
contentPlain,
markdown,
tags: [],
sourceRef: {
source: 'manual',
externalId: `file:${filename}`,
lastSyncedAt: Date.now(),
contentHash: hashContent(markdown),
},
};
}
/** Import an image file — create IMAGE note with stored file reference. */
export function importImageFile(filename: string, data: Uint8Array, mimeType: string): ConvertedNote {
const title = titleFromFilename(filename);
const md = `![${title}](/data/files/uploads/${filename})`;
const tiptapJson = markdownToTiptap(md);
return {
title,
content: tiptapJson,
contentPlain: title,
markdown: md,
tags: [],
type: 'IMAGE',
attachments: [{ filename, data, mimeType }],
sourceRef: {
source: 'manual',
externalId: `file:${filename}`,
lastSyncedAt: Date.now(),
contentHash: hashContent(String(data.length)),
},
};
}
/** Import a binary/unknown file as a FILE note. */
function importBinaryFile(filename: string, data: Uint8Array, mimeType: string): ConvertedNote {
const title = titleFromFilename(filename);
const md = `[${filename}](/data/files/uploads/${filename})`;
const tiptapJson = markdownToTiptap(md);
return {
title,
content: tiptapJson,
contentPlain: title,
markdown: md,
tags: [],
type: 'FILE',
attachments: [{ filename, data, mimeType }],
sourceRef: {
source: 'manual',
externalId: `file:${filename}`,
lastSyncedAt: Date.now(),
contentHash: hashContent(String(data.length)),
},
};
}
function titleFromFilename(filename: string): string {
return filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
}
function guessMime(ext: string): string {
const mimes: Record<string, string> = {
'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png',
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
'.bmp': 'image/bmp',
};
return mimes[ext] || 'application/octet-stream';
}