@elementor/editor-audits 4.3.0-1036

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.
Files changed (87) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/dist/index.d.mts +122 -0
  3. package/dist/index.d.ts +122 -0
  4. package/dist/index.js +2822 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.mjs +2801 -0
  7. package/dist/index.mjs.map +1 -0
  8. package/package.json +65 -0
  9. package/src/api/page-context-client.ts +19 -0
  10. package/src/audits/accessibility-policy.ts +32 -0
  11. package/src/audits/cookie-policy.ts +32 -0
  12. package/src/audits/deep-nesting.ts +45 -0
  13. package/src/audits/default-design-system.ts +32 -0
  14. package/src/audits/deprecated-widgets.ts +57 -0
  15. package/src/audits/heading-structure.ts +294 -0
  16. package/src/audits/hidden-elements.ts +41 -0
  17. package/src/audits/images-alt-text.ts +47 -0
  18. package/src/audits/images-too-large.ts +61 -0
  19. package/src/audits/nested-boxed-containers.ts +44 -0
  20. package/src/audits/page-excerpt.ts +30 -0
  21. package/src/audits/page-featured-image.ts +29 -0
  22. package/src/audits/page-title.ts +48 -0
  23. package/src/audits/prefer-global-colors.ts +52 -0
  24. package/src/audits/prefer-global-fonts.ts +50 -0
  25. package/src/audits/privacy-policy.ts +35 -0
  26. package/src/audits/robots-noindex.ts +35 -0
  27. package/src/audits/sections-and-columns.ts +39 -0
  28. package/src/audits/site-identity.ts +62 -0
  29. package/src/audits/too-many-widgets.ts +43 -0
  30. package/src/components/audit-feedback.tsx +178 -0
  31. package/src/components/audit-panel.tsx +55 -0
  32. package/src/components/category-icons.ts +17 -0
  33. package/src/components/count-summary-circle.tsx +66 -0
  34. package/src/components/fix-violation-with-angie.tsx +50 -0
  35. package/src/components/issues-category-row.tsx +62 -0
  36. package/src/components/pages/all-audits-page.tsx +65 -0
  37. package/src/components/pages/category-page.tsx +50 -0
  38. package/src/components/pages/error-page.tsx +21 -0
  39. package/src/components/pages/issues-page.tsx +48 -0
  40. package/src/components/pages/loading-page.tsx +12 -0
  41. package/src/components/pages/overview-page.tsx +140 -0
  42. package/src/components/pages/welcome-page.tsx +48 -0
  43. package/src/components/promotion-card.tsx +43 -0
  44. package/src/components/promotions.tsx +69 -0
  45. package/src/components/report-shell.tsx +110 -0
  46. package/src/components/score-bar.tsx +56 -0
  47. package/src/components/score-circle.tsx +60 -0
  48. package/src/components/severity-icons.tsx +26 -0
  49. package/src/components/status-section.tsx +47 -0
  50. package/src/components/subpage-header.tsx +29 -0
  51. package/src/components/violation-icons.tsx +33 -0
  52. package/src/components/violation-row.tsx +169 -0
  53. package/src/constants.ts +35 -0
  54. package/src/editor-app-bar.tsx +13 -0
  55. package/src/editor-panel.tsx +19 -0
  56. package/src/hooks/focus-violation.ts +53 -0
  57. package/src/hooks/use-audit-report.ts +43 -0
  58. package/src/hooks/use-audit-toggle-props.ts +17 -0
  59. package/src/index.ts +14 -0
  60. package/src/init.ts +14 -0
  61. package/src/register-audits.ts +56 -0
  62. package/src/register-promotions.ts +75 -0
  63. package/src/registry.ts +19 -0
  64. package/src/runner.ts +44 -0
  65. package/src/store/index.ts +2 -0
  66. package/src/store/selectors.ts +15 -0
  67. package/src/store/slice.ts +42 -0
  68. package/src/types.ts +112 -0
  69. package/src/utils/audit-status-summary.ts +112 -0
  70. package/src/utils/build-angie-prompt.ts +9 -0
  71. package/src/utils/collect-hardcoded-colors.ts +74 -0
  72. package/src/utils/collect-hardcoded-fonts.ts +70 -0
  73. package/src/utils/compute-report.ts +51 -0
  74. package/src/utils/find-audit-run.ts +5 -0
  75. package/src/utils/image-alt.ts +19 -0
  76. package/src/utils/image-like-sources.ts +70 -0
  77. package/src/utils/keyboard-click.ts +10 -0
  78. package/src/utils/match-global-by-value.ts +5 -0
  79. package/src/utils/page-attachments.ts +14 -0
  80. package/src/utils/read-kit-snapshot.ts +149 -0
  81. package/src/utils/report-storage.ts +16 -0
  82. package/src/utils/score-thresholds.ts +23 -0
  83. package/src/utils/severity-counts.ts +59 -0
  84. package/src/utils/sort-failed-audits.ts +19 -0
  85. package/src/utils/v1-snapshot.ts +27 -0
  86. package/src/utils/walk.ts +17 -0
  87. package/src/utils/window-config.ts +20 -0
