@elementor/editor-audits 4.4.0-1105 → 4.4.0-1107

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elementor/editor-audits",
3
- "version": "4.4.0-1105",
3
+ "version": "4.4.0-1107",
4
4
  "private": false,
5
5
  "author": "Elementor Team",
6
6
  "homepage": "https://elementor.com/",
@@ -39,19 +39,21 @@
39
39
  "dev": "tsup --config=../../tsup.dev.ts"
40
40
  },
41
41
  "dependencies": {
42
- "@elementor/editor": "4.4.0-1105",
43
- "@elementor/editor-mcp": "4.4.0-1105",
44
- "@elementor/editor-app-bar": "4.4.0-1105",
45
- "@elementor/editor-elements": "4.4.0-1105",
46
- "@elementor/editor-floating-panels": "4.4.0-1105",
47
- "@elementor/editor-notifications": "4.4.0-1105",
48
- "@elementor/editor-v1-adapters": "4.4.0-1105",
49
- "@elementor/events": "4.4.0-1105",
50
- "@elementor/http-client": "4.4.0-1105",
42
+ "@elementor/editor": "4.4.0-1107",
43
+ "@elementor/editor-mcp": "4.4.0-1107",
44
+ "@elementor/editor-app-bar": "4.4.0-1107",
45
+ "@elementor/editor-elements": "4.4.0-1107",
46
+ "@elementor/editor-floating-panels": "4.4.0-1107",
47
+ "@elementor/editor-notifications": "4.4.0-1107",
48
+ "@elementor/editor-props": "4.4.0-1107",
49
+ "@elementor/editor-styles": "4.4.0-1107",
50
+ "@elementor/editor-v1-adapters": "4.4.0-1107",
51
+ "@elementor/events": "4.4.0-1107",
52
+ "@elementor/http-client": "4.4.0-1107",
51
53
  "@elementor/icons": "~1.77.0",
52
- "@elementor/locations": "4.4.0-1105",
53
- "@elementor/session": "4.4.0-1105",
54
- "@elementor/store": "4.4.0-1105",
54
+ "@elementor/locations": "4.4.0-1107",
55
+ "@elementor/session": "4.4.0-1107",
56
+ "@elementor/store": "4.4.0-1107",
55
57
  "@elementor/ui": "1.37.5"
56
58
  },
