@aexol/spectral 0.9.126 → 0.9.127
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/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -0
- package/dist/extensions/seo/analyzers/content.d.ts +29 -6
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/content.js +243 -8
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/crawler.js +133 -7
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/drift.js +86 -29
- package/dist/extensions/seo/analyzers/images.d.ts +10 -4
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/images.js +153 -5
- package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/performance.js +77 -6
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/readability.js +17 -9
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/schema.js +99 -10
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/sitemap.js +57 -16
- package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/technical.js +408 -37
- package/dist/extensions/seo/config.d.ts +2 -0
- package/dist/extensions/seo/config.d.ts.map +1 -1
- package/dist/extensions/seo/config.js +2 -0
- package/dist/extensions/seo/tools/batch.js +1 -1
- package/dist/extensions/seo/tools/content.d.ts.map +1 -1
- package/dist/extensions/seo/tools/content.js +5 -3
- package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +391 -66
- package/dist/extensions/seo/tools/helpers.d.ts +15 -4
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
- package/dist/extensions/seo/tools/helpers.js +57 -6
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
- package/dist/extensions/seo/tools/performance.js +17 -11
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +513 -47
- package/dist/extensions/seo/types.d.ts +295 -3
- package/dist/extensions/seo/types.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.js +77 -11
- package/dist/extensions/seo/utils/parser.d.ts +3 -1
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
- package/dist/extensions/seo/utils/parser.js +105 -25
- package/dist/extensions/seo/utils/renderer.d.ts +6 -1
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
- package/dist/extensions/seo/utils/renderer.js +207 -1
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
- package/dist/extensions/seo/utils/url-safety.js +19 -3
- package/dist/extensions/web/tools/fetch.js +2 -2
- package/dist/memory/tools/compact-context.d.ts.map +1 -1
- package/dist/memory/tools/compact-context.js +10 -3
- package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent-loop.js +87 -4
- package/dist/sdk/agent-core/types.d.ts +1 -0
- package/dist/sdk/agent-core/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.d.ts +0 -7
- package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.js +21 -1
- package/package.json +1 -1
|
@@ -12,6 +12,7 @@ import Database from "better-sqlite3";
|
|
|
12
12
|
import { parsePage } from "../utils/parser.js";
|
|
13
13
|
import { fetchPage } from "../utils/fetcher.js";
|
|
14
14
|
import { normalizeUrl, urlHash } from "../utils/url-safety.js";
|
|
15
|
+
const SCHEMA_VERSION = 2;
|
|
15
16
|
let dbInstance = null;
|
|
16
17
|
function getDb(config) {
|
|
17
18
|
if (dbInstance)
|
|
@@ -19,27 +20,44 @@ function getDb(config) {
|
|
|
19
20
|
mkdirSync(dirname(config.dbPath), { recursive: true });
|
|
20
21
|
dbInstance = new Database(config.dbPath);
|
|
21
22
|
dbInstance.pragma("journal_mode = WAL");
|
|
23
|
+
// Create version table and track schema version.
|
|
22
24
|
dbInstance.exec(`
|
|
23
|
-
CREATE TABLE IF NOT EXISTS
|
|
24
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
25
|
-
url TEXT NOT NULL,
|
|
26
|
-
url_hash TEXT NOT NULL,
|
|
27
|
-
timestamp TEXT NOT NULL,
|
|
28
|
-
title TEXT,
|
|
29
|
-
meta_description TEXT,
|
|
30
|
-
canonical TEXT,
|
|
31
|
-
robots TEXT,
|
|
32
|
-
h1 TEXT,
|
|
33
|
-
h2_json TEXT,
|
|
34
|
-
h3_json TEXT,
|
|
35
|
-
schema_json TEXT,
|
|
36
|
-
og_json TEXT,
|
|
37
|
-
html_hash TEXT,
|
|
38
|
-
schema_hash TEXT,
|
|
39
|
-
status_code INTEGER
|
|
40
|
-
);
|
|
41
|
-
CREATE INDEX IF NOT EXISTS idx_url_hash ON baselines(url_hash);
|
|
25
|
+
CREATE TABLE IF NOT EXISTS schema_version (version INTEGER NOT NULL);
|
|
42
26
|
`);
|
|
27
|
+
const currentVersion = dbInstance.prepare("SELECT version FROM schema_version").get();
|
|
28
|
+
if (!currentVersion || currentVersion.version < 1) {
|
|
29
|
+
dbInstance.exec(`
|
|
30
|
+
CREATE TABLE IF NOT EXISTS baselines (
|
|
31
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
32
|
+
url TEXT NOT NULL,
|
|
33
|
+
url_hash TEXT NOT NULL,
|
|
34
|
+
timestamp TEXT NOT NULL,
|
|
35
|
+
title TEXT,
|
|
36
|
+
meta_description TEXT,
|
|
37
|
+
canonical TEXT,
|
|
38
|
+
robots TEXT,
|
|
39
|
+
h1 TEXT,
|
|
40
|
+
h2_json TEXT,
|
|
41
|
+
h3_json TEXT,
|
|
42
|
+
schema_json TEXT,
|
|
43
|
+
og_json TEXT,
|
|
44
|
+
html_hash TEXT,
|
|
45
|
+
schema_hash TEXT,
|
|
46
|
+
status_code INTEGER
|
|
47
|
+
);
|
|
48
|
+
CREATE INDEX IF NOT EXISTS idx_url_hash ON baselines(url_hash);
|
|
49
|
+
`);
|
|
50
|
+
dbInstance.prepare("DELETE FROM schema_version").run();
|
|
51
|
+
dbInstance.prepare("INSERT INTO schema_version (version) VALUES (1)").run();
|
|
52
|
+
}
|
|
53
|
+
if (!currentVersion || currentVersion.version < 2) {
|
|
54
|
+
dbInstance.exec(`
|
|
55
|
+
ALTER TABLE baselines ADD COLUMN expires_at TEXT NOT NULL DEFAULT '';
|
|
56
|
+
ALTER TABLE baselines ADD COLUMN headers_json TEXT NOT NULL DEFAULT '{}';
|
|
57
|
+
`);
|
|
58
|
+
dbInstance.prepare("DELETE FROM schema_version").run();
|
|
59
|
+
dbInstance.prepare("INSERT INTO schema_version (version) VALUES (2)").run();
|
|
60
|
+
}
|
|
43
61
|
return dbInstance;
|
|
44
62
|
}
|
|
45
63
|
/** Close the database connection (called on session shutdown). */
|
|
@@ -69,19 +87,25 @@ export async function captureBaseline(url, config) {
|
|
|
69
87
|
const ogJson = JSON.stringify(pageData.ogTags);
|
|
70
88
|
const htmlHash = hashContent(fetchResult.content);
|
|
71
89
|
const schemaHash = hashContent(schemaJson);
|
|
90
|
+
const headersJson = JSON.stringify(fetchResult.headers);
|
|
91
|
+
// Compute expiration (default: 30 days).
|
|
92
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
72
93
|
const db = getDb(config);
|
|
94
|
+
// Prune expired baselines before inserting.
|
|
95
|
+
db.prepare("DELETE FROM baselines WHERE expires_at < ? AND expires_at != ''").run(new Date().toISOString());
|
|
73
96
|
const stmt = db.prepare(`
|
|
74
97
|
INSERT INTO baselines
|
|
75
|
-
(url, url_hash, timestamp, title, meta_description, canonical, robots,
|
|
76
|
-
h1, h2_json, h3_json, schema_json, og_json, html_hash, schema_hash, status_code)
|
|
77
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
98
|
+
(url, url_hash, timestamp, expires_at, title, meta_description, canonical, robots,
|
|
99
|
+
h1, h2_json, h3_json, schema_json, og_json, html_hash, schema_hash, status_code, headers_json)
|
|
100
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
78
101
|
`);
|
|
79
|
-
const info = stmt.run(normalizeUrl(url), hash, timestamp, pageData.title, pageData.metaDescription, pageData.canonical, pageData.metaRobots, pageData.headings.find((h) => h.level === 1)?.text ?? null, h2Json, h3Json, schemaJson, ogJson, htmlHash, schemaHash, fetchResult.statusCode);
|
|
102
|
+
const info = stmt.run(normalizeUrl(url), hash, timestamp, expiresAt, pageData.title, pageData.metaDescription, pageData.canonical, pageData.metaRobots, pageData.headings.find((h) => h.level === 1)?.text ?? null, h2Json, h3Json, schemaJson, ogJson, htmlHash, schemaHash, fetchResult.statusCode, headersJson);
|
|
80
103
|
return {
|
|
81
104
|
id: Number(info.lastInsertRowid),
|
|
82
105
|
url: normalizeUrl(url),
|
|
83
106
|
urlHash: hash,
|
|
84
107
|
timestamp,
|
|
108
|
+
expiresAt,
|
|
85
109
|
title: pageData.title,
|
|
86
110
|
metaDescription: pageData.metaDescription,
|
|
87
111
|
canonical: pageData.canonical,
|
|
@@ -94,6 +118,7 @@ export async function captureBaseline(url, config) {
|
|
|
94
118
|
htmlHash,
|
|
95
119
|
schemaHash,
|
|
96
120
|
statusCode: fetchResult.statusCode,
|
|
121
|
+
headersJson,
|
|
97
122
|
};
|
|
98
123
|
}
|
|
99
124
|
/** Get a baseline by ID or the latest baseline for a URL. */
|
|
@@ -113,6 +138,7 @@ function rowToBaseline(row) {
|
|
|
113
138
|
url: row.url,
|
|
114
139
|
urlHash: row.url_hash,
|
|
115
140
|
timestamp: row.timestamp,
|
|
141
|
+
expiresAt: row.expires_at ?? "",
|
|
116
142
|
title: row.title ?? null,
|
|
117
143
|
metaDescription: row.meta_description ?? null,
|
|
118
144
|
canonical: row.canonical ?? null,
|
|
@@ -125,6 +151,7 @@ function rowToBaseline(row) {
|
|
|
125
151
|
htmlHash: row.html_hash ?? "",
|
|
126
152
|
schemaHash: row.schema_hash ?? "",
|
|
127
153
|
statusCode: row.status_code ?? 0,
|
|
154
|
+
headersJson: row.headers_json ?? "{}",
|
|
128
155
|
};
|
|
129
156
|
}
|
|
130
157
|
/** Compare current page state against a baseline. */
|
|
@@ -133,32 +160,52 @@ export async function compareBaseline(url, config, baselineId) {
|
|
|
133
160
|
if (!baseline) {
|
|
134
161
|
throw new Error(`No baseline found for ${url}${baselineId ? ` (id=${baselineId})` : ""}.`);
|
|
135
162
|
}
|
|
163
|
+
// Check if baseline has expired.
|
|
164
|
+
if (baseline.expiresAt && baseline.expiresAt < new Date().toISOString()) {
|
|
165
|
+
throw new Error(`Baseline has expired (${baseline.expiresAt}). Capture a new baseline.`);
|
|
166
|
+
}
|
|
136
167
|
const fetchResult = await fetchPage(url, config);
|
|
137
168
|
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
138
169
|
const changedFields = [];
|
|
139
170
|
const details = {};
|
|
171
|
+
const fieldSeverity = {};
|
|
140
172
|
const comparisons = [
|
|
141
|
-
{ field: "title", before: baseline.title, after: pageData.title },
|
|
142
|
-
{ field: "metaDescription", before: baseline.metaDescription, after: pageData.metaDescription },
|
|
143
|
-
{ field: "canonical", before: baseline.canonical, after: pageData.canonical },
|
|
144
|
-
{ field: "robots", before: baseline.robots, after: pageData.metaRobots },
|
|
145
|
-
{ field: "h1", before: baseline.h1, after: pageData.headings.find((h) => h.level === 1)?.text ?? null },
|
|
173
|
+
{ field: "title", before: baseline.title, after: pageData.title, severityHint: "high" },
|
|
174
|
+
{ field: "metaDescription", before: baseline.metaDescription, after: pageData.metaDescription, severityHint: "medium" },
|
|
175
|
+
{ field: "canonical", before: baseline.canonical, after: pageData.canonical, severityHint: "critical" },
|
|
176
|
+
{ field: "robots", before: baseline.robots, after: pageData.metaRobots, severityHint: "high" },
|
|
177
|
+
{ field: "h1", before: baseline.h1, after: pageData.headings.find((h) => h.level === 1)?.text ?? null, severityHint: "medium" },
|
|
146
178
|
{
|
|
147
179
|
field: "h2",
|
|
148
180
|
before: JSON.parse(baseline.h2Json),
|
|
149
181
|
after: pageData.headings.filter((h) => h.level === 2).map((h) => h.text),
|
|
182
|
+
severityHint: "low",
|
|
150
183
|
},
|
|
151
184
|
{
|
|
152
185
|
field: "h3",
|
|
153
186
|
before: JSON.parse(baseline.h3Json),
|
|
154
187
|
after: pageData.headings.filter((h) => h.level === 3).map((h) => h.text),
|
|
188
|
+
severityHint: "low",
|
|
155
189
|
},
|
|
156
190
|
{
|
|
157
191
|
field: "schema",
|
|
158
192
|
before: JSON.parse(baseline.schemaJson),
|
|
159
193
|
after: pageData.schemaBlocks.map((s) => ({ type: s.type, format: s.format })),
|
|
194
|
+
severityHint: "high",
|
|
195
|
+
},
|
|
196
|
+
{ field: "statusCode", before: baseline.statusCode, after: fetchResult.statusCode, severityHint: "critical" },
|
|
197
|
+
{
|
|
198
|
+
field: "headers",
|
|
199
|
+
before: JSON.parse(baseline.headersJson || "{}"),
|
|
200
|
+
after: fetchResult.headers,
|
|
201
|
+
severityHint: "medium",
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
field: "ogTags",
|
|
205
|
+
before: JSON.parse(baseline.ogJson),
|
|
206
|
+
after: pageData.ogTags,
|
|
207
|
+
severityHint: "low",
|
|
160
208
|
},
|
|
161
|
-
{ field: "statusCode", before: baseline.statusCode, after: fetchResult.statusCode },
|
|
162
209
|
];
|
|
163
210
|
for (const cmp of comparisons) {
|
|
164
211
|
const beforeStr = JSON.stringify(cmp.before);
|
|
@@ -166,6 +213,15 @@ export async function compareBaseline(url, config, baselineId) {
|
|
|
166
213
|
if (beforeStr !== afterStr) {
|
|
167
214
|
changedFields.push(cmp.field);
|
|
168
215
|
details[cmp.field] = { before: cmp.before, after: cmp.after };
|
|
216
|
+
fieldSeverity[cmp.field] = cmp.severityHint;
|
|
217
|
+
// Detect removed schema blocks (all blocks removed).
|
|
218
|
+
if (cmp.field === "schema") {
|
|
219
|
+
const beforeArr = Array.isArray(cmp.before) ? cmp.before : [];
|
|
220
|
+
const afterArr = Array.isArray(cmp.after) ? cmp.after : [];
|
|
221
|
+
if (beforeArr.length > 0 && afterArr.length === 0) {
|
|
222
|
+
fieldSeverity[cmp.field] = "critical";
|
|
223
|
+
}
|
|
224
|
+
}
|
|
169
225
|
}
|
|
170
226
|
}
|
|
171
227
|
// HTML hash comparison.
|
|
@@ -192,6 +248,7 @@ export async function compareBaseline(url, config, baselineId) {
|
|
|
192
248
|
comparedAt: new Date().toISOString(),
|
|
193
249
|
changedFields,
|
|
194
250
|
details,
|
|
251
|
+
fieldSeverity,
|
|
195
252
|
severity,
|
|
196
253
|
};
|
|
197
254
|
}
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
* Image SEO analyzer.
|
|
3
3
|
*
|
|
4
4
|
* Extracts image data from parsed HTML and flags common SEO issues:
|
|
5
|
-
* missing alt text, missing dimensions, lazy-loading method,
|
|
6
|
-
* format usage
|
|
5
|
+
* missing alt text, missing dimensions, lazy-loading method, modern
|
|
6
|
+
* format usage, responsive coverage, fetchpriority analysis,
|
|
7
|
+
* decorative vs missing alt distinction, aspect ratio checks,
|
|
8
|
+
* and SVG accessibility.
|
|
7
9
|
*/
|
|
8
|
-
import type { ImageAuditResult } from "../types.js";
|
|
9
|
-
|
|
10
|
+
import type { ImageAuditResult, ImageInfo, PageData } from "../types.js";
|
|
11
|
+
import type { SeoConfig } from "../config.js";
|
|
12
|
+
export declare function auditImages(html: string, url?: string, pageData?: PageData): ImageAuditResult;
|
|
13
|
+
export declare function checkImageUrls(images: ImageInfo[], config: SeoConfig): Promise<{
|
|
14
|
+
brokenUrls: string[];
|
|
15
|
+
}>;
|
|
10
16
|
//# sourceMappingURL=images.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/images.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"images.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/images.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAiB9C,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,gBAAgB,CAsK7F;AAED,wBAAsB,cAAc,CAClC,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,EAAE,SAAS,GAChB,OAAO,CAAC;IAAE,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC,CAoCnC"}
|
|
@@ -12,32 +12,130 @@ function getExtension(src) {
|
|
|
12
12
|
return dotIdx >= 0 ? src.slice(dotIdx).toLowerCase() : "";
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
export function auditImages(html, url) {
|
|
16
|
-
const
|
|
17
|
-
const images =
|
|
15
|
+
export function auditImages(html, url, pageData) {
|
|
16
|
+
const pd = pageData ?? parsePage(html, url);
|
|
17
|
+
const images = pd.images;
|
|
18
18
|
let missingAlt = 0;
|
|
19
|
+
let decorativeCount = 0;
|
|
19
20
|
let missingDimensions = 0;
|
|
20
21
|
const lazyCounts = { native: 0, dataSrc: 0, dataLazy: 0, none: 0 };
|
|
21
22
|
let modernFormats = 0;
|
|
22
23
|
const oversized = [];
|
|
24
|
+
let fetchpriorityHighCount = 0;
|
|
25
|
+
const fetchpriorityIssues = [];
|
|
26
|
+
const ratioIssues = [];
|
|
27
|
+
const dimensionHintIssues = [];
|
|
28
|
+
let decorativeWithAltIssues = 0;
|
|
29
|
+
let svgImgMissingAlt = 0;
|
|
30
|
+
let responsiveImages = 0;
|
|
23
31
|
for (const img of images) {
|
|
24
|
-
|
|
32
|
+
// Distinguish decorative (alt="" empty) from truly missing alt
|
|
33
|
+
if (img.isDecorative) {
|
|
34
|
+
decorativeCount++;
|
|
35
|
+
if (img.alt)
|
|
36
|
+
decorativeWithAltIssues++;
|
|
37
|
+
}
|
|
38
|
+
else if (!img.alt) {
|
|
25
39
|
missingAlt++;
|
|
40
|
+
}
|
|
26
41
|
if (!img.width || !img.height)
|
|
27
42
|
missingDimensions++;
|
|
28
43
|
lazyCounts[img.lazyMethod]++;
|
|
29
44
|
const ext = getExtension(img.src);
|
|
30
45
|
if (MODERN_FORMATS.includes(ext))
|
|
31
46
|
modernFormats++;
|
|
47
|
+
if (img.isSvg && !img.alt)
|
|
48
|
+
svgImgMissingAlt++;
|
|
32
49
|
// Flag potential oversized images (large numeric dimensions).
|
|
33
50
|
const w = img.width ? Number.parseInt(img.width, 10) : 0;
|
|
34
51
|
const h = img.height ? Number.parseInt(img.height, 10) : 0;
|
|
35
52
|
if (w > 2000 || h > 2000) {
|
|
36
53
|
oversized.push(img.src);
|
|
37
54
|
}
|
|
55
|
+
if (w > 0 && h > 0 && (w / h > 4 || h / w > 4)) {
|
|
56
|
+
dimensionHintIssues.push(`Extreme dimension ratio (${w}x${h}): ${img.src}`);
|
|
57
|
+
}
|
|
58
|
+
if ((!img.width || !img.height) && !img.srcset && !img.isInPicture) {
|
|
59
|
+
dimensionHintIssues.push(`Missing dimension hints (no width/height or srcset): ${img.src}`);
|
|
60
|
+
}
|
|
61
|
+
// fetchpriority analysis
|
|
62
|
+
if (img.fetchpriority === "high") {
|
|
63
|
+
fetchpriorityHighCount++;
|
|
64
|
+
if (w > 0 && h > 0 && (w < 100 || h < 100)) {
|
|
65
|
+
fetchpriorityIssues.push(`fetchpriority="high" on small image (${w}x${h}): ${img.src}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Aspect ratio checks
|
|
69
|
+
if (w > 0 && h > 0) {
|
|
70
|
+
const ratio = w / h;
|
|
71
|
+
if (ratio < 0.3 || ratio > 3.0) {
|
|
72
|
+
ratioIssues.push(`Extreme aspect ratio (${ratio.toFixed(2)}): ${img.src} (${w}x${h})`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Responsive coverage
|
|
76
|
+
if (img.srcset || img.sizes || img.isInPicture) {
|
|
77
|
+
responsiveImages++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Decoding attribute analysis
|
|
81
|
+
const decodingStats = { async: 0, sync: 0, auto: 0, none: 0 };
|
|
82
|
+
for (const img of images) {
|
|
83
|
+
if (img.decoding === "async")
|
|
84
|
+
decodingStats.async++;
|
|
85
|
+
else if (img.decoding === "sync")
|
|
86
|
+
decodingStats.sync++;
|
|
87
|
+
else if (img.decoding === "auto")
|
|
88
|
+
decodingStats.auto++;
|
|
89
|
+
else
|
|
90
|
+
decodingStats.none++;
|
|
91
|
+
}
|
|
92
|
+
// No LCP image prioritized when many images exist
|
|
93
|
+
if (images.length > 5 && fetchpriorityHighCount === 0) {
|
|
94
|
+
fetchpriorityIssues.push(`No LCP image prioritized with fetchpriority="high"`);
|
|
95
|
+
}
|
|
96
|
+
const responsiveCoverage = images.length > 0
|
|
97
|
+
? Math.round((responsiveImages / images.length) * 100)
|
|
98
|
+
: 100;
|
|
99
|
+
// Base score (existing formula using only truly missing alt)
|
|
100
|
+
const baseIssueCount = missingAlt + missingDimensions + oversized.length;
|
|
101
|
+
const baseScore = images.length === 0
|
|
102
|
+
? 100
|
|
103
|
+
: Math.max(0, Math.round(100 - (baseIssueCount / (images.length * 3)) * 100));
|
|
104
|
+
// Adjustments
|
|
105
|
+
let adjustedScore = baseScore;
|
|
106
|
+
// Penalize extreme aspect ratios
|
|
107
|
+
if (ratioIssues.length > 0) {
|
|
108
|
+
adjustedScore = Math.max(0, adjustedScore - Math.min(10, ratioIssues.length * 2));
|
|
109
|
+
}
|
|
110
|
+
// Penalize SVG accessibility issues
|
|
111
|
+
const svgIssues = pd.svgMissingTitle ?? 0;
|
|
112
|
+
if (svgIssues > 0) {
|
|
113
|
+
// Cap SVG penalty at 15 — many inline icon SVGs are intentional
|
|
114
|
+
adjustedScore = Math.max(0, adjustedScore - Math.min(15, svgIssues * 3));
|
|
115
|
+
}
|
|
116
|
+
// Bonus for good responsive coverage
|
|
117
|
+
if (responsiveCoverage > 75) {
|
|
118
|
+
adjustedScore = Math.min(100, adjustedScore + 5);
|
|
119
|
+
}
|
|
120
|
+
// Bonus for proper fetchpriority usage
|
|
121
|
+
if (fetchpriorityHighCount >= 1) {
|
|
122
|
+
adjustedScore = Math.min(100, adjustedScore + 3);
|
|
123
|
+
}
|
|
124
|
+
// Bonus for decoding='async' adoption
|
|
125
|
+
const totalWithDecoding = decodingStats.async + decodingStats.sync + decodingStats.auto + decodingStats.none;
|
|
126
|
+
if (totalWithDecoding > 0 && decodingStats.async / totalWithDecoding > 0.5) {
|
|
127
|
+
adjustedScore = Math.min(100, adjustedScore + 2);
|
|
128
|
+
}
|
|
129
|
+
else if (decodingStats.async > 0) {
|
|
130
|
+
adjustedScore = Math.min(100, adjustedScore + 1);
|
|
131
|
+
}
|
|
132
|
+
// Penalize SVG <img> missing alt
|
|
133
|
+
if (svgImgMissingAlt > 0) {
|
|
134
|
+
adjustedScore = Math.max(0, adjustedScore - Math.min(10, svgImgMissingAlt * 2));
|
|
38
135
|
}
|
|
39
136
|
return {
|
|
40
|
-
url: url ??
|
|
137
|
+
url: url ?? pd.url,
|
|
138
|
+
score: adjustedScore,
|
|
41
139
|
totalImages: images.length,
|
|
42
140
|
missingAlt,
|
|
43
141
|
missingDimensions,
|
|
@@ -45,5 +143,55 @@ export function auditImages(html, url) {
|
|
|
45
143
|
modernFormats,
|
|
46
144
|
oversized,
|
|
47
145
|
images,
|
|
146
|
+
pictureCount: pd.pictureCount,
|
|
147
|
+
svgCount: pd.svgCount,
|
|
148
|
+
svgMissingTitle: pd.svgMissingTitle,
|
|
149
|
+
responsiveImages,
|
|
150
|
+
responsiveCoverage,
|
|
151
|
+
fetchpriorityHighCount,
|
|
152
|
+
fetchpriorityIssues: fetchpriorityIssues.length > 0 ? fetchpriorityIssues : undefined,
|
|
153
|
+
decorativeCount,
|
|
154
|
+
ratioIssues: ratioIssues.length > 0 ? ratioIssues : undefined,
|
|
155
|
+
decodingStats,
|
|
156
|
+
svgImgMissingAlt: svgImgMissingAlt > 0 ? svgImgMissingAlt : undefined,
|
|
157
|
+
dimensionHintIssues: dimensionHintIssues.length > 0 ? dimensionHintIssues : undefined,
|
|
158
|
+
decorativeWithAltIssues: decorativeWithAltIssues > 0 ? decorativeWithAltIssues : undefined,
|
|
48
159
|
};
|
|
49
160
|
}
|
|
161
|
+
export async function checkImageUrls(images, config) {
|
|
162
|
+
const MAX_IMAGES = 20;
|
|
163
|
+
const CONCURRENCY = 5;
|
|
164
|
+
const TIMEOUT_MS = 5000;
|
|
165
|
+
const brokenUrls = [];
|
|
166
|
+
const targetImages = images.slice(0, MAX_IMAGES);
|
|
167
|
+
for (let i = 0; i < targetImages.length; i += CONCURRENCY) {
|
|
168
|
+
const batch = targetImages.slice(i, i + CONCURRENCY);
|
|
169
|
+
const results = await Promise.allSettled(batch.map(async (img) => {
|
|
170
|
+
const controller = new AbortController();
|
|
171
|
+
const timeoutId = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(img.src, {
|
|
174
|
+
method: "HEAD",
|
|
175
|
+
signal: controller.signal,
|
|
176
|
+
headers: { "User-Agent": config.userAgent },
|
|
177
|
+
});
|
|
178
|
+
if (res.status < 200 || res.status >= 400) {
|
|
179
|
+
return { url: img.src, broken: true };
|
|
180
|
+
}
|
|
181
|
+
return { url: img.src, broken: false };
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return { url: img.src, broken: true };
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
clearTimeout(timeoutId);
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
190
|
+
for (const result of results) {
|
|
191
|
+
if (result.status === "fulfilled" && result.value.broken) {
|
|
192
|
+
brokenUrls.push(result.value.url);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return { brokenUrls };
|
|
197
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { PerformanceResult, PageWeightResult, RenderBlockingResult, FontAuditResult, FetchResult } from "../types.js";
|
|
2
|
-
export declare function calculatePerformanceFromFetch(fetchResult: FetchResult, url: string): PerformanceResult;
|
|
3
|
-
export declare function analyzePageWeight(fetchResult: FetchResult, url: string): PageWeightResult;
|
|
1
|
+
import type { MeasuredPerformance, PerformanceResult, PageWeightResult, RenderBlockingResult, FontAuditResult, FetchResult } from "../types.js";
|
|
2
|
+
export declare function calculatePerformanceFromFetch(fetchResult: FetchResult, url: string, measuredPerformance?: MeasuredPerformance): PerformanceResult;
|
|
3
|
+
export declare function analyzePageWeight(fetchResult: FetchResult, url: string, measuredPerformance?: MeasuredPerformance): PageWeightResult;
|
|
4
4
|
export declare function analyzeRenderBlocking(fetchResult: FetchResult, url: string): RenderBlockingResult;
|
|
5
5
|
export declare function analyzeFonts(fetchResult: FetchResult, url: string): FontAuditResult;
|
|
6
6
|
//# sourceMappingURL=performance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"performance.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/performance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"performance.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/performance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAShJ,wBAAgB,6BAA6B,CAC3C,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,MAAM,EACX,mBAAmB,CAAC,EAAE,mBAAmB,GACxC,iBAAiB,CA8GnB;AA8CD,wBAAgB,iBAAiB,CAC/B,WAAW,EAAE,WAAW,EACxB,GAAG,EAAE,MAAM,EACX,mBAAmB,CAAC,EAAE,mBAAmB,GACxC,gBAAgB,CA4ElB;AAED,wBAAgB,qBAAqB,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,oBAAoB,CAgDjG;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,WAAW,EAAE,GAAG,EAAE,MAAM,GAAG,eAAe,CAyEnF"}
|
|
@@ -4,10 +4,39 @@ const CLS_GOOD = 0.1;
|
|
|
4
4
|
const CLS_POOR = 0.25;
|
|
5
5
|
const TBT_GOOD = 200;
|
|
6
6
|
const TBT_POOR = 600;
|
|
7
|
-
export function calculatePerformanceFromFetch(fetchResult, url) {
|
|
7
|
+
export function calculatePerformanceFromFetch(fetchResult, url, measuredPerformance) {
|
|
8
8
|
const content = fetchResult.content;
|
|
9
9
|
const headers = fetchResult.headers;
|
|
10
|
-
//
|
|
10
|
+
// If real browser measurements are available, use them.
|
|
11
|
+
if (measuredPerformance) {
|
|
12
|
+
const lcp = measuredPerformance.lcp ?? { value: 0, rating: "poor" };
|
|
13
|
+
const cls = measuredPerformance.cls ?? { value: 0, rating: "good" };
|
|
14
|
+
const tbt = measuredPerformance.tbt ?? { value: 0, rating: "good" };
|
|
15
|
+
const fcp = measuredPerformance.fcp ?? { value: 0, rating: "poor" };
|
|
16
|
+
const ttfb = measuredPerformance.ttfb ?? { value: 0, rating: "poor" };
|
|
17
|
+
const score = calculateCWVScore(lcp.rating, cls.rating, tbt.rating);
|
|
18
|
+
const result = {
|
|
19
|
+
url,
|
|
20
|
+
lcp: { value: lcp.value, rating: lcp.rating, element: lcp.element },
|
|
21
|
+
cls: { value: cls.value, rating: cls.rating },
|
|
22
|
+
tbt: { value: tbt.value, rating: tbt.rating },
|
|
23
|
+
fcp: { value: fcp.value, rating: fcp.rating },
|
|
24
|
+
ttfb: { value: ttfb.value, rating: ttfb.rating },
|
|
25
|
+
score,
|
|
26
|
+
source: "lab",
|
|
27
|
+
disclaimer: "Lab data measured in a single synthetic page load. This is not real-user (RUM) field data from Chrome UX Report.",
|
|
28
|
+
navigationTiming: measuredPerformance.navigationTiming,
|
|
29
|
+
longTasks: measuredPerformance.longTasks,
|
|
30
|
+
failedRequests: measuredPerformance.failedRequests,
|
|
31
|
+
layoutShiftCount: measuredPerformance.layoutShiftCount,
|
|
32
|
+
layoutShiftSources: measuredPerformance.layoutShiftSources,
|
|
33
|
+
};
|
|
34
|
+
if (measuredPerformance.navigationTiming) {
|
|
35
|
+
result.navigationTimingRatings = rateNavTiming(measuredPerformance.navigationTiming);
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
}
|
|
39
|
+
// Heuristic fallback: estimate metrics from HTML structure.
|
|
11
40
|
const htmlSize = fetchResult.contentLengthBytes;
|
|
12
41
|
const ttfbEstimate = estimateTTFB(headers);
|
|
13
42
|
// Count resources to estimate complexity
|
|
@@ -71,16 +100,31 @@ export function calculatePerformanceFromFetch(fetchResult, url) {
|
|
|
71
100
|
else
|
|
72
101
|
tbtRating = "poor";
|
|
73
102
|
// FCP estimate
|
|
74
|
-
const
|
|
103
|
+
const fcpValue = Math.round(ttfbEstimate + 200 + Math.random() * 300);
|
|
104
|
+
const fcpRating = fcpValue < 1800 ? "good" : fcpValue < 3000 ? "needs-improvement" : "poor";
|
|
105
|
+
const ttfbRating = ttfbEstimate < 800 ? "good" : ttfbEstimate < 1800 ? "needs-improvement" : "poor";
|
|
75
106
|
const score = calculateCWVScore(lcpRating, clsRating, tbtRating);
|
|
76
107
|
return {
|
|
77
108
|
url,
|
|
78
109
|
lcp: { value: lcpEstimate, rating: lcpRating },
|
|
79
110
|
cls: { value: Number(clsEstimate.toFixed(3)), rating: clsRating },
|
|
80
111
|
tbt: { value: tbtEstimate, rating: tbtRating },
|
|
81
|
-
fcp,
|
|
82
|
-
ttfb: ttfbEstimate,
|
|
112
|
+
fcp: { value: fcpValue, rating: fcpRating },
|
|
113
|
+
ttfb: { value: ttfbEstimate, rating: ttfbRating },
|
|
83
114
|
score,
|
|
115
|
+
source: "heuristic",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function rateNavTiming(nav) {
|
|
119
|
+
if (!nav)
|
|
120
|
+
return undefined;
|
|
121
|
+
const rate = (v, good, poor) => v < good ? "good" : v < poor ? "needs-improvement" : "poor";
|
|
122
|
+
return {
|
|
123
|
+
dns: { value: nav.dnsMs, rating: rate(nav.dnsMs, 100, 500) },
|
|
124
|
+
tcp: { value: nav.tcpMs, rating: rate(nav.tcpMs, 100, 500) },
|
|
125
|
+
ssl: { value: nav.sslMs, rating: rate(nav.sslMs, 100, 500) },
|
|
126
|
+
ttfb: { value: nav.ttfbMs, rating: rate(nav.ttfbMs, 800, 1800) },
|
|
127
|
+
download: { value: nav.downloadMs, rating: rate(nav.downloadMs, 500, 2000) },
|
|
84
128
|
};
|
|
85
129
|
}
|
|
86
130
|
function estimateTTFB(headers) {
|
|
@@ -109,7 +153,34 @@ function calculateCWVScore(lcp, cls, tbt) {
|
|
|
109
153
|
score -= 25;
|
|
110
154
|
return Math.max(0, score);
|
|
111
155
|
}
|
|
112
|
-
export function analyzePageWeight(fetchResult, url) {
|
|
156
|
+
export function analyzePageWeight(fetchResult, url, measuredPerformance) {
|
|
157
|
+
// If real browser measurements are available, use them for resource data.
|
|
158
|
+
if (measuredPerformance && (measuredPerformance.totalTransferKb !== undefined || measuredPerformance.topHeavyResources)) {
|
|
159
|
+
const totalKb = measuredPerformance.totalTransferKb ?? 0;
|
|
160
|
+
const resourceCount = measuredPerformance.resourceCount ?? 0;
|
|
161
|
+
const heavyResources = (measuredPerformance.topHeavyResources ?? []).map((r) => ({
|
|
162
|
+
url: r.url,
|
|
163
|
+
sizeKb: Math.round(r.transferSize / 1024),
|
|
164
|
+
type: r.type,
|
|
165
|
+
}));
|
|
166
|
+
// HTML size still comes from fetch result.
|
|
167
|
+
const htmlKb = Math.round(fetchResult.contentLengthBytes / 1024);
|
|
168
|
+
// Categorize by type from heavy resources list.
|
|
169
|
+
const byType = (type) => heavyResources.filter((r) => r.type === type).reduce((s, r) => s + r.sizeKb, 0);
|
|
170
|
+
return {
|
|
171
|
+
url: fetchResult.finalUrl || url,
|
|
172
|
+
totalKb,
|
|
173
|
+
htmlKb,
|
|
174
|
+
cssKb: byType("link"),
|
|
175
|
+
jsKb: byType("script"),
|
|
176
|
+
imageKb: byType("img"),
|
|
177
|
+
fontKb: byType("font"),
|
|
178
|
+
otherKb: 0,
|
|
179
|
+
resourceCount,
|
|
180
|
+
topHeavyResources: heavyResources,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
// Heuristic fallback.
|
|
113
184
|
const content = fetchResult.content;
|
|
114
185
|
const htmlKb = Math.round(fetchResult.contentLengthBytes / 1024);
|
|
115
186
|
// Estimate CSS size
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"readability.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/readability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"readability.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/readability.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAgDrD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CA0DhF;AAED,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAqBtE"}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
const ABBREVIATIONS = new Set([
|
|
2
|
+
"mr", "mrs", "ms", "dr", "prof", "sr", "jr", "st", "ave", "blvd",
|
|
3
|
+
"etc", "vs", "approx", "dept", "est", "eg", "ie", "inc", "ltd",
|
|
4
|
+
"co", "corp", "jan", "feb", "mar", "apr", "jun", "jul", "aug",
|
|
5
|
+
"sep", "oct", "nov", "dec",
|
|
6
|
+
]);
|
|
1
7
|
function countSentences(text) {
|
|
2
|
-
|
|
3
|
-
|
|
8
|
+
if (!text)
|
|
9
|
+
return 1;
|
|
10
|
+
let processed = text;
|
|
11
|
+
for (const abbr of ABBREVIATIONS) {
|
|
12
|
+
const escaped = abbr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
13
|
+
const re = new RegExp(`\\b${escaped}\\.`, "gi");
|
|
14
|
+
processed = processed.replace(re, (match) => match.replace(".", "##DOT##"));
|
|
15
|
+
}
|
|
16
|
+
const sentences = processed.split(/(?<=[.!?])\s+(?=[A-Z])/);
|
|
17
|
+
const count = sentences.length;
|
|
18
|
+
return count > 0 ? count : 1;
|
|
4
19
|
}
|
|
5
20
|
function countSyllables(word) {
|
|
6
21
|
const lower = word.toLowerCase().replace(/[^a-z]/g, "");
|
|
@@ -16,10 +31,8 @@ function countSyllables(word) {
|
|
|
16
31
|
}
|
|
17
32
|
prevWasVowel = isVowel;
|
|
18
33
|
}
|
|
19
|
-
// Silent e at end
|
|
20
34
|
if (lower.endsWith("e") && count > 1)
|
|
21
35
|
count--;
|
|
22
|
-
// le at end adds a syllable
|
|
23
36
|
if (lower.endsWith("le") && lower.length > 3)
|
|
24
37
|
count++;
|
|
25
38
|
return Math.max(1, count);
|
|
@@ -59,17 +72,12 @@ export function analyzeReadability(text, url) {
|
|
|
59
72
|
if (isComplexWord(clean))
|
|
60
73
|
complexWordCount++;
|
|
61
74
|
}
|
|
62
|
-
// Flesch Reading Ease
|
|
63
75
|
const fleschReadingEase = Number((206.835 - 1.015 * (wordCount / sentenceCount) - 84.6 * (totalSyllables / wordCount)).toFixed(1));
|
|
64
|
-
// Flesch-Kincaid Grade Level
|
|
65
76
|
const fleschKincaid = Number((0.39 * (wordCount / sentenceCount) + 11.8 * (totalSyllables / wordCount) - 15.59).toFixed(1));
|
|
66
|
-
// SMOG Index
|
|
67
77
|
const smog = complexWordCount > 0
|
|
68
78
|
? Number((1.043 * Math.sqrt(complexWordCount * (30 / sentenceCount)) + 3.1291).toFixed(1))
|
|
69
79
|
: 0;
|
|
70
|
-
// Automated Readability Index (ARI)
|
|
71
80
|
const ari = Number((4.71 * (totalLetters / wordCount) + 0.5 * (wordCount / sentenceCount) - 21.43).toFixed(1));
|
|
72
|
-
// Coleman-Liau Index
|
|
73
81
|
const L = (totalLetters / wordCount) * 100;
|
|
74
82
|
const S = (sentenceCount / wordCount) * 100;
|
|
75
83
|
const colemanLiau = Number((0.0588 * L - 0.296 * S - 15.8).toFixed(1));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/analyzers/schema.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAE,iBAAiB,EAAe,MAAM,aAAa,CAAC;AAgKlE,qCAAqC;AACrC,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,GAAG,iBAAiB,CAmFzE;AAID,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,cAAc,GAAG,eAAe,GAAG,SAAS,GAAG,aAAa,GAAG,SAAS,GAC5E,gBAAgB,GAAG,SAAS,GAAG,QAAQ,GAAG,aAAa,GAAG,wBAAwB,GAClF,aAAa,GAAG,aAAa,CAAC;IAChC,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB;AAED,sDAAsD;AACtD,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;CAAE,CAsMlH"}
|