@@ -0,0 +1,70 @@
1
+ const RESERVED_SETTING_KEYS = new Set( [ '__globals__', '__dynamic__' ] );
2
+ const REPEATER_ROW_ID_KEY = '_id';
3
+ const FONT_FAMILY_KEY_PATTERN = /font_family/;
4
+
5
+ export type HardcodedFont = {
6
+ path: string;
7
+ value: string;
8
+ };
9
+
10
+ function isGlobalFontReference( value: string ): boolean {
11
+ return value.startsWith( 'globals/' );
12
+ }
13
+
14
+ function isHardcodedFontCandidate( key: string, value: string ): boolean {
15
+ return FONT_FAMILY_KEY_PATTERN.test( key ) && value.trim().length > 0;
16
+ }
17
+
18
+ function formatPath( prefix: string, key: string ): string {
19
+ return prefix ? `${ prefix }.${ key }` : key;
20
+ }
21
+
22
+ function formatArrayPath( prefix: string, index: number ): string {
23
+ return `${ prefix }[${ index }]`;
24
+ }
25
+
26
+ export function collectHardcodedFonts( settings: Record< string, unknown >, pathPrefix = '' ): HardcodedFont[] {
27
+ const linkedGlobals = ( settings.__globals__ ?? {} ) as Record< string, string >;
28
+ const results: HardcodedFont[] = [];
29
+
30
+ for ( const [ key, value ] of Object.entries( settings ) ) {
31
+ if ( RESERVED_SETTING_KEYS.has( key ) || key === REPEATER_ROW_ID_KEY ) {
32
+ continue;
33
+ }
34
+
35
+ if ( linkedGlobals[ key ] ) {
36
+ continue;
37
+ }
38
+
39
+ if ( typeof value === 'string' ) {
40
+ if ( ! isGlobalFontReference( value ) && isHardcodedFontCandidate( key, value ) ) {
41
+ results.push( { path: formatPath( pathPrefix, key ), value } );
42
+ }
43
+ continue;
44
+ }
45
+
46
+ if ( Array.isArray( value ) ) {
47
+ const arrayPath = formatPath( pathPrefix, key );
48
+
49
+ value.forEach( ( item, index ) => {
50
+ if ( item && typeof item === 'object' && ! Array.isArray( item ) ) {
51
+ results.push(
52
+ ...collectHardcodedFonts(
53
+ item as Record< string, unknown >,
54
+ formatArrayPath( arrayPath, index )
55
+ )
56
+ );
57
+ }
58
+ } );
59
+ continue;
60
+ }
61
+
62
+ if ( value && typeof value === 'object' ) {
63
+ results.push(
64
+ ...collectHardcodedFonts( value as Record< string, unknown >, formatPath( pathPrefix, key ) )
65
+ );
66
+ }
67
+ }
68
+
69
+ return results;
70
+ }
@@ -0,0 +1,51 @@
1
+ import { ALL_CATEGORIES } from '../constants';
2
+ import { type AuditRun, type PageAuditReport } from '../types';
3
+ import { getPopulatedCategories } from './audit-status-summary';
4
+
5
+ type CategoryAccumulator = { totalWeight: number; passedWeight: number; total: number; failed: number };
6
+
7
+ export function computeReport( documentId: number, results: AuditRun[] ): PageAuditReport {
8
+ const accumulators = Object.fromEntries(
9
+ ALL_CATEGORIES.map( ( c ) => [ c, { totalWeight: 0, passedWeight: 0, total: 0, failed: 0 } ] )
10
+ ) as Record< string, CategoryAccumulator >;
11
+
12
+ for ( const { audit, result } of results ) {
13
+ if ( result.status === 'skipped' ) {
14
+ continue;
15
+ }
16
+
17
+ for ( const category of audit.categories ) {
18
+ const acc = accumulators[ category ];
19
+ acc.total++;
20
+ acc.totalWeight += audit.weight;
21
+
22
+ if ( result.status === 'pass' ) {
23
+ acc.passedWeight += audit.weight;
24
+ } else {
25
+ acc.failed++;
26
+ }
27
+ }
28
+ }
29
+
30
+ const categories = Object.fromEntries(
31
+ ALL_CATEGORIES.map( ( c ) => {
32
+ const acc = accumulators[ c ];
33
+ const score = acc.totalWeight === 0 ? 0 : Math.round( ( acc.passedWeight / acc.totalWeight ) * 100 );
34
+ return [ c, { score, failed: acc.failed, total: acc.total } ];
35
+ } )
36
+ ) as PageAuditReport[ 'categories' ];
37
+
38
+ const populated = getPopulatedCategories( categories, ALL_CATEGORIES );
39
+ const overall =
40
+ populated.length === 0
41
+ ? 0
42
+ : Math.round( populated.reduce( ( sum, c ) => sum + categories[ c ].score, 0 ) / populated.length );
43
+
44
+ return {
45
+ documentId,
46
+ runAt: Date.now(),
47
+ overall,
48
+ categories,
49
+ auditResults: results,
50
+ };
51
+ }
@@ -0,0 +1,5 @@
1
+ import { type AuditRun, type PageAuditReport } from '../types';
2
+
3
+ export function findAuditRun( report: PageAuditReport, auditId: string ): AuditRun | undefined {
4
+ return report.auditResults.find( ( run ) => run.audit.id === auditId );
5
+ }
@@ -0,0 +1,19 @@
1
+ import { type PageContextResponse } from '../types';
2
+ import { type ImageLikeMedia } from './image-like-sources';
3
+
4
+ export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
5
+ if ( ! media.id && ! media.url ) {
6
+ return true;
7
+ }
8
+
9
+ if ( media.id ) {
10
+ const alt = pageContext.image_sizes[ media.id ]?.alt ?? '';
11
+ return alt.trim().length > 0;
12
+ }
13
+
14
+ return ( media.alt ?? '' ).trim().length > 0;
15
+ }
16
+
17
+ export function isImageSourcePresent( media: ImageLikeMedia ): boolean {
18
+ return Boolean( media.id || media.url );
19
+ }
@@ -0,0 +1,70 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { walkElements } from './walk';
3
+
4
+ export const IMAGE_LIKE_WIDGETS = new Set( [ 'image', 'image-box', 'image-carousel', 'gallery', 'image-gallery' ] );
5
+
6
+ export type ImageLikeMedia = {
7
+ id?: number;
8
+ url?: string;
9
+ alt?: string;
10
+ };
11
+
12
+ export type ImageLikeSourceVisit = {
13
+ node: ElementSnapshotNode;
14
+ media: ImageLikeMedia;
15
+ };
16
+
17
+ export function hasPageImages( tree: ElementSnapshotNode[] ): boolean {
18
+ let found = false;
19
+
20
+ walkImageLikeSources( tree, ( { media } ) => {
21
+ if ( media.id || media.url ) {
22
+ found = true;
23
+ }
24
+ } );
25
+
26
+ return found;
27
+ }
28
+
29
+ export function walkImageLikeSources(
30
+ tree: ElementSnapshotNode[],
31
+ visit: ( source: ImageLikeSourceVisit ) => void
32
+ ): void {
33
+ walkElements( tree, ( node ) => {
34
+ if ( node.elType !== 'widget' ) {
35
+ return;
36
+ }
37
+
38
+ if ( ! IMAGE_LIKE_WIDGETS.has( node.widgetType ?? '' ) ) {
39
+ return;
40
+ }
41
+
42
+ for ( const media of collectMediaFromNode( node ) ) {
43
+ visit( { node, media } );
44
+ }
45
+ } );
46
+ }
47
+
48
+ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
49
+ const sources: ImageLikeMedia[] = [];
50
+
51
+ const image = node.settings.image as ImageLikeMedia | undefined;
52
+
53
+ if ( image?.id || image?.url ) {
54
+ sources.push( image );
55
+ }
56
+
57
+ const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
58
+ | ImageLikeMedia[]
59
+ | undefined;
60
+
61
+ if ( Array.isArray( gallery ) ) {
62
+ for ( const item of gallery ) {
63
+ if ( item?.id || item?.url ) {
64
+ sources.push( item );
65
+ }
66
+ }
67
+ }
68
+
69
+ return sources;
70
+ }
@@ -0,0 +1,10 @@
1
+ import type * as React from 'react';
2
+
3
+ export function onKeyboardClick( callback: () => void ) {
4
+ return ( event: React.KeyboardEvent ) => {
5
+ if ( event.key === 'Enter' || event.key === ' ' ) {
6
+ event.preventDefault();
7
+ callback();
8
+ }
9
+ };
10
+ }
@@ -0,0 +1,5 @@
1
+ export function findMatchingGlobalByValue< T extends { value: string } >( value: string, globals: T[] ): T | null {
2
+ const trimmed = value.trim();
3
+
4
+ return globals.find( ( global ) => global.value === trimmed ) ?? null;
5
+ }
@@ -0,0 +1,14 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { walkImageLikeSources } from './image-like-sources';
3
+
4
+ export function extractAttachmentIds( tree: ElementSnapshotNode[] ): number[] {
5
+ const ids = new Set< number >();
6
+
7
+ walkImageLikeSources( tree, ( { media } ) => {
8
+ if ( media.id ) {
9
+ ids.add( media.id );
10
+ }
11
+ } );
12
+
13
+ return Array.from( ids ).sort( ( a, b ) => a - b );
14
+ }
@@ -0,0 +1,149 @@
1
+ import { type KitSnapshot } from '../types';
2
+
3
+ type GlobalColorApiItem = {
4
+ id: string;
5
+ title?: string;
6
+ value?: string;
7
+ };
8
+
9
+ type GlobalTypographyApiItem = {
10
+ id: string;
11
+ title?: string;
12
+ value?: {
13
+ typography_font_family?: string;
14
+ };
15
+ };
16
+
17
+ type KitColorRow = {
18
+ _id: string;
19
+ color: string;
20
+ title?: string;
21
+ };
22
+
23
+ type KitTypographyRow = {
24
+ _id: string;
25
+ title: string;
26
+ typography_font_family?: string;
27
+ };
28
+
29
+ type AuditWindow = Window & {
30
+ elementor?: {
31
+ documents?: {
32
+ get: ( id: number ) => { config?: { settings?: { settings?: KitDocumentSettings } } } | undefined;
33
+ };
34
+ };
35
+ $e?: {
36
+ data?: {
37
+ get: (
38
+ command: string
39
+ ) => Promise< { data?: Record< string, GlobalColorApiItem | GlobalTypographyApiItem > } >;
40
+ };
41
+ };
42
+ };
43
+
44
+ type KitDocumentSettings = {
45
+ system_colors?: KitColorRow[];
46
+ custom_colors?: KitColorRow[];
47
+ system_typography?: KitTypographyRow[];
48
+ custom_typography?: KitTypographyRow[];
49
+ };
50
+
51
+ type GlobalWithValue = { value: string };
52
+
53
+ function hasNonEmptyGlobalValue< T extends GlobalWithValue >( item: T ): boolean {
54
+ return item.value.length > 0;
55
+ }
56
+
57
+ export async function readKitSnapshot( kitId: number ): Promise< KitSnapshot > {
58
+ const [ colorsFromApi, fontsFromApi ] = await Promise.all( [
59
+ readGlobalsFromApi( 'globals/colors', mapApiColor, hasNonEmptyGlobalValue ),
60
+ readGlobalsFromApi( 'globals/typography', mapApiTypography, hasNonEmptyGlobalValue ),
61
+ ] );
62
+
63
+ const documentSettings =
64
+ colorsFromApi.length === 0 || fontsFromApi.length === 0 ? getKitDocumentSettings( kitId ) : null;
65
+
66
+ const colors = colorsFromApi.length > 0 ? colorsFromApi : readColorsFromKitDocumentSettings( documentSettings );
67
+ const fonts = fontsFromApi.length > 0 ? fontsFromApi : readFontsFromKitDocumentSettings( documentSettings );
68
+
69
+ return { id: kitId, globals: { colors, fonts } };
70
+ }
71
+
72
+ async function readGlobalsFromApi< TApiItem, TGlobal extends GlobalWithValue >(
73
+ command: string,
74
+ mapItem: ( item: TApiItem ) => TGlobal,
75
+ hasValue: ( item: TGlobal ) => boolean
76
+ ): Promise< TGlobal[] > {
77
+ const $e = ( window as AuditWindow ).$e;
78
+
79
+ if ( ! $e?.data?.get ) {
80
+ return [];
81
+ }
82
+
83
+ try {
84
+ const result = await $e.data.get( command );
85
+ const data = result?.data ?? {};
86
+
87
+ return Object.values( data )
88
+ .map( ( item ) => mapItem( item as TApiItem ) )
89
+ .filter( hasValue );
90
+ } catch {
91
+ return [];
92
+ }
93
+ }
94
+
95
+ function mapApiColor( item: GlobalColorApiItem ): KitSnapshot[ 'globals' ][ 'colors' ][ number ] {
96
+ return {
97
+ id: item.id,
98
+ value: item.value ?? '',
99
+ title: item.title ?? item.id,
100
+ };
101
+ }
102
+
103
+ function mapApiTypography( item: GlobalTypographyApiItem ): KitSnapshot[ 'globals' ][ 'fonts' ][ number ] {
104
+ return {
105
+ id: item.id,
106
+ value: item.value?.typography_font_family ?? '',
107
+ title: item.title ?? item.id,
108
+ };
109
+ }
110
+
111
+ function readColorsFromKitDocumentSettings(
112
+ settings: KitDocumentSettings | null
113
+ ): KitSnapshot[ 'globals' ][ 'colors' ] {
114
+ if ( ! settings ) {
115
+ return [];
116
+ }
117
+
118
+ const rows = [ ...( settings.system_colors ?? [] ), ...( settings.custom_colors ?? [] ) ];
119
+
120
+ return rows
121
+ .map( ( row ) => ( {
122
+ id: row._id,
123
+ value: row.color,
124
+ title: row.title ?? row._id,
125
+ } ) )
126
+ .filter( hasNonEmptyGlobalValue );
127
+ }
128
+
129
+ function readFontsFromKitDocumentSettings( settings: KitDocumentSettings | null ): KitSnapshot[ 'globals' ][ 'fonts' ] {
130
+ if ( ! settings ) {
131
+ return [];
132
+ }
133
+
134
+ const rows = [ ...( settings.system_typography ?? [] ), ...( settings.custom_typography ?? [] ) ];
135
+
136
+ return rows
137
+ .map( ( row ) => ( {
138
+ id: row._id,
139
+ value: row.typography_font_family ?? '',
140
+ title: row.title ?? row._id,
141
+ } ) )
142
+ .filter( hasNonEmptyGlobalValue );
143
+ }
144
+
145
+ function getKitDocumentSettings( kitId: number ): KitDocumentSettings | null {
146
+ const document = ( window as AuditWindow ).elementor?.documents?.get?.( kitId );
147
+
148
+ return document?.config?.settings?.settings ?? null;
149
+ }
@@ -0,0 +1,16 @@
1
+ import { getSessionStorageItem, setSessionStorageItem } from '@elementor/session';
2
+
3
+ import { REPORT_STORAGE_KEY_PREFIX } from '../constants';
4
+ import { type PageAuditReport } from '../types';
5
+
6
+ function getStorageKey( documentId: number ): string {
7
+ return `${ REPORT_STORAGE_KEY_PREFIX }/${ documentId }`;
8
+ }
9
+
10
+ export function getPersistedReport( documentId: number ): PageAuditReport | null {
11
+ return getSessionStorageItem< PageAuditReport >( getStorageKey( documentId ) ) ?? null;
12
+ }
13
+
14
+ export function persistReport( documentId: number, report: PageAuditReport ): void {
15
+ setSessionStorageItem( getStorageKey( documentId ), report );
16
+ }
@@ -0,0 +1,23 @@
1
+ import { __ } from '@wordpress/i18n';
2
+
3
+ export const GOOD_THRESHOLD = 90;
4
+ export const OK_THRESHOLD = 50;
5
+
6
+ export type ScoreColor = 'success' | 'warning' | 'error';
7
+
8
+ export type ScoreTier = {
9
+ color: ScoreColor;
10
+ label: string;
11
+ };
12
+
13
+ export function getScoreTier( score: number ): ScoreTier {
14
+ if ( score >= GOOD_THRESHOLD ) {
15
+ return { color: 'success', label: __( 'Good', 'elementor' ) };
16
+ }
17
+
18
+ if ( score >= OK_THRESHOLD ) {
19
+ return { color: 'warning', label: __( 'Needs work', 'elementor' ) };
20
+ }
21
+
22
+ return { color: 'error', label: __( 'At risk', 'elementor' ) };
23
+ }
@@ -0,0 +1,59 @@
1
+ import { __, sprintf } from '@wordpress/i18n';
2
+
3
+ import { type AuditCategory, type AuditSeverity, type PageAuditReport } from '../types';
4
+
5
+ export type SeverityCounts = Record< AuditSeverity, number >;
6
+
7
+ export const ALL_SEVERITIES = [ 'error', 'warning', 'info' ] as const;
8
+
9
+ export function countSeverities( report: PageAuditReport, category?: AuditCategory ): SeverityCounts {
10
+ const counts: SeverityCounts = { error: 0, warning: 0, info: 0 };
11
+
12
+ for ( const { audit, result } of report.auditResults ) {
13
+ if ( result.status !== 'fail' ) {
14
+ continue;
15
+ }
16
+
17
+ if ( category && ! audit.categories.includes( category ) ) {
18
+ continue;
19
+ }
20
+
21
+ counts[ audit.severity ] += result.violations.length;
22
+ }
23
+
24
+ return counts;
25
+ }
26
+
27
+ export function severityPluralLabel( severity: AuditSeverity ): string {
28
+ switch ( severity ) {
29
+ case 'error':
30
+ return __( 'Errors', 'elementor' );
31
+ case 'warning':
32
+ return __( 'Warnings', 'elementor' );
33
+ case 'info':
34
+ return __( 'Info', 'elementor' );
35
+ }
36
+ }
37
+
38
+ export function severityRemainingCountLabel( severity: AuditSeverity, count: number ): string {
39
+ switch ( severity ) {
40
+ case 'error':
41
+ return sprintf(
42
+ /* translators: %d: number of remaining error violations. */
43
+ __( '%d errors', 'elementor' ),
44
+ count
45
+ );
46
+ case 'warning':
47
+ return sprintf(
48
+ /* translators: %d: number of remaining warning violations. */
49
+ __( '%d warnings', 'elementor' ),
50
+ count
51
+ );
52
+ case 'info':
53
+ return sprintf(
54
+ /* translators: %d: number of remaining info violations. */
55
+ __( '%d info', 'elementor' ),
56
+ count
57
+ );
58
+ }
59
+ }
@@ -0,0 +1,19 @@
1
+ import { type AuditMeta, type AuditSeverity } from '../types';
2
+
3
+ const SEVERITY_SORTING_RANK: Record< AuditSeverity, number > = {
4
+ error: 0,
5
+ warning: 1,
6
+ info: 2,
7
+ };
8
+
9
+ export function sortFailedAuditResults< T extends { audit: AuditMeta } >( results: T[] ): T[] {
10
+ return [ ...results ].sort( ( a, b ) => {
11
+ const rankDiff = SEVERITY_SORTING_RANK[ a.audit.severity ] - SEVERITY_SORTING_RANK[ b.audit.severity ];
12
+
13
+ if ( rankDiff !== 0 ) {
14
+ return rankDiff;
15
+ }
16
+
17
+ return a.audit.title.localeCompare( b.audit.title );
18
+ } );
19
+ }
@@ -0,0 +1,27 @@
1
+ import { type V1Element } from '@elementor/editor-elements';
2
+
3
+ import { type ElementSnapshotNode } from '../types';
4
+
5
+ export function buildSnapshotTree( elements: V1Element[] ): ElementSnapshotNode[] {
6
+ if ( elements.length === 0 ) {
7
+ return [];
8
+ }
9
+
10
+ const root = elements[ 0 ];
11
+ const children = root.children ?? [];
12
+
13
+ return children.map( toSnapshot );
14
+ }
15
+
16
+ function toSnapshot( element: V1Element ): ElementSnapshotNode {
17
+ const model = element.model;
18
+ const settings = element.settings?.toJSON?.() ?? {};
19
+
20
+ return {
21
+ id: element.id,
22
+ elType: ( model.get( 'elType' ) as string ) ?? '',
23
+ widgetType: model.get( 'widgetType' ) as string | undefined,
24
+ settings: settings as Record< string, unknown >,
25
+ elements: ( element.children ?? [] ).map( toSnapshot ),
26
+ };
27
+ }
@@ -0,0 +1,17 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+
3
+ export type WalkVisitor = ( node: ElementSnapshotNode, parents: readonly ElementSnapshotNode[] ) => void;
4
+
5
+ export function walkElements( tree: ElementSnapshotNode[], visit: WalkVisitor ): void {
6
+ const parents: ElementSnapshotNode[] = [];
7
+ recurse( tree, parents, visit );
8
+ }
9
+
10
+ function recurse( nodes: ElementSnapshotNode[], parents: ElementSnapshotNode[], visit: WalkVisitor ): void {
11
+ for ( const node of nodes ) {
12
+ visit( node, parents );
13
+ parents.push( node );
14
+ recurse( node.elements, parents, visit );
15
+ parents.pop();
16
+ }
17
+ }
@@ -0,0 +1,20 @@
1
+ type WindowConfig = {
2
+ restNamespace: string;
3
+ nonce: string;
4
+ };
5
+
6
+ declare global {
7
+ interface Window {
8
+ elementorAudits?: WindowConfig;
9
+ }
10
+ }
11
+
12
+ export function getWindowConfig(): WindowConfig {
13
+ const config = window.elementorAudits;
14
+
15
+ if ( ! config ) {
16
+ return { restNamespace: 'elementor/v1', nonce: '' };
17
+ }
18
+
19
+ return config;
20
+ }