@tuturuuu/utils 0.0.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +122 -3
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- package/src/index.ts +0 -1
package/src/node-diff.ts
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import type { JSONContent } from '@tiptap/core';
|
|
2
|
+
|
|
3
|
+
/** Node types that have unique identifiers beyond text content */
|
|
4
|
+
export type IdentifiableNodeType =
|
|
5
|
+
| 'image'
|
|
6
|
+
| 'imageResize'
|
|
7
|
+
| 'video'
|
|
8
|
+
| 'youtube'
|
|
9
|
+
| 'mention';
|
|
10
|
+
|
|
11
|
+
/** Attribute change details */
|
|
12
|
+
export interface AttributeChange {
|
|
13
|
+
key: string;
|
|
14
|
+
oldValue: unknown;
|
|
15
|
+
newValue: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Result of comparing two nodes */
|
|
19
|
+
export interface NodeDiffResult {
|
|
20
|
+
type: 'added' | 'removed' | 'modified';
|
|
21
|
+
nodeType: string;
|
|
22
|
+
path: number[];
|
|
23
|
+
oldNode?: JSONContent;
|
|
24
|
+
newNode?: JSONContent;
|
|
25
|
+
attributeChanges?: AttributeChange[];
|
|
26
|
+
/** Human-readable display label */
|
|
27
|
+
displayLabel: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Summary of node-level changes */
|
|
31
|
+
export interface NodeDiffSummary {
|
|
32
|
+
totalChanges: number;
|
|
33
|
+
added: NodeDiffResult[];
|
|
34
|
+
removed: NodeDiffResult[];
|
|
35
|
+
modified: NodeDiffResult[];
|
|
36
|
+
/** Quick flag for images/videos/embeds changes */
|
|
37
|
+
hasMediaChanges: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Node with its path in the tree */
|
|
41
|
+
interface FlattenedNode {
|
|
42
|
+
node: JSONContent;
|
|
43
|
+
path: number[];
|
|
44
|
+
identifier: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check if a node type is identifiable (has unique identifier beyond text)
|
|
49
|
+
*/
|
|
50
|
+
function isIdentifiableNodeType(type: string): type is IdentifiableNodeType {
|
|
51
|
+
return ['image', 'imageResize', 'video', 'youtube', 'mention'].includes(type);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Check if a node type is a media type
|
|
56
|
+
*/
|
|
57
|
+
function isMediaNodeType(type: string): boolean {
|
|
58
|
+
return ['image', 'imageResize', 'video', 'youtube'].includes(type);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Extract a unique identifier for a node.
|
|
63
|
+
* For images: use src URL
|
|
64
|
+
* For mentions: use id attribute
|
|
65
|
+
* For other identifiable nodes: use appropriate unique attribute
|
|
66
|
+
*/
|
|
67
|
+
export function getNodeIdentifier(node: JSONContent): string {
|
|
68
|
+
const type = node.type || 'unknown';
|
|
69
|
+
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 'image':
|
|
72
|
+
case 'imageResize':
|
|
73
|
+
// Use src URL as unique identifier for images
|
|
74
|
+
return `${type}:${node.attrs?.src || 'unknown'}`;
|
|
75
|
+
|
|
76
|
+
case 'video':
|
|
77
|
+
return `video:${node.attrs?.src || 'unknown'}`;
|
|
78
|
+
|
|
79
|
+
case 'youtube':
|
|
80
|
+
// YouTube embeds may use src or videoId
|
|
81
|
+
return `youtube:${node.attrs?.src || node.attrs?.videoId || 'unknown'}`;
|
|
82
|
+
|
|
83
|
+
case 'mention':
|
|
84
|
+
// Mentions have unique user/entity IDs
|
|
85
|
+
return `mention:${node.attrs?.id || 'unknown'}`;
|
|
86
|
+
|
|
87
|
+
default:
|
|
88
|
+
// For non-identifiable nodes, return type only (not tracked individually)
|
|
89
|
+
return type;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extract filename from a URL or path
|
|
95
|
+
*/
|
|
96
|
+
function extractFilename(urlOrPath: string): string {
|
|
97
|
+
try {
|
|
98
|
+
// Try to parse as URL first
|
|
99
|
+
const url = new URL(urlOrPath);
|
|
100
|
+
const pathname = url.pathname;
|
|
101
|
+
const filename = pathname.split('/').pop() || pathname;
|
|
102
|
+
// Decode and truncate if too long
|
|
103
|
+
const decoded = decodeURIComponent(filename);
|
|
104
|
+
return decoded.length > 40 ? `${decoded.slice(0, 37)}...` : decoded;
|
|
105
|
+
} catch {
|
|
106
|
+
// Not a valid URL, treat as path
|
|
107
|
+
const filename = urlOrPath.split('/').pop() || urlOrPath;
|
|
108
|
+
return filename.length > 40 ? `${filename.slice(0, 37)}...` : filename;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get a human-readable label for a node (for display in UI).
|
|
114
|
+
* e.g., "[Image: cat.png]" or "[Video: intro.mp4]"
|
|
115
|
+
*/
|
|
116
|
+
export function getNodeDisplayLabel(node: JSONContent): string {
|
|
117
|
+
const type = node.type || 'unknown';
|
|
118
|
+
|
|
119
|
+
switch (type) {
|
|
120
|
+
case 'image':
|
|
121
|
+
case 'imageResize': {
|
|
122
|
+
const src = node.attrs?.src;
|
|
123
|
+
const alt = node.attrs?.alt;
|
|
124
|
+
if (src) {
|
|
125
|
+
const filename = extractFilename(src);
|
|
126
|
+
return alt ? `${alt} (${filename})` : filename;
|
|
127
|
+
}
|
|
128
|
+
return alt || 'Image';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
case 'video': {
|
|
132
|
+
const src = node.attrs?.src;
|
|
133
|
+
if (src) {
|
|
134
|
+
return extractFilename(src);
|
|
135
|
+
}
|
|
136
|
+
return 'Video';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
case 'youtube': {
|
|
140
|
+
const src = node.attrs?.src || node.attrs?.videoId;
|
|
141
|
+
if (src) {
|
|
142
|
+
// For YouTube, show video ID or title if available
|
|
143
|
+
const title = node.attrs?.title;
|
|
144
|
+
if (title) return title;
|
|
145
|
+
// Extract video ID from URL if possible
|
|
146
|
+
try {
|
|
147
|
+
const url = new URL(src);
|
|
148
|
+
const videoId =
|
|
149
|
+
url.searchParams.get('v') || url.pathname.split('/').pop();
|
|
150
|
+
return videoId ? `YouTube: ${videoId}` : 'YouTube Video';
|
|
151
|
+
} catch {
|
|
152
|
+
return `YouTube: ${src.slice(0, 20)}...`;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return 'YouTube Video';
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
case 'mention': {
|
|
159
|
+
const label = node.attrs?.label || node.attrs?.id || 'mention';
|
|
160
|
+
return `@${label}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
default:
|
|
164
|
+
return type;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Flatten a JSONContent tree into a list of identifiable nodes with paths.
|
|
170
|
+
* Only extracts nodes that have unique identifiers (images, videos, mentions).
|
|
171
|
+
*/
|
|
172
|
+
export function flattenMediaNodes(
|
|
173
|
+
content: JSONContent | null | undefined,
|
|
174
|
+
path: number[] = []
|
|
175
|
+
): FlattenedNode[] {
|
|
176
|
+
if (!content) return [];
|
|
177
|
+
|
|
178
|
+
const result: FlattenedNode[] = [];
|
|
179
|
+
const type = content.type || '';
|
|
180
|
+
|
|
181
|
+
// If this is an identifiable node, add it to results
|
|
182
|
+
if (isIdentifiableNodeType(type)) {
|
|
183
|
+
result.push({
|
|
184
|
+
node: content,
|
|
185
|
+
path: [...path],
|
|
186
|
+
identifier: getNodeIdentifier(content),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Recursively process children
|
|
191
|
+
if (content.content && Array.isArray(content.content)) {
|
|
192
|
+
content.content.forEach((child, index) => {
|
|
193
|
+
result.push(...flattenMediaNodes(child, [...path, index]));
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Compare attributes of two nodes and return list of changes
|
|
202
|
+
*/
|
|
203
|
+
function compareAttributes(
|
|
204
|
+
oldNode: JSONContent,
|
|
205
|
+
newNode: JSONContent
|
|
206
|
+
): AttributeChange[] {
|
|
207
|
+
const changes: AttributeChange[] = [];
|
|
208
|
+
const oldAttrs = oldNode.attrs || {};
|
|
209
|
+
const newAttrs = newNode.attrs || {};
|
|
210
|
+
|
|
211
|
+
// Get all unique keys from both
|
|
212
|
+
const allKeys = new Set([...Object.keys(oldAttrs), ...Object.keys(newAttrs)]);
|
|
213
|
+
|
|
214
|
+
for (const key of allKeys) {
|
|
215
|
+
const oldVal = oldAttrs[key];
|
|
216
|
+
const newVal = newAttrs[key];
|
|
217
|
+
|
|
218
|
+
// Skip src since it's used for identification
|
|
219
|
+
if (key === 'src') continue;
|
|
220
|
+
|
|
221
|
+
// Compare values (stringify for deep comparison)
|
|
222
|
+
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
|
|
223
|
+
changes.push({
|
|
224
|
+
key,
|
|
225
|
+
oldValue: oldVal,
|
|
226
|
+
newValue: newVal,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return changes;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Parse JSON content from various input types
|
|
236
|
+
*/
|
|
237
|
+
export function parseJsonContent(value: unknown): JSONContent | null {
|
|
238
|
+
if (!value) return null;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
if (typeof value === 'string') {
|
|
242
|
+
return JSON.parse(value) as JSONContent;
|
|
243
|
+
}
|
|
244
|
+
if (typeof value === 'object') {
|
|
245
|
+
return value as JSONContent;
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Compare two JSONContent structures at the node level.
|
|
255
|
+
* Returns detailed diff information for each changed node.
|
|
256
|
+
*
|
|
257
|
+
* Algorithm:
|
|
258
|
+
* 1. Flatten both trees to extract identifiable nodes
|
|
259
|
+
* 2. Build maps by identifier
|
|
260
|
+
* 3. Find added (in new but not old), removed (in old but not new)
|
|
261
|
+
* 4. Find modified (same identifier, different attributes)
|
|
262
|
+
*/
|
|
263
|
+
export function computeNodeDiff(
|
|
264
|
+
oldContent: JSONContent | null | undefined,
|
|
265
|
+
newContent: JSONContent | null | undefined
|
|
266
|
+
): NodeDiffSummary {
|
|
267
|
+
// Flatten both trees
|
|
268
|
+
const oldNodes = flattenMediaNodes(oldContent);
|
|
269
|
+
const newNodes = flattenMediaNodes(newContent);
|
|
270
|
+
|
|
271
|
+
// Build maps by identifier
|
|
272
|
+
// Handle duplicates by appending index if identifier already exists
|
|
273
|
+
const oldByIdentifier = new Map<string, FlattenedNode>();
|
|
274
|
+
const oldIdentifierCounts = new Map<string, number>();
|
|
275
|
+
for (const node of oldNodes) {
|
|
276
|
+
const count = oldIdentifierCounts.get(node.identifier) || 0;
|
|
277
|
+
const uniqueId =
|
|
278
|
+
count > 0 ? `${node.identifier}#${count}` : node.identifier;
|
|
279
|
+
oldByIdentifier.set(uniqueId, { ...node, identifier: uniqueId });
|
|
280
|
+
oldIdentifierCounts.set(node.identifier, count + 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const newByIdentifier = new Map<string, FlattenedNode>();
|
|
284
|
+
const newIdentifierCounts = new Map<string, number>();
|
|
285
|
+
for (const node of newNodes) {
|
|
286
|
+
const count = newIdentifierCounts.get(node.identifier) || 0;
|
|
287
|
+
const uniqueId =
|
|
288
|
+
count > 0 ? `${node.identifier}#${count}` : node.identifier;
|
|
289
|
+
newByIdentifier.set(uniqueId, { ...node, identifier: uniqueId });
|
|
290
|
+
newIdentifierCounts.set(node.identifier, count + 1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
const added: NodeDiffResult[] = [];
|
|
294
|
+
const removed: NodeDiffResult[] = [];
|
|
295
|
+
const modified: NodeDiffResult[] = [];
|
|
296
|
+
|
|
297
|
+
// Find added nodes (in new but not in old)
|
|
298
|
+
for (const [id, newNode] of newByIdentifier) {
|
|
299
|
+
if (!oldByIdentifier.has(id)) {
|
|
300
|
+
added.push({
|
|
301
|
+
type: 'added',
|
|
302
|
+
nodeType: newNode.node.type || 'unknown',
|
|
303
|
+
path: newNode.path,
|
|
304
|
+
newNode: newNode.node,
|
|
305
|
+
displayLabel: getNodeDisplayLabel(newNode.node),
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Find removed nodes (in old but not in new)
|
|
311
|
+
for (const [id, oldNode] of oldByIdentifier) {
|
|
312
|
+
if (!newByIdentifier.has(id)) {
|
|
313
|
+
removed.push({
|
|
314
|
+
type: 'removed',
|
|
315
|
+
nodeType: oldNode.node.type || 'unknown',
|
|
316
|
+
path: oldNode.path,
|
|
317
|
+
oldNode: oldNode.node,
|
|
318
|
+
displayLabel: getNodeDisplayLabel(oldNode.node),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Find modified nodes (same identifier, different attributes)
|
|
324
|
+
for (const [id, oldNode] of oldByIdentifier) {
|
|
325
|
+
const newNode = newByIdentifier.get(id);
|
|
326
|
+
if (newNode) {
|
|
327
|
+
const attrChanges = compareAttributes(oldNode.node, newNode.node);
|
|
328
|
+
if (attrChanges.length > 0) {
|
|
329
|
+
modified.push({
|
|
330
|
+
type: 'modified',
|
|
331
|
+
nodeType: oldNode.node.type || 'unknown',
|
|
332
|
+
path: newNode.path,
|
|
333
|
+
oldNode: oldNode.node,
|
|
334
|
+
newNode: newNode.node,
|
|
335
|
+
attributeChanges: attrChanges,
|
|
336
|
+
displayLabel: getNodeDisplayLabel(newNode.node),
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check if any changes involve media nodes
|
|
343
|
+
const hasMediaChanges = [...added, ...removed, ...modified].some((diff) =>
|
|
344
|
+
isMediaNodeType(diff.nodeType)
|
|
345
|
+
);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
totalChanges: added.length + removed.length + modified.length,
|
|
349
|
+
added,
|
|
350
|
+
removed,
|
|
351
|
+
modified,
|
|
352
|
+
hasMediaChanges,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Get the image source URL from a node
|
|
358
|
+
*/
|
|
359
|
+
export function getNodeImageSrc(node: JSONContent): string | null {
|
|
360
|
+
if (node.type === 'image' || node.type === 'imageResize') {
|
|
361
|
+
return node.attrs?.src || null;
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Check if two JSONContent structures have any node-level differences
|
|
368
|
+
*/
|
|
369
|
+
export function hasNodeDifferences(
|
|
370
|
+
oldContent: JSONContent | null | undefined,
|
|
371
|
+
newContent: JSONContent | null | undefined
|
|
372
|
+
): boolean {
|
|
373
|
+
const diff = computeNodeDiff(oldContent, newContent);
|
|
374
|
+
return diff.totalChanges > 0;
|
|
375
|
+
}
|