@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,43 @@
1
+ import { useEffect } from 'react';
2
+ import { getCurrentDocumentId } from '@elementor/editor-elements';
3
+ import { __useDispatch as useDispatch, __useSelector as useSelector } from '@elementor/store';
4
+
5
+ import { runPageAudit } from '../runner';
6
+ import { type GlobalState, selectError, selectReport, selectStatus, slice } from '../store';
7
+ import { getPersistedReport, persistReport } from '../utils/report-storage';
8
+
9
+ export function useAuditReport() {
10
+ const status = useSelector( ( state: GlobalState ) => selectStatus( state ) );
11
+ const report = useSelector( ( state: GlobalState ) => selectReport( state ) );
12
+ const error = useSelector( ( state: GlobalState ) => selectError( state ) );
13
+ const dispatch = useDispatch();
14
+ const documentId = getCurrentDocumentId() ?? 0;
15
+
16
+ useEffect( () => {
17
+ if ( ! documentId || report?.documentId === documentId ) {
18
+ return;
19
+ }
20
+
21
+ const persisted = getPersistedReport( documentId );
22
+
23
+ if ( persisted ) {
24
+ dispatch( slice.actions.reportRestored( persisted ) );
25
+ } else if ( report ) {
26
+ dispatch( slice.actions.reportCleared() );
27
+ }
28
+ }, [ documentId, report, dispatch ] );
29
+
30
+ const run = async ( documentIdToRun: number ) => {
31
+ dispatch( slice.actions.runStarted() );
32
+
33
+ try {
34
+ const nextReport = await runPageAudit( documentIdToRun );
35
+ dispatch( slice.actions.runSucceeded( nextReport ) );
36
+ persistReport( documentIdToRun, nextReport );
37
+ } catch ( e ) {
38
+ dispatch( slice.actions.runFailed( e instanceof Error ? e.message : 'Unknown error' ) );
39
+ }
40
+ };
41
+
42
+ return { status, report, error, run };
43
+ }
@@ -0,0 +1,17 @@
1
+ import { type ToggleActionProps } from '@elementor/editor-app-bar';
2
+ import { ShieldCheckIcon } from '@elementor/icons';
3
+ import { __ } from '@wordpress/i18n';
4
+
5
+ import { auditPanel } from '../editor-panel';
6
+
7
+ export function useAuditToggleProps(): ToggleActionProps {
8
+ const { isOpen } = auditPanel.useFloatingPanelStatus();
9
+ const { toggle } = auditPanel.useFloatingPanelActions();
10
+
11
+ return {
12
+ title: __( 'Audit Page', 'elementor' ),
13
+ icon: ShieldCheckIcon,
14
+ selected: isOpen,
15
+ onClick: () => toggle(),
16
+ };
17
+ }
package/src/index.ts ADDED
@@ -0,0 +1,14 @@
1
+ export { init } from './init';
2
+ export { registerAudit } from './registry';
3
+ export { runPageAudit } from './runner';
4
+ export type {
5
+ Audit,
6
+ AuditCategory,
7
+ AuditContext,
8
+ AuditMeta,
9
+ AuditResult,
10
+ AuditRun,
11
+ AuditSeverity,
12
+ AuditViolation,
13
+ PageAuditReport,
14
+ } from './types';
package/src/init.ts ADDED
@@ -0,0 +1,14 @@
1
+ import { registerFloatingPanel } from '@elementor/editor-floating-panels';
2
+ import { __registerSlice as registerSlice } from '@elementor/store';
3
+
4
+ import { registerAppBarAuditsToggle } from './editor-app-bar';
5
+ import { auditPanel } from './editor-panel';
6
+ import { registerAllAudits } from './register-audits';
7
+ import { slice } from './store/slice';
8
+
9
+ export function init(): void {
10
+ registerSlice( slice );
11
+ registerAllAudits();
12
+ registerFloatingPanel( auditPanel.panel );
13
+ registerAppBarAuditsToggle();
14
+ }
@@ -0,0 +1,56 @@
1
+ import * as accessibilityPolicy from './audits/accessibility-policy';
2
+ import * as cookiePolicy from './audits/cookie-policy';
3
+ import * as deepNesting from './audits/deep-nesting';
4
+ import * as defaultDesignSystem from './audits/default-design-system';
5
+ import * as deprecatedWidgets from './audits/deprecated-widgets';
6
+ import * as headingStructure from './audits/heading-structure';
7
+ import * as hiddenElements from './audits/hidden-elements';
8
+ import * as imagesAltText from './audits/images-alt-text';
9
+ import * as imagesTooLarge from './audits/images-too-large';
10
+ import * as nestedBoxedContainers from './audits/nested-boxed-containers';
11
+ import * as pageExcerpt from './audits/page-excerpt';
12
+ import * as pageFeaturedImage from './audits/page-featured-image';
13
+ import * as pageTitle from './audits/page-title';
14
+ import * as preferGlobalColors from './audits/prefer-global-colors';
15
+ import * as preferGlobalFonts from './audits/prefer-global-fonts';
16
+ import * as privacyPolicy from './audits/privacy-policy';
17
+ import * as robotsNoindex from './audits/robots-noindex';
18
+ import * as sectionsAndColumns from './audits/sections-and-columns';
19
+ import * as siteIdentity from './audits/site-identity';
20
+ import * as tooManyWidgets from './audits/too-many-widgets';
21
+ import { registerAudit } from './registry';
22
+ import { type Audit } from './types';
23
+
24
+ const AUDITS: Audit[] = [
25
+ pageTitle.audit,
26
+ pageExcerpt.audit,
27
+ pageFeaturedImage.audit,
28
+
29
+ hiddenElements.audit,
30
+ deprecatedWidgets.audit,
31
+ tooManyWidgets.audit,
32
+ sectionsAndColumns.audit,
33
+ deepNesting.audit,
34
+ nestedBoxedContainers.audit,
35
+
36
+ defaultDesignSystem.audit,
37
+ siteIdentity.audit,
38
+ preferGlobalColors.audit,
39
+ preferGlobalFonts.audit,
40
+
41
+ headingStructure.audit,
42
+ imagesAltText.audit,
43
+ imagesTooLarge.audit,
44
+
45
+ robotsNoindex.audit,
46
+
47
+ privacyPolicy.audit,
48
+ accessibilityPolicy.audit,
49
+ cookiePolicy.audit,
50
+ ];
51
+
52
+ export function registerAllAudits(): void {
53
+ for ( const audit of AUDITS ) {
54
+ registerAudit( audit );
55
+ }
56
+ }
@@ -0,0 +1,75 @@
1
+ import { ElementorCookieIcon, PhotoIcon, ShieldHalfFilledIcon } from '@elementor/icons';
2
+ import { __, _n, sprintf } from '@wordpress/i18n';
3
+
4
+ import { type AuditRun } from './types';
5
+
6
+ export type PromotionIcon = typeof PhotoIcon;
7
+
8
+ export type PromotionConfig = {
9
+ auditId: string;
10
+ icon: PromotionIcon;
11
+ ctaLabel: string;
12
+ formatSubtitle: ( run: AuditRun ) => string | null;
13
+ getCtaUrl: ( run: AuditRun ) => string | undefined;
14
+ };
15
+
16
+ function firstFailExternalUrl( run: AuditRun ): string | undefined {
17
+ return run.result.status === 'fail' ? run.result.violations[ 0 ]?.externalUrl : undefined;
18
+ }
19
+
20
+ export const PROMOTIONS: PromotionConfig[] = [
21
+ {
22
+ auditId: 'audits/images-alt-text',
23
+ icon: PhotoIcon,
24
+ ctaLabel: __( 'Fix with Ally', 'elementor' ),
25
+ formatSubtitle: ( run ) => {
26
+ if ( run.result.status !== 'fail' ) {
27
+ return null;
28
+ }
29
+
30
+ const count = run.result.metadata?.missingAltImageCount ?? 0;
31
+
32
+ if ( count === 0 ) {
33
+ return null;
34
+ }
35
+
36
+ return sprintf(
37
+ /* translators: %d: number of images missing alt text. */
38
+ _n( '%d image', '%d images', count, 'elementor' ),
39
+ count
40
+ );
41
+ },
42
+ getCtaUrl: firstFailExternalUrl,
43
+ },
44
+ {
45
+ auditId: 'audits/cookie-policy',
46
+ icon: ElementorCookieIcon,
47
+ ctaLabel: __( 'Fix with Cookiez', 'elementor' ),
48
+ formatSubtitle: ( run ) =>
49
+ run.result.status === 'fail' ? __( 'Generate cookie policy', 'elementor' ) : null,
50
+ getCtaUrl: firstFailExternalUrl,
51
+ },
52
+ {
53
+ auditId: 'audits/images-too-large',
54
+ icon: ShieldHalfFilledIcon,
55
+ ctaLabel: __( 'Optimize all', 'elementor' ),
56
+ formatSubtitle: ( run ) => {
57
+ if ( run.result.status !== 'fail' ) {
58
+ return null;
59
+ }
60
+
61
+ const count = run.result.metadata?.oversizedImageCount ?? 0;
62
+
63
+ if ( count === 0 ) {
64
+ return null;
65
+ }
66
+
67
+ return sprintf(
68
+ /* translators: %d: number of oversized images. */
69
+ _n( '%d image', '%d images', count, 'elementor' ),
70
+ count
71
+ );
72
+ },
73
+ getCtaUrl: firstFailExternalUrl,
74
+ },
75
+ ];
@@ -0,0 +1,19 @@
1
+ import { type Audit } from './types';
2
+
3
+ const registry = new Map< string, Audit >();
4
+
5
+ export function registerAudit( audit: Audit ): void {
6
+ registry.set( audit.id, audit );
7
+ }
8
+
9
+ export function getRegisteredAudits(): Audit[] {
10
+ return Array.from( registry.values() );
11
+ }
12
+
13
+ export function hasAudit( id: string ): boolean {
14
+ return registry.has( id );
15
+ }
16
+
17
+ export function clearAuditRegistry(): void {
18
+ registry.clear();
19
+ }
package/src/runner.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { getElements } from '@elementor/editor-elements';
2
+
3
+ import { fetchPageContext } from './api/page-context-client';
4
+ import { getRegisteredAudits } from './registry';
5
+ import {
6
+ type Audit,
7
+ type AuditContext,
8
+ type AuditMeta,
9
+ type AuditRun,
10
+ type ElementsModelSnapshot,
11
+ type KitSnapshot,
12
+ type PageAuditReport,
13
+ } from './types';
14
+ import { computeReport } from './utils/compute-report';
15
+ import { extractAttachmentIds } from './utils/page-attachments';
16
+ import { readKitSnapshot } from './utils/read-kit-snapshot';
17
+ import { buildSnapshotTree } from './utils/v1-snapshot';
18
+
19
+ export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
20
+ const tree = buildSnapshotTree( getElements() );
21
+ const attachmentIds = extractAttachmentIds( tree );
22
+ const pageContext = await fetchPageContext( documentId, attachmentIds );
23
+
24
+ const elements: ElementsModelSnapshot = { documentId, tree };
25
+ const kit: KitSnapshot = await readKitSnapshot( pageContext.kit_id );
26
+
27
+ const ctx: AuditContext = { documentId, elements, pageContext, kit };
28
+ const registered = getRegisteredAudits();
29
+
30
+ const auditResults: AuditRun[] = await Promise.all(
31
+ registered.map( async ( audit ) => {
32
+ const { evaluate: _evaluate, ...meta }: AuditMeta & Pick< Audit, 'evaluate' > = audit;
33
+ try {
34
+ const result = await audit.evaluate( ctx );
35
+ return { audit: meta, result };
36
+ } catch ( error ) {
37
+ const reason = error instanceof Error ? error.message : 'unknown-error';
38
+ return { audit: meta, result: { status: 'skipped' as const, reason } };
39
+ }
40
+ } )
41
+ );
42
+
43
+ return computeReport( documentId, auditResults );
44
+ }
@@ -0,0 +1,2 @@
1
+ export { slice, type AuditsSliceState } from './slice';
2
+ export * from './selectors';
@@ -0,0 +1,15 @@
1
+ import { type AuditsSliceState } from './slice';
2
+
3
+ export type GlobalState = { audits: AuditsSliceState };
4
+
5
+ export function selectStatus( state: GlobalState ) {
6
+ return state.audits.status;
7
+ }
8
+
9
+ export function selectReport( state: GlobalState ) {
10
+ return state.audits.report;
11
+ }
12
+
13
+ export function selectError( state: GlobalState ) {
14
+ return state.audits.error;
15
+ }
@@ -0,0 +1,42 @@
1
+ import { __createSlice, type PayloadAction } from '@elementor/store';
2
+
3
+ import { type PageAuditReport } from '../types';
4
+
5
+ type SliceState = {
6
+ status: 'idle' | 'loading' | 'error' | 'ready';
7
+ report: PageAuditReport | null;
8
+ error: string | null;
9
+ };
10
+
11
+ const initialState: SliceState = { status: 'idle', report: null, error: null };
12
+
13
+ export const slice = __createSlice( {
14
+ name: 'audits',
15
+ initialState,
16
+ reducers: {
17
+ runStarted( state ) {
18
+ state.status = 'loading';
19
+ state.error = null;
20
+ },
21
+ runSucceeded( state, action: PayloadAction< PageAuditReport > ) {
22
+ state.status = 'ready';
23
+ state.report = action.payload;
24
+ },
25
+ runFailed( state, action: PayloadAction< string > ) {
26
+ state.status = 'error';
27
+ state.error = action.payload;
28
+ },
29
+ reportRestored( state, action: PayloadAction< PageAuditReport > ) {
30
+ state.status = 'ready';
31
+ state.report = action.payload;
32
+ state.error = null;
33
+ },
34
+ reportCleared( state ) {
35
+ state.status = 'idle';
36
+ state.report = null;
37
+ state.error = null;
38
+ },
39
+ },
40
+ } );
41
+
42
+ export type AuditsSliceState = SliceState;
package/src/types.ts ADDED
@@ -0,0 +1,112 @@
1
+ export type Audit = {
2
+ id: string;
3
+ title: string;
4
+ description: string;
5
+ fixHint: string;
6
+ categories: AuditCategory[];
7
+ severity: AuditSeverity;
8
+ weight: number;
9
+ evaluate: ( ctx: AuditContext ) => AuditResult | Promise< AuditResult >;
10
+ };
11
+
12
+ export type AuditCategory = 'best-practices' | 'seo' | 'accessibility' | 'performance' | 'compliance';
13
+
14
+ export type AuditSeverity = 'error' | 'warning' | 'info';
15
+
16
+ export type AuditMeta = Omit< Audit, 'evaluate' >;
17
+
18
+ export type AuditRun = {
19
+ audit: AuditMeta;
20
+ result: AuditResult;
21
+ };
22
+
23
+ export type AuditFailMetadata = {
24
+ missingAltImageCount?: number;
25
+ oversizedImageCount?: number;
26
+ };
27
+
28
+ export type AuditResult =
29
+ | { status: 'pass' }
30
+ | { status: 'fail'; violations: AuditViolation[]; metadata?: AuditFailMetadata }
31
+ | { status: 'skipped'; reason: string };
32
+
33
+ export type AuditViolation = {
34
+ auditId: string;
35
+ elementId?: string;
36
+ targetHint?: 'page-settings' | 'site-settings' | 'site-identity-settings' | 'element-settings';
37
+ externalUrl?: string;
38
+ label: string;
39
+ detail?: string;
40
+ angieFix?: boolean;
41
+ };
42
+
43
+ export type PageContextResponse = {
44
+ post_title: string | null;
45
+ post_excerpt: string | null;
46
+ featured_image_id: number | null;
47
+ image_sizes: Record<
48
+ number,
49
+ {
50
+ width: number;
51
+ height: number;
52
+ filesize_bytes: number;
53
+ mime: string;
54
+ src: string;
55
+ alt: string;
56
+ }
57
+ >;
58
+ kit_id: number;
59
+ kit_is_default_unchanged: boolean;
60
+ is_noindex: boolean;
61
+ reading_settings_url: string;
62
+ privacy_policy_url: string | null;
63
+ privacy_settings_url: string;
64
+ ally_plugin_active: boolean;
65
+ ally_plugin_url: string;
66
+ cookiez_plugin_active: boolean;
67
+ cookiez_plugin_url: string;
68
+ image_optimization_plugin_active: boolean;
69
+ image_optimization_plugin_url: string;
70
+ site_identity: {
71
+ site_name_set: boolean;
72
+ site_description_set: boolean;
73
+ site_logo_set: boolean;
74
+ site_favicon_set: boolean;
75
+ };
76
+ };
77
+
78
+ export type ElementSnapshotNode = {
79
+ id: string;
80
+ elType: string;
81
+ widgetType?: string;
82
+ settings: Record< string, unknown >;
83
+ elements: ElementSnapshotNode[];
84
+ };
85
+
86
+ export type ElementsModelSnapshot = {
87
+ documentId: number;
88
+ tree: ElementSnapshotNode[];
89
+ };
90
+
91
+ export type KitSnapshot = {
92
+ id: number;
93
+ globals: {
94
+ colors: Array< { id: string; value: string; title: string } >;
95
+ fonts: Array< { id: string; value: string; title: string } >;
96
+ };
97
+ };
98
+
99
+ export type AuditContext = {
100
+ documentId: number;
101
+ elements: ElementsModelSnapshot;
102
+ pageContext: PageContextResponse;
103
+ kit: KitSnapshot;
104
+ };
105
+
106
+ export type PageAuditReport = {
107
+ documentId: number;
108
+ runAt: number;
109
+ overall: number;
110
+ categories: Record< AuditCategory, { score: number; failed: number; total: number } >;
111
+ auditResults: AuditRun[];
112
+ };
@@ -0,0 +1,112 @@
1
+ import { type ChipProps } from '@elementor/ui';
2
+ import { __ } from '@wordpress/i18n';
3
+
4
+ import { type AuditCategory, type AuditResult, type AuditRun, type PageAuditReport } from '../types';
5
+ import { sortFailedAuditResults } from './sort-failed-audits';
6
+
7
+ export type AuditStatusGroup = 'fail' | 'pass' | 'skipped';
8
+
9
+ export type PartitionedAuditResults = {
10
+ failed: Array< AuditRun & { result: Extract< AuditResult, { status: 'fail' } > } >;
11
+ passed: Array< AuditRun & { result: Extract< AuditResult, { status: 'pass' } > } >;
12
+ skipped: Array< AuditRun & { result: Extract< AuditResult, { status: 'skipped' } > } >;
13
+ totalViolations: number;
14
+ };
15
+
16
+ type PartitionOptions = {
17
+ category?: AuditCategory;
18
+ sortFailed?: boolean;
19
+ };
20
+
21
+ export function partitionAuditResults(
22
+ report: PageAuditReport,
23
+ options: PartitionOptions = {}
24
+ ): PartitionedAuditResults {
25
+ const { category, sortFailed = true } = options;
26
+ const failed: PartitionedAuditResults[ 'failed' ] = [];
27
+ const passed: PartitionedAuditResults[ 'passed' ] = [];
28
+ const skipped: PartitionedAuditResults[ 'skipped' ] = [];
29
+
30
+ let totalViolations = 0;
31
+
32
+ for ( const run of report.auditResults ) {
33
+ if ( category && ! run.audit.categories.includes( category ) ) {
34
+ continue;
35
+ }
36
+
37
+ switch ( run.result.status ) {
38
+ case 'fail':
39
+ failed.push( { ...run, result: run.result } );
40
+ totalViolations += run.result.violations.length;
41
+ break;
42
+ case 'pass':
43
+ passed.push( { ...run, result: run.result } );
44
+ break;
45
+ case 'skipped':
46
+ skipped.push( { ...run, result: run.result } );
47
+ break;
48
+ }
49
+ }
50
+
51
+ return {
52
+ failed: sortFailed ? sortFailedAuditResults( failed ) : failed,
53
+ passed,
54
+ skipped,
55
+ totalViolations,
56
+ };
57
+ }
58
+
59
+ export function auditStatusDisplayCounts( report: PageAuditReport ): Record< AuditStatusGroup, number > {
60
+ let pass = 0;
61
+ let skipped = 0;
62
+ let totalViolations = 0;
63
+
64
+ for ( const { result } of report.auditResults ) {
65
+ switch ( result.status ) {
66
+ case 'fail':
67
+ totalViolations += result.violations.length;
68
+ break;
69
+ case 'pass':
70
+ pass++;
71
+ break;
72
+ case 'skipped':
73
+ skipped++;
74
+ break;
75
+ }
76
+ }
77
+
78
+ return {
79
+ fail: totalViolations,
80
+ pass,
81
+ skipped,
82
+ };
83
+ }
84
+
85
+ export function auditStatusColor( status: AuditStatusGroup ): ChipProps[ 'color' ] {
86
+ switch ( status ) {
87
+ case 'fail':
88
+ return 'error';
89
+ case 'pass':
90
+ return 'success';
91
+ case 'skipped':
92
+ return 'default';
93
+ }
94
+ }
95
+
96
+ export function auditStatusLabel( status: AuditStatusGroup ): string {
97
+ switch ( status ) {
98
+ case 'fail':
99
+ return __( 'Failed audits', 'elementor' );
100
+ case 'pass':
101
+ return __( 'Passed audits', 'elementor' );
102
+ case 'skipped':
103
+ return __( 'Skipped audits', 'elementor' );
104
+ }
105
+ }
106
+
107
+ export function getPopulatedCategories(
108
+ categoryTotals: PageAuditReport[ 'categories' ],
109
+ categories: readonly AuditCategory[]
110
+ ): AuditCategory[] {
111
+ return categories.filter( ( category ) => categoryTotals[ category ].total > 0 );
112
+ }
@@ -0,0 +1,9 @@
1
+ import { __, sprintf } from '@wordpress/i18n';
2
+
3
+ export function buildAngiePrompt( violationText: string ): string {
4
+ return sprintf(
5
+ /* translators: %s: audit violation description. */
6
+ __( 'Help me fix: %s', 'elementor' ),
7
+ violationText
8
+ );
9
+ }
@@ -0,0 +1,74 @@
1
+ const RESERVED_SETTING_KEYS = new Set( [ '__globals__', '__dynamic__' ] );
2
+ const REPEATER_ROW_ID_KEY = '_id';
3
+
4
+ const HEX_COLOR_PATTERN = /^#[0-9a-f]{3,8}$/i;
5
+ const RGB_COLOR_PATTERN = /^rgba?\(/i;
6
+
7
+ export type HardcodedColor = {
8
+ path: string;
9
+ value: string;
10
+ };
11
+
12
+ function isGlobalColorReference( value: string ): boolean {
13
+ return value.startsWith( 'globals/' );
14
+ }
15
+
16
+ function isHardcodedColorCandidate( value: string ): boolean {
17
+ const trimmed = value.trim();
18
+
19
+ return HEX_COLOR_PATTERN.test( trimmed ) || RGB_COLOR_PATTERN.test( trimmed );
20
+ }
21
+
22
+ function formatPath( prefix: string, key: string ): string {
23
+ return prefix ? `${ prefix }.${ key }` : key;
24
+ }
25
+
26
+ function formatArrayPath( prefix: string, index: number ): string {
27
+ return `${ prefix }[${ index }]`;
28
+ }
29
+
30
+ export function collectHardcodedColors( settings: Record< string, unknown >, pathPrefix = '' ): HardcodedColor[] {
31
+ const linkedGlobals = ( settings.__globals__ ?? {} ) as Record< string, string >;
32
+ const results: HardcodedColor[] = [];
33
+
34
+ for ( const [ key, value ] of Object.entries( settings ) ) {
35
+ if ( RESERVED_SETTING_KEYS.has( key ) || key === REPEATER_ROW_ID_KEY ) {
36
+ continue;
37
+ }
38
+
39
+ if ( linkedGlobals[ key ] ) {
40
+ continue;
41
+ }
42
+
43
+ if ( typeof value === 'string' ) {
44
+ if ( ! isGlobalColorReference( value ) && isHardcodedColorCandidate( value ) ) {
45
+ results.push( { path: formatPath( pathPrefix, key ), value } );
46
+ }
47
+ continue;
48
+ }
49
+
50
+ if ( Array.isArray( value ) ) {
51
+ const arrayPath = formatPath( pathPrefix, key );
52
+
53
+ value.forEach( ( item, index ) => {
54
+ if ( item && typeof item === 'object' && ! Array.isArray( item ) ) {
55
+ results.push(
56
+ ...collectHardcodedColors(
57
+ item as Record< string, unknown >,
58
+ formatArrayPath( arrayPath, index )
59
+ )
60
+ );
61
+ }
62
+ } );
63
+ continue;
64
+ }
65
+
66
+ if ( value && typeof value === 'object' ) {
67
+ results.push(
68
+ ...collectHardcodedColors( value as Record< string, unknown >, formatPath( pathPrefix, key ) )
69
+ );
70
+ }
71
+ }
72
+
73
+ return results;
74
+ }