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:
parent
531945cfe2
commit
a9802c1962
3 changed files with 45 additions and 6 deletions
|
|
@ -8,6 +8,8 @@ import { useTheme } from "../context/ThemeContext";
|
||||||
interface MarkdownBodyProps {
|
interface MarkdownBodyProps {
|
||||||
children: string;
|
children: string;
|
||||||
className?: 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;
|
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();
|
const { theme } = useTheme();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
@ -152,6 +154,12 @@ export function MarkdownBody({ children, className }: MarkdownBodyProps) {
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
img: resolveImageSrc
|
||||||
|
? ({ src, alt, ...imgProps }) => {
|
||||||
|
const resolved = src ? resolveImageSrc(src) : null;
|
||||||
|
return <img {...imgProps} src={resolved ?? src} alt={alt ?? ""} />;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
|
||||||
|
|
@ -470,10 +470,12 @@ function generateReadmeFromSelection(
|
||||||
function ExportPreviewPane({
|
function ExportPreviewPane({
|
||||||
selectedFile,
|
selectedFile,
|
||||||
content,
|
content,
|
||||||
|
allFiles,
|
||||||
onSkillClick,
|
onSkillClick,
|
||||||
}: {
|
}: {
|
||||||
selectedFile: string | null;
|
selectedFile: string | null;
|
||||||
content: CompanyPortabilityFileEntry | null;
|
content: CompanyPortabilityFileEntry | null;
|
||||||
|
allFiles: Record<string, CompanyPortabilityFileEntry>;
|
||||||
onSkillClick?: (skill: string) => void;
|
onSkillClick?: (skill: string) => void;
|
||||||
}) {
|
}) {
|
||||||
if (!selectedFile || content === null) {
|
if (!selectedFile || content === null) {
|
||||||
|
|
@ -487,6 +489,20 @@ function ExportPreviewPane({
|
||||||
const parsed = isMarkdown && textContent ? parseFrontmatter(textContent) : null;
|
const parsed = isMarkdown && textContent ? parseFrontmatter(textContent) : null;
|
||||||
const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : 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 (
|
return (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="border-b border-border px-5 py-3">
|
<div className="border-b border-border px-5 py-3">
|
||||||
|
|
@ -496,10 +512,10 @@ function ExportPreviewPane({
|
||||||
{parsed ? (
|
{parsed ? (
|
||||||
<>
|
<>
|
||||||
<FrontmatterCard data={parsed.data} onSkillClick={onSkillClick} />
|
<FrontmatterCard data={parsed.data} onSkillClick={onSkillClick} />
|
||||||
{parsed.body.trim() && <MarkdownBody>{parsed.body}</MarkdownBody>}
|
{parsed.body.trim() && <MarkdownBody resolveImageSrc={resolveImageSrc}>{parsed.body}</MarkdownBody>}
|
||||||
</>
|
</>
|
||||||
) : isMarkdown ? (
|
) : isMarkdown ? (
|
||||||
<MarkdownBody>{textContent ?? ""}</MarkdownBody>
|
<MarkdownBody resolveImageSrc={resolveImageSrc}>{textContent ?? ""}</MarkdownBody>
|
||||||
) : imageSrc ? (
|
) : imageSrc ? (
|
||||||
<div className="flex min-h-[520px] items-center justify-center rounded-lg border border-border bg-accent/10 p-6">
|
<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" />
|
<img src={imageSrc} alt={selectedFile} className="max-h-[480px] max-w-full object-contain" />
|
||||||
|
|
@ -924,7 +940,7 @@ export function CompanyExport() {
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div className="min-w-0 overflow-y-auto pl-6">
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -177,11 +177,13 @@ function importFileRowClassName(_node: FileTreeNode, checked: boolean) {
|
||||||
function ImportPreviewPane({
|
function ImportPreviewPane({
|
||||||
selectedFile,
|
selectedFile,
|
||||||
content,
|
content,
|
||||||
|
allFiles,
|
||||||
action,
|
action,
|
||||||
renamedTo,
|
renamedTo,
|
||||||
}: {
|
}: {
|
||||||
selectedFile: string | null;
|
selectedFile: string | null;
|
||||||
content: CompanyPortabilityFileEntry | null;
|
content: CompanyPortabilityFileEntry | null;
|
||||||
|
allFiles: Record<string, CompanyPortabilityFileEntry>;
|
||||||
action: string | null;
|
action: string | null;
|
||||||
renamedTo: string | null;
|
renamedTo: string | null;
|
||||||
}) {
|
}) {
|
||||||
|
|
@ -197,6 +199,18 @@ function ImportPreviewPane({
|
||||||
const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null;
|
const imageSrc = isPortableImageFile(selectedFile, content) ? getPortableFileDataUrl(selectedFile, content) : null;
|
||||||
const actionColor = action ? (ACTION_COLORS[action] ?? ACTION_COLORS.skip) : "";
|
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 (
|
return (
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
<div className="border-b border-border px-5 py-3">
|
<div className="border-b border-border px-5 py-3">
|
||||||
|
|
@ -223,10 +237,10 @@ function ImportPreviewPane({
|
||||||
{parsed ? (
|
{parsed ? (
|
||||||
<>
|
<>
|
||||||
<FrontmatterCard data={parsed.data} />
|
<FrontmatterCard data={parsed.data} />
|
||||||
{parsed.body.trim() && <MarkdownBody>{parsed.body}</MarkdownBody>}
|
{parsed.body.trim() && <MarkdownBody resolveImageSrc={resolveImageSrc}>{parsed.body}</MarkdownBody>}
|
||||||
</>
|
</>
|
||||||
) : isMarkdown ? (
|
) : isMarkdown ? (
|
||||||
<MarkdownBody>{textContent ?? ""}</MarkdownBody>
|
<MarkdownBody resolveImageSrc={resolveImageSrc}>{textContent ?? ""}</MarkdownBody>
|
||||||
) : imageSrc ? (
|
) : imageSrc ? (
|
||||||
<div className="flex min-h-[520px] items-center justify-center rounded-lg border border-border bg-accent/10 p-6">
|
<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" />
|
<img src={imageSrc} alt={selectedFile} className="max-h-[480px] max-w-full object-contain" />
|
||||||
|
|
@ -1265,6 +1279,7 @@ export function CompanyImport() {
|
||||||
<ImportPreviewPane
|
<ImportPreviewPane
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile}
|
||||||
content={previewContent}
|
content={previewContent}
|
||||||
|
allFiles={importPreview?.files ?? {}}
|
||||||
action={selectedAction}
|
action={selectedAction}
|
||||||
renamedTo={selectedFile ? (renameMap.get(selectedFile) ?? null) : null}
|
renamedTo={selectedFile ? (renameMap.get(selectedFile) ?? null) : null}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue