lighthouse 10.0.2-dev.20230321 → 10.0.2-dev.20230322
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/core/audits/audit.js +1 -1
- package/core/audits/byte-efficiency/byte-efficiency-audit.d.ts +2 -0
- package/core/audits/byte-efficiency/byte-efficiency-audit.js +5 -1
- package/core/audits/byte-efficiency/unminified-javascript.js +2 -1
- package/core/audits/dobetterweb/doctype.js +3 -2
- package/core/computed/metrics/cumulative-layout-shift.d.ts +3 -3
- package/core/computed/metrics/cumulative-layout-shift.js +21 -9
- package/core/computed/unused-css.js +8 -7
- package/core/gather/driver/prepare.js +15 -0
- package/core/lib/page-functions.d.ts +17 -5
- package/core/lib/page-functions.js +41 -22
- package/core/lib/tracehouse/trace-processor.d.ts +8 -0
- package/core/lib/tracehouse/trace-processor.js +15 -2
- package/core/lib/url-utils.js +1 -1
- package/dist/report/bundle.esm.js +33 -0
- package/dist/report/flow.js +1 -1
- package/dist/report/standalone.js +1 -1
- package/package.json +1 -1
- package/shared/tsconfig.json +1 -1
- package/shared/util.d.ts +6 -0
- package/shared/util.js +33 -0
- package/types/artifacts.d.ts +1 -0
package/core/audits/audit.js
CHANGED
|
@@ -225,7 +225,7 @@ class Audit {
|
|
|
225
225
|
const lineNumber = lineIndex + 1;
|
|
226
226
|
/** @type LH.Audit.Details.SnippetValue['lines'][0] */
|
|
227
227
|
const lineDetail = {
|
|
228
|
-
content:
|
|
228
|
+
content: Util.truncate(line, maxLineLength),
|
|
229
229
|
lineNumber,
|
|
230
230
|
};
|
|
231
231
|
if (line.length > maxLineLength) {
|
|
@@ -26,6 +26,8 @@ export type ByteEfficiencyProduct = {
|
|
|
26
26
|
export class ByteEfficiencyAudit extends Audit {
|
|
27
27
|
/**
|
|
28
28
|
* Creates a score based on the wastedMs value using linear interpolation between control points.
|
|
29
|
+
* A negative wastedMs is scored as 1, assuming time is not being wasted with respect to the
|
|
30
|
+
* opportunity being measured.
|
|
29
31
|
*
|
|
30
32
|
* @param {number} wastedMs
|
|
31
33
|
* @return {number}
|
|
@@ -39,12 +39,14 @@ const WASTED_MS_FOR_SCORE_OF_ZERO = 5000;
|
|
|
39
39
|
class ByteEfficiencyAudit extends Audit {
|
|
40
40
|
/**
|
|
41
41
|
* Creates a score based on the wastedMs value using linear interpolation between control points.
|
|
42
|
+
* A negative wastedMs is scored as 1, assuming time is not being wasted with respect to the
|
|
43
|
+
* opportunity being measured.
|
|
42
44
|
*
|
|
43
45
|
* @param {number} wastedMs
|
|
44
46
|
* @return {number}
|
|
45
47
|
*/
|
|
46
48
|
static scoreForWastedMs(wastedMs) {
|
|
47
|
-
if (wastedMs
|
|
49
|
+
if (wastedMs <= 0) {
|
|
48
50
|
return 1;
|
|
49
51
|
} else if (wastedMs < WASTED_MS_FOR_AVERAGE) {
|
|
50
52
|
return linearInterpolation(0, 1, WASTED_MS_FOR_AVERAGE, 0.75, wastedMs);
|
|
@@ -214,6 +216,8 @@ class ByteEfficiencyAudit extends Audit {
|
|
|
214
216
|
|
|
215
217
|
const wastedBytes = results.reduce((sum, item) => sum + item.wastedBytes, 0);
|
|
216
218
|
|
|
219
|
+
// `wastedMs` may be negative, if making the opportunity change could be detrimental.
|
|
220
|
+
// This is useful information in the LHR and should be preserved.
|
|
217
221
|
let wastedMs;
|
|
218
222
|
if (gatherContext.gatherMode === 'navigation') {
|
|
219
223
|
if (!graph) throw Error('Page dependency graph should always be computed in navigation mode');
|
|
@@ -8,6 +8,7 @@ import {ByteEfficiencyAudit} from './byte-efficiency-audit.js';
|
|
|
8
8
|
import * as i18n from '../../lib/i18n/i18n.js';
|
|
9
9
|
import {computeJSTokenLength as computeTokenLength} from '../../lib/minification-estimator.js';
|
|
10
10
|
import {getRequestForScript, isInline} from '../../lib/script-helpers.js';
|
|
11
|
+
import {Util} from '../../../shared/util.js';
|
|
11
12
|
|
|
12
13
|
const UIStrings = {
|
|
13
14
|
/** Imperative title of a Lighthouse audit that tells the user to minify the page’s JS code to reduce file size. This is displayed in a list of audit titles that Lighthouse generates. */
|
|
@@ -84,7 +85,7 @@ class UnminifiedJavaScript extends ByteEfficiencyAudit {
|
|
|
84
85
|
const networkRecord = getRequestForScript(networkRecords, script);
|
|
85
86
|
|
|
86
87
|
const displayUrl = isInline(script) ?
|
|
87
|
-
`inline: ${script.content
|
|
88
|
+
`inline: ${Util.truncate(script.content, 40)}` :
|
|
88
89
|
script.url;
|
|
89
90
|
try {
|
|
90
91
|
const result = UnminifiedJavaScript.computeWaste(script.content, displayUrl, networkRecord);
|
|
@@ -67,10 +67,11 @@ class Doctype extends Audit {
|
|
|
67
67
|
const doctypeSystemId = artifacts.Doctype.systemId;
|
|
68
68
|
const compatMode = artifacts.Doctype.documentCompatMode;
|
|
69
69
|
|
|
70
|
+
const trace = artifacts.traces?.[Audit.DEFAULT_PASS];
|
|
71
|
+
|
|
70
72
|
/** @type {LH.Crdp.Audits.QuirksModeIssueDetails[]} */
|
|
71
73
|
let quirksModeIssues = [];
|
|
72
|
-
if (
|
|
73
|
-
const trace = artifacts.traces[Audit.DEFAULT_PASS];
|
|
74
|
+
if (trace && artifacts.InspectorIssues) {
|
|
74
75
|
const processedTrace = await ProcessedTrace.request(trace, context);
|
|
75
76
|
const mainFrameId = processedTrace.mainFrameInfo.frameId;
|
|
76
77
|
quirksModeIssues =
|
|
@@ -13,17 +13,16 @@ declare const CumulativeLayoutShiftComputed: typeof CumulativeLayoutShift & {
|
|
|
13
13
|
totalCumulativeLayoutShift: number;
|
|
14
14
|
}>;
|
|
15
15
|
};
|
|
16
|
-
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
|
|
17
16
|
declare class CumulativeLayoutShift {
|
|
18
17
|
/**
|
|
19
18
|
* Returns all LayoutShift events that had no recent input.
|
|
20
19
|
* Only a `weightedScore` per event is returned. For non-main-frame events, this is
|
|
21
20
|
* the only score that matters. For main-frame events, `weighted_score_delta === score`.
|
|
22
21
|
* @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
|
|
23
|
-
* @param {LH.
|
|
22
|
+
* @param {LH.Artifacts.ProcessedTrace} processedTrace
|
|
24
23
|
* @return {Array<LayoutShiftEvent>}
|
|
25
24
|
*/
|
|
26
|
-
static getLayoutShiftEvents(
|
|
25
|
+
static getLayoutShiftEvents(processedTrace: LH.Artifacts.ProcessedTrace): Array<LayoutShiftEvent>;
|
|
27
26
|
/**
|
|
28
27
|
* Calculates cumulative layout shifts per cluster (session) of LayoutShift
|
|
29
28
|
* events -- where a new cluster is created when there's a gap of more than
|
|
@@ -50,4 +49,5 @@ declare class CumulativeLayoutShift {
|
|
|
50
49
|
totalCumulativeLayoutShift: number;
|
|
51
50
|
}>;
|
|
52
51
|
}
|
|
52
|
+
import { ProcessedTrace } from "../processed-trace.js";
|
|
53
53
|
//# sourceMappingURL=cumulative-layout-shift.d.ts.map
|
|
@@ -9,16 +9,18 @@ import {ProcessedTrace} from '../processed-trace.js';
|
|
|
9
9
|
|
|
10
10
|
/** @typedef {{ts: number, isMainFrame: boolean, weightedScore: number}} LayoutShiftEvent */
|
|
11
11
|
|
|
12
|
+
const RECENT_INPUT_WINDOW = 500;
|
|
13
|
+
|
|
12
14
|
class CumulativeLayoutShift {
|
|
13
15
|
/**
|
|
14
16
|
* Returns all LayoutShift events that had no recent input.
|
|
15
17
|
* Only a `weightedScore` per event is returned. For non-main-frame events, this is
|
|
16
18
|
* the only score that matters. For main-frame events, `weighted_score_delta === score`.
|
|
17
19
|
* @see https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/layout/layout_shift_tracker.cc;l=492-495;drc=de3b3a8a8839269c6b44403fa38a13a1ed12fed5
|
|
18
|
-
* @param {LH.
|
|
20
|
+
* @param {LH.Artifacts.ProcessedTrace} processedTrace
|
|
19
21
|
* @return {Array<LayoutShiftEvent>}
|
|
20
22
|
*/
|
|
21
|
-
static getLayoutShiftEvents(
|
|
23
|
+
static getLayoutShiftEvents(processedTrace) {
|
|
22
24
|
const layoutShiftEvents = [];
|
|
23
25
|
|
|
24
26
|
// Chromium will set `had_recent_input` if there was recent user input, which
|
|
@@ -26,9 +28,20 @@ class CumulativeLayoutShift {
|
|
|
26
28
|
// Lighthouse changes the emulation size. This results in the first few shift
|
|
27
29
|
// events having `had_recent_input` set, so ignore it for those events.
|
|
28
30
|
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1094974.
|
|
29
|
-
let
|
|
31
|
+
let mustRespectHadRecentInput = false;
|
|
32
|
+
|
|
33
|
+
// Even if emulation was applied before navigating, Chrome will issue a viewport
|
|
34
|
+
// change event after a navigation starts which is treated as an interaction when
|
|
35
|
+
// deciding the `had_recent_input` flag. Anything within 500ms of this event should
|
|
36
|
+
// always be counted for CLS regardless of the `had_recent_input` flag.
|
|
37
|
+
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1302667
|
|
38
|
+
let viewportChangeTs = processedTrace.timestamps.timeOrigin;
|
|
39
|
+
const firstViewportEvent = processedTrace.frameEvents.find(event => event.name === 'viewport');
|
|
40
|
+
if (firstViewportEvent) {
|
|
41
|
+
viewportChangeTs = firstViewportEvent.ts;
|
|
42
|
+
}
|
|
30
43
|
|
|
31
|
-
for (const event of
|
|
44
|
+
for (const event of processedTrace.frameTreeEvents) {
|
|
32
45
|
if (event.name !== 'LayoutShift' ||
|
|
33
46
|
!event.args.data ||
|
|
34
47
|
event.args.data.is_main_frame === undefined) {
|
|
@@ -42,11 +55,10 @@ class CumulativeLayoutShift {
|
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
if (event.args.data.had_recent_input) {
|
|
45
|
-
|
|
46
|
-
if (
|
|
58
|
+
const timing = (event.ts - viewportChangeTs) / 1000;
|
|
59
|
+
if (timing > RECENT_INPUT_WINDOW || mustRespectHadRecentInput) continue;
|
|
47
60
|
} else {
|
|
48
|
-
|
|
49
|
-
ignoreHadRecentInput = false;
|
|
61
|
+
mustRespectHadRecentInput = true;
|
|
50
62
|
}
|
|
51
63
|
|
|
52
64
|
layoutShiftEvents.push({
|
|
@@ -106,7 +118,7 @@ class CumulativeLayoutShift {
|
|
|
106
118
|
const processedTrace = await ProcessedTrace.request(trace, context);
|
|
107
119
|
|
|
108
120
|
const allFrameShiftEvents =
|
|
109
|
-
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace
|
|
121
|
+
CumulativeLayoutShift.getLayoutShiftEvents(processedTrace);
|
|
110
122
|
const mainFrameShiftEvents = allFrameShiftEvents.filter(e => e.isMainFrame);
|
|
111
123
|
|
|
112
124
|
// The original Cumulative Layout Shift metric, the sum of all main-frame shift events.
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import {makeComputedArtifact} from './computed-artifact.js';
|
|
8
8
|
import {ByteEfficiencyAudit} from '../audits/byte-efficiency/byte-efficiency-audit.js';
|
|
9
9
|
import {NetworkRecords} from './network-records.js';
|
|
10
|
+
import {Util} from '../../shared/util.js';
|
|
10
11
|
|
|
11
12
|
const PREVIEW_LENGTH = 100;
|
|
12
13
|
|
|
@@ -87,8 +88,7 @@ class UnusedCSS {
|
|
|
87
88
|
* @return {string}
|
|
88
89
|
*/
|
|
89
90
|
static determineContentPreview(content) {
|
|
90
|
-
let preview = (content || '')
|
|
91
|
-
.slice(0, PREVIEW_LENGTH * 5)
|
|
91
|
+
let preview = Util.truncate(content || '', PREVIEW_LENGTH * 5, '')
|
|
92
92
|
.replace(/( {2,}|\t)+/g, ' ') // remove leading indentation if present
|
|
93
93
|
.replace(/\n\s+}/g, '\n}') // completely remove indentation of closing braces
|
|
94
94
|
.trim(); // trim the leading whitespace
|
|
@@ -101,16 +101,17 @@ class UnusedCSS {
|
|
|
101
101
|
firstRuleStart > firstRuleEnd ||
|
|
102
102
|
firstRuleStart > PREVIEW_LENGTH) {
|
|
103
103
|
// We couldn't determine the first rule-set or it's not within the preview
|
|
104
|
-
preview =
|
|
104
|
+
preview = Util.truncate(preview, PREVIEW_LENGTH);
|
|
105
105
|
} else if (firstRuleEnd < PREVIEW_LENGTH) {
|
|
106
106
|
// The entire first rule-set fits within the preview
|
|
107
|
-
preview = preview.slice(0, firstRuleEnd + 1) + '
|
|
107
|
+
preview = preview.slice(0, firstRuleEnd + 1) + ' …';
|
|
108
108
|
} else {
|
|
109
109
|
// The first rule-set doesn't fit within the preview, just show as many as we can
|
|
110
|
-
const
|
|
110
|
+
const truncated = Util.truncate(preview, PREVIEW_LENGTH, '');
|
|
111
|
+
const lastSemicolonIndex = truncated.lastIndexOf(';');
|
|
111
112
|
preview = lastSemicolonIndex < firstRuleStart ?
|
|
112
|
-
|
|
113
|
-
preview.slice(0, lastSemicolonIndex + 1) + '
|
|
113
|
+
truncated + '… } …' :
|
|
114
|
+
preview.slice(0, lastSemicolonIndex + 1) + ' … } …';
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
|
|
@@ -167,10 +167,23 @@ async function prepareTargetForTimespanMode(driver, settings) {
|
|
|
167
167
|
disableThrottling: false,
|
|
168
168
|
blockedUrlPatterns: undefined,
|
|
169
169
|
});
|
|
170
|
+
await warmUpIntlSegmenter(driver);
|
|
170
171
|
|
|
171
172
|
log.timeEnd(status);
|
|
172
173
|
}
|
|
173
174
|
|
|
175
|
+
/**
|
|
176
|
+
* Ensure the `Intl.Segmenter` created in `pageFunctions.truncate` is cached by v8 before
|
|
177
|
+
* recording the trace begins.
|
|
178
|
+
*
|
|
179
|
+
* @param {LH.Gatherer.FRTransitionalDriver} driver
|
|
180
|
+
*/
|
|
181
|
+
async function warmUpIntlSegmenter(driver) {
|
|
182
|
+
await driver.executionContext.evaluate(pageFunctions.truncate, {
|
|
183
|
+
args: ['aaa', 2],
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
174
187
|
/**
|
|
175
188
|
* Prepares a target to be analyzed in navigation mode by enabling protocol domains, emulation, and new document
|
|
176
189
|
* handlers for global APIs or error handling.
|
|
@@ -197,6 +210,8 @@ async function prepareTargetForNavigationMode(driver, settings) {
|
|
|
197
210
|
await shimRequestIdleCallbackOnNewDocument(driver, settings);
|
|
198
211
|
}
|
|
199
212
|
|
|
213
|
+
await warmUpIntlSegmenter(driver);
|
|
214
|
+
|
|
200
215
|
log.timeEnd(status);
|
|
201
216
|
}
|
|
202
217
|
|
|
@@ -10,16 +10,12 @@ export namespace pageFunctions {
|
|
|
10
10
|
export { isPositionFixed };
|
|
11
11
|
export { wrapRequestIdleCallback };
|
|
12
12
|
export { getBoundingClientRect };
|
|
13
|
+
export { truncate };
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* `typed-query-selector`'s CSS selector parser.
|
|
16
17
|
*/
|
|
17
18
|
export type ParseSelector<T extends string> = import('typed-query-selector/parser').ParseSelector<T>;
|
|
18
|
-
/**
|
|
19
|
-
* @license Copyright 2018 The Lighthouse Authors. All Rights Reserved.
|
|
20
|
-
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
21
|
-
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
22
|
-
*/
|
|
23
19
|
/**
|
|
24
20
|
* @fileoverview
|
|
25
21
|
* Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
|
|
@@ -65,6 +61,9 @@ declare function getElementsInDocument<T extends string>(selector: T): import("t
|
|
|
65
61
|
* @return {string}
|
|
66
62
|
*/
|
|
67
63
|
declare function getOuterHTMLSnippet(element: Element | ShadowRoot, ignoreAttrs?: Array<string> | undefined, snippetCharacterLimit?: number): string;
|
|
64
|
+
declare namespace getOuterHTMLSnippet {
|
|
65
|
+
function toString(): string;
|
|
66
|
+
}
|
|
68
67
|
/**
|
|
69
68
|
* Computes a memory/CPU performance benchmark index to determine rough device class.
|
|
70
69
|
* @see https://github.com/GoogleChrome/lighthouse/issues/9085
|
|
@@ -125,6 +124,9 @@ declare function getNodeSelector(element: Element): string;
|
|
|
125
124
|
* @return {string | null}
|
|
126
125
|
*/
|
|
127
126
|
declare function getNodeLabel(element: Element): string | null;
|
|
127
|
+
declare namespace getNodeLabel {
|
|
128
|
+
function toString(): string;
|
|
129
|
+
}
|
|
128
130
|
/**
|
|
129
131
|
* This function checks if an element or an ancestor of an element is `position:fixed`.
|
|
130
132
|
* In addition we ensure that the element is capable of behaving as a `position:fixed`
|
|
@@ -145,5 +147,15 @@ declare function wrapRequestIdleCallback(cpuSlowdownMultiplier: number): void;
|
|
|
145
147
|
* @return {LH.Artifacts.Rect}
|
|
146
148
|
*/
|
|
147
149
|
declare function getBoundingClientRect(element: Element): LH.Artifacts.Rect;
|
|
150
|
+
/**
|
|
151
|
+
*
|
|
152
|
+
* @param {string} string
|
|
153
|
+
* @param {number} characterLimit
|
|
154
|
+
* @return {string}
|
|
155
|
+
*/
|
|
156
|
+
declare function truncate(string: string, characterLimit: number): string;
|
|
157
|
+
declare namespace truncate {
|
|
158
|
+
function toString(): string;
|
|
159
|
+
}
|
|
148
160
|
export {};
|
|
149
161
|
//# sourceMappingURL=page-functions.d.ts.map
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import {Util} from '../../shared/util.js';
|
|
8
|
+
|
|
7
9
|
/**
|
|
8
10
|
* @fileoverview
|
|
9
11
|
* Helper functions that are passed by `toString()` by Driver to be evaluated in target page.
|
|
@@ -132,10 +134,9 @@ function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit =
|
|
|
132
134
|
}
|
|
133
135
|
|
|
134
136
|
// Elide attribute value if too long.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
137
|
+
const truncatedString = truncate(attributeValue, ATTRIBUTE_CHAR_LIMIT);
|
|
138
|
+
if (truncatedString !== attributeValue) dirty = true;
|
|
139
|
+
attributeValue = truncatedString;
|
|
139
140
|
|
|
140
141
|
if (dirty) {
|
|
141
142
|
// Style attributes can be blocked by the CSP if they are set via `setAttribute`.
|
|
@@ -372,22 +373,6 @@ function isPositionFixed(element) {
|
|
|
372
373
|
* @return {string | null}
|
|
373
374
|
*/
|
|
374
375
|
function getNodeLabel(element) {
|
|
375
|
-
// Inline so that audits that import getNodeLabel don't
|
|
376
|
-
// also need to import truncate
|
|
377
|
-
/**
|
|
378
|
-
* @param {string} str
|
|
379
|
-
* @param {number} maxLength
|
|
380
|
-
* @return {string}
|
|
381
|
-
*/
|
|
382
|
-
function truncate(str, maxLength) {
|
|
383
|
-
if (str.length <= maxLength) {
|
|
384
|
-
return str;
|
|
385
|
-
}
|
|
386
|
-
// Take advantage of string iterator multi-byte character awareness.
|
|
387
|
-
// Regular `.slice` will ignore unicode character boundaries and lead to malformed text.
|
|
388
|
-
return Array.from(str).slice(0, maxLength - 1).join('') + '…';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
376
|
const tagName = element.tagName.toLowerCase();
|
|
392
377
|
// html and body content is too broad to be useful, since they contain all page content
|
|
393
378
|
if (tagName !== 'html' && tagName !== 'body') {
|
|
@@ -518,14 +503,47 @@ function getNodeDetails(element) {
|
|
|
518
503
|
return details;
|
|
519
504
|
}
|
|
520
505
|
|
|
506
|
+
/**
|
|
507
|
+
*
|
|
508
|
+
* @param {string} string
|
|
509
|
+
* @param {number} characterLimit
|
|
510
|
+
* @return {string}
|
|
511
|
+
*/
|
|
512
|
+
function truncate(string, characterLimit) {
|
|
513
|
+
return Util.truncate(string, characterLimit);
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/** @type {string} */
|
|
517
|
+
const truncateRawString = truncate.toString();
|
|
518
|
+
truncate.toString = () => `function truncate(string, characterLimit) {
|
|
519
|
+
const Util = { ${Util.truncate} };
|
|
520
|
+
return (${truncateRawString})(string, characterLimit);
|
|
521
|
+
}`;
|
|
522
|
+
|
|
523
|
+
/** @type {string} */
|
|
524
|
+
const getNodeLabelRawString = getNodeLabel.toString();
|
|
525
|
+
getNodeLabel.toString = () => `function getNodeLabel(element) {
|
|
526
|
+
${truncate};
|
|
527
|
+
return (${getNodeLabelRawString})(element);
|
|
528
|
+
}`;
|
|
529
|
+
|
|
530
|
+
/** @type {string} */
|
|
531
|
+
const getOuterHTMLSnippetRawString = getOuterHTMLSnippet.toString();
|
|
532
|
+
// eslint-disable-next-line max-len
|
|
533
|
+
getOuterHTMLSnippet.toString = () => `function getOuterHTMLSnippet(element, ignoreAttrs = [], snippetCharacterLimit = 500) {
|
|
534
|
+
${truncate};
|
|
535
|
+
return (${getOuterHTMLSnippetRawString})(element, ignoreAttrs, snippetCharacterLimit);
|
|
536
|
+
}`;
|
|
537
|
+
|
|
521
538
|
/** @type {string} */
|
|
522
539
|
const getNodeDetailsRawString = getNodeDetails.toString();
|
|
523
540
|
getNodeDetails.toString = () => `function getNodeDetails(element) {
|
|
541
|
+
${truncate};
|
|
524
542
|
${getNodePath};
|
|
525
543
|
${getNodeSelector};
|
|
526
544
|
${getBoundingClientRect};
|
|
527
|
-
${
|
|
528
|
-
${
|
|
545
|
+
${getOuterHTMLSnippetRawString};
|
|
546
|
+
${getNodeLabelRawString};
|
|
529
547
|
return (${getNodeDetailsRawString})(element);
|
|
530
548
|
}`;
|
|
531
549
|
|
|
@@ -541,4 +559,5 @@ export const pageFunctions = {
|
|
|
541
559
|
isPositionFixed,
|
|
542
560
|
wrapRequestIdleCallback,
|
|
543
561
|
getBoundingClientRect,
|
|
562
|
+
truncate,
|
|
544
563
|
};
|
|
@@ -179,6 +179,14 @@ export class TraceProcessor {
|
|
|
179
179
|
* @return {evt is LCPCandidateEvent}
|
|
180
180
|
*/
|
|
181
181
|
static isLCPCandidateEvent(evt: LH.TraceEvent): evt is LCPCandidateEvent;
|
|
182
|
+
/**
|
|
183
|
+
* The associated frame ID is set in different locations for different trace events.
|
|
184
|
+
* This function checks all known locations for the frame ID and returns `undefined` if it's not found.
|
|
185
|
+
*
|
|
186
|
+
* @param {LH.TraceEvent} evt
|
|
187
|
+
* @return {string|undefined}
|
|
188
|
+
*/
|
|
189
|
+
static getFrameId(evt: LH.TraceEvent): string | undefined;
|
|
182
190
|
/**
|
|
183
191
|
* Returns the maximum LCP event across all frames in `events`.
|
|
184
192
|
* Sets `invalidated` flag if LCP of every frame is invalidated.
|
|
@@ -567,6 +567,19 @@ class TraceProcessor {
|
|
|
567
567
|
);
|
|
568
568
|
}
|
|
569
569
|
|
|
570
|
+
/**
|
|
571
|
+
* The associated frame ID is set in different locations for different trace events.
|
|
572
|
+
* This function checks all known locations for the frame ID and returns `undefined` if it's not found.
|
|
573
|
+
*
|
|
574
|
+
* @param {LH.TraceEvent} evt
|
|
575
|
+
* @return {string|undefined}
|
|
576
|
+
*/
|
|
577
|
+
static getFrameId(evt) {
|
|
578
|
+
return evt.args?.data?.frame ||
|
|
579
|
+
evt.args.data?.frameID ||
|
|
580
|
+
evt.args.frame;
|
|
581
|
+
}
|
|
582
|
+
|
|
570
583
|
/**
|
|
571
584
|
* Returns the maximum LCP event across all frames in `events`.
|
|
572
585
|
* Sets `invalidated` flag if LCP of every frame is invalidated.
|
|
@@ -712,13 +725,13 @@ class TraceProcessor {
|
|
|
712
725
|
// Filter to just events matching the main frame ID, just to make sure.
|
|
713
726
|
/** @param {LH.TraceEvent} e */
|
|
714
727
|
function associatedToMainFrame(e) {
|
|
715
|
-
const frameId =
|
|
728
|
+
const frameId = TraceProcessor.getFrameId(e);
|
|
716
729
|
return frameId === mainFrameInfo.frameId;
|
|
717
730
|
}
|
|
718
731
|
|
|
719
732
|
/** @param {LH.TraceEvent} e */
|
|
720
733
|
function associatedToAllFrames(e) {
|
|
721
|
-
const frameId =
|
|
734
|
+
const frameId = TraceProcessor.getFrameId(e);
|
|
722
735
|
return frameId ? inspectedTreeFrameIds.includes(frameId) : false;
|
|
723
736
|
}
|
|
724
737
|
const frameEvents = keyEvents.filter(e => associatedToMainFrame(e));
|
package/core/lib/url-utils.js
CHANGED
|
@@ -138,7 +138,7 @@ class UrlUtils {
|
|
|
138
138
|
static elideDataURI(url) {
|
|
139
139
|
try {
|
|
140
140
|
const parsed = new URL(url);
|
|
141
|
-
return parsed.protocol === 'data:' ?
|
|
141
|
+
return parsed.protocol === 'data:' ? Util.truncate(url, 100) : url;
|
|
142
142
|
} catch (e) {
|
|
143
143
|
return url;
|
|
144
144
|
}
|
|
@@ -153,6 +153,39 @@ class Util {
|
|
|
153
153
|
return segments;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} string
|
|
158
|
+
* @param {number} characterLimit
|
|
159
|
+
* @param {string} ellipseSuffix
|
|
160
|
+
*/
|
|
161
|
+
static truncate(string, characterLimit, ellipseSuffix = '…') {
|
|
162
|
+
// Early return for the case where there are fewer bytes than the character limit.
|
|
163
|
+
if (string.length <= characterLimit) {
|
|
164
|
+
return string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
|
168
|
+
const iterator = segmenter.segment(string)[Symbol.iterator]();
|
|
169
|
+
|
|
170
|
+
let lastSegmentIndex = 0;
|
|
171
|
+
for (let i = 0; i <= characterLimit - ellipseSuffix.length; i++) {
|
|
172
|
+
const result = iterator.next();
|
|
173
|
+
if (result.done) {
|
|
174
|
+
return string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lastSegmentIndex = result.value.index;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < ellipseSuffix.length; i++) {
|
|
181
|
+
if (iterator.next().done) {
|
|
182
|
+
return string;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return string.slice(0, lastSegmentIndex) + ellipseSuffix;
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/**
|
|
157
190
|
* @param {URL} parsedUrl
|
|
158
191
|
* @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
|
package/dist/report/flow.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/const oe="…",re={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},se=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class le{static get RATINGS(){return re}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const a=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const t=n[e];if(!t)continue;const i=e%2!=0;a.push({isCode:i,text:t})}return a}static splitMarkdownLink(e){const a=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,t,i]=n.splice(0,3);e&&a.push({isLink:!1,text:e}),t&&i&&a.push({isLink:!0,text:t,linkHref:i})}return a}static getURLDisplayName(e,a){const n=void 0!==(a=a||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?a.numPathParts:2,t=void 0===a.preserveQuery||a.preserveQuery,i=a.preserveHost||!1;let o;if("about:"===e.protocol||"data:"===e.protocol)o=e.href;else{o=e.pathname;const a=o.split("/").filter((e=>e.length));n&&a.length>n&&(o=oe+a.slice(-1*n).join("/")),i&&(o=`${e.host}/${o.replace(/^\//,"")}`),t&&(o=`${o}${e.search}`)}if("data:"!==e.protocol&&(o=o.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),o=o.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),o=o.replace(/(\d{3})\d{6,}/g,"$1…"),o=o.replace(/\u2026+/g,oe),o.length>64&&o.includes("?")&&(o=o.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),o.length>64&&(o=o.replace(/\?.*/,"?…")))),o.length>64){const e=o.lastIndexOf(".");o=e>=0?o.slice(0,63-(o.length-e))+`…${o.slice(e)}`:o.slice(0,63)+oe}return o}static parseURL(e){const a=new URL(e);return{file:le.getURLDisplayName(a),hostname:a.hostname,origin:a.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const a=e.split(".").slice(-2);return se.includes(a[0])?`.${a.join(".")}`:`.${a[a.length-1]}`}static getRootDomain(e){const a=le.createOrReturnURL(e).hostname,n=le.getTld(a).split(".");return a.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,a,n){if(0===a.length)return e.slice(0,2*n+1);const t=new Set;return(a=a.sort(((e,a)=>(e.lineNumber||0)-(a.lineNumber||0)))).forEach((({lineNumber:e})=>{let a=e-n,i=e+n;for(;a<1;)a++,i++;t.has(a-3-1)&&(a-=3);for(let e=a;e<=i;e++){const a=e;t.add(a)}})),e.filter((e=>t.has(e.lineNumber)))}}
|
|
17
|
+
*/const oe="…",re={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},se=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class le{static get RATINGS(){return re}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const a=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const t=n[e];if(!t)continue;const i=e%2!=0;a.push({isCode:i,text:t})}return a}static splitMarkdownLink(e){const a=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,t,i]=n.splice(0,3);e&&a.push({isLink:!1,text:e}),t&&i&&a.push({isLink:!0,text:t,linkHref:i})}return a}static truncate(e,a,n="…"){if(e.length<=a)return e;const t=new Intl.Segmenter(void 0,{granularity:"grapheme"}).segment(e)[Symbol.iterator]();let i=0;for(let o=0;o<=a-n.length;o++){const a=t.next();if(a.done)return e;i=a.value.index}for(let a=0;a<n.length;a++)if(t.next().done)return e;return e.slice(0,i)+n}static getURLDisplayName(e,a){const n=void 0!==(a=a||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?a.numPathParts:2,t=void 0===a.preserveQuery||a.preserveQuery,i=a.preserveHost||!1;let o;if("about:"===e.protocol||"data:"===e.protocol)o=e.href;else{o=e.pathname;const a=o.split("/").filter((e=>e.length));n&&a.length>n&&(o=oe+a.slice(-1*n).join("/")),i&&(o=`${e.host}/${o.replace(/^\//,"")}`),t&&(o=`${o}${e.search}`)}if("data:"!==e.protocol&&(o=o.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),o=o.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),o=o.replace(/(\d{3})\d{6,}/g,"$1…"),o=o.replace(/\u2026+/g,oe),o.length>64&&o.includes("?")&&(o=o.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),o.length>64&&(o=o.replace(/\?.*/,"?…")))),o.length>64){const e=o.lastIndexOf(".");o=e>=0?o.slice(0,63-(o.length-e))+`…${o.slice(e)}`:o.slice(0,63)+oe}return o}static parseURL(e){const a=new URL(e);return{file:le.getURLDisplayName(a),hostname:a.hostname,origin:a.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const a=e.split(".").slice(-2);return se.includes(a[0])?`.${a.join(".")}`:`.${a[a.length-1]}`}static getRootDomain(e){const a=le.createOrReturnURL(e).hostname,n=le.getTld(a).split(".");return a.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,a,n){if(0===a.length)return e.slice(0,2*n+1);const t=new Set;return(a=a.sort(((e,a)=>(e.lineNumber||0)-(a.lineNumber||0)))).forEach((({lineNumber:e})=>{let a=e-n,i=e+n;for(;a<1;)a++,i++;t.has(a-3-1)&&(a-=3);for(let e=a;e<=i;e++){const a=e;t.add(a)}})),e.filter((e=>t.has(e.lineNumber)))}}
|
|
18
18
|
/**
|
|
19
19
|
* @license Copyright 2023 The Lighthouse Authors. All Rights Reserved.
|
|
20
20
|
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
15
|
* See the License for the specific language governing permissions and
|
|
16
16
|
* limitations under the License.
|
|
17
|
-
*/const e="…",t={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},n=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class r{static get RATINGS(){return t}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const t=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const r=n[e];if(!r)continue;const o=e%2!=0;t.push({isCode:o,text:r})}return t}static splitMarkdownLink(e){const t=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,r,o]=n.splice(0,3);e&&t.push({isLink:!1,text:e}),r&&o&&t.push({isLink:!0,text:r,linkHref:o})}return t}static getURLDisplayName(t,n){const r=void 0!==(n=n||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?n.numPathParts:2,o=void 0===n.preserveQuery||n.preserveQuery,i=n.preserveHost||!1;let a;if("about:"===t.protocol||"data:"===t.protocol)a=t.href;else{a=t.pathname;const n=a.split("/").filter((e=>e.length));r&&n.length>r&&(a=e+n.slice(-1*r).join("/")),i&&(a=`${t.host}/${a.replace(/^\//,"")}`),o&&(a=`${a}${t.search}`)}if("data:"!==t.protocol&&(a=a.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),a=a.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),a=a.replace(/(\d{3})\d{6,}/g,"$1…"),a=a.replace(/\u2026+/g,e),a.length>64&&a.includes("?")&&(a=a.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),a.length>64&&(a=a.replace(/\?.*/,"?…")))),a.length>64){const t=a.lastIndexOf(".");a=t>=0?a.slice(0,63-(a.length-t))+`…${a.slice(t)}`:a.slice(0,63)+e}return a}static parseURL(e){const t=new URL(e);return{file:r.getURLDisplayName(t),hostname:t.hostname,origin:t.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const t=e.split(".").slice(-2);return n.includes(t[0])?`.${t.join(".")}`:`.${t[t.length-1]}`}static getRootDomain(e){const t=r.createOrReturnURL(e).hostname,n=r.getTld(t).split(".");return t.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,t,n){if(0===t.length)return e.slice(0,2*n+1);const r=new Set;return(t=t.sort(((e,t)=>(e.lineNumber||0)-(t.lineNumber||0)))).forEach((({lineNumber:e})=>{let t=e-n,o=e+n;for(;t<1;)t++,o++;r.has(t-3-1)&&(t-=3);for(let e=t;e<=o;e++){const t=e;r.add(t)}})),e.filter((e=>r.has(e.lineNumber)))}}
|
|
17
|
+
*/const e="…",t={PASS:{label:"pass",minScore:.9},AVERAGE:{label:"average",minScore:.5},FAIL:{label:"fail"},ERROR:{label:"error"}},n=["com","co","gov","edu","ac","org","go","gob","or","net","in","ne","nic","gouv","web","spb","blog","jus","kiev","mil","wi","qc","ca","bel","on"];class r{static get RATINGS(){return t}static get PASS_THRESHOLD(){return.9}static get MS_DISPLAY_VALUE(){return"%10d ms"}static getFinalDisplayedUrl(e){if(e.finalDisplayedUrl)return e.finalDisplayedUrl;if(e.finalUrl)return e.finalUrl;throw new Error("Could not determine final displayed URL")}static getMainDocumentUrl(e){return e.mainDocumentUrl||e.finalUrl}static getFullPageScreenshot(e){if(e.fullPageScreenshot)return e.fullPageScreenshot;return e.audits["full-page-screenshot"]?.details}static splitMarkdownCodeSpans(e){const t=[],n=e.split(/`(.*?)`/g);for(let e=0;e<n.length;e++){const r=n[e];if(!r)continue;const o=e%2!=0;t.push({isCode:o,text:r})}return t}static splitMarkdownLink(e){const t=[],n=e.split(/\[([^\]]+?)\]\((https?:\/\/.*?)\)/g);for(;n.length;){const[e,r,o]=n.splice(0,3);e&&t.push({isLink:!1,text:e}),r&&o&&t.push({isLink:!0,text:r,linkHref:o})}return t}static truncate(e,t,n="…"){if(e.length<=t)return e;const r=new Intl.Segmenter(void 0,{granularity:"grapheme"}).segment(e)[Symbol.iterator]();let o=0;for(let i=0;i<=t-n.length;i++){const t=r.next();if(t.done)return e;o=t.value.index}for(let t=0;t<n.length;t++)if(r.next().done)return e;return e.slice(0,o)+n}static getURLDisplayName(t,n){const r=void 0!==(n=n||{numPathParts:void 0,preserveQuery:void 0,preserveHost:void 0}).numPathParts?n.numPathParts:2,o=void 0===n.preserveQuery||n.preserveQuery,i=n.preserveHost||!1;let a;if("about:"===t.protocol||"data:"===t.protocol)a=t.href;else{a=t.pathname;const n=a.split("/").filter((e=>e.length));r&&n.length>r&&(a=e+n.slice(-1*r).join("/")),i&&(a=`${t.host}/${a.replace(/^\//,"")}`),o&&(a=`${a}${t.search}`)}if("data:"!==t.protocol&&(a=a.replace(/([a-f0-9]{7})[a-f0-9]{13}[a-f0-9]*/g,"$1…"),a=a.replace(/([a-zA-Z0-9-_]{9})(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])[a-zA-Z0-9-_]{10,}/g,"$1…"),a=a.replace(/(\d{3})\d{6,}/g,"$1…"),a=a.replace(/\u2026+/g,e),a.length>64&&a.includes("?")&&(a=a.replace(/\?([^=]*)(=)?.*/,"?$1$2…"),a.length>64&&(a=a.replace(/\?.*/,"?…")))),a.length>64){const t=a.lastIndexOf(".");a=t>=0?a.slice(0,63-(a.length-t))+`…${a.slice(t)}`:a.slice(0,63)+e}return a}static parseURL(e){const t=new URL(e);return{file:r.getURLDisplayName(t),hostname:t.hostname,origin:t.origin}}static createOrReturnURL(e){return e instanceof URL?e:new URL(e)}static getTld(e){const t=e.split(".").slice(-2);return n.includes(t[0])?`.${t.join(".")}`:`.${t[t.length-1]}`}static getRootDomain(e){const t=r.createOrReturnURL(e).hostname,n=r.getTld(t).split(".");return t.split(".").slice(-n.length).join(".")}static filterRelevantLines(e,t,n){if(0===t.length)return e.slice(0,2*n+1);const r=new Set;return(t=t.sort(((e,t)=>(e.lineNumber||0)-(t.lineNumber||0)))).forEach((({lineNumber:e})=>{let t=e-n,o=e+n;for(;t<1;)t++,o++;r.has(t-3-1)&&(t-=3);for(let e=t;e<=o;e++){const t=e;r.add(t)}})),e.filter((e=>r.has(e.lineNumber)))}}
|
|
18
18
|
/**
|
|
19
19
|
* @license
|
|
20
20
|
* Copyright 2017 The Lighthouse Authors. All Rights Reserved.
|
package/package.json
CHANGED
package/shared/tsconfig.json
CHANGED
package/shared/util.d.ts
CHANGED
|
@@ -67,6 +67,12 @@ export class Util {
|
|
|
67
67
|
isLink: false;
|
|
68
68
|
text: string;
|
|
69
69
|
}>;
|
|
70
|
+
/**
|
|
71
|
+
* @param {string} string
|
|
72
|
+
* @param {number} characterLimit
|
|
73
|
+
* @param {string} ellipseSuffix
|
|
74
|
+
*/
|
|
75
|
+
static truncate(string: string, characterLimit: number, ellipseSuffix?: string): string;
|
|
70
76
|
/**
|
|
71
77
|
* @param {URL} parsedUrl
|
|
72
78
|
* @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
|
package/shared/util.js
CHANGED
|
@@ -153,6 +153,39 @@ class Util {
|
|
|
153
153
|
return segments;
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
/**
|
|
157
|
+
* @param {string} string
|
|
158
|
+
* @param {number} characterLimit
|
|
159
|
+
* @param {string} ellipseSuffix
|
|
160
|
+
*/
|
|
161
|
+
static truncate(string, characterLimit, ellipseSuffix = '…') {
|
|
162
|
+
// Early return for the case where there are fewer bytes than the character limit.
|
|
163
|
+
if (string.length <= characterLimit) {
|
|
164
|
+
return string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const segmenter = new Intl.Segmenter(undefined, {granularity: 'grapheme'});
|
|
168
|
+
const iterator = segmenter.segment(string)[Symbol.iterator]();
|
|
169
|
+
|
|
170
|
+
let lastSegmentIndex = 0;
|
|
171
|
+
for (let i = 0; i <= characterLimit - ellipseSuffix.length; i++) {
|
|
172
|
+
const result = iterator.next();
|
|
173
|
+
if (result.done) {
|
|
174
|
+
return string;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
lastSegmentIndex = result.value.index;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
for (let i = 0; i < ellipseSuffix.length; i++) {
|
|
181
|
+
if (iterator.next().done) {
|
|
182
|
+
return string;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return string.slice(0, lastSegmentIndex) + ellipseSuffix;
|
|
187
|
+
}
|
|
188
|
+
|
|
156
189
|
/**
|
|
157
190
|
* @param {URL} parsedUrl
|
|
158
191
|
* @param {{numPathParts?: number, preserveQuery?: boolean, preserveHost?: boolean}=} options
|