@wordbricks/playwright-mcp 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/cli-wrapper.js +15 -14
  2. package/cli.js +1 -1
  3. package/config.d.ts +11 -6
  4. package/index.d.ts +7 -5
  5. package/index.js +1 -1
  6. package/package.json +34 -57
  7. package/LICENSE +0 -202
  8. package/lib/browserContextFactory.js +0 -326
  9. package/lib/browserServerBackend.js +0 -84
  10. package/lib/config.js +0 -286
  11. package/lib/context.js +0 -309
  12. package/lib/extension/cdpRelay.js +0 -346
  13. package/lib/extension/extensionContextFactory.js +0 -56
  14. package/lib/frameworkPatterns.js +0 -35
  15. package/lib/hooks/antiBotDetectionHook.js +0 -171
  16. package/lib/hooks/core.js +0 -144
  17. package/lib/hooks/eventConsumer.js +0 -52
  18. package/lib/hooks/events.js +0 -42
  19. package/lib/hooks/formatToolCallEvent.js +0 -16
  20. package/lib/hooks/frameworkStateHook.js +0 -182
  21. package/lib/hooks/grouping.js +0 -72
  22. package/lib/hooks/jsonLdDetectionHook.js +0 -175
  23. package/lib/hooks/networkFilters.js +0 -82
  24. package/lib/hooks/networkSetup.js +0 -59
  25. package/lib/hooks/networkTrackingHook.js +0 -67
  26. package/lib/hooks/pageHeightHook.js +0 -75
  27. package/lib/hooks/registry.js +0 -42
  28. package/lib/hooks/requireTabHook.js +0 -26
  29. package/lib/hooks/schema.js +0 -89
  30. package/lib/hooks/waitHook.js +0 -33
  31. package/lib/index.js +0 -39
  32. package/lib/mcp/inProcessTransport.js +0 -72
  33. package/lib/mcp/proxyBackend.js +0 -115
  34. package/lib/mcp/server.js +0 -86
  35. package/lib/mcp/tool.js +0 -38
  36. package/lib/mcp/transport.js +0 -181
  37. package/lib/playwrightTransformer.js +0 -497
  38. package/lib/program.js +0 -110
  39. package/lib/response.js +0 -186
  40. package/lib/sessionLog.js +0 -121
  41. package/lib/tab.js +0 -249
  42. package/lib/tools/common.js +0 -55
  43. package/lib/tools/console.js +0 -33
  44. package/lib/tools/dialogs.js +0 -47
  45. package/lib/tools/evaluate.js +0 -53
  46. package/lib/tools/extractFrameworkState.js +0 -214
  47. package/lib/tools/files.js +0 -45
  48. package/lib/tools/form.js +0 -57
  49. package/lib/tools/getSnapshot.js +0 -37
  50. package/lib/tools/getVisibleHtml.js +0 -52
  51. package/lib/tools/install.js +0 -51
  52. package/lib/tools/keyboard.js +0 -78
  53. package/lib/tools/mouse.js +0 -99
  54. package/lib/tools/navigate.js +0 -70
  55. package/lib/tools/network.js +0 -123
  56. package/lib/tools/networkDetail.js +0 -229
  57. package/lib/tools/networkSearch/bodySearch.js +0 -147
  58. package/lib/tools/networkSearch/grouping.js +0 -28
  59. package/lib/tools/networkSearch/helpers.js +0 -32
  60. package/lib/tools/networkSearch/searchHtml.js +0 -67
  61. package/lib/tools/networkSearch/types.js +0 -1
  62. package/lib/tools/networkSearch/urlSearch.js +0 -82
  63. package/lib/tools/networkSearch.js +0 -268
  64. package/lib/tools/pdf.js +0 -40
  65. package/lib/tools/repl.js +0 -402
  66. package/lib/tools/screenshot.js +0 -79
  67. package/lib/tools/scroll.js +0 -126
  68. package/lib/tools/snapshot.js +0 -144
  69. package/lib/tools/tabs.js +0 -59
  70. package/lib/tools/tool.js +0 -33
  71. package/lib/tools/utils.js +0 -74
  72. package/lib/tools/wait.js +0 -55
  73. package/lib/tools.js +0 -67
  74. package/lib/utils/adBlockFilter.js +0 -87
  75. package/lib/utils/codegen.js +0 -51
  76. package/lib/utils/extensionPath.js +0 -10
  77. package/lib/utils/fileUtils.js +0 -36
  78. package/lib/utils/graphql.js +0 -258
  79. package/lib/utils/guid.js +0 -22
  80. package/lib/utils/httpServer.js +0 -39
  81. package/lib/utils/log.js +0 -21
  82. package/lib/utils/manualPromise.js +0 -111
  83. package/lib/utils/networkFormat.js +0 -12
  84. package/lib/utils/package.js +0 -20
  85. package/lib/utils/result.js +0 -2
  86. package/lib/utils/sanitizeHtml.js +0 -98
  87. package/lib/utils/truncate.js +0 -103
  88. package/lib/utils/withTimeout.js +0 -7
  89. package/src/index.ts +0 -50
