mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-08-25 15:52:21 +10:00
feat: add semantic CSS stylesheets (#3274)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor Agent
parent
4ac19f81b3
commit
d2ffbf9618
@@ -0,0 +1,198 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user