1import type { PickedElementPayload, PreviewAnnotationPayload } from "@t3tools/contracts";
2
3function isStringOrNull(value: unknown): value is string | null {
4 return value === null || typeof value === "string";
5}
6
7function isFiniteNumberOrNull(value: unknown): value is number | null {
8 return value === null || (typeof value === "number" && Number.isFinite(value));
9}
10
11function isPickedStackFrame(value: unknown): boolean {
12 if (typeof value !== "object" || value === null) return false;
13 const frame = value as Record<string, unknown>;
14 return (
15 isStringOrNull(frame["functionName"]) &&
16 isStringOrNull(frame["fileName"]) &&
17 isFiniteNumberOrNull(frame["lineNumber"]) &&
18 isFiniteNumberOrNull(frame["columnNumber"])
19 );
20}
21
22export function isPickedElementPayload(value: unknown): value is PickedElementPayload {
23 if (typeof value !== "object" || value === null) return false;
24 const c = value as Record<string, unknown>;
25 if (typeof c["pageUrl"] !== "string") return false;
26 if (typeof c["tagName"] !== "string") return false;
27 if (typeof c["htmlPreview"] !== "string") return false;
28 if (typeof c["styles"] !== "string") return false;
29 if (typeof c["pickedAt"] !== "string") return false;
30 if (!isStringOrNull(c["pageTitle"])) return false;
31 if (!isStringOrNull(c["selector"])) return false;
32 if (!isStringOrNull(c["componentName"])) return false;
33 if (c["source"] !== null && !isPickedStackFrame(c["source"])) return false;
34 if (!Array.isArray(c["stack"])) return false;
35 if (!c["stack"].every(isPickedStackFrame)) return false;
36 return true;
37}
38
39function isRect(value: unknown): boolean {
40 if (typeof value !== "object" || value === null) return false;
41 const rect = value as Record<string, unknown>;
42 return ["x", "y", "width", "height"].every(
43 (key) => typeof rect[key] === "number" && Number.isFinite(rect[key]),
44 );
45}
46
47function isPoint(value: unknown): boolean {
48 if (typeof value !== "object" || value === null) return false;
49 const point = value as Record<string, unknown>;
50 return (
51 typeof point["x"] === "number" &&
52 Number.isFinite(point["x"]) &&
53 typeof point["y"] === "number" &&
54 Number.isFinite(point["y"])
55 );
56}
57
58export function isPreviewAnnotationPayload(value: unknown): value is PreviewAnnotationPayload {
59 if (typeof value !== "object" || value === null) return false;
60 const annotation = value as Record<string, unknown>;
61 if (typeof annotation["id"] !== "string") return false;
62 if (typeof annotation["pageUrl"] !== "string") return false;
63 if (!isStringOrNull(annotation["pageTitle"])) return false;
64 if (typeof annotation["comment"] !== "string") return false;
65 if (typeof annotation["createdAt"] !== "string") return false;
66 if (annotation["screenshot"] !== null) return false;
67
68 const elements = annotation["elements"];
69 if (!Array.isArray(elements)) return false;
70 if (
71 !elements.every((entry) => {
72 if (typeof entry !== "object" || entry === null) return false;
73 const target = entry as Record<string, unknown>;
74 return (
75 typeof target["id"] === "string" &&
76 isPickedElementPayload(target["element"]) &&
77 isRect(target["rect"])
78 );
79 })
80 ) {
81 return false;
82 }
83
84 const regions = annotation["regions"];
85 if (!Array.isArray(regions)) return false;
86 if (
87 !regions.every((entry) => {
88 if (typeof entry !== "object" || entry === null) return false;
89 const target = entry as Record<string, unknown>;
90 return typeof target["id"] === "string" && isRect(target["rect"]);
91 })
92 ) {
93 return false;
94 }
95
96 const strokes = annotation["strokes"];
97 if (!Array.isArray(strokes)) return false;
98 if (
99 !strokes.every((entry) => {
100 if (typeof entry !== "object" || entry === null) return false;
101 const target = entry as Record<string, unknown>;
102 return (
103 typeof target["id"] === "string" &&
104 typeof target["color"] === "string" &&
105 typeof target["width"] === "number" &&
106 Number.isFinite(target["width"]) &&
107 Array.isArray(target["points"]) &&
108 target["points"].every(isPoint) &&
109 isRect(target["bounds"])
110 );
111 })
112 ) {
113 return false;
114 }
115
116 const styleChanges = annotation["styleChanges"];
117 if (!Array.isArray(styleChanges)) return false;
118 if (
119 !styleChanges.every((entry) => {
120 if (typeof entry !== "object" || entry === null) return false;
121 const change = entry as Record<string, unknown>;
122 return (
123 typeof change["targetId"] === "string" &&
124 isStringOrNull(change["selector"]) &&
125 typeof change["property"] === "string" &&
126 typeof change["previousValue"] === "string" &&
127 typeof change["value"] === "string"
128 );
129 })
130 ) {
131 return false;
132 }
133 return true;
134}