Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 55 additions & 12 deletions packages/fern-docs/bundle/src/components/util/processIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,24 @@ export interface ProcessIconOptions {
files?: Record<string, FileData>;
}

export const processIcon = ({ node, fallback, files }: ProcessIconOptions): ReactNode | undefined => {
if (!hasMetadata(node) && node.type !== "link") {
return undefined;
}
interface ThemedIcon {
dark?: string;
light?: string;
}

function isThemedIcon(icon: unknown): icon is ThemedIcon {
return (
typeof icon === "object" &&
icon != null &&
("dark" in icon || "light" in icon) &&
(typeof (icon as ThemedIcon).dark === "string" || (icon as ThemedIcon).dark === undefined) &&
(typeof (icon as ThemedIcon).light === "string" || (icon as ThemedIcon).light === undefined)
);
}

if (node.icon?.startsWith("file:")) {
const fileId = node.icon.slice(5); // Remove "file:" prefix
function processIconString(iconString: string, files: Record<string, FileData> | undefined): ReactNode | undefined {
if (iconString.startsWith("file:")) {
const fileId = iconString.slice(5);
const fileData = files?.[fileId];

if (fileData) {
Expand All @@ -35,21 +46,53 @@ export const processIcon = ({ node, fallback, files }: ProcessIconOptions): Reac
/>
</NoZoom>
);
} else {
return undefined;
}
return undefined;
}

if (node.icon?.startsWith("<") && node.icon?.endsWith(">")) {
if (iconString.startsWith("<") && iconString.endsWith(">")) {
return (
<NoZoom>
<span className="size-5" dangerouslySetInnerHTML={{ __html: node.icon }} />
<span className="size-5" dangerouslySetInnerHTML={{ __html: iconString }} />
</NoZoom>
);
}

if (node.icon) {
return <FaIconServer icon={node.icon} />;
return <FaIconServer icon={iconString} />;
}

export const processIcon = ({ node, fallback, files }: ProcessIconOptions): ReactNode | undefined => {
if (!hasMetadata(node) && node.type !== "link") {
return undefined;
}

const icon = (node as { icon?: unknown }).icon;

if (isThemedIcon(icon)) {
const darkIcon = icon.dark;
const lightIcon = icon.light;

if (darkIcon && !lightIcon) {
return processIconString(darkIcon, files);
}
if (lightIcon && !darkIcon) {
return processIconString(lightIcon, files);
}

if (darkIcon && lightIcon) {
return (
<>
<span className="dark:hidden">{processIconString(lightIcon, files)}</span>
<span className="hidden dark:inline-block">{processIconString(darkIcon, files)}</span>
</>
);
}

return undefined;
}

if (typeof icon === "string") {
return processIconString(icon, files);
}

if (fallback) {
Expand Down
96 changes: 74 additions & 22 deletions packages/fern-docs/components/src/processIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,31 @@ export interface ProcessIconOptions {
files?: Record<string, FileData>;
}

interface ThemedIcon {
dark?: string;
light?: string;
}

function isThemedIcon(icon: unknown): icon is ThemedIcon {
return (
typeof icon === "object" &&
icon != null &&
("dark" in icon || "light" in icon) &&
(typeof (icon as ThemedIcon).dark === "string" || (icon as ThemedIcon).dark === undefined) &&
(typeof (icon as ThemedIcon).light === "string" || (icon as ThemedIcon).light === undefined)
);
}

/**
* TODO:
* This is a duplicate of the processIcon function in the bundle. This uses the FaIconServer
* component, which does not yet utilize next image caching. Until that is added, we are leaving
* the original processIcon function in the bundle.
* Helper function to process a single icon string (handles file:, inline SVG, or font icon)
*/
export const processIcon = ({
node,
fallback,
forceClientRender,
files
}: ProcessIconOptions): ReactNode | undefined => {
if (!hasMetadata(node) && node.type !== "link") {
return undefined;
}

if (node.icon?.startsWith("file:")) {
const fileId = node.icon.slice(5); // Remove "file:" prefix
function processIconString(
iconString: string,
files: Record<string, FileData> | undefined,
forceClientRender?: boolean
): ReactNode | undefined {
if (iconString.startsWith("file:")) {
const fileId = iconString.slice(5); // Remove "file:" prefix
const fileData = files?.[fileId];

if (fileData) {
Expand All @@ -45,21 +52,66 @@ export const processIcon = ({
/>
</NoZoom>
);
} else {
return undefined;
}
return undefined;
}

if (node.icon?.startsWith("<") && node.icon?.endsWith(">")) {
if (iconString.startsWith("<") && iconString.endsWith(">")) {
return (
<NoZoom>
<span className="size-5" dangerouslySetInnerHTML={{ __html: node.icon }} />
<span className="size-5" dangerouslySetInnerHTML={{ __html: iconString }} />
</NoZoom>
);
}

if (node.icon) {
return <FaIconServer icon={node.icon} forceClientRender={forceClientRender} />;
return <FaIconServer icon={iconString} forceClientRender={forceClientRender} />;
}

/**
* TODO:
* This is a duplicate of the processIcon function in the bundle. This uses the FaIconServer
* component, which does not yet utilize next image caching. Until that is added, we are leaving
* the original processIcon function in the bundle.
*/
export const processIcon = ({
node,
fallback,
forceClientRender,
files
}: ProcessIconOptions): ReactNode | undefined => {
if (!hasMetadata(node) && node.type !== "link") {
return undefined;
}

const icon = (node as { icon?: unknown }).icon;

if (isThemedIcon(icon)) {
const darkIcon = icon.dark;
const lightIcon = icon.light;

if (darkIcon && !lightIcon) {
return processIconString(darkIcon, files, forceClientRender);
}
if (lightIcon && !darkIcon) {
return processIconString(lightIcon, files, forceClientRender);
}

if (darkIcon && lightIcon) {
return (
<>
<span className="dark:hidden">{processIconString(lightIcon, files, forceClientRender)}</span>
<span className="hidden dark:inline-block">
{processIconString(darkIcon, files, forceClientRender)}
</span>
</>
);
}

return undefined;
}

if (typeof icon === "string") {
return processIconString(icon, files, forceClientRender);
}

if (fallback) {
Expand Down