mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-25 07:42:20 +10:00
199 lines
7.3 KiB
TypeScript
199 lines
7.3 KiB
TypeScript
import type { SemanticNode } from "@reactive-resume/resume/stylesheet";
|
|
import type { StylesheetMode } from "@reactive-resume/schema/resume/stylesheet";
|
|
import type { ReactNode } from "react";
|
|
import type { ResolvedPdfNodePresentation } from "./adapter";
|
|
import { createContext, use, useMemo } from "react";
|
|
import { semanticNodeKeys } from "./node-keys";
|
|
|
|
export type ResolvedResumePresentation = Readonly<Record<string, ResolvedPdfNodePresentation>>;
|
|
|
|
type SemanticRenderProviderProps = {
|
|
presentation: ResolvedResumePresentation;
|
|
mode: StylesheetMode;
|
|
sourceTree: SemanticNode;
|
|
renderTree: SemanticNode;
|
|
children: ReactNode;
|
|
};
|
|
|
|
const EMPTY_NODE = Object.freeze({}) satisfies ResolvedPdfNodePresentation;
|
|
const SemanticRenderContext = createContext<{
|
|
presentation: ResolvedResumePresentation;
|
|
mode: StylesheetMode;
|
|
renderedNodeKeys: ReadonlySet<string>;
|
|
sourceNodeKeys: ReadonlySet<string>;
|
|
renderedChildKeys: ReadonlyMap<string, readonly string[]>;
|
|
sourceChildKeys: ReadonlyMap<string, readonly string[]>;
|
|
sourceNodes: readonly SemanticNode[];
|
|
renderOrder: ReadonlyMap<string, number>;
|
|
} | null>(null);
|
|
const SemanticNodeKeyContext = createContext<string | undefined>(undefined);
|
|
|
|
const flattenTree = (root: SemanticNode): SemanticNode[] => {
|
|
const nodes: SemanticNode[] = [];
|
|
const visit = (node: SemanticNode) => {
|
|
nodes.push(node);
|
|
for (const child of node.children) visit(child);
|
|
};
|
|
visit(root);
|
|
return nodes;
|
|
};
|
|
|
|
const indexRenderTree = (root: SemanticNode) => {
|
|
const keys = new Set<string>();
|
|
const childKeys = new Map<string, readonly string[]>();
|
|
const order = new Map<string, number>();
|
|
const visit = (node: SemanticNode) => {
|
|
keys.add(node.key);
|
|
order.set(node.key, order.size);
|
|
childKeys.set(
|
|
node.key,
|
|
node.children.map(({ key }) => key),
|
|
);
|
|
for (const child of node.children) visit(child);
|
|
};
|
|
visit(root);
|
|
return {
|
|
renderedNodeKeys: keys as ReadonlySet<string>,
|
|
renderedChildKeys: childKeys as ReadonlyMap<string, readonly string[]>,
|
|
renderOrder: order as ReadonlyMap<string, number>,
|
|
};
|
|
};
|
|
|
|
export function SemanticRenderProvider({
|
|
presentation,
|
|
mode,
|
|
sourceTree,
|
|
renderTree,
|
|
children,
|
|
}: SemanticRenderProviderProps) {
|
|
const treeIndex = useMemo(() => indexRenderTree(renderTree), [renderTree]);
|
|
const sourceNodes = useMemo(() => flattenTree(sourceTree), [sourceTree]);
|
|
const sourceNodeKeys = useMemo(
|
|
() => new Set(sourceNodes.map(({ key }) => key)) as ReadonlySet<string>,
|
|
[sourceNodes],
|
|
);
|
|
const sourceChildKeys = useMemo(
|
|
() =>
|
|
new Map(
|
|
sourceNodes.map(({ key, children: sourceChildren }) => [key, sourceChildren.map(({ key }) => key)]),
|
|
) as ReadonlyMap<string, readonly string[]>,
|
|
[sourceNodes],
|
|
);
|
|
const value = useMemo(
|
|
() => ({ presentation, mode, sourceNodeKeys, sourceChildKeys, sourceNodes, ...treeIndex }),
|
|
[mode, presentation, sourceChildKeys, sourceNodeKeys, sourceNodes, treeIndex],
|
|
);
|
|
return <SemanticRenderContext.Provider value={value}>{children}</SemanticRenderContext.Provider>;
|
|
}
|
|
|
|
export function useResolvedNode(nodeKey: string | undefined): ResolvedPdfNodePresentation {
|
|
const context = use(SemanticRenderContext);
|
|
if (!context || !nodeKey) return EMPTY_NODE;
|
|
return context.presentation[nodeKey] ?? EMPTY_NODE;
|
|
}
|
|
|
|
export const useSemanticRenderMode = (): StylesheetMode => use(SemanticRenderContext)?.mode ?? "legacy";
|
|
|
|
export const useSemanticNodeVisible = (nodeKey: string | undefined): boolean => {
|
|
const context = use(SemanticRenderContext);
|
|
if (context?.mode !== "semantic" || !nodeKey) return true;
|
|
return context.renderedNodeKeys.has(nodeKey);
|
|
};
|
|
|
|
export const useSemanticNodeExists = (nodeKey: string | undefined): boolean => {
|
|
const context = use(SemanticRenderContext);
|
|
return Boolean(nodeKey && context?.sourceNodeKeys.has(nodeKey));
|
|
};
|
|
|
|
export const useRenderedChildKeys = (nodeKey: string | undefined): readonly string[] | undefined => {
|
|
const context = use(SemanticRenderContext);
|
|
if (context?.mode !== "semantic" || !nodeKey) return undefined;
|
|
const rendered = context.renderedChildKeys.get(nodeKey) ?? [];
|
|
const source = context.sourceChildKeys.get(nodeKey) ?? [];
|
|
return rendered.length === source.length && rendered.every((key, index) => key === source[index])
|
|
? undefined
|
|
: rendered;
|
|
};
|
|
|
|
export type RenderedChildEntry<T> = {
|
|
nodeKey: string;
|
|
value: T;
|
|
};
|
|
|
|
export const projectRenderedChildren = <T,>(
|
|
renderedChildKeys: readonly string[] | undefined,
|
|
entries: readonly RenderedChildEntry<T>[],
|
|
): T[] => {
|
|
if (!renderedChildKeys) return entries.map(({ value }) => value);
|
|
const valuesByNodeKey = new Map(entries.map(({ nodeKey, value }) => [nodeKey, value]));
|
|
return renderedChildKeys.flatMap((nodeKey) => {
|
|
const value = valuesByNodeKey.get(nodeKey);
|
|
return value === undefined ? [] : [value];
|
|
});
|
|
};
|
|
|
|
export const useSemanticSectionNodeKey = (pageNodeKey: string, sectionId: string): string => {
|
|
const context = use(SemanticRenderContext);
|
|
const section = context?.sourceNodes.find(
|
|
(node) => node.kind === "section" && node.id === sectionId && node.key.startsWith(`${pageNodeKey}/`),
|
|
);
|
|
return section?.key ?? semanticNodeKeys.section(semanticNodeKeys.region(pageNodeKey, "main"), sectionId);
|
|
};
|
|
|
|
export const useRenderedSectionIds = (pageNodeKey: string, authoredIds: readonly string[]): string[] => {
|
|
const context = use(SemanticRenderContext);
|
|
if (context?.mode !== "semantic") return [...authoredIds];
|
|
|
|
const authored = new Set(authoredIds);
|
|
return context.sourceNodes
|
|
.filter(
|
|
(node) =>
|
|
node.kind === "section" &&
|
|
node.id !== undefined &&
|
|
authored.has(node.id) &&
|
|
node.key.startsWith(`${pageNodeKey}/`) &&
|
|
context.renderedNodeKeys.has(node.key),
|
|
)
|
|
.sort(
|
|
(left, right) =>
|
|
(context.renderOrder.get(left.key) ?? Number.MAX_SAFE_INTEGER) -
|
|
(context.renderOrder.get(right.key) ?? Number.MAX_SAFE_INTEGER),
|
|
)
|
|
.flatMap(({ id }) => (id ? [id] : []));
|
|
};
|
|
|
|
export const useSemanticNodeBindings = () => {
|
|
const context = use(SemanticRenderContext);
|
|
|
|
return {
|
|
resolveNode: (nodeKey: string | undefined): ResolvedPdfNodePresentation => {
|
|
if (!context || !nodeKey) return EMPTY_NODE;
|
|
return context.presentation[nodeKey] ?? EMPTY_NODE;
|
|
},
|
|
isNodeVisible: (nodeKey: string | undefined): boolean => {
|
|
if (context?.mode !== "semantic" || !nodeKey) return true;
|
|
return context.renderedNodeKeys.has(nodeKey);
|
|
},
|
|
renderedChildKeysFor: (nodeKey: string | undefined): readonly string[] | undefined => {
|
|
if (context?.mode !== "semantic" || !nodeKey) return undefined;
|
|
const rendered = context.renderedChildKeys.get(nodeKey) ?? [];
|
|
const source = context.sourceChildKeys.get(nodeKey) ?? [];
|
|
return rendered.length === source.length && rendered.every((key, index) => key === source[index])
|
|
? undefined
|
|
: rendered;
|
|
},
|
|
};
|
|
};
|
|
|
|
export function SemanticNodeKeyProvider({ nodeKey, children }: { nodeKey: string | undefined; children: ReactNode }) {
|
|
return <SemanticNodeKeyContext.Provider value={nodeKey}>{children}</SemanticNodeKeyContext.Provider>;
|
|
}
|
|
|
|
export const useSemanticNodeKey = (): string | undefined => use(SemanticNodeKeyContext);
|
|
|
|
export function SemanticItemNodeKeyProvider({ itemId, children }: { itemId: string; children: ReactNode }) {
|
|
const parentNodeKey = useSemanticNodeKey();
|
|
const nodeKey = parentNodeKey ? semanticNodeKeys.item(parentNodeKey, itemId) : undefined;
|
|
return <SemanticNodeKeyProvider nodeKey={nodeKey}>{children}</SemanticNodeKeyProvider>;
|
|
}
|