@@ -1,258 +0,0 @@
1
- import { pipe, filter, join } from '@fxts/core';
2
- const getString = (v) => typeof v === 'string' ? v : undefined;
3
- const hasPersistedQuery = (v) => {
4
- return isPlainObject(v) && 'persistedQuery' in v;
5
- };
6
- const readGraphQLishBody = (v) => {
7
- if (!isPlainObject(v))
8
- return undefined;
9
- const q = getString(v['query']);
10
- const variables = v['variables'];
11
- const operationName = getString(v['operationName']);
12
- const hasPersisted = hasPersistedQuery(v['extensions']);
13
- if (!q && !hasPersisted && !operationName)
14
- return undefined;
15
- return { query: q, variables, operationName, hasPersisted };
16
- };
17
- export const isPlainObject = (value) => {
18
- return !!value && typeof value === 'object' && !Array.isArray(value);
19
- };
20
- function getHeader(headers, name, altName) {
21
- if (!headers)
22
- return undefined;
23
- const v1 = headers[name];
24
- if (typeof v1 === 'string')
25
- return v1;
26
- if (Array.isArray(v1))
27
- return v1.join(', ');
28
- if (altName) {
29
- const v2 = headers[altName];
30
- if (typeof v2 === 'string')
31
- return v2;
32
- if (Array.isArray(v2))
33
- return v2.join(', ');
34
- }
35
- return undefined;
36
- }
37
- export const safeJSONParse = (text) => {
38
- if (!text)
39
- return undefined;
40
- try {
41
- return JSON.parse(text);
42
- }
43
- catch {
44
- return undefined;
45
- }
46
- };
47
- const isOperationType = (s) => {
48
- return s === 'query' || s === 'mutation' || s === 'subscription';
49
- };
50
- const parseOperationFromQuery = (query) => {
51
- if (!query)
52
- return { type: 'unknown' };
53
- const trimmed = query.trim().replace(/^#.*$/gm, '').trim();
54
- if (!trimmed)
55
- return { type: 'unknown' };
56
- if (trimmed.startsWith('{'))
57
- return { type: 'query' };
58
- const m = /^(query|mutation|subscription)\b\s*([A-Za-z_][A-Za-z0-9_]*)?/i.exec(trimmed);
59
- if (m) {
60
- const maybe = m[1].toLowerCase();
61
- const type = isOperationType(maybe) ? maybe : 'unknown';
62
- const name = m[2] || undefined;
63
- return { type, name };
64
- }
65
- return { type: 'unknown' };
66
- };
67
- export const parseGraphQLRequestFromHttp = (method, url, headersIn, bodyText) => {
68
- const contentType = getHeader(headersIn, 'content-type', 'Content-Type') || '';
69
- const u = new URL(url, 'http://dummy'); // base to satisfy URL if relative
70
- let query;
71
- let variables;
72
- let operationName;
73
- let isPersistedQuery = false;
74
- if ((method || 'GET').toUpperCase() === 'GET') {
75
- const params = u.searchParams;
76
- const queryParam = params.get('query');
77
- const opNameParam = params.get('operationName') ?? undefined;
78
- const vars = params.get('variables');
79
- const ext = params.get('extensions');
80
- const extObj = ext ? safeJSONParse(ext) : undefined;
81
- const hasPersisted = isPlainObject(extObj) && 'persistedQuery' in extObj;
82
- if (queryParam) {
83
- // Only treat as GraphQL if the query actually looks like GraphQL
84
- const parsedOp = parseOperationFromQuery(queryParam);
85
- if (parsedOp.type !== 'unknown') {
86
- query = queryParam;
87
- operationName = opNameParam;
88
- if (vars)
89
- variables = safeJSONParse(vars);
90
- }
91
- }
92
- else if (hasPersisted) {
93
- // Accept persisted queries even without a `query` param
94
- isPersistedQuery = true;
95
- operationName = opNameParam;
96
- if (vars)
97
- variables = safeJSONParse(vars);
98
- }
99
- }
100
- else {
101
- // POST/others
102
- if (contentType.includes('application/json') ||
103
- contentType.includes('application/graphql') ||
104
- contentType.includes('application/x-www-form-urlencoded') ||
105
- typeof bodyText === 'string') {
106
- const bodyObj = safeJSONParse(bodyText || '');
107
- const parsed = readGraphQLishBody(bodyObj);
108
- if (parsed) {
109
- if (parsed.query) {
110
- const parsedOp = parseOperationFromQuery(parsed.query);
111
- if (parsedOp.type !== 'unknown') {
112
- query = parsed.query;
113
- variables = parsed.variables;
114
- operationName = parsed.operationName;
115
- }
116
- }
117
- else if (parsed.hasPersisted) {
118
- isPersistedQuery = true;
119
- variables = parsed.variables;
120
- operationName = parsed.operationName;
121
- }
122
- }
123
- }
124
- }
125
- if (!query && !isPersistedQuery)
126
- return undefined;
127
- const { type: operationType, name } = parseOperationFromQuery(query);
128
- if (operationName && !name) {
129
- // Respect provided operationName when query is anonymous
130
- return { operationType, operationName, query, variables, isPersistedQuery };
131
- }
132
- return { operationType, operationName: name ?? operationName, query, variables, isPersistedQuery };
133
- };
134
- export const summarizeGraphQL = (parsed) => {
135
- const type = parsed.operationType === 'unknown' ? 'operation' : parsed.operationType;
136
- return pipe([
137
- parsed.operationName ? `${type} ${parsed.operationName}` : type,
138
- ], filter((v) => typeof v === 'string'), join(' '));
139
- };
140
- export const extractGraphQLResponseInfo = (bodyText) => {
141
- const json = safeJSONParse(bodyText || '');
142
- if (!isPlainObject(json))
143
- return undefined;
144
- const errorsVal = json['errors'];
145
- const dataVal = json['data'];
146
- const hasGraphQLShape = Array.isArray(errorsVal) || isPlainObject(dataVal);
147
- if (!hasGraphQLShape)
148
- return undefined;
149
- const errors = Array.isArray(errorsVal) ? errorsVal : [];
150
- const dataKeys = isPlainObject(dataVal) ? Object.keys(dataVal) : [];
151
- const topMessages = errors.slice(0, 3).map((e) => {
152
- if (isPlainObject(e)) {
153
- const m = e['message'];
154
- if (typeof m === 'string')
155
- return m;
156
- }
157
- try {
158
- return JSON.stringify(e);
159
- }
160
- catch {
161
- return String(e);
162
- }
163
- });
164
- return { hasErrors: errors.length > 0, errorCount: errors.length, topMessages, dataKeys };
165
- };
166
- /**
167
- * Minify a GraphQL operation string by removing comments and unnecessary whitespace,
168
- * while preserving string literals and required token separators.
169
- */
170
- export const minifyGraphQLQuery = (input) => {
171
- let out = '';
172
- let i = 0;
173
- const len = input.length;
174
- let inString = null;
175
- let escaped = false;
176
- let pendingSpace = false;
177
- const isIdent = (ch) => /[A-Za-z0-9_]/.test(ch);
178
- const isWhitespace = (ch) => ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t' || ch === '\f';
179
- const isPunct = (ch) => '{}()[]:!@$,=|&.'.includes(ch);
180
- while (i < len) {
181
- const ch = input[i];
182
- if (inString) {
183
- out += ch;
184
- if (escaped)
185
- escaped = false;
186
- else if (ch === '\\')
187
- escaped = true;
188
- else if (ch === inString)
189
- inString = null;
190
- i++;
191
- continue;
192
- }
193
- // Not inside string
194
- if (ch === '"' || ch === '\'') {
195
- // flush pending space
196
- if (pendingSpace) {
197
- out += ' ';
198
- pendingSpace = false;
199
- }
200
- inString = ch;
201
- out += ch;
202
- i++;
203
- continue;
204
- }
205
- // Line comment start (# ... end of line)
206
- if (ch === '#') {
207
- // Skip until end of line
208
- while (i < len && input[i] !== '\n')
209
- i++;
210
- continue;
211
- }
212
- // Whitespace handling
213
- if (isWhitespace(ch)) {
214
- // Defer deciding about a space until we see the next significant char
215
- pendingSpace = true;
216
- i++;
217
- continue;
218
- }
219
- // Punctuation: never needs surrounding spaces
220
- if (isPunct(ch)) {
221
- out += ch;
222
- pendingSpace = false;
223
- i++;
224
- continue;
225
- }
226
- // ch is part of an identifier/number or other token
227
- if (pendingSpace) {
228
- // Add a space only if the previous output char and current char both look like identifiers
229
- const prev = out[out.length - 1] || '';
230
- if (isIdent(prev) && isIdent(ch))
231
- out += ' ';
232
- pendingSpace = false;
233
- }
234
- out += ch;
235
- i++;
236
- }
237
- return out.trim();
238
- };
239
- /**
240
- * Recursively minify GraphQL query strings within a typical GraphQL request body.
241
- * - If body is an object or array, it returns a new value with any `query` string minimized.
242
- * - Other fields are preserved as-is.
243
- */
244
- export const minifyGraphQLRequestBody = (body) => {
245
- if (Array.isArray(body))
246
- return body.map(item => minifyGraphQLRequestBody(item));
247
- if (isPlainObject(body)) {
248
- const out = {};
249
- for (const [k, v] of Object.entries(body)) {
250
- if (k === 'query' && typeof v === 'string')
251
- out[k] = minifyGraphQLQuery(v);
252
- else
253
- out[k] = minifyGraphQLRequestBody(v);
254
- }
255
- return out;
256
- }
257
- return body;
258
- };
package/lib/utils/guid.js DELETED
@@ -1,22 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import crypto from 'crypto';
17
- export function createGuid() {
18
- return crypto.randomBytes(16).toString('hex');
19
- }
20
- export function createHash(data) {
21
- return crypto.createHash('sha256').update(data).digest('hex').slice(0, 7);
22
- }
@@ -1,39 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import assert from 'assert';
17
- import http from 'http';
18
- export async function startHttpServer(config) {
19
- const { host, port } = config;
20
- const httpServer = http.createServer();
21
- await new Promise((resolve, reject) => {
22
- httpServer.on('error', reject);
23
- httpServer.listen(port, host, () => {
24
- resolve();
25
- httpServer.removeListener('error', reject);
26
- });
27
- });
28
- return httpServer;
29
- }
30
- export function httpAddressToString(address) {
31
- assert(address, 'Could not bind server socket');
32
- if (typeof address === 'string')
33
- return address;
34
- const resolvedPort = address.port;
35
- let resolvedHost = address.family === 'IPv4' ? address.address : `[${address.address}]`;
36
- if (resolvedHost === '0.0.0.0' || resolvedHost === '[::]')
37
- resolvedHost = 'localhost';
38
- return `http://${resolvedHost}:${resolvedPort}`;
39
- }
package/lib/utils/log.js DELETED
@@ -1,21 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import debug from 'debug';
17
- const errorsDebug = debug('pw:mcp:errors');
18
- export function logUnhandledError(error) {
19
- errorsDebug(error);
20
- }
21
- export const testDebug = debug('pw:mcp:test');
@@ -1,111 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- export class ManualPromise extends Promise {
17
- _resolve;
18
- _reject;
19
- _isDone;
20
- constructor() {
21
- let resolve;
22
- let reject;
23
- super((f, r) => {
24
- resolve = f;
25
- reject = r;
26
- });
27
- this._isDone = false;
28
- this._resolve = resolve;
29
- this._reject = reject;
30
- }
31
- isDone() {
32
- return this._isDone;
33
- }
34
- resolve(t) {
35
- this._isDone = true;
36
- this._resolve(t);
37
- }
38
- reject(e) {
39
- this._isDone = true;
40
- this._reject(e);
41
- }
42
- static get [Symbol.species]() {
43
- return Promise;
44
- }
45
- get [Symbol.toStringTag]() {
46
- return 'ManualPromise';
47
- }
48
- }
49
- export class LongStandingScope {
50
- _terminateError;
51
- _closeError;
52
- _terminatePromises = new Map();
53
- _isClosed = false;
54
- reject(error) {
55
- this._isClosed = true;
56
- this._terminateError = error;
57
- for (const p of this._terminatePromises.keys())
58
- p.resolve(error);
59
- }
60
- close(error) {
61
- this._isClosed = true;
62
- this._closeError = error;
63
- for (const [p, frames] of this._terminatePromises)
64
- p.resolve(cloneError(error, frames));
65
- }
66
- isClosed() {
67
- return this._isClosed;
68
- }
69
- static async raceMultiple(scopes, promise) {
70
- return Promise.race(scopes.map(s => s.race(promise)));
71
- }
72
- async race(promise) {
73
- return this._race(Array.isArray(promise) ? promise : [promise], false);
74
- }
75
- async safeRace(promise, defaultValue) {
76
- return this._race([promise], true, defaultValue);
77
- }
78
- async _race(promises, safe, defaultValue) {
79
- const terminatePromise = new ManualPromise();
80
- const frames = captureRawStack();
81
- if (this._terminateError)
82
- terminatePromise.resolve(this._terminateError);
83
- if (this._closeError)
84
- terminatePromise.resolve(cloneError(this._closeError, frames));
85
- this._terminatePromises.set(terminatePromise, frames);
86
- try {
87
- return await Promise.race([
88
- terminatePromise.then(e => safe ? defaultValue : Promise.reject(e)),
89
- ...promises
90
- ]);
91
- }
92
- finally {
93
- this._terminatePromises.delete(terminatePromise);
94
- }
95
- }
96
- }
97
- function cloneError(error, frames) {
98
- const clone = new Error();
99
- clone.name = error.name;
100
- clone.message = error.message;
101
- clone.stack = [error.name + ':' + error.message, ...frames].join('\n');
102
- return clone;
103
- }
104
- function captureRawStack() {
105
- const stackTraceLimit = Error.stackTraceLimit;
106
- Error.stackTraceLimit = 50;
107
- const error = new Error();
108
- const stack = error.stack || '';
109
- Error.stackTraceLimit = stackTraceLimit;
110
- return stack.split('\n');
111
- }
@@ -1,12 +0,0 @@
1
- import { parseGraphQLRequestFromHttp, summarizeGraphQL } from './graphql.js';
2
- import { formatUrlWithTrimmedParams } from '../hooks/networkFilters.js';
3
- export const formatNetworkSummaryLine = (input, opts) => {
4
- const method = (input.method || '').toUpperCase();
5
- const formattedUrl = opts?.trimParams === false ? input.url : formatUrlWithTrimmedParams(input.url);
6
- const st = input.statusText ? ` ${input.statusText}` : '';
7
- let line = `${method} ${formattedUrl} → ${input.status}${st}`;
8
- const gql = parseGraphQLRequestFromHttp(input.method, input.url, input.headers || {}, input.postData ?? undefined);
9
- if (gql)
10
- line += ` [GraphQL: ${summarizeGraphQL(gql)}]`;
11
- return line;
12
- };
@@ -1,20 +0,0 @@
1
- /**
2
- * Copyright (c) Microsoft Corporation.
3
- *
4
- * Licensed under the Apache License, Version 2.0 (the "License");
5
- * you may not use this file except in compliance with the License.
6
- * You may obtain a copy of the License at
7
- *
8
- * http://www.apache.org/licenses/LICENSE-2.0
9
- *
10
- * Unless required by applicable law or agreed to in writing, software
11
- * distributed under the License is distributed on an "AS IS" BASIS,
12
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- * See the License for the specific language governing permissions and
14
- * limitations under the License.
15
- */
16
- import fs from 'fs';
17
- import path from 'path';
18
- import url from 'url';
19
- const __filename = url.fileURLToPath(import.meta.url);
20
- export const packageJSON = JSON.parse(fs.readFileSync(path.join(path.dirname(__filename), '..', '..', 'package.json'), 'utf8'));
@@ -1,2 +0,0 @@
1
- export const Ok = (value) => ({ ok: true, value });
2
- export const Err = (error) => ({ ok: false, error });
@@ -1,98 +0,0 @@
1
- import * as cheerio from 'cheerio';
2
- import { ElementType } from 'domelementtype';
3
- import { pipe, when } from '@fxts/core';
4
- const I18N_PATTERNS = [
5
- /window\.__i18n\s*=/i,
6
- /window\.__translations\s*=/i,
7
- /window\.i18n\s*=/i,
8
- /window\._translations\s*=/i,
9
- /\btranslations\s*:\s*{/i,
10
- /\blocales\s*:\s*{/i,
11
- /\bmessages\s*:\s*{/i,
12
- /\bi18nData\s*=/i,
13
- /\btranslationData\s*=/i,
14
- ];
15
- export const isI18nScript = (content) => {
16
- const text = content || '';
17
- const hasI18nPattern = I18N_PATTERNS.some(pattern => pattern.test(text));
18
- const langCodePattern = /["'](?:[a-z]{2}(?:[_-][A-Z]{2})?|zh-(?:CN|TW|HK)|pt-BR|en-(?:US|GB|CA|AU)|es-(?:ES|MX|AR)|fr-(?:FR|CA)|de-(?:DE|AT|CH))["']\s*:\s*{/g;
19
- const matches = text.match(langCodePattern) || [];
20
- const hasLangCodeStructure = matches.length >= 2;
21
- return hasI18nPattern || hasLangCodeStructure;
22
- };
23
- export const removeI18nScripts = ($) => {
24
- $('script').each((_, el) => {
25
- const content = $(el).html() || '';
26
- if (isI18nScript(content))
27
- $(el).remove();
28
- });
29
- return $;
30
- };
31
- export const removeScriptTags = ($) => {
32
- $('script').remove();
33
- return $;
34
- };
35
- export const removeStyleTags = ($) => {
36
- $('style').remove();
37
- return $;
38
- };
39
- export const NON_ESSENTIAL_RELS = [
40
- 'stylesheet', 'preload', 'prefetch', 'preconnect', 'dns-prefetch',
41
- 'modulepreload', 'icon', 'shortcut icon', 'apple-touch-icon',
42
- 'apple-touch-icon-precomposed', 'manifest', 'pingback', 'prerender',
43
- 'subresource'
44
- ];
45
- export const removeNonEssentialLinks = ($) => {
46
- $('link').each((_, el) => {
47
- const rel = ($(el).attr('rel') || '').toLowerCase();
48
- const type = ($(el).attr('type') || '').toLowerCase();
49
- if (type === 'text/css') {
50
- $(el).remove();
51
- return;
52
- }
53
- if (rel) {
54
- const relValues = rel.split(/\s+/);
55
- const hasNonEssential = relValues.some(value => NON_ESSENTIAL_RELS.includes(value) || (value === 'shortcut' && relValues.includes('icon')));
56
- if (hasNonEssential)
57
- $(el).remove();
58
- }
59
- });
60
- return $;
61
- };
62
- export const removeMetaTags = ($) => {
63
- $('meta').remove();
64
- return $;
65
- };
66
- export const removeHtmlComments = ($) => {
67
- $('*').contents().each((_, node) => {
68
- if (node.type === ElementType.Comment)
69
- $(node).remove();
70
- });
71
- return $;
72
- };
73
- export const SVG_ATTRIBUTES_TO_REMOVE = [
74
- 'xmlns',
75
- 'd',
76
- 'fill', 'stroke', 'stroke-width', 'stroke-linecap', 'stroke-linejoin', 'stroke-dasharray',
77
- 'opacity', 'fill-opacity', 'stroke-opacity',
78
- 'transform', 'rotate', 'scale', 'translate',
79
- 'filter', 'mask', 'clip-path',
80
- 'paint-order', 'vector-effect', 'shape-rendering',
81
- 'gradientUnits', 'gradientTransform', 'patternUnits', 'patternTransform',
82
- 'marker-start', 'marker-mid', 'marker-end',
83
- 'style'
84
- ];
85
- export const stripSvgAttributes = ($) => {
86
- $('svg, svg *').each((_, el) => {
87
- SVG_ATTRIBUTES_TO_REMOVE.forEach(attr => {
88
- $(el).removeAttr(attr);
89
- });
90
- });
91
- return $;
92
- };
93
- export const minifyHtml = (html) => html.replace(/>\s+</g, '><').trim();
94
- export const sanitizeHtml = (html, options) => {
95
- if (!options.shouldRemoveScripts && !options.shouldRemoveStyles)
96
- return html;
97
- return pipe(cheerio.load(html, { xmlMode: false }), when(() => !options.shouldRemoveScripts, removeI18nScripts), when(() => options.shouldRemoveScripts, removeScriptTags), when(() => options.shouldRemoveStyles, removeStyleTags), removeNonEssentialLinks, removeMetaTags, removeHtmlComments, stripSvgAttributes, $ => minifyHtml($.root().html() || ''));
98
- };