57
59
  "peerDependencies": {
@@ -4,13 +4,16 @@ import { type PageContextResponse } from '../types';
4
4
  import { isNonceInvalidError, refreshAuditsNonce } from '../utils/session-expiration';
5
5
  import { getWindowConfig } from '../utils/window-config';
6
6
 
7
- export async function fetchPageContext( documentId: number, attachmentIds: number[] ): Promise< PageContextResponse > {
8
- return requestPageContext( documentId, attachmentIds, true );
7
+ export async function fetchPageContext(
8
+ documentId: number,
9
+ imageSizeRequests: string[]
10
+ ): Promise< PageContextResponse > {
11
+ return requestPageContext( documentId, imageSizeRequests, true );
9
12
  }
10
13
 
11
14
  async function requestPageContext(
12
15
  documentId: number,
13
- attachmentIds: number[],
16
+ imageSizeRequests: string[],
14
17
  allowNonceRetry: boolean
15
18
  ): Promise< PageContextResponse > {
16
19
  const { restNamespace, nonce } = getWindowConfig();
@@ -20,7 +23,7 @@ async function requestPageContext(
20
23
  const response = await httpService().get< PageContextResponse >( url, {
21
24
  params: {
22
25
  document_id: documentId,
23
- attachment_ids: attachmentIds,
26
+ image_size_requests: imageSizeRequests,
24
27
  },
25
28
  headers: { 'X-WP-Nonce': nonce },
26
29
  } );
@@ -33,6 +36,6 @@ async function requestPageContext(
33
36
 
34
37
  await refreshAuditsNonce();
35
38
 
36
- return requestPageContext( documentId, attachmentIds, false );
39
+ return requestPageContext( documentId, imageSizeRequests, false );
37
40
  }
38
41
  }
@@ -0,0 +1,39 @@
1
+ import { __ } from '@wordpress/i18n';
2
+
3
+ import { type Audit } from '../types';
4
+
5
+ export const audit: Audit = {
6
+ id: 'audits/accessibility-statement',
7
+ title: __( 'Create an accessibility statement', 'elementor' ),
8
+ description: __(
9
+ 'An accessibility statement showcases your efforts to create an inclusive online space, highlighting helpful features and a commitment to accessibility.',
10
+ 'elementor'
11
+ ),
12
+ fixHint: __( 'Use Ally to create and publish an accessibility statement for your site.', 'elementor' ),
13
+ categories: [ 'accessibility' ],
14
+ severity: 'info',
15
+ weight: 1,
16
+ evaluate: ( ctx ) => {
17
+ const isReady = ctx.pageContext.ally_plugin_active;
18
+
19
+ if ( isReady && ctx.pageContext.ally_accessibility_statement_created ) {
20
+ return { status: 'pass' };
21
+ }
22
+
23
+ return {
24
+ status: 'fail',
25
+ violations: [
26
+ {
27
+ auditId: audit.id,
28
+ label: __( 'No accessibility statement has been created for this site.', 'elementor' ),
29
+ externalUrl: isReady
30
+ ? ctx.pageContext.ally_accessibility_statement_url
31
+ : ctx.pageContext.ally_plugin_url,
32
+ ctaLabel: isReady ? __( 'Create', 'elementor' ) : undefined,
33
+ secondaryCtaLabel: __( 'Learn more', 'elementor' ),
34
+ secondaryCtaUrl: 'https://go.elementor.com/acc-plg-learn-more',
35
+ },
36
+ ],
37
+ };
38
+ },
39
+ };
@@ -0,0 +1,41 @@
1
+ import { __ } from '@wordpress/i18n';
2
+
3
+ import { type Audit } from '../types';
4
+ import { hasAccessibilityWidgetScript } from '../utils/scan-accessibility-widgets';
5
+
6
+ export const audit: Audit = {
7
+ id: 'audits/accessibility-widget',
8
+ title: __( 'Add an accessibility widget', 'elementor' ),
9
+ description: __( 'Make your site more inclusive by adding an accessibility widget.', 'elementor' ),
10
+ fixHint: __( 'Activate Ally’s accessibility widget to give visitors on-page accessibility tools.', 'elementor' ),
11
+ categories: [ 'accessibility' ],
12
+ severity: 'info',
13
+ weight: 1,
14
+ evaluate: ( ctx ) => {
15
+ if ( ! ctx.pageContext.frontend_url ) {
16
+ return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
17
+ }
18
+
19
+ if ( ! ctx.renderedHtml ) {
20
+ return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
21
+ }
22
+
23
+ if ( hasAccessibilityWidgetScript( ctx.renderedHtml ) ) {
24
+ return { status: 'pass' };
25
+ }
26
+
27
+ const isReady = ctx.pageContext.ally_plugin_active;
28
+
29
+ return {
30
+ status: 'fail',
31
+ violations: [
32
+ {
33
+ auditId: audit.id,
34
+ label: __( 'No accessibility widget is active on this site.', 'elementor' ),
35
+ externalUrl: isReady ? ctx.pageContext.ally_widget_settings_url : ctx.pageContext.ally_plugin_url,
36
+ ctaLabel: __( 'Activate', 'elementor' ),
37
+ },
38
+ ],
39
+ };
40
+ },
41
+ };
@@ -1,7 +1,6 @@
1
1
  import { __ } from '@wordpress/i18n';
2
2
 
3
3
  import { type Audit } from '../types';
4
- import { fetchRenderedHtml } from '../utils/fetch-rendered-html';
5
4
  import { hasConsentDefaultCall, hasGoogleTracking } from '../utils/scan-consent-signals';
6
5
 
7
6
  export const audit: Audit = {
@@ -18,22 +17,20 @@ export const audit: Audit = {
18
17
  categories: [ 'compliance' ],
19
18
  severity: 'warning',
20
19
  weight: 1,
21
- evaluate: async ( ctx ) => {
20
+ evaluate: ( ctx ) => {
22
21
  if ( ! ctx.pageContext.frontend_url ) {
23
22
  return { status: 'skipped', reason: __( 'Page is not published.', 'elementor' ) };
24
23
  }
25
24
 
26
- const html = await fetchRenderedHtml( ctx.pageContext.frontend_url );
27
-
28
- if ( ! html ) {
25
+ if ( ! ctx.renderedHtml ) {
29
26
  return { status: 'skipped', reason: __( 'Could not fetch the published page.', 'elementor' ) };
30
27
  }
31
28
 
32
- if ( ! hasGoogleTracking( html ) ) {
29
+ if ( ! hasGoogleTracking( ctx.renderedHtml ) ) {
33
30
  return { status: 'skipped', reason: __( 'No Google tracking product detected.', 'elementor' ) };
34
31
  }
35
32
 
36
- if ( hasConsentDefaultCall( html ) ) {
33
+ if ( hasConsentDefaultCall( ctx.renderedHtml ) ) {
37
34
  return { status: 'pass' };
38
35
  }
39
36
 
@@ -1,7 +1,11 @@
1
1
  import { __, sprintf } from '@wordpress/i18n';
2
2
 
3
- import { type Audit, type AuditViolation } from '../types';
4
- import { hasPageImages, walkImageLikeSources } from '../utils/image-like-sources';
3
+ import { type Audit, type AuditViolation, type ElementSnapshotNode } from '../types';
4
+ import { walkAtomicBackgroundImageSources } from '../utils/atomic-background-image-sources';
5
+ import { walkAtomicImageSources } from '../utils/atomic-image-sources';
6
+ import { walkBackgroundImageSources } from '../utils/background-image-sources';
7
+ import { type ImageLikeMedia, walkImageLikeSources } from '../utils/image-like-sources';
8
+ import { buildImageSizeKey } from '../utils/image-size-key';
5
9
 
6
10
  const SIZE_THRESHOLD_BYTES = 500 * 1024;
7
11
  const BYTES_PER_KB = 1024;
@@ -15,21 +19,22 @@ export const audit: Audit = {
15
19
  severity: 'warning',
16
20
  weight: 7,
17
21
  evaluate: ( ctx ) => {
18
- if ( ! hasPageImages( ctx.elements.tree ) ) {
19
- return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
20
- }
21
-
22
22
  const widgetMaxKb = new Map< string, number >();
23
23
  let oversizedImageCount = 0;
24
+ let hasAnyImage = false;
25
+
26
+ const evaluateSource = ( node: ElementSnapshotNode, media: ImageLikeMedia ) => {
27
+ if ( media.id || media.url ) {
28
+ hasAnyImage = true;
29
+ }
24
30
 
25
- walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => {
26
31
  const id = media.id;
27
32
 
28
33
  if ( ! id ) {
29
34
  return;
30
35
  }
31
36
 
32
- const size = ctx.pageContext.image_sizes[ id ];
37
+ const size = ctx.pageContext.image_sizes[ buildImageSizeKey( { id, size: media.size } ) ];
33
38
 
34
39
  if ( ! size || size.filesize_bytes <= SIZE_THRESHOLD_BYTES ) {
35
40
  return;
@@ -40,7 +45,16 @@ export const audit: Audit = {
40
45
  const kb = Math.round( size.filesize_bytes / BYTES_PER_KB );
41
46
  const currentMax = widgetMaxKb.get( node.id ) ?? 0;
42
47
  widgetMaxKb.set( node.id, Math.max( currentMax, kb ) );
43
- } );
48
+ };
49
+
50
+ walkImageLikeSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
51
+ walkBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
52
+ walkAtomicImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
53
+ walkAtomicBackgroundImageSources( ctx.elements.tree, ( { node, media } ) => evaluateSource( node, media ) );
54
+
55
+ if ( ! hasAnyImage ) {
56
+ return { status: 'skipped', reason: __( 'No images', 'elementor' ) };
57
+ }
44
58
 
45
59
  const violations: AuditViolation[] = Array.from( widgetMaxKb.entries() ).map( ( [ elementId, kb ] ) => ( {
46
60
  auditId: audit.id,
@@ -4,9 +4,10 @@ import { Button } from '@elementor/ui';
4
4
  type Props = {
5
5
  ctaLabel: string;
6
6
  externalUrl: string;
7
+ variant?: 'outlined' | 'text';
7
8
  };
8
9
 
9
- export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
10
+ export default function ViolationCtaButton( { ctaLabel, externalUrl, variant = 'outlined' }: Props ) {
10
11
  const handleClick = ( event: React.MouseEvent< HTMLButtonElement > ) => {
11
12
  event.stopPropagation();
12
13
  event.preventDefault();
@@ -15,7 +16,7 @@ export default function ViolationCtaButton( { ctaLabel, externalUrl }: Props ) {
15
16
  };
16
17
 
17
18
  return (
18
- <Button variant="outlined" color="secondary" size="small" onClick={ handleClick }>
19
+ <Button variant={ variant } color="secondary" size="small" onClick={ handleClick }>
19
20
  { ctaLabel }
20
21
  </Button>
21
22
  );
@@ -164,10 +164,19 @@ export default function ViolationRow( { audit, skipReason, violations }: Props )
164
164
  />
165
165
  ) }
166
166
  { violation.ctaLabel && violation.externalUrl ? (
167
- <ViolationCtaButton
168
- ctaLabel={ violation.ctaLabel }
169
- externalUrl={ violation.externalUrl }
170
- />
167
+ <Box sx={ { display: 'flex', alignItems: 'center', gap: 0.5 } }>
168
+ <ViolationCtaButton
169
+ ctaLabel={ violation.ctaLabel }
170
+ externalUrl={ violation.externalUrl }
171
+ />
172
+ { violation.secondaryCtaLabel && violation.secondaryCtaUrl && (
173
+ <ViolationCtaButton
174
+ ctaLabel={ violation.secondaryCtaLabel }
175
+ externalUrl={ violation.secondaryCtaUrl }
176
+ variant="text"
177
+ />
178
+ ) }
179
+ </Box>
171
180
  ) : (
172
181
  <EyeIcon
173
182
  className="violation-hover-icon"
@@ -1,4 +1,6 @@
1
1
  import * as accessibilityPolicy from './audits/accessibility-policy';
2
+ import * as accessibilityStatement from './audits/accessibility-statement';
3
+ import * as accessibilityWidget from './audits/accessibility-widget';
2
4
  import * as cookiePolicy from './audits/cookie-policy';
3
5
  import * as deepNesting from './audits/deep-nesting';
4
6
  import * as defaultDesignSystem from './audits/default-design-system';
@@ -48,6 +50,8 @@ const AUDITS: Audit[] = [
48
50
 
49
51
  privacyPolicy.audit,
50
52
  accessibilityPolicy.audit,
53
+ accessibilityStatement.audit,
54
+ accessibilityWidget.audit,
51
55
  cookiePolicy.audit,
52
56
  scanForCookies.audit,
53
57
  googleConsentMode.audit,
package/src/runner.ts CHANGED
@@ -12,19 +12,23 @@ import {
12
12
  type PageAuditReport,
13
13
  } from './types';
14
14
  import { computeReport } from './utils/compute-report';
15
- import { extractAttachmentIds } from './utils/page-attachments';
15
+ import { fetchRenderedHtml } from './utils/fetch-rendered-html';
16
+ import { extractImageSizeRequests } from './utils/page-attachments';
16
17
  import { readKitSnapshot } from './utils/read-kit-snapshot';
17
18
  import { buildSnapshotTree } from './utils/v1-snapshot';
18
19
 
19
20
  export async function runPageAudit( documentId: number ): Promise< PageAuditReport > {
20
21
  const tree = buildSnapshotTree( getElements() );
21
- const attachmentIds = extractAttachmentIds( tree );
22
- const pageContext = await fetchPageContext( documentId, attachmentIds );
22
+ const imageSizeRequests = extractImageSizeRequests( tree );
23
+ const pageContext = await fetchPageContext( documentId, imageSizeRequests );
23
24
 
24
25
  const elements: ElementsModelSnapshot = { documentId, tree };
25
- const kit: KitSnapshot = await readKitSnapshot( pageContext.kit_id );
26
+ const [ kit, renderedHtml ]: [ KitSnapshot, string | null ] = await Promise.all( [
27
+ readKitSnapshot( pageContext.kit_id ),
28
+ fetchRenderedHtml( pageContext.frontend_url ),
29
+ ] );
26
30
 
27
- const ctx: AuditContext = { documentId, elements, pageContext, kit };
31
+ const ctx: AuditContext = { documentId, elements, pageContext, kit, renderedHtml };
28
32
  const registered = getRegisteredAudits();
29
33
 
30
34
  const auditResults: AuditRun[] = await Promise.all(
package/src/types.ts CHANGED
@@ -40,14 +40,17 @@ export type AuditViolation = {
40
40
  angieFix?: boolean;
41
41
  angiePrompt?: string;
42
42
  ctaLabel?: string;
43
+ secondaryCtaLabel?: string;
44
+ secondaryCtaUrl?: string;
43
45
  };
44
46
 
45
47
  export type PageContextResponse = {
46
48
  post_title: string | null;
47
49
  post_excerpt: string | null;
48
50
  featured_image_id: number | null;
51
+ /** Keyed by the composite `buildImageSizeKey` result (`"{attachmentId}:{size}"`), since one attachment can resolve to different files at different sizes. */
49
52
  image_sizes: Record<
50
- number,
53
+ string,
51
54
  {
52
55
  width: number;
53
56
  height: number;
@@ -65,6 +68,9 @@ export type PageContextResponse = {
65
68
  privacy_settings_url: string;
66
69
  ally_plugin_active: boolean;
67
70
  ally_plugin_url: string;
71
+ ally_accessibility_statement_created: boolean;
72
+ ally_accessibility_statement_url: string;
73
+ ally_widget_settings_url: string;
68
74
  cookiez_plugin_active: boolean;
69
75
  cookiez_plugin_url: string;
70
76
  cookiez_plugin_installed: boolean;
@@ -108,6 +114,7 @@ export type AuditContext = {
108
114
  elements: ElementsModelSnapshot;
109
115
  pageContext: PageContextResponse;
110
116
  kit: KitSnapshot;
117
+ renderedHtml: string | null;
111
118
  };
112
119
 
113
120
  export type PageAuditReport = {
@@ -0,0 +1,56 @@
1
+ import { getElementStyles } from '@elementor/editor-elements';
2
+ import {
3
+ backgroundImageOverlayPropTypeUtil,
4
+ backgroundOverlayPropTypeUtil,
5
+ backgroundPropTypeUtil,
6
+ } from '@elementor/editor-props';
7
+ import { type StyleDefinition } from '@elementor/editor-styles';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { extractAtomicImageMedia } from './atomic-image-sources';
11
+ import { type ImageLikeMedia } from './image-like-sources';
12
+ import { walkElements } from './walk';
13
+
14
+ export type AtomicBackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkAtomicBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: AtomicBackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ const styles = getElementStyles( node.id );
25
+
26
+ if ( ! styles ) {
27
+ return;
28
+ }
29
+
30
+ for ( const media of extractBackgroundImageMedia( styles ) ) {
31
+ visit( { node, media } );
32
+ }
33
+ } );
34
+ }
35
+
36
+ function extractBackgroundImageMedia( styles: Record< string, StyleDefinition > ): ImageLikeMedia[] {
37
+ const sources: ImageLikeMedia[] = [];
38
+
39
+ for ( const style of Object.values( styles ) ) {
40
+ for ( const variant of style.variants ) {
41
+ const background = backgroundPropTypeUtil.extract( variant.props.background );
42
+ const overlays = backgroundOverlayPropTypeUtil.extract( background?.[ 'background-overlay' ] ) ?? [];
43
+
44
+ for ( const overlay of overlays ) {
45
+ const imageOverlay = backgroundImageOverlayPropTypeUtil.extract( overlay );
46
+ const media = extractAtomicImageMedia( imageOverlay?.image );
47
+
48
+ if ( media ) {
49
+ sources.push( media );
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return sources;
56
+ }
@@ -0,0 +1,62 @@
1
+ import {
2
+ imageAttachmentIdPropType,
3
+ imagePropTypeUtil,
4
+ imageSrcPropTypeUtil,
5
+ stringPropTypeUtil,
6
+ urlPropTypeUtil,
7
+ } from '@elementor/editor-props';
8
+
9
+ import { type ElementSnapshotNode } from '../types';
10
+ import { type ImageLikeMedia } from './image-like-sources';
11
+ import { walkElements } from './walk';
12
+
13
+ const ATOMIC_IMAGE_PROP_KEYS = [ 'image', 'poster' ] as const;
14
+
15
+ export type AtomicImageSourceVisit = {
16
+ node: ElementSnapshotNode;
17
+ media: ImageLikeMedia;
18
+ };
19
+
20
+ export function walkAtomicImageSources(
21
+ tree: ElementSnapshotNode[],
22
+ visit: ( source: AtomicImageSourceVisit ) => void
23
+ ): void {
24
+ walkElements( tree, ( node ) => {
25
+ if ( node.elType !== 'widget' ) {
26
+ return;
27
+ }
28
+
29
+ for ( const key of ATOMIC_IMAGE_PROP_KEYS ) {
30
+ const media = extractAtomicImageMedia( node.settings[ key ] );
31
+
32
+ if ( media ) {
33
+ visit( { node, media } );
34
+ }
35
+ }
36
+ } );
37
+ }
38
+
39
+ export function extractAtomicImageMedia( imageProp: unknown ): ImageLikeMedia | null {
40
+ const image = imagePropTypeUtil.extract( imageProp );
41
+
42
+ if ( ! image ) {
43
+ return null;
44
+ }
45
+
46
+ const src = imageSrcPropTypeUtil.extract( image.src );
47
+
48
+ if ( ! src ) {
49
+ return null;
50
+ }
51
+
52
+ const id = imageAttachmentIdPropType.extract( src.id ) ?? undefined;
53
+ const url = urlPropTypeUtil.extract( src.url ) ?? undefined;
54
+
55
+ if ( ! id && ! url ) {
56
+ return null;
57
+ }
58
+
59
+ const size = stringPropTypeUtil.extract( image.size ) ?? undefined;
60
+
61
+ return { id, url, size };
62
+ }
@@ -0,0 +1,32 @@
1
+ import { type ElementSnapshotNode } from '../types';
2
+ import { type ImageLikeMedia } from './image-like-sources';
3
+ import { walkElements } from './walk';
4
+
5
+ const BACKGROUND_IMAGE_SETTING_KEYS = [
6
+ 'background_image',
7
+ 'background_hover_image',
8
+ 'background_overlay_image',
9
+ 'background_overlay_hover_image',
10
+ '_background_image',
11
+ '_background_hover_image',
12
+ ] as const;
13
+
14
+ export type BackgroundImageSourceVisit = {
15
+ node: ElementSnapshotNode;
16
+ media: ImageLikeMedia;
17
+ };
18
+
19
+ export function walkBackgroundImageSources(
20
+ tree: ElementSnapshotNode[],
21
+ visit: ( source: BackgroundImageSourceVisit ) => void
22
+ ): void {
23
+ walkElements( tree, ( node ) => {
24
+ for ( const key of BACKGROUND_IMAGE_SETTING_KEYS ) {
25
+ const media = node.settings[ key ] as ImageLikeMedia | undefined;
26
+
27
+ if ( media?.id || media?.url ) {
28
+ visit( { node, media } );
29
+ }
30
+ }
31
+ } );
32
+ }
@@ -1,5 +1,6 @@
1
1
  import { type PageContextResponse } from '../types';
2
2
  import { type ImageLikeMedia } from './image-like-sources';
3
+ import { buildImageSizeKey } from './image-size-key';
3
4
 
4
5
  export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContextResponse ): boolean {
5
6
  if ( ! media.id && ! media.url ) {
@@ -7,7 +8,8 @@ export function hasMeaningfulAlt( media: ImageLikeMedia, pageContext: PageContex
7
8
  }
8
9
 
9
10
  if ( media.id ) {
10
- const alt = pageContext.image_sizes[ media.id ]?.alt ?? '';
11
+ const key = buildImageSizeKey( { id: media.id, size: media.size } );
12
+ const alt = pageContext.image_sizes[ key ]?.alt ?? '';
11
13
  return alt.trim().length > 0;
12
14
  }
13
15
 
@@ -7,6 +7,7 @@ export type ImageLikeMedia = {
7
7
  id?: number;
8
8
  url?: string;
9
9
  alt?: string;
10
+ size?: string;
10
11
  };
11
12
 
12
13
  export type ImageLikeSourceVisit = {
@@ -51,7 +52,7 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
51
52
  const image = node.settings.image as ImageLikeMedia | undefined;
52
53
 
53
54
  if ( image?.id || image?.url ) {
54
- sources.push( image );
55
+ sources.push( { ...image, size: node.settings.image_size as string | undefined } );
55
56
  }
56
57
 
57
58
  const gallery = ( node.settings.carousel ?? node.settings.gallery ?? node.settings.wp_gallery ) as
@@ -59,9 +60,11 @@ function collectMediaFromNode( node: ElementSnapshotNode ): ImageLikeMedia[] {
59
60
  | undefined;
60
61
 
61
62
  if ( Array.isArray( gallery ) ) {
63
+ const gallerySize = node.settings.thumbnail_size as string | undefined;
64
+
62
65
  for ( const item of gallery ) {
63
66
  if ( item?.id || item?.url ) {
64
- sources.push( item );
67
+ sources.push( { ...item, size: gallerySize } );
65
68
  }
66
69
  }
67
70
  }
@@ -0,0 +1,8 @@
1
+ export type ImageSizeRequest = {
2
+ id: number;
3
+ size?: string;
4
+ };
5
+
6
+ export function buildImageSizeKey( { id, size }: ImageSizeRequest ): string {
7
+ return `${ id }:${ size || 'full' }`;
8
+ }
@@ -1,14 +1,23 @@
1
1
  import { type ElementSnapshotNode } from '../types';
2
- import { walkImageLikeSources } from './image-like-sources';
2
+ import { walkAtomicBackgroundImageSources } from './atomic-background-image-sources';
3
+ import { walkAtomicImageSources } from './atomic-image-sources';
4
+ import { walkBackgroundImageSources } from './background-image-sources';
5
+ import { type ImageLikeMedia, walkImageLikeSources } from './image-like-sources';
6
+ import { buildImageSizeKey } from './image-size-key';
3
7
 
4
- export function extractAttachmentIds( tree: ElementSnapshotNode[] ): number[] {
5
- const ids = new Set< number >();
8
+ export function extractImageSizeRequests( tree: ElementSnapshotNode[] ): string[] {
9
+ const keys = new Set< string >();
6
10
 
7
- walkImageLikeSources( tree, ( { media } ) => {
11
+ const collect = ( media: ImageLikeMedia ) => {
8
12
  if ( media.id ) {
9
- ids.add( media.id );
13
+ keys.add( buildImageSizeKey( { id: media.id, size: media.size } ) );
10
14
  }
11
- } );
15
+ };
12
16
 
13
- return Array.from( ids ).sort( ( a, b ) => a - b );
17
+ walkImageLikeSources( tree, ( { media } ) => collect( media ) );
18
+ walkBackgroundImageSources( tree, ( { media } ) => collect( media ) );
19
+ walkAtomicImageSources( tree, ( { media } ) => collect( media ) );
20
+ walkAtomicBackgroundImageSources( tree, ( { media } ) => collect( media ) );
21
+
22
+ return Array.from( keys ).sort();
14
23
  }
@@ -0,0 +1,7 @@
1
+ // Script sources of known accessibility widgets. Ally is listed first; append third-party
2
+ // widget script sources here (UserWay, AccessiBe, EqualWeb, etc.) once confirmed.
3
+ const ACCESSIBILITY_WIDGET_SCRIPT_SOURCES = [ 'cdn.elementor.com/a11y/widget.js' ];
4
+
5
+ export function hasAccessibilityWidgetScript( html: string ): boolean {
6
+ return ACCESSIBILITY_WIDGET_SCRIPT_SOURCES.some( ( src ) => html.includes( src ) );
7
+ }