@elementor/editor-audits 4.4.0-1080 → 4.4.0-1081

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/dist/index.mjs CHANGED
@@ -103,8 +103,8 @@ async function requestPageContext(documentId, attachmentIds, allowNonceRetry) {
103
103
 
104
104
  // src/registry.ts
105
105
  var registry = /* @__PURE__ */ new Map();
106
- function registerAudit(audit22) {
107
- registry.set(audit22.id, audit22);
106
+ function registerAudit(audit23) {
107
+ registry.set(audit23.id, audit23);
108
108
  }
109
109
  function getRegisteredAudits() {
110
110
  return Array.from(registry.values());
@@ -141,8 +141,8 @@ var CATEGORY_LABELS = {
141
141
  import { __ as __2 } from "@wordpress/i18n";
142
142
 
143
143
  // src/utils/is-scored-audit.ts
144
- function isScoredAudit(audit22) {
145
- return audit22.weight > 0;
144
+ function isScoredAudit(audit23) {
145
+ return audit23.weight > 0;
146
146
  }
147
147
 
148
148
  // src/utils/sort-failed-audits.ts
@@ -198,8 +198,8 @@ function auditStatusDisplayCounts(report) {
198
198
  let pass = 0;
199
199
  let skipped = 0;
200
200
  let totalViolations = 0;
201
- for (const { audit: audit22, result } of report.auditResults) {
202
- if (!isScoredAudit(audit22)) {
201
+ for (const { audit: audit23, result } of report.auditResults) {
202
+ if (!isScoredAudit(audit23)) {
203
203
  continue;
204
204
  }
205
205
  switch (result.status) {
@@ -249,16 +249,16 @@ function computeReport(documentId, results) {
249
249
  const accumulators = Object.fromEntries(
250
250
  ALL_CATEGORIES.map((c) => [c, { totalWeight: 0, passedWeight: 0, total: 0, failed: 0 }])
251
251
  );
252
- for (const { audit: audit22, result } of results) {
252
+ for (const { audit: audit23, result } of results) {
253
253
  if (result.status === "skipped") {
254
254
  continue;
255
255
  }
256
- for (const category of audit22.categories) {
256
+ for (const category of audit23.categories) {
257
257
  const acc = accumulators[category];
258
258
  acc.total++;
259
- acc.totalWeight += audit22.weight;
259
+ acc.totalWeight += audit23.weight;
260
260
  if (result.status === "pass") {
261
- acc.passedWeight += audit22.weight;
261
+ acc.passedWeight += audit23.weight;
262
262
  } else {
263
263
  acc.failed++;
264
264
  }
@@ -447,10 +447,10 @@ async function runPageAudit(documentId) {
447
447
  const ctx = { documentId, elements, pageContext, kit };
448
448
  const registered = getRegisteredAudits();
449
449
  const auditResults = await Promise.all(
450
- registered.map(async (audit22) => {
451
- const { evaluate: _evaluate, ...meta } = audit22;
450
+ registered.map(async (audit23) => {
451
+ const { evaluate: _evaluate, ...meta } = audit23;
452
452
  try {
453
- const result = await audit22.evaluate(ctx);
453
+ const result = await audit23.evaluate(ctx);
454
454
  return { audit: meta, result };
455
455
  } catch (error) {
456
456
  const reason = error instanceof Error ? error.message : "unknown-error";
@@ -950,13 +950,13 @@ function ViolationIcon({ violation, widgetIcon }) {
950
950
  function SkipReasonTooltip({ reason }) {
951
951
  return /* @__PURE__ */ React11.createElement(Tooltip3, { title: reason, placement: "top" }, /* @__PURE__ */ React11.createElement(Box7, { "aria-label": reason, component: "span", sx: { display: "inline-flex", alignItems: "center" } }, /* @__PURE__ */ React11.createElement(HelpIcon, { fontSize: "small", color: "action" })));
952
952
  }
953
- function StatusIndicator({ audit: audit22, violations }) {
953
+ function StatusIndicator({ audit: audit23, violations }) {
954
954
  if (violations) {
955
- return /* @__PURE__ */ React11.createElement(React11.Fragment, null, isScoredAudit(audit22) && /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, violations.length), /* @__PURE__ */ React11.createElement(SeverityIcon, { severity: audit22.severity }));
955
+ return /* @__PURE__ */ React11.createElement(React11.Fragment, null, isScoredAudit(audit23) && /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", color: "text.secondary", fontWeight: "bold" }, violations.length), /* @__PURE__ */ React11.createElement(SeverityIcon, { severity: audit23.severity }));
956
956
  }
957
957
  return /* @__PURE__ */ React11.createElement(CheckIcon, { fontSize: "small", color: "success" });
958
958
  }
959
- function ViolationRow({ audit: audit22, skipReason, violations }) {
959
+ function ViolationRow({ audit: audit23, skipReason, violations }) {
960
960
  const [expanded, setExpanded] = useState3(false);
961
961
  const toggleExpanded = () => setExpanded((value) => !value);
962
962
  return /* @__PURE__ */ React11.createElement(Box7, { sx: { borderBottom: 1, borderColor: "divider", paddingBlock: 0.5 } }, /* @__PURE__ */ React11.createElement(Box7, { sx: { display: "flex", alignItems: "center", gap: 0.5 } }, /* @__PURE__ */ React11.createElement(
@@ -972,8 +972,8 @@ function ViolationRow({ audit: audit22, skipReason, violations }) {
972
972
  },
973
973
  onClick: toggleExpanded
974
974
  },
975
- /* @__PURE__ */ React11.createElement(Typography6, { variant: "body2", sx: { flex: 1 } }, audit22.title),
976
- !skipReason && /* @__PURE__ */ React11.createElement(StatusIndicator, { audit: audit22, violations })
975
+ /* @__PURE__ */ React11.createElement(Typography6, { variant: "body2", sx: { flex: 1 } }, audit23.title),
976
+ !skipReason && /* @__PURE__ */ React11.createElement(StatusIndicator, { audit: audit23, violations })
977
977
  ), skipReason && /* @__PURE__ */ React11.createElement(SkipReasonTooltip, { reason: skipReason }), /* @__PURE__ */ React11.createElement(
978
978
  IconButton5,
979
979
  {
@@ -999,7 +999,7 @@ function ViolationRow({ audit: audit22, skipReason, violations }) {
999
999
  icon: /* @__PURE__ */ React11.createElement(AlertCircleIcon2, { fontSize: "small", color: "secondary", "aria-hidden": true })
1000
1000
  },
1001
1001
  /* @__PURE__ */ React11.createElement(AlertTitle, null, /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.primary", fontWeight: "bold" }, __11("What's the issue", "elementor"))),
1002
- /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit22.description)
1002
+ /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit23.description)
1003
1003
  ), /* @__PURE__ */ React11.createElement(
1004
1004
  Alert,
1005
1005
  {
@@ -1008,7 +1008,7 @@ function ViolationRow({ audit: audit22, skipReason, violations }) {
1008
1008
  icon: /* @__PURE__ */ React11.createElement(BulbIcon, { fontSize: "small", color: "info", "aria-hidden": true })
1009
1009
  },
1010
1010
  /* @__PURE__ */ React11.createElement(AlertTitle, null, /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.primary", fontWeight: "bold" }, __11("How to resolve", "elementor"))),
1011
- /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit22.fixHint)
1011
+ /* @__PURE__ */ React11.createElement(Typography6, { variant: "caption", component: "p", color: "text.secondary" }, audit23.fixHint)
1012
1012
  )), violations && violations.length > 0 && /* @__PURE__ */ React11.createElement(Box7, { role: "list", sx: { paddingBlockEnd: 1, paddingInlineStart: 2 } }, violations.map((violation, idx) => {
1013
1013
  const widgetIcon = violation.elementId ? getElementIcon(violation.elementId) : null;
1014
1014
  const elementTitle = violation.elementId ? getElementTitle(violation.elementId) : null;
@@ -1148,17 +1148,17 @@ import { __ as __14, sprintf as sprintf2 } from "@wordpress/i18n";
1148
1148
  var ALL_SEVERITIES = ["error", "warning", "info"];
1149
1149
  function countSeverities(report, category) {
1150
1150
  const counts = { error: 0, warning: 0, info: 0 };
1151
- for (const { audit: audit22, result } of report.auditResults) {
1151
+ for (const { audit: audit23, result } of report.auditResults) {
1152
1152
  if (result.status !== "fail") {
1153
1153
  continue;
1154
1154
  }
1155
- if (category && !audit22.categories.includes(category)) {
1155
+ if (category && !audit23.categories.includes(category)) {
1156
1156
  continue;
1157
1157
  }
1158
- if (!isScoredAudit(audit22)) {
1158
+ if (!isScoredAudit(audit23)) {
1159
1159
  continue;
1160
1160
  }
1161
- counts[audit22.severity] += result.violations.length;
1161
+ counts[audit23.severity] += result.violations.length;
1162
1162
  }
1163
1163
  return counts;
1164
1164
  }
@@ -1969,8 +1969,107 @@ var audit5 = {
1969
1969
  }
1970
1970
  };
1971
1971
 
1972
- // src/audits/heading-structure.ts
1972
+ // src/audits/google-consent-mode.ts
1973
1973
  import { __ as __28 } from "@wordpress/i18n";
1974
+
1975
+ // src/utils/fetch-rendered-html.ts
1976
+ var FETCH_TIMEOUT_MS = 1e4;
1977
+ async function fetchRenderedHtml(url) {
1978
+ if (!url) {
1979
+ return null;
1980
+ }
1981
+ const timeoutController = new AbortController();
1982
+ const timeoutId = setTimeout(() => timeoutController.abort(), FETCH_TIMEOUT_MS);
1983
+ try {
1984
+ const response = await fetch(url, { credentials: "omit", signal: timeoutController.signal });
1985
+ if (!response.ok) {
1986
+ return null;
1987
+ }
1988
+ return await response.text();
1989
+ } catch {
1990
+ return null;
1991
+ } finally {
1992
+ clearTimeout(timeoutId);
1993
+ }
1994
+ }
1995
+
1996
+ // src/utils/scan-consent-signals.ts
1997
+ var GOOGLE_TRACKING_PATTERNS = [
1998
+ /googletagmanager\.com\/gtag\/js\?id=(G|AW)-/,
1999
+ /googletagmanager\.com\/gtm\.js/,
2000
+ /\bGTM-[A-Z0-9]+\b/,
2001
+ /gtag\(\s*['"]config['"]\s*,\s*['"](G|AW)-/
2002
+ ];
2003
+ var CONSENT_DEFAULT_PATTERNS = [
2004
+ /gtag\(\s*['"]consent['"]\s*,\s*['"]default['"]/,
2005
+ /dataLayer\.push\(\s*\[\s*['"]consent['"]\s*,\s*['"]default['"]/
2006
+ ];
2007
+ function hasGoogleTracking(html) {
2008
+ return GOOGLE_TRACKING_PATTERNS.some((pattern) => pattern.test(html));
2009
+ }
2010
+ function hasConsentDefaultCall(html) {
2011
+ return CONSENT_DEFAULT_PATTERNS.some((pattern) => pattern.test(html));
2012
+ }
2013
+
2014
+ // src/audits/google-consent-mode.ts
2015
+ var audit6 = {
2016
+ id: "audits/google-consent-mode",
2017
+ title: __28("Google Consent Mode", "elementor"),
2018
+ description: __28(
2019
+ "Google Analytics, Ads, or Tag Manager are running on this site and may not receive a consent signal, which risks losing conversion/analytics data and compliance exposure.",
2020
+ "elementor"
2021
+ ),
2022
+ fixHint: __28(
2023
+ "Turn on Google Consent Mode in your cookie consent plugin so Google tags respect visitor consent choices.",
2024
+ "elementor"
2025
+ ),
2026
+ categories: ["compliance"],
2027
+ severity: "warning",
2028
+ weight: 1,
2029
+ evaluate: async (ctx) => {
2030
+ if (!ctx.pageContext.frontend_url) {
2031
+ return { status: "skipped", reason: __28("Page is not published.", "elementor") };
2032
+ }
2033
+ const html = await fetchRenderedHtml(ctx.pageContext.frontend_url);
2034
+ if (!html) {
2035
+ return { status: "skipped", reason: __28("Could not fetch the published page.", "elementor") };
2036
+ }
2037
+ if (!hasGoogleTracking(html)) {
2038
+ return { status: "skipped", reason: __28("No Google tracking product detected.", "elementor") };
2039
+ }
2040
+ if (hasConsentDefaultCall(html)) {
2041
+ return { status: "pass" };
2042
+ }
2043
+ const label = __28("No Google Consent Mode signal was found on the page.", "elementor");
2044
+ const isCookiezReady = ctx.pageContext.cookiez_plugin_installed && ctx.pageContext.cookiez_plugin_active;
2045
+ if (isCookiezReady) {
2046
+ return {
2047
+ status: "fail",
2048
+ violations: [
2049
+ {
2050
+ auditId: audit6.id,
2051
+ label,
2052
+ ctaLabel: __28("Enable", "elementor"),
2053
+ externalUrl: ctx.pageContext.cookiez_consent_mode_settings_url
2054
+ }
2055
+ ]
2056
+ };
2057
+ }
2058
+ return {
2059
+ status: "fail",
2060
+ violations: [
2061
+ {
2062
+ auditId: audit6.id,
2063
+ label,
2064
+ externalUrl: ctx.pageContext.cookiez_plugin_action_url
2065
+ }
2066
+ ]
2067
+ };
2068
+ }
2069
+ };
2070
+
2071
+ // src/audits/heading-structure.ts
2072
+ import { __ as __29 } from "@wordpress/i18n";
1974
2073
  var HEADING_LEVELS = ["h1", "h2", "h3", "h4", "h5", "h6"];
1975
2074
  var HEADING_CONTROLS_REGISTRY = {
1976
2075
  // Core
@@ -2146,47 +2245,47 @@ function extractPageHeadings(tree) {
2146
2245
  });
2147
2246
  return headings;
2148
2247
  }
2149
- var audit6 = {
2248
+ var audit7 = {
2150
2249
  id: "audits/heading-structure",
2151
- title: __28("Heading structure", "elementor"),
2152
- description: __28("Pages should have exactly one H1 and a non-skipping heading order.", "elementor"),
2153
- fixHint: __28("Ensure your page has one H1 and that heading levels do not skip (no H2 \u2192 H4).", "elementor"),
2250
+ title: __29("Heading structure", "elementor"),
2251
+ description: __29("Pages should have exactly one H1 and a non-skipping heading order.", "elementor"),
2252
+ fixHint: __29("Ensure your page has one H1 and that heading levels do not skip (no H2 \u2192 H4).", "elementor"),
2154
2253
  categories: ["seo", "accessibility"],
2155
2254
  severity: "error",
2156
2255
  weight: 10,
2157
2256
  evaluate: (ctx) => {
2158
2257
  if (ctx.elements.tree.length === 0) {
2159
- return { status: "skipped", reason: __28("No elements", "elementor") };
2258
+ return { status: "skipped", reason: __29("No elements", "elementor") };
2160
2259
  }
2161
2260
  const headings = extractPageHeadings(ctx.elements.tree);
2162
2261
  const violations = [];
2163
2262
  if (headings.length === 0) {
2164
2263
  violations.push({
2165
- auditId: audit6.id,
2166
- label: __28("No headings found on the page.", "elementor")
2264
+ auditId: audit7.id,
2265
+ label: __29("No headings found on the page.", "elementor")
2167
2266
  });
2168
2267
  } else {
2169
2268
  const h1Count = headings.filter((h) => h.level === 1).length;
2170
2269
  if (h1Count === 0) {
2171
- violations.push({ auditId: audit6.id, label: __28("No H1 on the page.", "elementor") });
2270
+ violations.push({ auditId: audit7.id, label: __29("No H1 on the page.", "elementor") });
2172
2271
  }
2173
2272
  if (h1Count > 1) {
2174
2273
  headings.filter((h) => h.level === 1).slice(1).forEach(
2175
2274
  (h) => violations.push({
2176
- auditId: audit6.id,
2275
+ auditId: audit7.id,
2177
2276
  elementId: h.elementId,
2178
2277
  targetHint: "element-settings",
2179
- label: __28("More than one H1 on the page.", "elementor")
2278
+ label: __29("More than one H1 on the page.", "elementor")
2180
2279
  })
2181
2280
  );
2182
2281
  }
2183
2282
  for (let i = 1; i < headings.length; i++) {
2184
2283
  if (headings[i].level - headings[i - 1].level > 1) {
2185
2284
  violations.push({
2186
- auditId: audit6.id,
2285
+ auditId: audit7.id,
2187
2286
  elementId: headings[i].elementId,
2188
2287
  targetHint: "element-settings",
2189
- label: __28("Heading level skipped.", "elementor")
2288
+ label: __29("Heading level skipped.", "elementor")
2190
2289
  });
2191
2290
  }
2192
2291
  }
@@ -2196,33 +2295,33 @@ var audit6 = {
2196
2295
  };
2197
2296
 
2198
2297
  // src/audits/hidden-elements.ts
2199
- import { __ as __29 } from "@wordpress/i18n";
2298
+ import { __ as __30 } from "@wordpress/i18n";
2200
2299
  function isHiddenOnAllDevices(settings) {
2201
2300
  return Boolean(settings.hide_desktop) && Boolean(settings.hide_tablet) && Boolean(settings.hide_mobile);
2202
2301
  }
2203
- var audit7 = {
2302
+ var audit8 = {
2204
2303
  id: "audits/hidden-elements",
2205
- title: __29("Hidden elements", "elementor"),
2206
- description: __29(
2304
+ title: __30("Hidden elements", "elementor"),
2305
+ description: __30(
2207
2306
  "Elements hidden on all devices simultaneously are effectively dead code in the page tree.",
2208
2307
  "elementor"
2209
2308
  ),
2210
- fixHint: __29("Remove the element or make it visible on at least one device breakpoint.", "elementor"),
2309
+ fixHint: __30("Remove the element or make it visible on at least one device breakpoint.", "elementor"),
2211
2310
  categories: ["best-practices", "performance"],
2212
2311
  severity: "info",
2213
2312
  weight: 3,
2214
2313
  evaluate: (ctx) => {
2215
2314
  if (ctx.elements.tree.length === 0) {
2216
- return { status: "skipped", reason: __29("No elements", "elementor") };
2315
+ return { status: "skipped", reason: __30("No elements", "elementor") };
2217
2316
  }
2218
2317
  const violations = [];
2219
2318
  walkElements(ctx.elements.tree, (node) => {
2220
2319
  if (isHiddenOnAllDevices(node.settings)) {
2221
2320
  violations.push({
2222
- auditId: audit7.id,
2321
+ auditId: audit8.id,
2223
2322
  elementId: node.id,
2224
2323
  targetHint: "element-settings",
2225
- label: __29("Element is hidden on all devices.", "elementor")
2324
+ label: __30("Element is hidden on all devices.", "elementor")
2226
2325
  });
2227
2326
  }
2228
2327
  });
@@ -2231,7 +2330,7 @@ var audit7 = {
2231
2330
  };
2232
2331
 
2233
2332
  // src/audits/images-alt-text.ts
2234
- import { __ as __30 } from "@wordpress/i18n";
2333
+ import { __ as __31 } from "@wordpress/i18n";
2235
2334
 
2236
2335
  // src/utils/image-alt.ts
2237
2336
  function hasMeaningfulAlt(media, pageContext) {
@@ -2249,17 +2348,17 @@ function isImageSourcePresent(media) {
2249
2348
  }
2250
2349
 
2251
2350
  // src/audits/images-alt-text.ts
2252
- var audit8 = {
2351
+ var audit9 = {
2253
2352
  id: "audits/images-alt-text",
2254
- title: __30("Images alt text", "elementor"),
2255
- description: __30("Every image needs a meaningful alt attribute for screen readers and image-search.", "elementor"),
2256
- fixHint: __30("Open the image's settings and add an Alt Text describing the image.", "elementor"),
2353
+ title: __31("Images alt text", "elementor"),
2354
+ description: __31("Every image needs a meaningful alt attribute for screen readers and image-search.", "elementor"),
2355
+ fixHint: __31("Open the image's settings and add an Alt Text describing the image.", "elementor"),
2257
2356
  categories: ["seo", "accessibility"],
2258
2357
  severity: "error",
2259
2358
  weight: 10,
2260
2359
  evaluate: (ctx) => {
2261
2360
  if (!hasPageImages(ctx.elements.tree)) {
2262
- return { status: "skipped", reason: __30("No images", "elementor") };
2361
+ return { status: "skipped", reason: __31("No images", "elementor") };
2263
2362
  }
2264
2363
  const violations = [];
2265
2364
  const failedWidgetIds = /* @__PURE__ */ new Set();
@@ -2272,10 +2371,10 @@ var audit8 = {
2272
2371
  if (!failedWidgetIds.has(node.id)) {
2273
2372
  failedWidgetIds.add(node.id);
2274
2373
  violations.push({
2275
- auditId: audit8.id,
2374
+ auditId: audit9.id,
2276
2375
  elementId: node.id,
2277
2376
  targetHint: "element-settings",
2278
- label: __30("Image is missing alt text.", "elementor"),
2377
+ label: __31("Image is missing alt text.", "elementor"),
2279
2378
  externalUrl: ctx.pageContext.ally_plugin_url
2280
2379
  });
2281
2380
  }
@@ -2285,20 +2384,20 @@ var audit8 = {
2285
2384
  };
2286
2385
 
2287
2386
  // src/audits/images-too-large.ts
2288
- import { __ as __31, sprintf as sprintf5 } from "@wordpress/i18n";
2387
+ import { __ as __32, sprintf as sprintf5 } from "@wordpress/i18n";
2289
2388
  var SIZE_THRESHOLD_BYTES = 500 * 1024;
2290
2389
  var BYTES_PER_KB = 1024;
2291
- var audit9 = {
2390
+ var audit10 = {
2292
2391
  id: "audits/images-too-large",
2293
- title: __31("Oversized images", "elementor"),
2294
- description: __31("Large image files slow down the page.", "elementor"),
2295
- fixHint: __31("Replace the image with a smaller version or enable image optimization.", "elementor"),
2392
+ title: __32("Oversized images", "elementor"),
2393
+ description: __32("Large image files slow down the page.", "elementor"),
2394
+ fixHint: __32("Replace the image with a smaller version or enable image optimization.", "elementor"),
2296
2395
  categories: ["performance"],
2297
2396
  severity: "warning",
2298
2397
  weight: 7,
2299
2398
  evaluate: (ctx) => {
2300
2399
  if (!hasPageImages(ctx.elements.tree)) {
2301
- return { status: "skipped", reason: __31("No images", "elementor") };
2400
+ return { status: "skipped", reason: __32("No images", "elementor") };
2302
2401
  }
2303
2402
  const widgetMaxKb = /* @__PURE__ */ new Map();
2304
2403
  let oversizedImageCount = 0;
@@ -2317,12 +2416,12 @@ var audit9 = {
2317
2416
  widgetMaxKb.set(node.id, Math.max(currentMax, kb));
2318
2417
  });
2319
2418
  const violations = Array.from(widgetMaxKb.entries()).map(([elementId, kb]) => ({
2320
- auditId: audit9.id,
2419
+ auditId: audit10.id,
2321
2420
  elementId,
2322
2421
  targetHint: "element-settings",
2323
2422
  label: sprintf5(
2324
2423
  /* translators: %d is the image file size in kilobytes. */
2325
- __31("Image is %d KB (over 500 KB).", "elementor"),
2424
+ __32("Image is %d KB (over 500 KB).", "elementor"),
2326
2425
  kb
2327
2426
  ),
2328
2427
  externalUrl: ctx.pageContext.image_optimization_plugin_url
@@ -2332,21 +2431,21 @@ var audit9 = {
2332
2431
  };
2333
2432
 
2334
2433
  // src/audits/nested-boxed-containers.ts
2335
- import { __ as __32 } from "@wordpress/i18n";
2434
+ import { __ as __33 } from "@wordpress/i18n";
2336
2435
  function isBoxed(settings) {
2337
2436
  return settings.content_width === "boxed";
2338
2437
  }
2339
- var audit10 = {
2438
+ var audit11 = {
2340
2439
  id: "audits/nested-boxed-containers",
2341
- title: __32("Boxed container nested inside a boxed parent", "elementor"),
2342
- description: __32("An inner container does not need to be boxed when its parent already is.", "elementor"),
2343
- fixHint: __32("Change the inner container's content width to Full Width.", "elementor"),
2440
+ title: __33("Boxed container nested inside a boxed parent", "elementor"),
2441
+ description: __33("An inner container does not need to be boxed when its parent already is.", "elementor"),
2442
+ fixHint: __33("Change the inner container's content width to Full Width.", "elementor"),
2344
2443
  categories: ["best-practices", "performance"],
2345
2444
  severity: "info",
2346
2445
  weight: 5,
2347
2446
  evaluate: (ctx) => {
2348
2447
  if (ctx.elements.tree.length === 0) {
2349
- return { status: "skipped", reason: __32("No elements", "elementor") };
2448
+ return { status: "skipped", reason: __33("No elements", "elementor") };
2350
2449
  }
2351
2450
  const violations = [];
2352
2451
  walkElements(ctx.elements.tree, (node, parents) => {
@@ -2356,10 +2455,10 @@ var audit10 = {
2356
2455
  const nearestContainerAncestor = [...parents].reverse().find((p) => p.elType === "container");
2357
2456
  if (nearestContainerAncestor && isBoxed(nearestContainerAncestor.settings)) {
2358
2457
  violations.push({
2359
- auditId: audit10.id,
2458
+ auditId: audit11.id,
2360
2459
  elementId: node.id,
2361
2460
  targetHint: "element-settings",
2362
- label: __32("Nested boxed container.", "elementor")
2461
+ label: __33("Nested boxed container.", "elementor")
2363
2462
  });
2364
2463
  }
2365
2464
  });
@@ -2368,12 +2467,12 @@ var audit10 = {
2368
2467
  };
2369
2468
 
2370
2469
  // src/audits/page-excerpt.ts
2371
- import { __ as __33 } from "@wordpress/i18n";
2372
- var audit11 = {
2470
+ import { __ as __34 } from "@wordpress/i18n";
2471
+ var audit12 = {
2373
2472
  id: "audits/page-excerpt",
2374
- title: __33("Page excerpt", "elementor"),
2375
- description: __33("A descriptive excerpt helps search engines and previews summarize the page.", "elementor"),
2376
- fixHint: __33("Open Page Settings and write a short excerpt.", "elementor"),
2473
+ title: __34("Page excerpt", "elementor"),
2474
+ description: __34("A descriptive excerpt helps search engines and previews summarize the page.", "elementor"),
2475
+ fixHint: __34("Open Page Settings and write a short excerpt.", "elementor"),
2377
2476
  categories: ["seo"],
2378
2477
  severity: "warning",
2379
2478
  weight: 5,
@@ -2385,8 +2484,8 @@ var audit11 = {
2385
2484
  status: "fail",
2386
2485
  violations: [
2387
2486
  {
2388
- auditId: audit11.id,
2389
- label: __33("Page has no excerpt.", "elementor"),
2487
+ auditId: audit12.id,
2488
+ label: __34("Page has no excerpt.", "elementor"),
2390
2489
  targetHint: "page-settings",
2391
2490
  angieFix: true
2392
2491
  }
@@ -2396,12 +2495,12 @@ var audit11 = {
2396
2495
  };
2397
2496
 
2398
2497
  // src/audits/page-featured-image.ts
2399
- import { __ as __34 } from "@wordpress/i18n";
2400
- var audit12 = {
2498
+ import { __ as __35 } from "@wordpress/i18n";
2499
+ var audit13 = {
2401
2500
  id: "audits/page-featured-image",
2402
- title: __34("Page featured image", "elementor"),
2403
- description: __34("Featured images are used by social shares and many themes for hero visuals.", "elementor"),
2404
- fixHint: __34("Open Page Settings and set a featured image.", "elementor"),
2501
+ title: __35("Page featured image", "elementor"),
2502
+ description: __35("Featured images are used by social shares and many themes for hero visuals.", "elementor"),
2503
+ fixHint: __35("Open Page Settings and set a featured image.", "elementor"),
2405
2504
  categories: ["seo"],
2406
2505
  severity: "info",
2407
2506
  weight: 5,
@@ -2413,8 +2512,8 @@ var audit12 = {
2413
2512
  status: "fail",
2414
2513
  violations: [
2415
2514
  {
2416
- auditId: audit12.id,
2417
- label: __34("No featured image set.", "elementor"),
2515
+ auditId: audit13.id,
2516
+ label: __35("No featured image set.", "elementor"),
2418
2517
  targetHint: "page-settings"
2419
2518
  }
2420
2519
  ]
@@ -2423,13 +2522,13 @@ var audit12 = {
2423
2522
  };
2424
2523
 
2425
2524
  // src/audits/page-title.ts
2426
- import { __ as __35 } from "@wordpress/i18n";
2525
+ import { __ as __36 } from "@wordpress/i18n";
2427
2526
  var MAX_TITLE_LENGTH = 60;
2428
- var audit13 = {
2527
+ var audit14 = {
2429
2528
  id: "audits/page-title",
2430
- title: __35("Page title", "elementor"),
2431
- description: __35("Pages need a clear title for SEO and screen-reader navigation.", "elementor"),
2432
- fixHint: __35("Open Page Settings and add a title.", "elementor"),
2529
+ title: __36("Page title", "elementor"),
2530
+ description: __36("Pages need a clear title for SEO and screen-reader navigation.", "elementor"),
2531
+ fixHint: __36("Open Page Settings and add a title.", "elementor"),
2433
2532
  categories: ["seo"],
2434
2533
  severity: "error",
2435
2534
  weight: 10,
@@ -2440,8 +2539,8 @@ var audit13 = {
2440
2539
  status: "fail",
2441
2540
  violations: [
2442
2541
  {
2443
- auditId: audit13.id,
2444
- label: __35("Page has no title.", "elementor"),
2542
+ auditId: audit14.id,
2543
+ label: __36("Page has no title.", "elementor"),
2445
2544
  targetHint: "page-settings",
2446
2545
  angieFix: true
2447
2546
  }
@@ -2453,8 +2552,8 @@ var audit13 = {
2453
2552
  status: "fail",
2454
2553
  violations: [
2455
2554
  {
2456
- auditId: audit13.id,
2457
- label: __35("Page title is too long.", "elementor"),
2555
+ auditId: audit14.id,
2556
+ label: __36("Page title is too long.", "elementor"),
2458
2557
  targetHint: "page-settings",
2459
2558
  angieFix: true
2460
2559
  }
@@ -2466,7 +2565,7 @@ var audit13 = {
2466
2565
  };
2467
2566
 
2468
2567
  // src/audits/prefer-global-colors.ts
2469
- import { __ as __36, sprintf as sprintf6 } from "@wordpress/i18n";
2568
+ import { __ as __37, sprintf as sprintf6 } from "@wordpress/i18n";
2470
2569
 
2471
2570
  // src/utils/collect-hardcoded-colors.ts
2472
2571
  var RESERVED_SETTING_KEYS = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
@@ -2532,20 +2631,20 @@ function findMatchingGlobalByValue(value, globals) {
2532
2631
  }
2533
2632
 
2534
2633
  // src/audits/prefer-global-colors.ts
2535
- var audit14 = {
2634
+ var audit15 = {
2536
2635
  id: "audits/prefer-global-colors",
2537
- title: __36("Prefer global colors", "elementor"),
2538
- description: __36("Global colors make the design consistent and easy to update site-wide.", "elementor"),
2539
- fixHint: __36("Replace the hard-coded value with the matching global color.", "elementor"),
2636
+ title: __37("Prefer global colors", "elementor"),
2637
+ description: __37("Global colors make the design consistent and easy to update site-wide.", "elementor"),
2638
+ fixHint: __37("Replace the hard-coded value with the matching global color.", "elementor"),
2540
2639
  categories: ["best-practices"],
2541
2640
  severity: "info",
2542
2641
  weight: 3,
2543
2642
  evaluate: (ctx) => {
2544
2643
  if (ctx.elements.tree.length === 0) {
2545
- return { status: "skipped", reason: __36("No elements", "elementor") };
2644
+ return { status: "skipped", reason: __37("No elements", "elementor") };
2546
2645
  }
2547
2646
  if (ctx.kit.globals.colors.length === 0) {
2548
- return { status: "skipped", reason: __36("No global colors", "elementor") };
2647
+ return { status: "skipped", reason: __37("No global colors", "elementor") };
2549
2648
  }
2550
2649
  const violations = [];
2551
2650
  walkElements(ctx.elements.tree, (node) => {
@@ -2555,13 +2654,13 @@ var audit14 = {
2555
2654
  continue;
2556
2655
  }
2557
2656
  violations.push({
2558
- auditId: audit14.id,
2657
+ auditId: audit15.id,
2559
2658
  elementId: node.id,
2560
2659
  targetHint: "element-settings",
2561
2660
  angieFix: true,
2562
2661
  label: sprintf6(
2563
2662
  /* translators: 1: hard-coded color value, 2: global color title. */
2564
- __36('replace "%1$s" with "%2$s" global color', "elementor"),
2663
+ __37('replace "%1$s" with "%2$s" global color', "elementor"),
2565
2664
  value,
2566
2665
  global.title
2567
2666
  )
@@ -2573,7 +2672,7 @@ var audit14 = {
2573
2672
  };
2574
2673
 
2575
2674
  // src/audits/prefer-global-fonts.ts
2576
- import { __ as __37, sprintf as sprintf7 } from "@wordpress/i18n";
2675
+ import { __ as __38, sprintf as sprintf7 } from "@wordpress/i18n";
2577
2676
 
2578
2677
  // src/utils/collect-hardcoded-fonts.ts
2579
2678
  var RESERVED_SETTING_KEYS2 = /* @__PURE__ */ new Set(["__globals__", "__dynamic__"]);
@@ -2631,20 +2730,20 @@ function collectHardcodedFonts(settings, pathPrefix = "") {
2631
2730
  }
2632
2731
 
2633
2732
  // src/audits/prefer-global-fonts.ts
2634
- var audit15 = {
2733
+ var audit16 = {
2635
2734
  id: "audits/prefer-global-fonts",
2636
- title: __37("Prefer global fonts", "elementor"),
2637
- description: __37("Global fonts make the design consistent and easy to update site-wide.", "elementor"),
2638
- fixHint: __37("Replace the hard-coded value with the matching global font.", "elementor"),
2735
+ title: __38("Prefer global fonts", "elementor"),
2736
+ description: __38("Global fonts make the design consistent and easy to update site-wide.", "elementor"),
2737
+ fixHint: __38("Replace the hard-coded value with the matching global font.", "elementor"),
2639
2738
  categories: ["best-practices", "performance"],
2640
2739
  severity: "info",
2641
2740
  weight: 3,
2642
2741
  evaluate: (ctx) => {
2643
2742
  if (ctx.elements.tree.length === 0) {
2644
- return { status: "skipped", reason: __37("No elements", "elementor") };
2743
+ return { status: "skipped", reason: __38("No elements", "elementor") };
2645
2744
  }
2646
2745
  if (ctx.kit.globals.fonts.length === 0) {
2647
- return { status: "skipped", reason: __37("No global fonts", "elementor") };
2746
+ return { status: "skipped", reason: __38("No global fonts", "elementor") };
2648
2747
  }
2649
2748
  const violations = [];
2650
2749
  walkElements(ctx.elements.tree, (node) => {
@@ -2654,12 +2753,12 @@ var audit15 = {
2654
2753
  continue;
2655
2754
  }
2656
2755
  violations.push({
2657
- auditId: audit15.id,
2756
+ auditId: audit16.id,
2658
2757
  elementId: node.id,
2659
2758
  targetHint: "element-settings",
2660
2759
  label: sprintf7(
2661
2760
  /* translators: %s: global font title. */
2662
- __37('replace hardcoded typography with "%s" global font', "elementor"),
2761
+ __38('replace hardcoded typography with "%s" global font', "elementor"),
2663
2762
  global.title
2664
2763
  )
2665
2764
  });
@@ -2670,15 +2769,15 @@ var audit15 = {
2670
2769
  };
2671
2770
 
2672
2771
  // src/audits/privacy-policy.ts
2673
- import { __ as __38 } from "@wordpress/i18n";
2674
- var audit16 = {
2772
+ import { __ as __39 } from "@wordpress/i18n";
2773
+ var audit17 = {
2675
2774
  id: "audits/privacy-policy",
2676
- title: __38("Privacy policy", "elementor"),
2677
- description: __38(
2775
+ title: __39("Privacy policy", "elementor"),
2776
+ description: __39(
2678
2777
  "A privacy policy page is required by privacy regulations such as GDPR (EU) and CCPA (US).",
2679
2778
  "elementor"
2680
2779
  ),
2681
- fixHint: __38(
2780
+ fixHint: __39(
2682
2781
  "Go to Settings > Privacy in the WordPress admin and assign a published privacy policy page.",
2683
2782
  "elementor"
2684
2783
  ),
@@ -2693,10 +2792,10 @@ var audit16 = {
2693
2792
  status: "fail",
2694
2793
  violations: [
2695
2794
  {
2696
- auditId: audit16.id,
2697
- label: __38("No privacy policy page is set.", "elementor"),
2795
+ auditId: audit17.id,
2796
+ label: __39("No privacy policy page is set.", "elementor"),
2698
2797
  externalUrl: ctx.pageContext.privacy_settings_url,
2699
- ctaLabel: __38("Create", "elementor")
2798
+ ctaLabel: __39("Create", "elementor")
2700
2799
  }
2701
2800
  ]
2702
2801
  };
@@ -2704,15 +2803,15 @@ var audit16 = {
2704
2803
  };
2705
2804
 
2706
2805
  // src/audits/robots-noindex.ts
2707
- import { __ as __39 } from "@wordpress/i18n";
2708
- var audit17 = {
2806
+ import { __ as __40 } from "@wordpress/i18n";
2807
+ var audit18 = {
2709
2808
  id: "audits/robots-noindex",
2710
- title: __39("Search engine visibility", "elementor"),
2711
- description: __39(
2809
+ title: __40("Search engine visibility", "elementor"),
2810
+ description: __40(
2712
2811
  "When search engine visibility is checked in WordPress Settings -> Reading -> Search Engine Visibility, search engines are discouraged from indexing the website.",
2713
2812
  "elementor"
2714
2813
  ),
2715
- fixHint: __39(
2814
+ fixHint: __40(
2716
2815
  'Go to Settings \u2192 Reading in the WordPress admin and uncheck "Discourage search engines from indexing this site" when the website should be indexed.',
2717
2816
  "elementor"
2718
2817
  ),
@@ -2727,8 +2826,8 @@ var audit17 = {
2727
2826
  status: "fail",
2728
2827
  violations: [
2729
2828
  {
2730
- auditId: audit17.id,
2731
- label: __39("Search engines are discouraged from indexing this website.", "elementor"),
2829
+ auditId: audit18.id,
2830
+ label: __40("Search engines are discouraged from indexing this website.", "elementor"),
2732
2831
  externalUrl: ctx.pageContext.reading_settings_url
2733
2832
  }
2734
2833
  ]
@@ -2737,15 +2836,15 @@ var audit17 = {
2737
2836
  };
2738
2837
 
2739
2838
  // src/audits/scan-for-cookies.ts
2740
- import { __ as __40 } from "@wordpress/i18n";
2741
- var audit18 = {
2839
+ import { __ as __41 } from "@wordpress/i18n";
2840
+ var audit19 = {
2742
2841
  id: "audits/scan-for-cookies",
2743
- title: __40("Scan for cookies", "elementor"),
2744
- description: __40(
2842
+ title: __41("Scan for cookies", "elementor"),
2843
+ description: __41(
2745
2844
  "Your site may be setting cookies you haven't disclosed. Some countries require a banner even without a full consent policy. Scan to see what needs disclosure.",
2746
2845
  "elementor"
2747
2846
  ),
2748
- fixHint: __40("Run a Cookiez scan on this page to see which cookies need disclosure.", "elementor"),
2847
+ fixHint: __41("Run a Cookiez scan on this page to see which cookies need disclosure.", "elementor"),
2749
2848
  categories: ["compliance"],
2750
2849
  severity: "info",
2751
2850
  weight: 0,
@@ -2755,8 +2854,8 @@ var audit18 = {
2755
2854
  status: "fail",
2756
2855
  violations: [
2757
2856
  {
2758
- auditId: audit18.id,
2759
- label: __40("This page has not been scanned for cookies yet.", "elementor"),
2857
+ auditId: audit19.id,
2858
+ label: __41("This page has not been scanned for cookies yet.", "elementor"),
2760
2859
  externalUrl: isReady ? ctx.pageContext.cookiez_scan_url : ctx.pageContext.cookiez_plugin_action_url
2761
2860
  }
2762
2861
  ]
@@ -2765,29 +2864,29 @@ var audit18 = {
2765
2864
  };
2766
2865
 
2767
2866
  // src/audits/sections-and-columns.ts
2768
- import { __ as __41 } from "@wordpress/i18n";
2769
- var audit19 = {
2867
+ import { __ as __42 } from "@wordpress/i18n";
2868
+ var audit20 = {
2770
2869
  id: "audits/sections-and-columns",
2771
- title: __41("Sections and columns", "elementor"),
2772
- description: __41(
2870
+ title: __42("Sections and columns", "elementor"),
2871
+ description: __42(
2773
2872
  "Sections and columns are legacy elements. Containers render fewer DOM nodes and are more flexible.",
2774
2873
  "elementor"
2775
2874
  ),
2776
- fixHint: __41("Use the Container Converter to replace each section/column with a container.", "elementor"),
2875
+ fixHint: __42("Use the Container Converter to replace each section/column with a container.", "elementor"),
2777
2876
  categories: ["best-practices", "performance"],
2778
2877
  severity: "warning",
2779
2878
  weight: 7,
2780
2879
  evaluate: (ctx) => {
2781
2880
  if (ctx.elements.tree.length === 0) {
2782
- return { status: "skipped", reason: __41("No elements", "elementor") };
2881
+ return { status: "skipped", reason: __42("No elements", "elementor") };
2783
2882
  }
2784
2883
  const violations = [];
2785
2884
  walkElements(ctx.elements.tree, (node) => {
2786
2885
  if (node.elType === "section" || node.elType === "column") {
2787
2886
  violations.push({
2788
- auditId: audit19.id,
2887
+ auditId: audit20.id,
2789
2888
  elementId: node.id,
2790
- label: node.elType === "section" ? __41("Section element", "elementor") : __41("Column element", "elementor")
2889
+ label: node.elType === "section" ? __42("Section element", "elementor") : __42("Column element", "elementor")
2791
2890
  });
2792
2891
  }
2793
2892
  });
@@ -2796,15 +2895,15 @@ var audit19 = {
2796
2895
  };
2797
2896
 
2798
2897
  // src/audits/site-identity.ts
2799
- import { __ as __42 } from "@wordpress/i18n";
2800
- var audit20 = {
2898
+ import { __ as __43 } from "@wordpress/i18n";
2899
+ var audit21 = {
2801
2900
  id: "audits/site-identity",
2802
- title: __42("Site identity", "elementor"),
2803
- description: __42(
2901
+ title: __43("Site identity", "elementor"),
2902
+ description: __43(
2804
2903
  "Site name, description, logo, and favicon establish your brand and appear in search results and browser tabs.",
2805
2904
  "elementor"
2806
2905
  ),
2807
- fixHint: __42("Open Site Settings \u2192 Site Identity and complete all the missing fields.", "elementor"),
2906
+ fixHint: __43("Open Site Settings \u2192 Site Identity and complete all the missing fields.", "elementor"),
2808
2907
  categories: ["best-practices", "seo"],
2809
2908
  severity: "info",
2810
2909
  weight: 7,
@@ -2813,30 +2912,30 @@ var audit20 = {
2813
2912
  const violations = [];
2814
2913
  if (!identity.site_name_set) {
2815
2914
  violations.push({
2816
- auditId: audit20.id,
2817
- label: __42("Site name is missing or still uses the default.", "elementor"),
2915
+ auditId: audit21.id,
2916
+ label: __43("Site name is missing or still uses the default.", "elementor"),
2818
2917
  targetHint: "site-identity-settings"
2819
2918
  });
2820
2919
  }
2821
2920
  if (!identity.site_description_set) {
2822
2921
  violations.push({
2823
- auditId: audit20.id,
2824
- label: __42("Site description is missing or still uses the default.", "elementor"),
2922
+ auditId: audit21.id,
2923
+ label: __43("Site description is missing or still uses the default.", "elementor"),
2825
2924
  targetHint: "site-identity-settings",
2826
2925
  angieFix: true
2827
2926
  });
2828
2927
  }
2829
2928
  if (!identity.site_logo_set) {
2830
2929
  violations.push({
2831
- auditId: audit20.id,
2832
- label: __42("Site logo is not set.", "elementor"),
2930
+ auditId: audit21.id,
2931
+ label: __43("Site logo is not set.", "elementor"),
2833
2932
  targetHint: "site-identity-settings"
2834
2933
  });
2835
2934
  }
2836
2935
  if (!identity.site_favicon_set) {
2837
2936
  violations.push({
2838
- auditId: audit20.id,
2839
- label: __42("Site favicon is not set.", "elementor"),
2937
+ auditId: audit21.id,
2938
+ label: __43("Site favicon is not set.", "elementor"),
2840
2939
  targetHint: "site-identity-settings"
2841
2940
  });
2842
2941
  }
@@ -2851,19 +2950,19 @@ var audit20 = {
2851
2950
  };
2852
2951
 
2853
2952
  // src/audits/too-many-widgets.ts
2854
- import { __ as __43 } from "@wordpress/i18n";
2953
+ import { __ as __44 } from "@wordpress/i18n";
2855
2954
  var WIDGET_COUNT_THRESHOLD = 100;
2856
- var audit21 = {
2955
+ var audit22 = {
2857
2956
  id: "audits/too-many-widgets",
2858
- title: __43("Too many widgets", "elementor"),
2859
- description: __43("Excessive DOM size caused by too many widgets degrades rendering performance.", "elementor"),
2860
- fixHint: __43("Reduce the number of widgets on the page by removing or combining elements.", "elementor"),
2957
+ title: __44("Too many widgets", "elementor"),
2958
+ description: __44("Excessive DOM size caused by too many widgets degrades rendering performance.", "elementor"),
2959
+ fixHint: __44("Reduce the number of widgets on the page by removing or combining elements.", "elementor"),
2861
2960
  categories: ["best-practices", "performance"],
2862
2961
  severity: "warning",
2863
2962
  weight: 5,
2864
2963
  evaluate: (ctx) => {
2865
2964
  if (ctx.elements.tree.length === 0) {
2866
- return { status: "skipped", reason: __43("No elements", "elementor") };
2965
+ return { status: "skipped", reason: __44("No elements", "elementor") };
2867
2966
  }
2868
2967
  let widgetCount = 0;
2869
2968
  walkElements(ctx.elements.tree, (node) => {
@@ -2878,8 +2977,8 @@ var audit21 = {
2878
2977
  status: "fail",
2879
2978
  violations: [
2880
2979
  {
2881
- auditId: audit21.id,
2882
- label: __43("Page has too many widgets.", "elementor")
2980
+ auditId: audit22.id,
2981
+ label: __44("Page has too many widgets.", "elementor")
2883
2982
  }
2884
2983
  ]
2885
2984
  };
@@ -2888,31 +2987,32 @@ var audit21 = {
2888
2987
 
2889
2988
  // src/register-audits.ts
2890
2989
  var AUDITS = [
2891
- audit13,
2892
- audit11,
2990
+ audit14,
2893
2991
  audit12,
2894
- audit7,
2992
+ audit13,
2993
+ audit8,
2895
2994
  audit5,
2896
- audit21,
2897
- audit19,
2995
+ audit22,
2996
+ audit20,
2898
2997
  audit3,
2899
- audit10,
2998
+ audit11,
2900
2999
  audit4,
2901
- audit20,
2902
- audit14,
3000
+ audit21,
2903
3001
  audit15,
2904
- audit6,
2905
- audit8,
3002
+ audit16,
3003
+ audit7,
2906
3004
  audit9,
3005
+ audit10,
3006
+ audit18,
2907
3007
  audit17,
2908
- audit16,
2909
3008
  audit,
2910
3009
  audit2,
2911
- audit18
3010
+ audit19,
3011
+ audit6
2912
3012
  ];
2913
3013
  function registerAllAudits() {
2914
- for (const audit22 of AUDITS) {
2915
- registerAudit(audit22);
3014
+ for (const audit23 of AUDITS) {
3015
+ registerAudit(audit23);
2916
3016
  }
2917
3017
  }
2918
3018