Resolve relative image paths in export/import markdown viewer
The MarkdownBody component now accepts an optional resolveImageSrc callback that maps relative image paths (like images/org-chart.png) to base64 data URLs from the portable file entries. This fixes the export README showing a broken image instead of the org chart PNG. Applied to both CompanyExport and CompanyImport preview panes. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -8,6 +8,8 @@ import { useTheme } from "../context/ThemeContext";
|
||||
interface MarkdownBodyProps {
|
||||
children: string;
|
||||
className?: string;
|
||||
/** Optional resolver for relative image paths (e.g. within export packages) */
|
||||
resolveImageSrc?: (src: string) => string | null;
|
||||
}
|
||||
|
||||
let mermaidLoaderPromise: Promise<typeof import("mermaid").default> | null = null;
|
||||
@@ -112,7 +114,7 @@ function MermaidDiagramBlock({ source, darkMode }: { source: string; darkMode: b
|
||||
);
|
||||
}
|
||||
|
||||
export function MarkdownBody({ children, className }: MarkdownBodyProps) {
|
||||
export function MarkdownBody({ children, className, resolveImageSrc }: MarkdownBodyProps) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<div
|
||||
@@ -152,6 +154,12 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
|
||||
</a>
|
||||
);
|
||||
},
|
||||
img: resolveImageSrc
|
||||
? ({ src, alt, ...imgProps }) => {
|
||||
const resolved = src ? resolveImageSrc(src) : null;
|
||||
return <img {...imgProps} src={resolved ?? src} alt={alt ?? ""} />;
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -470,10 +470,12 @@ function generateReadmeFromSelection(
|
||||
function ExportPreviewPane({
|
||||
selectedFile,
|
||||
content,
|
||||
allFiles,
|
||||
onSkillClick,
|
||||
}: {
|
||||
selectedFile: string | null;
|
||||
content: CompanyPortabilityFileEntry | null;
|
||||
allFiles: Record<string, CompanyPortabilityFileEntry>;
|
||||
onSkillClick?: (skill: string) => void;
|
||||
}) {
|
||||
if (!selectedFile || content === null) {
|
||||
@@ -487,6 +489,20 @@ function ExportPreviewPane({
|
||||
const parsed = isMarkdown && textContent ? parseFrontmatter(textContent) : null;
|
||||
const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null;
|
||||
|
||||
// Resolve relative image paths within the export package (e.g. images/org-chart.png)
|
||||
const resolveImageSrc = isMarkdown
|
||||
? (src: string) => {
|
||||
// Skip absolute URLs and data URIs
|
||||
if (/^(?:https?:|data:)/i.test(src)) return null;
|
||||
// Resolve relative to the directory of the current markdown file
|
||||
const dir = selectedFile.includes("/") ? selectedFile.slice(0, selectedFile.lastIndexOf("/") + 1) : "";
|
||||
const resolved = dir + src;
|
||||
const entry = allFiles[resolved] ?? allFiles[src];
|
||||
if (!entry) return null;
|
||||
return getPortableFileDataUrl(resolved in allFiles ? resolved : src, entry);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="border-b border-border px-5 py-3">
|
||||
@@ -496,10 +512,10 @@ function ExportPreviewPane({
|
||||
{parsed ? (
|
||||
<>
|
||||
<FrontmatterCard data={parsed.data} onSkillClick={onSkillClick} />
|
||||
{parsed.body.trim() && <MarkdownBody>{parsed.body}</MarkdownBody>}
|
||||
{parsed.body.trim() && <MarkdownBody resolveImageSrc={resolveImageSrc}>{parsed.body}</MarkdownBody>}
|
||||
</>
|
||||
) : isMarkdown ? (
|
||||
<MarkdownBody>{textContent ?? ""}</MarkdownBody>
|
||||
<MarkdownBody resolveImageSrc={resolveImageSrc}>{textContent ?? ""}</MarkdownBody>
|
||||
) : imageSrc ? (
|
||||
<div className="flex min-h-[520px] items-center justify-center rounded-lg border border-border bg-accent/10 p-6">
|
||||
<img src={imageSrc} alt={selectedFile} className="max-h-[480px] max-w-full object-contain" />
|
||||
@@ -924,7 +940,7 @@ export function CompanyExport() {
|
||||
</div>
|
||||
</aside>
|
||||
<div className="min-w-0 overflow-y-auto pl-6">
|
||||
<ExportPreviewPane selectedFile={selectedFile} content={previewContent} onSkillClick={handleSkillClick} />
|
||||
<ExportPreviewPane selectedFile={selectedFile} content={previewContent} allFiles={effectiveFiles} onSkillClick={handleSkillClick} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -177,11 +177,13 @@ function importFileRowClassName(_node: FileTreeNode, checked: boolean) {
|
||||
function ImportPreviewPane({
|
||||
selectedFile,
|
||||
content,
|
||||
allFiles,
|
||||
action,
|
||||
renamedTo,
|
||||
}: {
|
||||
selectedFile: string | null;
|
||||
content: CompanyPortabilityFileEntry | null;
|
||||
allFiles: Record<string, CompanyPortabilityFileEntry>;
|
||||
action: string | null;
|
||||
renamedTo: string | null;
|
||||
}) {
|
||||
@@ -197,6 +199,18 @@ function ImportPreviewPane({
|
||||
const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null;
|
||||
const actionColor = action ? (ACTION_COLORS[action] ?? ACTION_COLORS.skip) : "";
|
||||
|
||||
// Resolve relative image paths within the import package
|
||||
const resolveImageSrc = isMarkdown
|
||||
? (src: string) => {
|
||||
if (/^(?:https?:|data:)/i.test(src)) return null;
|
||||
const dir = selectedFile.includes("/") ? selectedFile.slice(0, selectedFile.lastIndexOf("/") + 1) : "";
|
||||
const resolved = dir + src;
|
||||
const entry = allFiles[resolved] ?? allFiles[src];
|
||||
if (!entry) return null;
|
||||
return getPortableFileDataUrl(resolved in allFiles ? resolved : src, entry);
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div className="min-w-0">
|
||||
<div className="border-b border-border px-5 py-3">
|
||||
@@ -223,10 +237,10 @@ function ImportPreviewPane({
|
||||
{parsed ? (
|
||||
<>
|
||||
<FrontmatterCard data={parsed.data} />
|
||||
{parsed.body.trim() && <MarkdownBody>{parsed.body}</MarkdownBody>}
|
||||
{parsed.body.trim() && <MarkdownBody resolveImageSrc={resolveImageSrc}>{parsed.body}</MarkdownBody>}
|
||||
</>
|
||||
) : isMarkdown ? (
|
||||
<MarkdownBody>{textContent ?? ""}</MarkdownBody>
|
||||
<MarkdownBody resolveImageSrc={resolveImageSrc}>{textContent ?? ""}</MarkdownBody>
|
||||
) : imageSrc ? (
|
||||
<div className="flex min-h-[520px] items-center justify-center rounded-lg border border-border bg-accent/10 p-6">
|
||||
<img src={imageSrc} alt={selectedFile} className="max-h-[480px] max-w-full object-contain" />
|
||||
@@ -1265,6 +1279,7 @@ export function CompanyImport() {
|
||||
<ImportPreviewPane
|
||||
selectedFile={selectedFile}
|
||||
content={previewContent}
|
||||
allFiles={importPreview?.files ?? {}}
|
||||
action={selectedAction}
|
||||
renamedTo={selectedFile ? (renameMap.get(selectedFile) ?? null) : null}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user