auto-loading-skeleton 1.0.2 → 1.0.3
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/LICENSE +21 -21
- package/README.md +242 -242
- package/package.json +48 -48
- package/src/AutoSkeleton.jsx +72 -72
- package/src/SkeletonItem.jsx +99 -99
- package/src/analyzer.js +113 -113
- package/src/index.d.ts +51 -51
- package/src/index.js +102 -102
- package/src/renderer.js +84 -84
- package/src/styles.js +76 -76
package/src/AutoSkeleton.jsx
CHANGED
|
@@ -1,72 +1,72 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AutoSkeleton.jsx
|
|
3
|
-
* Main wrapper component.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* <AutoSkeleton loading={isLoading}>
|
|
7
|
-
* <ProductCard />
|
|
8
|
-
* </AutoSkeleton>
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useEffect, useMemo } from 'react';
|
|
12
|
-
import { analyzeTree } from './analyzer.js';
|
|
13
|
-
import { renderSkeletonNodes } from './renderer.js';
|
|
14
|
-
import { injectStyles } from './styles.js';
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* @typedef {Object} AutoSkeletonProps
|
|
18
|
-
* @property {boolean} loading - Show skeleton when true
|
|
19
|
-
* @property {React.ReactNode} children - The real UI component(s)
|
|
20
|
-
* @property {'shimmer'|'pulse'|'wave'|'none'} [animation='shimmer']
|
|
21
|
-
* @property {number} [repeat=1] - Repeat skeleton N times (e.g. list items)
|
|
22
|
-
* @property {object} [theme] - { baseColor, highlightColor }
|
|
23
|
-
* @property {React.CSSProperties} [style] - Wrapper style
|
|
24
|
-
* @property {string} [className] - Wrapper className
|
|
25
|
-
* @property {string} [ariaLabel] - aria-label for the skeleton wrapper
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
export function AutoSkeleton({
|
|
29
|
-
loading = false,
|
|
30
|
-
children,
|
|
31
|
-
animation = 'shimmer',
|
|
32
|
-
repeat = 1,
|
|
33
|
-
theme = {},
|
|
34
|
-
style,
|
|
35
|
-
className,
|
|
36
|
-
ariaLabel = 'Loading…',
|
|
37
|
-
}) {
|
|
38
|
-
// Inject global CSS once
|
|
39
|
-
useEffect(() => { injectStyles(); }, []);
|
|
40
|
-
|
|
41
|
-
// Analyse the child tree once (memoised — re-runs only when children change)
|
|
42
|
-
const skeletonNodes = useMemo(() => {
|
|
43
|
-
if (!loading) return null;
|
|
44
|
-
const childArray = React.Children.toArray(children);
|
|
45
|
-
return childArray.flatMap(child => analyzeTree(child));
|
|
46
|
-
}, [loading, children]);
|
|
47
|
-
|
|
48
|
-
if (!loading) return children;
|
|
49
|
-
|
|
50
|
-
const options = { animation, theme };
|
|
51
|
-
const units = Array.from({ length: repeat }, (_, i) =>
|
|
52
|
-
React.createElement(
|
|
53
|
-
'div',
|
|
54
|
-
{ key: i, style: i > 0 ? { marginTop: 16 } : undefined },
|
|
55
|
-
renderSkeletonNodes(skeletonNodes, options)
|
|
56
|
-
)
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return React.createElement(
|
|
60
|
-
'div',
|
|
61
|
-
{
|
|
62
|
-
role: 'status',
|
|
63
|
-
'aria-busy': true,
|
|
64
|
-
'aria-label': ariaLabel,
|
|
65
|
-
style,
|
|
66
|
-
className,
|
|
67
|
-
},
|
|
68
|
-
...units
|
|
69
|
-
);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export default AutoSkeleton;
|
|
1
|
+
/**
|
|
2
|
+
* AutoSkeleton.jsx
|
|
3
|
+
* Main wrapper component.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <AutoSkeleton loading={isLoading}>
|
|
7
|
+
* <ProductCard />
|
|
8
|
+
* </AutoSkeleton>
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useEffect, useMemo } from 'react';
|
|
12
|
+
import { analyzeTree } from './analyzer.js';
|
|
13
|
+
import { renderSkeletonNodes } from './renderer.js';
|
|
14
|
+
import { injectStyles } from './styles.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} AutoSkeletonProps
|
|
18
|
+
* @property {boolean} loading - Show skeleton when true
|
|
19
|
+
* @property {React.ReactNode} children - The real UI component(s)
|
|
20
|
+
* @property {'shimmer'|'pulse'|'wave'|'none'} [animation='shimmer']
|
|
21
|
+
* @property {number} [repeat=1] - Repeat skeleton N times (e.g. list items)
|
|
22
|
+
* @property {object} [theme] - { baseColor, highlightColor }
|
|
23
|
+
* @property {React.CSSProperties} [style] - Wrapper style
|
|
24
|
+
* @property {string} [className] - Wrapper className
|
|
25
|
+
* @property {string} [ariaLabel] - aria-label for the skeleton wrapper
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export function AutoSkeleton({
|
|
29
|
+
loading = false,
|
|
30
|
+
children,
|
|
31
|
+
animation = 'shimmer',
|
|
32
|
+
repeat = 1,
|
|
33
|
+
theme = {},
|
|
34
|
+
style,
|
|
35
|
+
className,
|
|
36
|
+
ariaLabel = 'Loading…',
|
|
37
|
+
}) {
|
|
38
|
+
// Inject global CSS once
|
|
39
|
+
useEffect(() => { injectStyles(); }, []);
|
|
40
|
+
|
|
41
|
+
// Analyse the child tree once (memoised — re-runs only when children change)
|
|
42
|
+
const skeletonNodes = useMemo(() => {
|
|
43
|
+
if (!loading) return null;
|
|
44
|
+
const childArray = React.Children.toArray(children);
|
|
45
|
+
return childArray.flatMap(child => analyzeTree(child));
|
|
46
|
+
}, [loading, children]);
|
|
47
|
+
|
|
48
|
+
if (!loading) return children;
|
|
49
|
+
|
|
50
|
+
const options = { animation, theme };
|
|
51
|
+
const units = Array.from({ length: repeat }, (_, i) =>
|
|
52
|
+
React.createElement(
|
|
53
|
+
'div',
|
|
54
|
+
{ key: i, style: i > 0 ? { marginTop: 16 } : undefined },
|
|
55
|
+
renderSkeletonNodes(skeletonNodes, options)
|
|
56
|
+
)
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return React.createElement(
|
|
60
|
+
'div',
|
|
61
|
+
{
|
|
62
|
+
role: 'status',
|
|
63
|
+
'aria-busy': true,
|
|
64
|
+
'aria-label': ariaLabel,
|
|
65
|
+
style,
|
|
66
|
+
className,
|
|
67
|
+
},
|
|
68
|
+
...units
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export default AutoSkeleton;
|
package/src/SkeletonItem.jsx
CHANGED
|
@@ -1,99 +1,99 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SkeletonItem.jsx
|
|
3
|
-
* Low-level primitive for manual skeleton building if needed.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* <SkeletonItem type="text" lines={2} />
|
|
7
|
-
* <SkeletonItem type="avatar" size={48} />
|
|
8
|
-
* <SkeletonItem type="image" width="100%" height={200} />
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import React, { useEffect } from 'react';
|
|
12
|
-
import { buildClassName, injectStyles } from './styles.js';
|
|
13
|
-
|
|
14
|
-
export function SkeletonItem({
|
|
15
|
-
type = 'text',
|
|
16
|
-
lines = 1,
|
|
17
|
-
width,
|
|
18
|
-
height,
|
|
19
|
-
size,
|
|
20
|
-
animation = 'shimmer',
|
|
21
|
-
theme = {},
|
|
22
|
-
style: extraStyle = {},
|
|
23
|
-
}) {
|
|
24
|
-
useEffect(() => { injectStyles(); }, []);
|
|
25
|
-
|
|
26
|
-
const cssVars = {
|
|
27
|
-
...(theme.baseColor && { '--skeleton-base-color': theme.baseColor }),
|
|
28
|
-
...(theme.highlightColor && { '--skeleton-highlight-color': theme.highlightColor }),
|
|
29
|
-
...extraStyle,
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
switch (type) {
|
|
33
|
-
case 'text': {
|
|
34
|
-
const cls = buildClassName(animation, 'auto-skeleton-text-line');
|
|
35
|
-
if (lines === 1) {
|
|
36
|
-
return React.createElement('div', {
|
|
37
|
-
className: cls,
|
|
38
|
-
style: { width: width || '80%', ...cssVars },
|
|
39
|
-
'aria-hidden': true,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
const lineEls = Array.from({ length: lines }, (_, i) =>
|
|
43
|
-
React.createElement('div', {
|
|
44
|
-
key: i,
|
|
45
|
-
className: cls,
|
|
46
|
-
style: { width: i === lines - 1 ? '65%' : '100%', ...cssVars },
|
|
47
|
-
'aria-hidden': true,
|
|
48
|
-
})
|
|
49
|
-
);
|
|
50
|
-
return React.createElement('div', {
|
|
51
|
-
className: 'auto-skeleton-text-block',
|
|
52
|
-
style: cssVars,
|
|
53
|
-
'aria-hidden': true,
|
|
54
|
-
}, ...lineEls);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
case 'avatar': {
|
|
58
|
-
const px = size ? `${size}px` : (width || '40px');
|
|
59
|
-
return React.createElement('div', {
|
|
60
|
-
className: buildClassName(animation, 'auto-skeleton-avatar-block'),
|
|
61
|
-
style: { width: px, height: size ? `${size}px` : (height || px), ...cssVars },
|
|
62
|
-
'aria-hidden': true,
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
case 'image': {
|
|
67
|
-
return React.createElement('div', {
|
|
68
|
-
className: buildClassName(animation, 'auto-skeleton-image-block'),
|
|
69
|
-
style: {
|
|
70
|
-
width: width || '100%',
|
|
71
|
-
...(height ? { paddingTop: 0, height } : {}),
|
|
72
|
-
...cssVars,
|
|
73
|
-
},
|
|
74
|
-
'aria-hidden': true,
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
case 'button': {
|
|
79
|
-
return React.createElement('div', {
|
|
80
|
-
className: buildClassName(animation, 'auto-skeleton-button-block'),
|
|
81
|
-
style: { width: width || 100, ...cssVars },
|
|
82
|
-
'aria-hidden': true,
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
case 'input': {
|
|
87
|
-
return React.createElement('div', {
|
|
88
|
-
className: 'auto-skeleton-input-block',
|
|
89
|
-
style: cssVars,
|
|
90
|
-
'aria-hidden': true,
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
default:
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export default SkeletonItem;
|
|
1
|
+
/**
|
|
2
|
+
* SkeletonItem.jsx
|
|
3
|
+
* Low-level primitive for manual skeleton building if needed.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* <SkeletonItem type="text" lines={2} />
|
|
7
|
+
* <SkeletonItem type="avatar" size={48} />
|
|
8
|
+
* <SkeletonItem type="image" width="100%" height={200} />
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import React, { useEffect } from 'react';
|
|
12
|
+
import { buildClassName, injectStyles } from './styles.js';
|
|
13
|
+
|
|
14
|
+
export function SkeletonItem({
|
|
15
|
+
type = 'text',
|
|
16
|
+
lines = 1,
|
|
17
|
+
width,
|
|
18
|
+
height,
|
|
19
|
+
size,
|
|
20
|
+
animation = 'shimmer',
|
|
21
|
+
theme = {},
|
|
22
|
+
style: extraStyle = {},
|
|
23
|
+
}) {
|
|
24
|
+
useEffect(() => { injectStyles(); }, []);
|
|
25
|
+
|
|
26
|
+
const cssVars = {
|
|
27
|
+
...(theme.baseColor && { '--skeleton-base-color': theme.baseColor }),
|
|
28
|
+
...(theme.highlightColor && { '--skeleton-highlight-color': theme.highlightColor }),
|
|
29
|
+
...extraStyle,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
switch (type) {
|
|
33
|
+
case 'text': {
|
|
34
|
+
const cls = buildClassName(animation, 'auto-skeleton-text-line');
|
|
35
|
+
if (lines === 1) {
|
|
36
|
+
return React.createElement('div', {
|
|
37
|
+
className: cls,
|
|
38
|
+
style: { width: width || '80%', ...cssVars },
|
|
39
|
+
'aria-hidden': true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const lineEls = Array.from({ length: lines }, (_, i) =>
|
|
43
|
+
React.createElement('div', {
|
|
44
|
+
key: i,
|
|
45
|
+
className: cls,
|
|
46
|
+
style: { width: i === lines - 1 ? '65%' : '100%', ...cssVars },
|
|
47
|
+
'aria-hidden': true,
|
|
48
|
+
})
|
|
49
|
+
);
|
|
50
|
+
return React.createElement('div', {
|
|
51
|
+
className: 'auto-skeleton-text-block',
|
|
52
|
+
style: cssVars,
|
|
53
|
+
'aria-hidden': true,
|
|
54
|
+
}, ...lineEls);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
case 'avatar': {
|
|
58
|
+
const px = size ? `${size}px` : (width || '40px');
|
|
59
|
+
return React.createElement('div', {
|
|
60
|
+
className: buildClassName(animation, 'auto-skeleton-avatar-block'),
|
|
61
|
+
style: { width: px, height: size ? `${size}px` : (height || px), ...cssVars },
|
|
62
|
+
'aria-hidden': true,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
case 'image': {
|
|
67
|
+
return React.createElement('div', {
|
|
68
|
+
className: buildClassName(animation, 'auto-skeleton-image-block'),
|
|
69
|
+
style: {
|
|
70
|
+
width: width || '100%',
|
|
71
|
+
...(height ? { paddingTop: 0, height } : {}),
|
|
72
|
+
...cssVars,
|
|
73
|
+
},
|
|
74
|
+
'aria-hidden': true,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'button': {
|
|
79
|
+
return React.createElement('div', {
|
|
80
|
+
className: buildClassName(animation, 'auto-skeleton-button-block'),
|
|
81
|
+
style: { width: width || 100, ...cssVars },
|
|
82
|
+
'aria-hidden': true,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
case 'input': {
|
|
87
|
+
return React.createElement('div', {
|
|
88
|
+
className: 'auto-skeleton-input-block',
|
|
89
|
+
style: cssVars,
|
|
90
|
+
'aria-hidden': true,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
default:
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default SkeletonItem;
|
package/src/analyzer.js
CHANGED
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
const ELEMENT_TYPES = {
|
|
2
|
-
TEXT: 'text',
|
|
3
|
-
IMAGE: 'image',
|
|
4
|
-
AVATAR: 'avatar',
|
|
5
|
-
BUTTON: 'button',
|
|
6
|
-
INPUT: 'input',
|
|
7
|
-
CONTAINER: 'container',
|
|
8
|
-
ICON: 'icon',
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
function inferType(element) {
|
|
12
|
-
if (!element || typeof element !== 'object') return ELEMENT_TYPES.TEXT;
|
|
13
|
-
var type = element.type;
|
|
14
|
-
var props = element.props || {};
|
|
15
|
-
var tag = typeof type === 'string' ? type.toLowerCase() : '';
|
|
16
|
-
var cls = (props.className || '').toLowerCase();
|
|
17
|
-
var alt = (props.alt || '').toLowerCase();
|
|
18
|
-
var role = (props.role || '').toLowerCase();
|
|
19
|
-
|
|
20
|
-
if (tag === 'img') {
|
|
21
|
-
var isAvatar = cls.includes('avatar') || cls.includes('profile') || alt.includes('avatar');
|
|
22
|
-
return isAvatar ? ELEMENT_TYPES.AVATAR : ELEMENT_TYPES.IMAGE;
|
|
23
|
-
}
|
|
24
|
-
if (tag === 'button' || role === 'button') return ELEMENT_TYPES.BUTTON;
|
|
25
|
-
if (tag === 'input' || tag === 'textarea' || tag === 'select') return ELEMENT_TYPES.INPUT;
|
|
26
|
-
if (tag === 'svg' || cls.includes('icon')) return ELEMENT_TYPES.ICON;
|
|
27
|
-
if (cls.includes('avatar') || cls.includes('profile-pic')) return ELEMENT_TYPES.AVATAR;
|
|
28
|
-
return ELEMENT_TYPES.CONTAINER;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function isTextLeaf(element) {
|
|
32
|
-
if (!element || typeof element !== 'object') return false;
|
|
33
|
-
var children = element.props && element.props.children;
|
|
34
|
-
if (typeof children === 'string') return true;
|
|
35
|
-
if (typeof children === 'number') return true;
|
|
36
|
-
if (Array.isArray(children)) {
|
|
37
|
-
return children.every(function(c) { return typeof c === 'string' || typeof c === 'number'; });
|
|
38
|
-
}
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function extractStyleHints(element) {
|
|
43
|
-
var props = (element && element.props) || {};
|
|
44
|
-
var style = props.style || {};
|
|
45
|
-
return {
|
|
46
|
-
width: style.width,
|
|
47
|
-
height: style.height,
|
|
48
|
-
borderRadius: style.borderRadius,
|
|
49
|
-
display: style.display,
|
|
50
|
-
flexDirection: style.flexDirection,
|
|
51
|
-
alignItems: style.alignItems,
|
|
52
|
-
gap: style.gap,
|
|
53
|
-
padding: style.padding,
|
|
54
|
-
margin: style.margin,
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function analyzeElement(element, depth) {
|
|
59
|
-
depth = depth || 0;
|
|
60
|
-
if (element === null || element === undefined) return null;
|
|
61
|
-
if (typeof element === 'string' || typeof element === 'number') {
|
|
62
|
-
return { nodeType: ELEMENT_TYPES.TEXT, content: String(element), depth: depth };
|
|
63
|
-
}
|
|
64
|
-
if (typeof element === 'boolean') return null;
|
|
65
|
-
|
|
66
|
-
if (Array.isArray(element)) {
|
|
67
|
-
return element.map(function(el) { return analyzeElement(el, depth); }).filter(Boolean);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
var type = element.type;
|
|
71
|
-
var props = element.props || {};
|
|
72
|
-
var styleHints = extractStyleHints(element);
|
|
73
|
-
|
|
74
|
-
// Structural elements take priority over text-leaf detection
|
|
75
|
-
var earlyType = inferType(element);
|
|
76
|
-
if (earlyType === ELEMENT_TYPES.BUTTON || earlyType === ELEMENT_TYPES.INPUT) {
|
|
77
|
-
var lbl = earlyType === ELEMENT_TYPES.BUTTON && typeof props.children === 'string' ? props.children : '';
|
|
78
|
-
return { nodeType: earlyType, label: lbl, styleHints: styleHints, depth: depth };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (isTextLeaf(element)) {
|
|
82
|
-
var tag = typeof type === 'string' ? type.toLowerCase() : '';
|
|
83
|
-
var isHeading = /^h[1-6]$/.test(tag);
|
|
84
|
-
return { nodeType: ELEMENT_TYPES.TEXT, isHeading: isHeading, tag: tag, content: String(props.children), styleHints: styleHints, depth: depth };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
var nodeType = inferType(element);
|
|
88
|
-
|
|
89
|
-
if (nodeType === ELEMENT_TYPES.IMAGE || nodeType === ELEMENT_TYPES.AVATAR || nodeType === ELEMENT_TYPES.ICON) {
|
|
90
|
-
return { nodeType: nodeType, styleHints: styleHints, depth: depth };
|
|
91
|
-
}
|
|
92
|
-
if (nodeType === ELEMENT_TYPES.BUTTON) {
|
|
93
|
-
return { nodeType: ELEMENT_TYPES.BUTTON, label: typeof props.children === 'string' ? props.children : '', styleHints: styleHints, depth: depth };
|
|
94
|
-
}
|
|
95
|
-
if (nodeType === ELEMENT_TYPES.INPUT) {
|
|
96
|
-
return { nodeType: ELEMENT_TYPES.INPUT, styleHints: styleHints, depth: depth };
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
var rawChildren = props.children;
|
|
100
|
-
var children = [];
|
|
101
|
-
if (rawChildren) {
|
|
102
|
-
var arr = Array.isArray(rawChildren) ? rawChildren : [rawChildren];
|
|
103
|
-
arr.forEach(function(child) {
|
|
104
|
-
var result = analyzeElement(child, depth + 1);
|
|
105
|
-
if (Array.isArray(result)) { result.forEach(function(r) { if (r) children.push(r); }); }
|
|
106
|
-
else if (result) children.push(result);
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return { nodeType: ELEMENT_TYPES.CONTAINER, tag: typeof type === 'string' ? type : 'div', styleHints: styleHints, children: children, depth: depth };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
module.exports = { analyzeElement: analyzeElement, ELEMENT_TYPES: ELEMENT_TYPES };
|
|
1
|
+
const ELEMENT_TYPES = {
|
|
2
|
+
TEXT: 'text',
|
|
3
|
+
IMAGE: 'image',
|
|
4
|
+
AVATAR: 'avatar',
|
|
5
|
+
BUTTON: 'button',
|
|
6
|
+
INPUT: 'input',
|
|
7
|
+
CONTAINER: 'container',
|
|
8
|
+
ICON: 'icon',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function inferType(element) {
|
|
12
|
+
if (!element || typeof element !== 'object') return ELEMENT_TYPES.TEXT;
|
|
13
|
+
var type = element.type;
|
|
14
|
+
var props = element.props || {};
|
|
15
|
+
var tag = typeof type === 'string' ? type.toLowerCase() : '';
|
|
16
|
+
var cls = (props.className || '').toLowerCase();
|
|
17
|
+
var alt = (props.alt || '').toLowerCase();
|
|
18
|
+
var role = (props.role || '').toLowerCase();
|
|
19
|
+
|
|
20
|
+
if (tag === 'img') {
|
|
21
|
+
var isAvatar = cls.includes('avatar') || cls.includes('profile') || alt.includes('avatar');
|
|
22
|
+
return isAvatar ? ELEMENT_TYPES.AVATAR : ELEMENT_TYPES.IMAGE;
|
|
23
|
+
}
|
|
24
|
+
if (tag === 'button' || role === 'button') return ELEMENT_TYPES.BUTTON;
|
|
25
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') return ELEMENT_TYPES.INPUT;
|
|
26
|
+
if (tag === 'svg' || cls.includes('icon')) return ELEMENT_TYPES.ICON;
|
|
27
|
+
if (cls.includes('avatar') || cls.includes('profile-pic')) return ELEMENT_TYPES.AVATAR;
|
|
28
|
+
return ELEMENT_TYPES.CONTAINER;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function isTextLeaf(element) {
|
|
32
|
+
if (!element || typeof element !== 'object') return false;
|
|
33
|
+
var children = element.props && element.props.children;
|
|
34
|
+
if (typeof children === 'string') return true;
|
|
35
|
+
if (typeof children === 'number') return true;
|
|
36
|
+
if (Array.isArray(children)) {
|
|
37
|
+
return children.every(function(c) { return typeof c === 'string' || typeof c === 'number'; });
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractStyleHints(element) {
|
|
43
|
+
var props = (element && element.props) || {};
|
|
44
|
+
var style = props.style || {};
|
|
45
|
+
return {
|
|
46
|
+
width: style.width,
|
|
47
|
+
height: style.height,
|
|
48
|
+
borderRadius: style.borderRadius,
|
|
49
|
+
display: style.display,
|
|
50
|
+
flexDirection: style.flexDirection,
|
|
51
|
+
alignItems: style.alignItems,
|
|
52
|
+
gap: style.gap,
|
|
53
|
+
padding: style.padding,
|
|
54
|
+
margin: style.margin,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function analyzeElement(element, depth) {
|
|
59
|
+
depth = depth || 0;
|
|
60
|
+
if (element === null || element === undefined) return null;
|
|
61
|
+
if (typeof element === 'string' || typeof element === 'number') {
|
|
62
|
+
return { nodeType: ELEMENT_TYPES.TEXT, content: String(element), depth: depth };
|
|
63
|
+
}
|
|
64
|
+
if (typeof element === 'boolean') return null;
|
|
65
|
+
|
|
66
|
+
if (Array.isArray(element)) {
|
|
67
|
+
return element.map(function(el) { return analyzeElement(el, depth); }).filter(Boolean);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
var type = element.type;
|
|
71
|
+
var props = element.props || {};
|
|
72
|
+
var styleHints = extractStyleHints(element);
|
|
73
|
+
|
|
74
|
+
// Structural elements take priority over text-leaf detection
|
|
75
|
+
var earlyType = inferType(element);
|
|
76
|
+
if (earlyType === ELEMENT_TYPES.BUTTON || earlyType === ELEMENT_TYPES.INPUT) {
|
|
77
|
+
var lbl = earlyType === ELEMENT_TYPES.BUTTON && typeof props.children === 'string' ? props.children : '';
|
|
78
|
+
return { nodeType: earlyType, label: lbl, styleHints: styleHints, depth: depth };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (isTextLeaf(element)) {
|
|
82
|
+
var tag = typeof type === 'string' ? type.toLowerCase() : '';
|
|
83
|
+
var isHeading = /^h[1-6]$/.test(tag);
|
|
84
|
+
return { nodeType: ELEMENT_TYPES.TEXT, isHeading: isHeading, tag: tag, content: String(props.children), styleHints: styleHints, depth: depth };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
var nodeType = inferType(element);
|
|
88
|
+
|
|
89
|
+
if (nodeType === ELEMENT_TYPES.IMAGE || nodeType === ELEMENT_TYPES.AVATAR || nodeType === ELEMENT_TYPES.ICON) {
|
|
90
|
+
return { nodeType: nodeType, styleHints: styleHints, depth: depth };
|
|
91
|
+
}
|
|
92
|
+
if (nodeType === ELEMENT_TYPES.BUTTON) {
|
|
93
|
+
return { nodeType: ELEMENT_TYPES.BUTTON, label: typeof props.children === 'string' ? props.children : '', styleHints: styleHints, depth: depth };
|
|
94
|
+
}
|
|
95
|
+
if (nodeType === ELEMENT_TYPES.INPUT) {
|
|
96
|
+
return { nodeType: ELEMENT_TYPES.INPUT, styleHints: styleHints, depth: depth };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
var rawChildren = props.children;
|
|
100
|
+
var children = [];
|
|
101
|
+
if (rawChildren) {
|
|
102
|
+
var arr = Array.isArray(rawChildren) ? rawChildren : [rawChildren];
|
|
103
|
+
arr.forEach(function(child) {
|
|
104
|
+
var result = analyzeElement(child, depth + 1);
|
|
105
|
+
if (Array.isArray(result)) { result.forEach(function(r) { if (r) children.push(r); }); }
|
|
106
|
+
else if (result) children.push(result);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { nodeType: ELEMENT_TYPES.CONTAINER, tag: typeof type === 'string' ? type : 'div', styleHints: styleHints, children: children, depth: depth };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = { analyzeElement: analyzeElement, ELEMENT_TYPES: ELEMENT_TYPES };
|