chrome-devtools-frontend 1.0.996044 → 1.0.996595
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/AUTHORS +1 -0
- package/front_end/core/i18n/locales/en-US.json +3 -0
- package/front_end/core/i18n/locales/en-XL.json +3 -0
- package/front_end/core/sdk/CSSMatchedStyles.ts +158 -33
- package/front_end/core/sdk/CSSMetadata.ts +1 -8
- package/front_end/generated/InspectorBackendCommands.js +33 -2
- package/front_end/generated/protocol.ts +42 -7
- package/front_end/models/issues_manager/DeprecationIssue.ts +2 -2
- package/front_end/panels/changes/ChangesView.ts +25 -10
- package/front_end/panels/elements/ElementsPanel.ts +7 -6
- package/front_end/panels/elements/StylesSidebarPane.ts +39 -15
- package/front_end/panels/elements/stylesSectionTree.css +1 -0
- package/front_end/panels/elements/stylesSidebarPane.css +1 -1
- package/front_end/panels/profiler/HeapSnapshotGridNodes.ts +1 -0
- package/front_end/ui/components/diff_view/diffView.css +2 -0
- package/front_end/ui/components/tree_outline/TreeOutline.ts +18 -7
- package/front_end/ui/legacy/Toolbar.ts +5 -0
- package/front_end/ui/legacy/softDropDownButton.css +4 -0
- package/package.json +1 -1
- package/scripts/eslint_rules/lib/inline_type_imports.js +158 -0
- package/scripts/eslint_rules/tests/inline_type_imports_test.js +106 -0
- package/scripts/javascript_natives/index.js +1 -2
package/AUTHORS
CHANGED
@@ -26,6 +26,7 @@ Edward Trist <edwardtrist@gmail.com>
|
|
26
26
|
Ergün Erdoğmuş <erdogmusergun@gmail.com>
|
27
27
|
Eric Rannaud <eric.rannaud@gmail.com>
|
28
28
|
Feng Lu <lufengd3@gmail.com>
|
29
|
+
Feng Yu <f3n67u@gmail.com>
|
29
30
|
Gabriel Luong <gabriel.luong@gmail.com>
|
30
31
|
Gautham Banasandra <gautham.bangalore@gmail.com>
|
31
32
|
Jacky Hu <flameddd@gmail.com>
|
@@ -12563,6 +12563,9 @@
|
|
12563
12563
|
"ui/legacy/TargetCrashedScreen.ts | oncePageIsReloadedDevtoolsWill": {
|
12564
12564
|
"message": "Once page is reloaded, DevTools will automatically reconnect."
|
12565
12565
|
},
|
12566
|
+
"ui/legacy/Toolbar.ts | clearInput": {
|
12567
|
+
"message": "Clear input"
|
12568
|
+
},
|
12566
12569
|
"ui/legacy/Toolbar.ts | notPressed": {
|
12567
12570
|
"message": "not pressed"
|
12568
12571
|
},
|
@@ -12563,6 +12563,9 @@
|
|
12563
12563
|
"ui/legacy/TargetCrashedScreen.ts | oncePageIsReloadedDevtoolsWill": {
|
12564
12564
|
"message": "Ôńĉé p̂áĝé îś r̂él̂óâd́êd́, D̂év̂T́ôól̂ś ŵíl̂ĺ âút̂óm̂át̂íĉál̂ĺŷ ŕêćôńn̂éĉt́."
|
12565
12565
|
},
|
12566
|
+
"ui/legacy/Toolbar.ts | clearInput": {
|
12567
|
+
"message": "Ĉĺêár̂ ín̂ṕût́"
|
12568
|
+
},
|
12566
12569
|
"ui/legacy/Toolbar.ts | notPressed": {
|
12567
12570
|
"message": "n̂ót̂ ṕr̂éŝśêd́"
|
12568
12571
|
},
|
@@ -2,10 +2,11 @@
|
|
2
2
|
// Use of this source code is governed by a BSD-style license that can be
|
3
3
|
// found in the LICENSE file.
|
4
4
|
|
5
|
+
import * as Protocol from '../../generated/protocol.js';
|
5
6
|
import * as TextUtils from '../../models/text_utils/text_utils.js';
|
6
|
-
import type * as Protocol from '../../generated/protocol.js';
|
7
7
|
|
8
8
|
import {cssMetadata, CustomVariableRegex, VariableRegex} from './CSSMetadata.js';
|
9
|
+
|
9
10
|
import type {CSSModel} from './CSSModel.js';
|
10
11
|
import type {CSSProperty} from './CSSProperty.js';
|
11
12
|
import {CSSKeyframesRule, CSSStyleRule} from './CSSRule.js';
|
@@ -22,6 +23,7 @@ export class CSSMatchedStyles {
|
|
22
23
|
readonly #inheritedStyles: Set<CSSStyleDeclaration>;
|
23
24
|
readonly #mainDOMCascade: DOMInheritanceCascade;
|
24
25
|
readonly #pseudoDOMCascades: Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>;
|
26
|
+
readonly #customHighlightPseudoDOMCascades: Map<string, DOMInheritanceCascade>;
|
25
27
|
readonly #styleToDOMCascade: Map<CSSStyleDeclaration, DOMInheritanceCascade>;
|
26
28
|
|
27
29
|
constructor(
|
@@ -48,10 +50,13 @@ export class CSSMatchedStyles {
|
|
48
50
|
}
|
49
51
|
|
50
52
|
this.#mainDOMCascade = this.buildMainCascade(inlinePayload, attributesPayload, matchedPayload, inheritedPayload);
|
51
|
-
this.#pseudoDOMCascades
|
53
|
+
[this.#pseudoDOMCascades, this.#customHighlightPseudoDOMCascades] =
|
54
|
+
this.buildPseudoCascades(pseudoPayload, inheritedPseudoPayload);
|
52
55
|
|
53
56
|
this.#styleToDOMCascade = new Map();
|
54
|
-
for (const domCascade of Array.from(this.#
|
57
|
+
for (const domCascade of Array.from(this.#customHighlightPseudoDOMCascades.values())
|
58
|
+
.concat(Array.from(this.#pseudoDOMCascades.values()))
|
59
|
+
.concat(this.#mainDOMCascade)) {
|
55
60
|
for (const style of domCascade.styles()) {
|
56
61
|
this.#styleToDOMCascade.set(style, domCascade);
|
57
62
|
}
|
@@ -206,36 +211,133 @@ export class CSSMatchedStyles {
|
|
206
211
|
}
|
207
212
|
}
|
208
213
|
|
214
|
+
/**
|
215
|
+
* Pseudo rule matches received via the inspector protocol are grouped by pseudo type.
|
216
|
+
* For custom highlight pseudos, we need to instead group the rule matches by highlight
|
217
|
+
* name in order to produce separate cascades for each highlight name. This is necessary
|
218
|
+
* so that styles of ::highlight(foo) are not shown as overriding styles of ::highlight(bar).
|
219
|
+
*
|
220
|
+
* This helper function takes a list of rule matches and generates separate NodeCascades
|
221
|
+
* for each custom highlight name that was matched.
|
222
|
+
*/
|
223
|
+
private buildSplitCustomHighlightCascades(
|
224
|
+
rules: Protocol.CSS.RuleMatch[], node: DOMNode, isInherited: boolean,
|
225
|
+
pseudoCascades: Map<string, NodeCascade[]>): void {
|
226
|
+
const splitHighlightRules = new Map<string, CSSStyleDeclaration[]>();
|
227
|
+
|
228
|
+
for (let j = rules.length - 1; j >= 0; --j) {
|
229
|
+
const highlightNamesToMatchingSelectorIndices = this.customHighlightNamesToMatchingSelectorIndices(rules[j]);
|
230
|
+
|
231
|
+
for (const [highlightName, matchingSelectors] of highlightNamesToMatchingSelectorIndices) {
|
232
|
+
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[j].rule);
|
233
|
+
this.#nodeForStyleInternal.set(pseudoRule.style, node);
|
234
|
+
if (isInherited) {
|
235
|
+
this.#inheritedStyles.add(pseudoRule.style);
|
236
|
+
}
|
237
|
+
this.addMatchingSelectors(node, pseudoRule, matchingSelectors);
|
238
|
+
|
239
|
+
const ruleListForHighlightName = splitHighlightRules.get(highlightName);
|
240
|
+
if (ruleListForHighlightName) {
|
241
|
+
ruleListForHighlightName.push(pseudoRule.style);
|
242
|
+
} else {
|
243
|
+
splitHighlightRules.set(highlightName, [pseudoRule.style]);
|
244
|
+
}
|
245
|
+
}
|
246
|
+
}
|
247
|
+
|
248
|
+
for (const [highlightName, highlightStyles] of splitHighlightRules) {
|
249
|
+
const nodeCascade = new NodeCascade(this, highlightStyles, isInherited, true /* #isHighlightPseudoCascade*/);
|
250
|
+
const cascadeListForHighlightName = pseudoCascades.get(highlightName);
|
251
|
+
if (cascadeListForHighlightName) {
|
252
|
+
cascadeListForHighlightName.push(nodeCascade);
|
253
|
+
} else {
|
254
|
+
pseudoCascades.set(highlightName, [nodeCascade]);
|
255
|
+
}
|
256
|
+
}
|
257
|
+
}
|
258
|
+
|
259
|
+
/**
|
260
|
+
* Return a mapping of the highlight names in the specified RuleMatch to
|
261
|
+
* the indices of selectors in that selector list with that highlight name.
|
262
|
+
*
|
263
|
+
* For example, consider the following ruleset:
|
264
|
+
* span::highlight(foo), div, #mySpan::highlight(bar), .highlighted::highlight(foo) {
|
265
|
+
* color: blue;
|
266
|
+
* }
|
267
|
+
*
|
268
|
+
* For a <span id="mySpan" class="highlighted"></span>, a RuleMatch for that span
|
269
|
+
* would have matchingSelectors [0, 2, 3] indicating that the span
|
270
|
+
* matches all of the highlight selectors.
|
271
|
+
*
|
272
|
+
* For that RuleMatch, this function would produce the following map:
|
273
|
+
* {
|
274
|
+
* "foo": [0, 3],
|
275
|
+
* "bar": [2]
|
276
|
+
* }
|
277
|
+
*
|
278
|
+
* @param ruleMatch
|
279
|
+
* @returns A mapping of highlight names to lists of indices into the selector
|
280
|
+
* list associated with ruleMatch. The indices correspond to the selectors in the rule
|
281
|
+
* associated with the key's highlight name.
|
282
|
+
*/
|
283
|
+
private customHighlightNamesToMatchingSelectorIndices(ruleMatch: Protocol.CSS.RuleMatch): Map<string, number[]> {
|
284
|
+
const highlightNamesToMatchingSelectors = new Map<string, number[]>();
|
285
|
+
|
286
|
+
for (let i = 0; i < ruleMatch.matchingSelectors.length; i++) {
|
287
|
+
const matchingSelectorIndex = ruleMatch.matchingSelectors[i];
|
288
|
+
const selectorText = ruleMatch.rule.selectorList.selectors[matchingSelectorIndex].text;
|
289
|
+
const highlightNameMatch = selectorText.match(/::highlight\((.*)\)/);
|
290
|
+
if (highlightNameMatch) {
|
291
|
+
const highlightName = highlightNameMatch[1];
|
292
|
+
const selectorsForName = highlightNamesToMatchingSelectors.get(highlightName);
|
293
|
+
if (selectorsForName) {
|
294
|
+
selectorsForName.push(matchingSelectorIndex);
|
295
|
+
} else {
|
296
|
+
highlightNamesToMatchingSelectors.set(highlightName, [matchingSelectorIndex]);
|
297
|
+
}
|
298
|
+
}
|
299
|
+
}
|
300
|
+
return highlightNamesToMatchingSelectors;
|
301
|
+
}
|
302
|
+
|
209
303
|
private buildPseudoCascades(
|
210
304
|
pseudoPayload: Protocol.CSS.PseudoElementMatches[],
|
211
305
|
inheritedPseudoPayload: Protocol.CSS.InheritedPseudoElementMatches[]):
|
212
|
-
Map<Protocol.DOM.PseudoType, DOMInheritanceCascade> {
|
306
|
+
[Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>, Map<string, DOMInheritanceCascade>] {
|
213
307
|
const pseudoInheritanceCascades = new Map<Protocol.DOM.PseudoType, DOMInheritanceCascade>();
|
308
|
+
const customHighlightPseudoInheritanceCascades = new Map<string, DOMInheritanceCascade>();
|
214
309
|
if (!pseudoPayload) {
|
215
|
-
return pseudoInheritanceCascades;
|
310
|
+
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
|
216
311
|
}
|
217
312
|
|
218
313
|
const pseudoCascades = new Map<Protocol.DOM.PseudoType, NodeCascade[]>();
|
314
|
+
const customHighlightPseudoCascades = new Map<string, NodeCascade[]>();
|
219
315
|
for (let i = 0; i < pseudoPayload.length; ++i) {
|
220
316
|
const entryPayload = pseudoPayload[i];
|
221
317
|
// PseudoElement nodes are not created unless "content" css property is set.
|
222
318
|
const pseudoElement = this.#nodeInternal.pseudoElements().get(entryPayload.pseudoType) || null;
|
223
319
|
const pseudoStyles = [];
|
224
320
|
const rules = entryPayload.matches || [];
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
321
|
+
|
322
|
+
if (entryPayload.pseudoType === Protocol.DOM.PseudoType.Highlight) {
|
323
|
+
this.buildSplitCustomHighlightCascades(
|
324
|
+
rules, this.#nodeInternal, false /* #isInherited */, customHighlightPseudoCascades);
|
325
|
+
} else {
|
326
|
+
for (let j = rules.length - 1; j >= 0; --j) {
|
327
|
+
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[j].rule);
|
328
|
+
pseudoStyles.push(pseudoRule.style);
|
329
|
+
const nodeForStyle =
|
330
|
+
cssMetadata().isHighlightPseudoType(entryPayload.pseudoType) ? this.#nodeInternal : pseudoElement;
|
331
|
+
this.#nodeForStyleInternal.set(pseudoRule.style, nodeForStyle);
|
332
|
+
if (nodeForStyle) {
|
333
|
+
this.addMatchingSelectors(nodeForStyle, pseudoRule, rules[j].matchingSelectors);
|
334
|
+
}
|
233
335
|
}
|
336
|
+
const isHighlightPseudoCascade = cssMetadata().isHighlightPseudoType(entryPayload.pseudoType);
|
337
|
+
const nodeCascade = new NodeCascade(
|
338
|
+
this, pseudoStyles, false /* #isInherited */, isHighlightPseudoCascade /* #isHighlightPseudoCascade*/);
|
339
|
+
pseudoCascades.set(entryPayload.pseudoType, [nodeCascade]);
|
234
340
|
}
|
235
|
-
const isHighlightPseudoCascade = cssMetadata().isHighlightPseudoType(entryPayload.pseudoType);
|
236
|
-
const nodeCascade = new NodeCascade(
|
237
|
-
this, pseudoStyles, false /* #isInherited */, isHighlightPseudoCascade /* #isHighlightPseudoCascade*/);
|
238
|
-
pseudoCascades.set(entryPayload.pseudoType, [nodeCascade]);
|
239
341
|
}
|
240
342
|
|
241
343
|
if (inheritedPseudoPayload) {
|
@@ -244,24 +346,30 @@ export class CSSMatchedStyles {
|
|
244
346
|
const inheritedPseudoMatches = inheritedPseudoPayload[i].pseudoElements;
|
245
347
|
for (let j = 0; j < inheritedPseudoMatches.length; ++j) {
|
246
348
|
const inheritedEntryPayload = inheritedPseudoMatches[j];
|
247
|
-
const pseudoStyles = [];
|
248
349
|
const rules = inheritedEntryPayload.matches || [];
|
249
|
-
for (let k = rules.length - 1; k >= 0; --k) {
|
250
|
-
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[k].rule);
|
251
|
-
pseudoStyles.push(pseudoRule.style);
|
252
|
-
this.#nodeForStyleInternal.set(pseudoRule.style, parentNode);
|
253
|
-
this.#inheritedStyles.add(pseudoRule.style);
|
254
|
-
this.addMatchingSelectors(parentNode, pseudoRule, rules[k].matchingSelectors);
|
255
|
-
}
|
256
350
|
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
const cascadeListForPseudoType = pseudoCascades.get(inheritedEntryPayload.pseudoType);
|
261
|
-
if (cascadeListForPseudoType) {
|
262
|
-
cascadeListForPseudoType.push(nodeCascade);
|
351
|
+
if (inheritedEntryPayload.pseudoType === Protocol.DOM.PseudoType.Highlight) {
|
352
|
+
this.buildSplitCustomHighlightCascades(
|
353
|
+
rules, parentNode, true /* #isInherited */, customHighlightPseudoCascades);
|
263
354
|
} else {
|
264
|
-
|
355
|
+
const pseudoStyles = [];
|
356
|
+
for (let k = rules.length - 1; k >= 0; --k) {
|
357
|
+
const pseudoRule = new CSSStyleRule(this.#cssModelInternal, rules[k].rule);
|
358
|
+
pseudoStyles.push(pseudoRule.style);
|
359
|
+
this.#nodeForStyleInternal.set(pseudoRule.style, parentNode);
|
360
|
+
this.#inheritedStyles.add(pseudoRule.style);
|
361
|
+
this.addMatchingSelectors(parentNode, pseudoRule, rules[k].matchingSelectors);
|
362
|
+
}
|
363
|
+
|
364
|
+
const isHighlightPseudoCascade = cssMetadata().isHighlightPseudoType(inheritedEntryPayload.pseudoType);
|
365
|
+
const nodeCascade = new NodeCascade(
|
366
|
+
this, pseudoStyles, true /* #isInherited */, isHighlightPseudoCascade /* #isHighlightPseudoCascade*/);
|
367
|
+
const cascadeListForPseudoType = pseudoCascades.get(inheritedEntryPayload.pseudoType);
|
368
|
+
if (cascadeListForPseudoType) {
|
369
|
+
cascadeListForPseudoType.push(nodeCascade);
|
370
|
+
} else {
|
371
|
+
pseudoCascades.set(inheritedEntryPayload.pseudoType, [nodeCascade]);
|
372
|
+
}
|
265
373
|
}
|
266
374
|
}
|
267
375
|
|
@@ -275,7 +383,11 @@ export class CSSMatchedStyles {
|
|
275
383
|
pseudoInheritanceCascades.set(pseudoType, new DOMInheritanceCascade(nodeCascade));
|
276
384
|
}
|
277
385
|
|
278
|
-
|
386
|
+
for (const [highlightName, nodeCascade] of customHighlightPseudoCascades.entries()) {
|
387
|
+
customHighlightPseudoInheritanceCascades.set(highlightName, new DOMInheritanceCascade(nodeCascade));
|
388
|
+
}
|
389
|
+
|
390
|
+
return [pseudoInheritanceCascades, customHighlightPseudoInheritanceCascades];
|
279
391
|
}
|
280
392
|
|
281
393
|
private addMatchingSelectors(
|
@@ -405,6 +517,15 @@ export class CSSMatchedStyles {
|
|
405
517
|
return new Set(this.#pseudoDOMCascades.keys());
|
406
518
|
}
|
407
519
|
|
520
|
+
customHighlightPseudoStyles(highlightName: string): CSSStyleDeclaration[] {
|
521
|
+
const domCascade = this.#customHighlightPseudoDOMCascades.get(highlightName);
|
522
|
+
return domCascade ? domCascade.styles() : [];
|
523
|
+
}
|
524
|
+
|
525
|
+
customHighlightPseudoNames(): Set<string> {
|
526
|
+
return new Set(this.#customHighlightPseudoDOMCascades.keys());
|
527
|
+
}
|
528
|
+
|
408
529
|
private containsInherited(style: CSSStyleDeclaration): boolean {
|
409
530
|
const properties = style.allProperties();
|
410
531
|
for (let i = 0; i < properties.length; ++i) {
|
@@ -462,6 +583,10 @@ export class CSSMatchedStyles {
|
|
462
583
|
for (const domCascade of this.#pseudoDOMCascades.values()) {
|
463
584
|
domCascade.reset();
|
464
585
|
}
|
586
|
+
|
587
|
+
for (const domCascade of this.#customHighlightPseudoDOMCascades.values()) {
|
588
|
+
domCascade.reset();
|
589
|
+
}
|
465
590
|
}
|
466
591
|
}
|
467
592
|
|
@@ -313,15 +313,8 @@ export class CSSMetadata {
|
|
313
313
|
}
|
314
314
|
|
315
315
|
isHighlightPseudoType(pseudoType: Protocol.DOM.PseudoType): boolean {
|
316
|
-
// TODO(crbug.com/1164461) Currently devtools-frontend groups all custom highlight
|
317
|
-
// pseudos together in the same pseudo cascade, regardless of highlight name. This means that
|
318
|
-
// the result of displaying "overloaded" highlight styles as crossed-out can produce
|
319
|
-
// misleading results, because properties from highlights with one name can be shown as overloaded by
|
320
|
-
// properties from highlights with another name.
|
321
|
-
// So until that is fixed, don't include custom highlights among the highlight pseudos
|
322
|
-
// for which we apply overloaded property annotations.
|
323
316
|
return (
|
324
|
-
|
317
|
+
pseudoType === Protocol.DOM.PseudoType.Highlight || pseudoType === Protocol.DOM.PseudoType.Selection ||
|
325
318
|
pseudoType === Protocol.DOM.PseudoType.TargetText || pseudoType === Protocol.DOM.PseudoType.GrammarError ||
|
326
319
|
pseudoType === Protocol.DOM.PseudoType.SpellingError);
|
327
320
|
}
|
@@ -273,38 +273,69 @@ export function registerCommands(inspectorBackend) {
|
|
273
273
|
'Audits.GenericIssueErrorType', {CrossOriginPortalPostMessageError: 'CrossOriginPortalPostMessageError'});
|
274
274
|
inspectorBackend.registerEnum('Audits.DeprecationIssueType', {
|
275
275
|
AuthorizationCoveredByWildcard: 'AuthorizationCoveredByWildcard',
|
276
|
+
BatteryStatusInsecureOrigin: 'BatteryStatusInsecureOrigin',
|
277
|
+
CanRequestURLHTTPContainingNewline: 'CanRequestURLHTTPContainingNewline',
|
278
|
+
ChromeLoadTimesConnectionInfo: 'ChromeLoadTimesConnectionInfo',
|
279
|
+
ChromeLoadTimesFirstPaintAfterLoadTime: 'ChromeLoadTimesFirstPaintAfterLoadTime',
|
280
|
+
ChromeLoadTimesWasAlternateProtocolAvailable: 'ChromeLoadTimesWasAlternateProtocolAvailable',
|
276
281
|
CookieWithTruncatingChar: 'CookieWithTruncatingChar',
|
277
282
|
CrossOriginAccessBasedOnDocumentDomain: 'CrossOriginAccessBasedOnDocumentDomain',
|
278
283
|
CrossOriginWindowAlert: 'CrossOriginWindowAlert',
|
279
284
|
CrossOriginWindowConfirm: 'CrossOriginWindowConfirm',
|
285
|
+
CSSSelectorInternalMediaControlsOverlayCastButton: 'CSSSelectorInternalMediaControlsOverlayCastButton',
|
286
|
+
CustomCursorIntersectsViewport: 'CustomCursorIntersectsViewport',
|
280
287
|
DeprecationExample: 'DeprecationExample',
|
281
288
|
DocumentDomainSettingWithoutOriginAgentClusterHeader: 'DocumentDomainSettingWithoutOriginAgentClusterHeader',
|
289
|
+
EventPath: 'EventPath',
|
282
290
|
GeolocationInsecureOrigin: 'GeolocationInsecureOrigin',
|
283
291
|
GeolocationInsecureOriginDeprecatedNotRemoved: 'GeolocationInsecureOriginDeprecatedNotRemoved',
|
284
292
|
GetUserMediaInsecureOrigin: 'GetUserMediaInsecureOrigin',
|
293
|
+
HostCandidateAttributeGetter: 'HostCandidateAttributeGetter',
|
294
|
+
InsecurePrivateNetworkSubresourceRequest: 'InsecurePrivateNetworkSubresourceRequest',
|
285
295
|
LegacyConstraintGoogCpuOveruseDetection: 'LegacyConstraintGoogCpuOveruseDetection',
|
286
296
|
LegacyConstraintGoogIPv6: 'LegacyConstraintGoogIPv6',
|
287
297
|
LegacyConstraintGoogScreencastMinBitrate: 'LegacyConstraintGoogScreencastMinBitrate',
|
288
298
|
LegacyConstraintGoogSuspendBelowMinBitrate: 'LegacyConstraintGoogSuspendBelowMinBitrate',
|
289
299
|
LocalCSSFileExtensionRejected: 'LocalCSSFileExtensionRejected',
|
300
|
+
MediaElementAudioSourceNode: 'MediaElementAudioSourceNode',
|
301
|
+
MediaSourceAbortRemove: 'MediaSourceAbortRemove',
|
302
|
+
MediaSourceDurationTruncatingBuffered: 'MediaSourceDurationTruncatingBuffered',
|
303
|
+
NoSysexWebMIDIWithoutPermission: 'NoSysexWebMIDIWithoutPermission',
|
290
304
|
NotificationInsecureOrigin: 'NotificationInsecureOrigin',
|
305
|
+
NotificationPermissionRequestedIframe: 'NotificationPermissionRequestedIframe',
|
291
306
|
ObsoleteWebRtcCipherSuite: 'ObsoleteWebRtcCipherSuite',
|
307
|
+
PaymentRequestBasicCard: 'PaymentRequestBasicCard',
|
308
|
+
PaymentRequestShowWithoutGesture: 'PaymentRequestShowWithoutGesture',
|
292
309
|
PictureSourceSrc: 'PictureSourceSrc',
|
293
310
|
PrefixedCancelAnimationFrame: 'PrefixedCancelAnimationFrame',
|
294
311
|
PrefixedRequestAnimationFrame: 'PrefixedRequestAnimationFrame',
|
312
|
+
PrefixedStorageInfo: 'PrefixedStorageInfo',
|
313
|
+
PrefixedVideoDisplayingFullscreen: 'PrefixedVideoDisplayingFullscreen',
|
314
|
+
PrefixedVideoEnterFullscreen: 'PrefixedVideoEnterFullscreen',
|
315
|
+
PrefixedVideoEnterFullScreen: 'PrefixedVideoEnterFullScreen',
|
316
|
+
PrefixedVideoExitFullscreen: 'PrefixedVideoExitFullscreen',
|
317
|
+
PrefixedVideoExitFullScreen: 'PrefixedVideoExitFullScreen',
|
318
|
+
PrefixedVideoSupportsFullscreen: 'PrefixedVideoSupportsFullscreen',
|
319
|
+
RangeExpand: 'RangeExpand',
|
320
|
+
RequestedSubresourceWithEmbeddedCredentials: 'RequestedSubresourceWithEmbeddedCredentials',
|
295
321
|
RTCConstraintEnableDtlsSrtpFalse: 'RTCConstraintEnableDtlsSrtpFalse',
|
296
322
|
RTCConstraintEnableDtlsSrtpTrue: 'RTCConstraintEnableDtlsSrtpTrue',
|
297
323
|
RTCPeerConnectionComplexPlanBSdpUsingDefaultSdpSemantics:
|
298
324
|
'RTCPeerConnectionComplexPlanBSdpUsingDefaultSdpSemantics',
|
299
325
|
RTCPeerConnectionLegacyCreateWithMediaConstraints: 'RTCPeerConnectionLegacyCreateWithMediaConstraints',
|
326
|
+
RTCPeerConnectionSdpSemanticsPlanB: 'RTCPeerConnectionSdpSemanticsPlanB',
|
327
|
+
RtcpMuxPolicyNegotiate: 'RtcpMuxPolicyNegotiate',
|
300
328
|
RTPDataChannel: 'RTPDataChannel',
|
329
|
+
SelectionAddRangeIntersect: 'SelectionAddRangeIntersect',
|
301
330
|
SharedArrayBufferConstructedWithoutIsolation: 'SharedArrayBufferConstructedWithoutIsolation',
|
331
|
+
TextToSpeech_DisallowedByAutoplay: 'TextToSpeech_DisallowedByAutoplay',
|
302
332
|
Untranslated: 'Untranslated',
|
303
333
|
V8SharedArrayBufferConstructedInExtensionWithoutIsolation:
|
304
334
|
'V8SharedArrayBufferConstructedInExtensionWithoutIsolation',
|
305
335
|
WebCodecsVideoFrameDefaultTimestamp: 'WebCodecsVideoFrameDefaultTimestamp',
|
306
336
|
XHRJSONEncodingDetection: 'XHRJSONEncodingDetection',
|
307
|
-
XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload: 'XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload'
|
337
|
+
XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload: 'XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload',
|
338
|
+
XRSupportsSession: 'XRSupportsSession'
|
308
339
|
});
|
309
340
|
inspectorBackend.registerEnum(
|
310
341
|
'Audits.ClientHintIssueReason',
|
@@ -2103,6 +2134,7 @@ export function registerCommands(inspectorBackend) {
|
|
2103
2134
|
InterestCohort: 'interest-cohort',
|
2104
2135
|
JoinAdInterestGroup: 'join-ad-interest-group',
|
2105
2136
|
KeyboardMap: 'keyboard-map',
|
2137
|
+
LocalFonts: 'local-fonts',
|
2106
2138
|
Magnetometer: 'magnetometer',
|
2107
2139
|
Microphone: 'microphone',
|
2108
2140
|
Midi: 'midi',
|
@@ -2450,7 +2482,6 @@ export function registerCommands(inspectorBackend) {
|
|
2450
2482
|
{'name': 'marginLeft', 'type': 'number', 'optional': true},
|
2451
2483
|
{'name': 'marginRight', 'type': 'number', 'optional': true},
|
2452
2484
|
{'name': 'pageRanges', 'type': 'string', 'optional': true},
|
2453
|
-
{'name': 'ignoreInvalidPageRanges', 'type': 'boolean', 'optional': true},
|
2454
2485
|
{'name': 'headerTemplate', 'type': 'string', 'optional': true},
|
2455
2486
|
{'name': 'footerTemplate', 'type': 'string', 'optional': true},
|
2456
2487
|
{'name': 'preferCSSPageSize', 'type': 'boolean', 'optional': true},
|
@@ -1020,38 +1020,69 @@ export namespace Audits {
|
|
1020
1020
|
|
1021
1021
|
export const enum DeprecationIssueType {
|
1022
1022
|
AuthorizationCoveredByWildcard = 'AuthorizationCoveredByWildcard',
|
1023
|
+
BatteryStatusInsecureOrigin = 'BatteryStatusInsecureOrigin',
|
1024
|
+
CanRequestURLHTTPContainingNewline = 'CanRequestURLHTTPContainingNewline',
|
1025
|
+
ChromeLoadTimesConnectionInfo = 'ChromeLoadTimesConnectionInfo',
|
1026
|
+
ChromeLoadTimesFirstPaintAfterLoadTime = 'ChromeLoadTimesFirstPaintAfterLoadTime',
|
1027
|
+
ChromeLoadTimesWasAlternateProtocolAvailable = 'ChromeLoadTimesWasAlternateProtocolAvailable',
|
1023
1028
|
CookieWithTruncatingChar = 'CookieWithTruncatingChar',
|
1024
1029
|
CrossOriginAccessBasedOnDocumentDomain = 'CrossOriginAccessBasedOnDocumentDomain',
|
1025
1030
|
CrossOriginWindowAlert = 'CrossOriginWindowAlert',
|
1026
1031
|
CrossOriginWindowConfirm = 'CrossOriginWindowConfirm',
|
1032
|
+
CSSSelectorInternalMediaControlsOverlayCastButton = 'CSSSelectorInternalMediaControlsOverlayCastButton',
|
1033
|
+
CustomCursorIntersectsViewport = 'CustomCursorIntersectsViewport',
|
1027
1034
|
DeprecationExample = 'DeprecationExample',
|
1028
1035
|
DocumentDomainSettingWithoutOriginAgentClusterHeader = 'DocumentDomainSettingWithoutOriginAgentClusterHeader',
|
1036
|
+
EventPath = 'EventPath',
|
1029
1037
|
GeolocationInsecureOrigin = 'GeolocationInsecureOrigin',
|
1030
1038
|
GeolocationInsecureOriginDeprecatedNotRemoved = 'GeolocationInsecureOriginDeprecatedNotRemoved',
|
1031
1039
|
GetUserMediaInsecureOrigin = 'GetUserMediaInsecureOrigin',
|
1040
|
+
HostCandidateAttributeGetter = 'HostCandidateAttributeGetter',
|
1041
|
+
InsecurePrivateNetworkSubresourceRequest = 'InsecurePrivateNetworkSubresourceRequest',
|
1032
1042
|
LegacyConstraintGoogCpuOveruseDetection = 'LegacyConstraintGoogCpuOveruseDetection',
|
1033
1043
|
LegacyConstraintGoogIPv6 = 'LegacyConstraintGoogIPv6',
|
1034
1044
|
LegacyConstraintGoogScreencastMinBitrate = 'LegacyConstraintGoogScreencastMinBitrate',
|
1035
1045
|
LegacyConstraintGoogSuspendBelowMinBitrate = 'LegacyConstraintGoogSuspendBelowMinBitrate',
|
1036
1046
|
LocalCSSFileExtensionRejected = 'LocalCSSFileExtensionRejected',
|
1047
|
+
MediaElementAudioSourceNode = 'MediaElementAudioSourceNode',
|
1048
|
+
MediaSourceAbortRemove = 'MediaSourceAbortRemove',
|
1049
|
+
MediaSourceDurationTruncatingBuffered = 'MediaSourceDurationTruncatingBuffered',
|
1050
|
+
NoSysexWebMIDIWithoutPermission = 'NoSysexWebMIDIWithoutPermission',
|
1037
1051
|
NotificationInsecureOrigin = 'NotificationInsecureOrigin',
|
1052
|
+
NotificationPermissionRequestedIframe = 'NotificationPermissionRequestedIframe',
|
1038
1053
|
ObsoleteWebRtcCipherSuite = 'ObsoleteWebRtcCipherSuite',
|
1054
|
+
PaymentRequestBasicCard = 'PaymentRequestBasicCard',
|
1055
|
+
PaymentRequestShowWithoutGesture = 'PaymentRequestShowWithoutGesture',
|
1039
1056
|
PictureSourceSrc = 'PictureSourceSrc',
|
1040
1057
|
PrefixedCancelAnimationFrame = 'PrefixedCancelAnimationFrame',
|
1041
1058
|
PrefixedRequestAnimationFrame = 'PrefixedRequestAnimationFrame',
|
1059
|
+
PrefixedStorageInfo = 'PrefixedStorageInfo',
|
1060
|
+
PrefixedVideoDisplayingFullscreen = 'PrefixedVideoDisplayingFullscreen',
|
1061
|
+
PrefixedVideoEnterFullscreen = 'PrefixedVideoEnterFullscreen',
|
1062
|
+
PrefixedVideoEnterFullScreen = 'PrefixedVideoEnterFullScreen',
|
1063
|
+
PrefixedVideoExitFullscreen = 'PrefixedVideoExitFullscreen',
|
1064
|
+
PrefixedVideoExitFullScreen = 'PrefixedVideoExitFullScreen',
|
1065
|
+
PrefixedVideoSupportsFullscreen = 'PrefixedVideoSupportsFullscreen',
|
1066
|
+
RangeExpand = 'RangeExpand',
|
1067
|
+
RequestedSubresourceWithEmbeddedCredentials = 'RequestedSubresourceWithEmbeddedCredentials',
|
1042
1068
|
RTCConstraintEnableDtlsSrtpFalse = 'RTCConstraintEnableDtlsSrtpFalse',
|
1043
1069
|
RTCConstraintEnableDtlsSrtpTrue = 'RTCConstraintEnableDtlsSrtpTrue',
|
1044
1070
|
RTCPeerConnectionComplexPlanBSdpUsingDefaultSdpSemantics =
|
1045
1071
|
'RTCPeerConnectionComplexPlanBSdpUsingDefaultSdpSemantics',
|
1046
1072
|
RTCPeerConnectionLegacyCreateWithMediaConstraints = 'RTCPeerConnectionLegacyCreateWithMediaConstraints',
|
1073
|
+
RTCPeerConnectionSdpSemanticsPlanB = 'RTCPeerConnectionSdpSemanticsPlanB',
|
1074
|
+
RtcpMuxPolicyNegotiate = 'RtcpMuxPolicyNegotiate',
|
1047
1075
|
RTPDataChannel = 'RTPDataChannel',
|
1076
|
+
SelectionAddRangeIntersect = 'SelectionAddRangeIntersect',
|
1048
1077
|
SharedArrayBufferConstructedWithoutIsolation = 'SharedArrayBufferConstructedWithoutIsolation',
|
1078
|
+
TextToSpeech_DisallowedByAutoplay = 'TextToSpeech_DisallowedByAutoplay',
|
1049
1079
|
Untranslated = 'Untranslated',
|
1050
1080
|
V8SharedArrayBufferConstructedInExtensionWithoutIsolation =
|
1051
1081
|
'V8SharedArrayBufferConstructedInExtensionWithoutIsolation',
|
1052
1082
|
WebCodecsVideoFrameDefaultTimestamp = 'WebCodecsVideoFrameDefaultTimestamp',
|
1053
1083
|
XHRJSONEncodingDetection = 'XHRJSONEncodingDetection',
|
1054
1084
|
XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload = 'XMLHttpRequestSynchronousInNonWorkerOutsideBeforeUnload',
|
1085
|
+
XRSupportsSession = 'XRSupportsSession',
|
1055
1086
|
}
|
1056
1087
|
|
1057
1088
|
/**
|
@@ -5162,6 +5193,8 @@ export namespace Emulation {
|
|
5162
5193
|
architecture: string;
|
5163
5194
|
model: string;
|
5164
5195
|
mobile: boolean;
|
5196
|
+
bitness?: string;
|
5197
|
+
wow64?: boolean;
|
5165
5198
|
}
|
5166
5199
|
|
5167
5200
|
/**
|
@@ -10200,6 +10233,7 @@ export namespace Page {
|
|
10200
10233
|
InterestCohort = 'interest-cohort',
|
10201
10234
|
JoinAdInterestGroup = 'join-ad-interest-group',
|
10202
10235
|
KeyboardMap = 'keyboard-map',
|
10236
|
+
LocalFonts = 'local-fonts',
|
10203
10237
|
Magnetometer = 'magnetometer',
|
10204
10238
|
Microphone = 'microphone',
|
10205
10239
|
Midi = 'midi',
|
@@ -11300,15 +11334,16 @@ export namespace Page {
|
|
11300
11334
|
*/
|
11301
11335
|
marginRight?: number;
|
11302
11336
|
/**
|
11303
|
-
* Paper ranges to print, e.g., '1-5, 8, 11-13'.
|
11304
|
-
*
|
11337
|
+
* Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are
|
11338
|
+
* printed in the document order, not in the order specified, and no
|
11339
|
+
* more than once.
|
11340
|
+
* Defaults to empty string, which implies the entire document is printed.
|
11341
|
+
* The page numbers are quietly capped to actual page count of the
|
11342
|
+
* document, and ranges beyond the end of the document are ignored.
|
11343
|
+
* If this results in no pages to print, an error is reported.
|
11344
|
+
* It is an error to specify a range with start greater than end.
|
11305
11345
|
*/
|
11306
11346
|
pageRanges?: string;
|
11307
|
-
/**
|
11308
|
-
* Whether to silently ignore invalid but successfully parsed page ranges, such as '3-2'.
|
11309
|
-
* Defaults to false.
|
11310
|
-
*/
|
11311
|
-
ignoreInvalidPageRanges?: boolean;
|
11312
11347
|
/**
|
11313
11348
|
* HTML template for the print header. Should be valid HTML markup with following
|
11314
11349
|
* classes used to inject printing values into them:
|
@@ -61,12 +61,12 @@ const UIStrings = {
|
|
61
61
|
documentDomainSettingWithoutOriginAgentClusterHeader:
|
62
62
|
'Relaxing the same-origin policy by setting `document.domain` is deprecated, and will be disabled by default. To continue using this feature, please opt-out of origin-keyed agent clusters by sending an `Origin-Agent-Cluster: ?0` header along with the HTTP response for the document and frames. See https://developer.chrome.com/blog/immutable-document-domain/ for more details.',
|
63
63
|
/**
|
64
|
-
*@description
|
64
|
+
*@description Warning displayed to developers when the Geolocation API is used from an insecure origin (one that isn't localhost or doesn't use HTTPS) to notify them that this use is no longer supported.
|
65
65
|
*/
|
66
66
|
geolocationInsecureOrigin:
|
67
67
|
'`getCurrentPosition()` and `watchPosition()` no longer work on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.',
|
68
68
|
/**
|
69
|
-
*@description
|
69
|
+
*@description Warning displayed to developers when the Geolocation API is used from an insecure origin (one that isn't localhost or doesn't use HTTPS) to notify them that this use is deprecated.
|
70
70
|
*/
|
71
71
|
geolocationInsecureOriginDeprecatedNotRemoved:
|
72
72
|
'`getCurrentPosition()` and `watchPosition()` are deprecated on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.',
|
@@ -6,6 +6,7 @@ import * as Common from '../../core/common/common.js';
|
|
6
6
|
import * as Host from '../../core/host/host.js';
|
7
7
|
import * as i18n from '../../core/i18n/i18n.js';
|
8
8
|
import * as Root from '../../core/root/root.js';
|
9
|
+
import type * as Formatter from '../../models/formatter/formatter.js';
|
9
10
|
import {formatCSSChangesFromDiff} from '../../panels/utils/utils.js';
|
10
11
|
import * as Diff from '../../third_party/diff/diff.js';
|
11
12
|
import * as DiffView from '../../ui/components/diff_view/diff_view.js';
|
@@ -72,6 +73,7 @@ export class ChangesView extends UI.Widget.VBox {
|
|
72
73
|
private readonly workspaceDiff: WorkspaceDiff.WorkspaceDiff.WorkspaceDiffImpl;
|
73
74
|
readonly changesSidebar: ChangesSidebar;
|
74
75
|
private selectedUISourceCode: Workspace.UISourceCode.UISourceCode|null;
|
76
|
+
#selectedSourceCodeFormattedMapping?: Formatter.ScriptFormatter.FormatterSourceMapping;
|
75
77
|
private readonly diffContainer: HTMLElement;
|
76
78
|
private readonly toolbar: UI.Toolbar.Toolbar;
|
77
79
|
private readonly diffStats: UI.Toolbar.ToolbarText;
|
@@ -167,16 +169,27 @@ export class ChangesView extends UI.Widget.VBox {
|
|
167
169
|
if (!this.selectedUISourceCode) {
|
168
170
|
return;
|
169
171
|
}
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
172
|
+
|
173
|
+
for (const target of event.composedPath()) {
|
174
|
+
if (!(target instanceof HTMLElement)) {
|
175
|
+
continue;
|
176
|
+
}
|
177
|
+
const selection = target.ownerDocument.getSelection();
|
178
|
+
if (selection?.toString()) {
|
179
|
+
// We abort source revelation when user has text selection.
|
180
|
+
break;
|
181
|
+
}
|
182
|
+
if (target.classList.contains('diff-line-content') && target.hasAttribute('data-line-number')) {
|
183
|
+
let lineNumber = Number(target.dataset.lineNumber) - 1;
|
184
|
+
// Unfortunately, caretRangeFromPoint is broken in shadow
|
185
|
+
// roots, which makes determining the character offset more
|
186
|
+
// work than justified here.
|
187
|
+
if (Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.PRECISE_CHANGES) &&
|
188
|
+
this.#selectedSourceCodeFormattedMapping) {
|
189
|
+
lineNumber = this.#selectedSourceCodeFormattedMapping.formattedToOriginal(lineNumber, 0)[0];
|
179
190
|
}
|
191
|
+
void Common.Revealer.reveal(this.selectedUISourceCode.uiLocation(lineNumber, 0), false);
|
192
|
+
event.consume(true);
|
180
193
|
break;
|
181
194
|
} else if (target.classList.contains('diff-listing')) {
|
182
195
|
break;
|
@@ -220,10 +233,12 @@ export class ChangesView extends UI.Widget.VBox {
|
|
220
233
|
return;
|
221
234
|
}
|
222
235
|
const diffResponse = await this.workspaceDiff.requestDiff(
|
223
|
-
uiSourceCode,
|
236
|
+
uiSourceCode,
|
237
|
+
{shouldFormatDiff: Root.Runtime.experiments.isEnabled(Root.Runtime.ExperimentName.PRECISE_CHANGES)});
|
224
238
|
if (this.selectedUISourceCode !== uiSourceCode) {
|
225
239
|
return;
|
226
240
|
}
|
241
|
+
this.#selectedSourceCodeFormattedMapping = diffResponse?.formattedCurrentMapping;
|
227
242
|
this.renderDiffRows(diffResponse?.diff);
|
228
243
|
}
|
229
244
|
|
@@ -878,18 +878,19 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
|
|
878
878
|
return node;
|
879
879
|
}
|
880
880
|
|
881
|
-
async revealAndSelectNode(
|
881
|
+
async revealAndSelectNode(nodeToReveal: SDK.DOMModel.DOMNode, focus: boolean, omitHighlight?: boolean):
|
882
|
+
Promise<void> {
|
882
883
|
this.omitDefaultSelection = true;
|
883
884
|
|
884
|
-
node = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get() ?
|
885
|
-
|
886
|
-
this.leaveUserAgentShadowDOM(
|
885
|
+
const node = Common.Settings.Settings.instance().moduleSetting('showUAShadowDOM').get() ?
|
886
|
+
nodeToReveal :
|
887
|
+
this.leaveUserAgentShadowDOM(nodeToReveal);
|
887
888
|
if (!omitHighlight) {
|
888
889
|
node.highlightForTwoSeconds();
|
889
890
|
}
|
890
891
|
|
891
892
|
if (this.accessibilityTreeView) {
|
892
|
-
void this.accessibilityTreeView.revealAndSelectNode(
|
893
|
+
void this.accessibilityTreeView.revealAndSelectNode(nodeToReveal);
|
893
894
|
}
|
894
895
|
|
895
896
|
await UI.ViewManager.ViewManager.instance().showView('elements', false, !focus);
|
@@ -1029,7 +1030,7 @@ export class ElementsPanel extends UI.Panel.Panel implements UI.SearchableView.S
|
|
1029
1030
|
};
|
1030
1031
|
|
1031
1032
|
this.sidebarPaneView = UI.ViewManager.ViewManager.instance().createTabbedLocation(
|
1032
|
-
() => UI.ViewManager.ViewManager.instance().showView('elements'));
|
1033
|
+
() => UI.ViewManager.ViewManager.instance().showView('elements'), 'Styles-pane-sidebar', false, true);
|
1033
1034
|
const tabbedPane = this.sidebarPaneView.tabbedPane();
|
1034
1035
|
if (this.splitMode !== _splitMode.Vertical) {
|
1035
1036
|
this.splitWidget.installResizer(tabbedPane.headerElement());
|
@@ -859,17 +859,38 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
|
|
859
859
|
}
|
860
860
|
}
|
861
861
|
|
862
|
+
const customHighlightPseudoRulesets: {
|
863
|
+
highlightName: string|null,
|
864
|
+
pseudoType: Protocol.DOM.PseudoType,
|
865
|
+
pseudoStyles: SDK.CSSStyleDeclaration.CSSStyleDeclaration[],
|
866
|
+
}[] = Array.from(matchedStyles.customHighlightPseudoNames()).map(highlightName => {
|
867
|
+
return {
|
868
|
+
'highlightName': highlightName,
|
869
|
+
'pseudoType': Protocol.DOM.PseudoType.Highlight,
|
870
|
+
'pseudoStyles': matchedStyles.customHighlightPseudoStyles(highlightName),
|
871
|
+
};
|
872
|
+
});
|
873
|
+
|
862
874
|
let pseudoTypes: Protocol.DOM.PseudoType[] = [];
|
863
875
|
const keys = matchedStyles.pseudoTypes();
|
864
876
|
if (keys.delete(Protocol.DOM.PseudoType.Before)) {
|
865
877
|
pseudoTypes.push(Protocol.DOM.PseudoType.Before);
|
866
878
|
}
|
867
879
|
pseudoTypes = pseudoTypes.concat([...keys].sort());
|
868
|
-
|
880
|
+
|
881
|
+
const otherPseudoRulesets: {
|
882
|
+
highlightName: string|null,
|
883
|
+
pseudoType: Protocol.DOM.PseudoType,
|
884
|
+
pseudoStyles: SDK.CSSStyleDeclaration.CSSStyleDeclaration[],
|
885
|
+
}[] = pseudoTypes.map(pseudoType => {
|
886
|
+
return {'highlightName': null, 'pseudoType': pseudoType, 'pseudoStyles': matchedStyles.pseudoStyles(pseudoType)};
|
887
|
+
});
|
888
|
+
|
889
|
+
const pseudoRulesets = customHighlightPseudoRulesets.concat(otherPseudoRulesets);
|
890
|
+
for (const pseudo of pseudoRulesets) {
|
869
891
|
lastParentNode = null;
|
870
|
-
|
871
|
-
|
872
|
-
const style = pseudoStyles[i];
|
892
|
+
for (let i = 0; i < pseudo.pseudoStyles.length; ++i) {
|
893
|
+
const style = pseudo.pseudoStyles[i];
|
873
894
|
const parentNode = matchedStyles.isInherited(style) ? matchedStyles.nodeForStyle(style) : null;
|
874
895
|
|
875
896
|
// Start a new SectionBlock if this is the first rule for this pseudo type, or if this
|
@@ -877,10 +898,11 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
|
|
877
898
|
if (i === 0 || parentNode !== lastParentNode) {
|
878
899
|
lastLayers = null;
|
879
900
|
if (parentNode) {
|
880
|
-
const block =
|
901
|
+
const block =
|
902
|
+
await SectionBlock.createInheritedPseudoTypeBlock(pseudo.pseudoType, pseudo.highlightName, parentNode);
|
881
903
|
blocks.push(block);
|
882
904
|
} else {
|
883
|
-
const block = SectionBlock.createPseudoTypeBlock(pseudoType);
|
905
|
+
const block = SectionBlock.createPseudoTypeBlock(pseudo.pseudoType, pseudo.highlightName);
|
884
906
|
blocks.push(block);
|
885
907
|
}
|
886
908
|
}
|
@@ -889,9 +911,7 @@ export class StylesSidebarPane extends Common.ObjectWrapper.eventMixin<EventType
|
|
889
911
|
addLayerSeparator(style);
|
890
912
|
const lastBlock = blocks[blocks.length - 1];
|
891
913
|
this.idleCallbackManager.schedule(() => {
|
892
|
-
const section =
|
893
|
-
new HighlightPseudoStylePropertiesSection(this, matchedStyles, style, sectionIdx) :
|
894
|
-
new StylePropertiesSection(this, matchedStyles, style, sectionIdx);
|
914
|
+
const section = new HighlightPseudoStylePropertiesSection(this, matchedStyles, style, sectionIdx);
|
895
915
|
sectionIdx++;
|
896
916
|
lastBlock.sections.push(section);
|
897
917
|
});
|
@@ -1287,19 +1307,23 @@ export class SectionBlock {
|
|
1287
1307
|
this.sections = [];
|
1288
1308
|
}
|
1289
1309
|
|
1290
|
-
static createPseudoTypeBlock(pseudoType: Protocol.DOM.PseudoType): SectionBlock {
|
1310
|
+
static createPseudoTypeBlock(pseudoType: Protocol.DOM.PseudoType, pseudoArgument: string|null): SectionBlock {
|
1291
1311
|
const separatorElement = document.createElement('div');
|
1292
1312
|
separatorElement.className = 'sidebar-separator';
|
1293
|
-
|
1313
|
+
const pseudoArgumentString = pseudoArgument ? `(${pseudoArgument})` : '';
|
1314
|
+
const pseudoTypeString = `${pseudoType}${pseudoArgumentString}`;
|
1315
|
+
separatorElement.textContent = i18nString(UIStrings.pseudoSElement, {PH1: pseudoTypeString});
|
1294
1316
|
return new SectionBlock(separatorElement);
|
1295
1317
|
}
|
1296
1318
|
|
1297
|
-
static async createInheritedPseudoTypeBlock(
|
1298
|
-
|
1319
|
+
static async createInheritedPseudoTypeBlock(
|
1320
|
+
pseudoType: Protocol.DOM.PseudoType, pseudoArgument: string|null,
|
1321
|
+
node: SDK.DOMModel.DOMNode): Promise<SectionBlock> {
|
1299
1322
|
const separatorElement = document.createElement('div');
|
1300
1323
|
separatorElement.className = 'sidebar-separator';
|
1301
|
-
|
1302
|
-
|
1324
|
+
const pseudoArgumentString = pseudoArgument ? `(${pseudoArgument})` : '';
|
1325
|
+
const pseudoTypeString = `${pseudoType}${pseudoArgumentString}`;
|
1326
|
+
UI.UIUtils.createTextChild(separatorElement, i18nString(UIStrings.inheritedFromSPseudoOf, {PH1: pseudoTypeString}));
|
1303
1327
|
const link = await Common.Linkifier.Linkifier.linkify(node, {
|
1304
1328
|
preventKeyboardFocus: true,
|
1305
1329
|
tooltip: undefined,
|
@@ -11,12 +11,14 @@
|
|
11
11
|
font-size: var(--source-code-font-size);
|
12
12
|
white-space: pre;
|
13
13
|
line-height: 1.2em;
|
14
|
+
user-select: text;
|
14
15
|
}
|
15
16
|
|
16
17
|
.diff-line-number {
|
17
18
|
color: var(--color-line-number);
|
18
19
|
padding: 0 3px 0 9px;
|
19
20
|
text-align: right;
|
21
|
+
user-select: none;
|
20
22
|
}
|
21
23
|
|
22
24
|
.diff-line-marker {
|
@@ -255,25 +255,36 @@ export class TreeOutline<TreeNodeDataType> extends HTMLElement {
|
|
255
255
|
return this.#selectedTreeNode;
|
256
256
|
}
|
257
257
|
|
258
|
-
async #
|
258
|
+
async #flattenSubtree(node: TreeNodeWithChildren<TreeNodeDataType>, filter: (node: TreeNodeDataType) => FilterOption):
|
259
|
+
Promise<TreeNode<TreeNodeDataType>[]> {
|
259
260
|
const children = await getNodeChildren(node);
|
260
|
-
if (!this.#nodeFilter) {
|
261
|
-
return children;
|
262
|
-
}
|
263
261
|
const filteredChildren = [];
|
264
262
|
for (const child of children) {
|
265
|
-
const filtering =
|
263
|
+
const filtering = filter(child.treeNodeData);
|
266
264
|
// We always include the selected node in the tree, regardless of its filtering status.
|
267
|
-
|
265
|
+
const toBeSelected = this.#isSelectedNode(child) || child.id === this.#nodeIdPendingFocus;
|
266
|
+
// If a node is already expanded we should not flatten it away.
|
267
|
+
const expanded = this.#nodeExpandedMap.get(child.id);
|
268
|
+
if (filtering === FilterOption.SHOW || toBeSelected || expanded) {
|
268
269
|
filteredChildren.push(child);
|
269
270
|
} else if (filtering === FilterOption.FLATTEN && isExpandableNode(child)) {
|
270
|
-
const grandChildren = await this.#
|
271
|
+
const grandChildren = await this.#flattenSubtree(child, filter);
|
271
272
|
filteredChildren.push(...grandChildren);
|
272
273
|
}
|
273
274
|
}
|
274
275
|
return filteredChildren;
|
275
276
|
}
|
276
277
|
|
278
|
+
async #fetchNodeChildren(node: TreeNodeWithChildren<TreeNodeDataType>): Promise<TreeNode<TreeNodeDataType>[]> {
|
279
|
+
const children = await getNodeChildren(node);
|
280
|
+
const filter = this.#nodeFilter;
|
281
|
+
if (!filter) {
|
282
|
+
return children;
|
283
|
+
}
|
284
|
+
const filteredDescendants = await this.#flattenSubtree(node, filter);
|
285
|
+
return filteredDescendants.length ? filteredDescendants : children;
|
286
|
+
}
|
287
|
+
|
277
288
|
#setNodeExpandedState(node: TreeNode<TreeNodeDataType>, newExpandedState: boolean): void {
|
278
289
|
this.#nodeExpandedMap.set(node.id, newExpandedState);
|
279
290
|
}
|
@@ -57,6 +57,10 @@ const UIStrings = {
|
|
57
57
|
*@description Announced screen reader message for ToolbarSettingToggle when the setting is toggled off.
|
58
58
|
*/
|
59
59
|
notPressed: 'not pressed',
|
60
|
+
/**
|
61
|
+
*@description Tooltip shown when the user hovers over the clear icon to empty the text input.
|
62
|
+
*/
|
63
|
+
clearInput: 'Clear input',
|
60
64
|
};
|
61
65
|
const str_ = i18n.i18n.registerUIStrings('ui/legacy/Toolbar.ts', UIStrings);
|
62
66
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
@@ -701,6 +705,7 @@ export class ToolbarInput extends ToolbarItem<ToolbarInput.EventTypes> {
|
|
701
705
|
}
|
702
706
|
|
703
707
|
const clearButton = this.element.createChild('div', 'toolbar-input-clear-button');
|
708
|
+
clearButton.title = UIStrings.clearInput;
|
704
709
|
clearButton.appendChild(Icon.create('mediumicon-gray-cross-active', 'search-cancel-button'));
|
705
710
|
clearButton.addEventListener('click', () => {
|
706
711
|
this.setValue('', true);
|
@@ -23,6 +23,10 @@ button.soft-dropdown > .title {
|
|
23
23
|
text-overflow: ellipsis;
|
24
24
|
}
|
25
25
|
|
26
|
+
button.soft-dropdown:hover:not(:active) > .title {
|
27
|
+
color: var(--color-text-primary);
|
28
|
+
}
|
29
|
+
|
26
30
|
@media (forced-colors: active) {
|
27
31
|
button.soft-dropdown {
|
28
32
|
border: 1px solid ButtonText;
|
package/package.json
CHANGED
@@ -0,0 +1,158 @@
|
|
1
|
+
// Copyright 2022 The Chromium Authors. All rights reserved.
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
3
|
+
// found in the LICENSE file.
|
4
|
+
'use strict';
|
5
|
+
|
6
|
+
module.exports = {
|
7
|
+
meta: {
|
8
|
+
type: 'problem',
|
9
|
+
|
10
|
+
docs: {
|
11
|
+
description: 'Inline type imports.',
|
12
|
+
category: 'Possible Errors',
|
13
|
+
},
|
14
|
+
fixable: 'code',
|
15
|
+
messages: {
|
16
|
+
inlineTypeImport: 'Type imports must be imported in the same import statement as values, using the type keyword',
|
17
|
+
convertTypeImport: 'Type imports must use the type modifier on each item, not on the overall import statement',
|
18
|
+
},
|
19
|
+
schema: [] // no options
|
20
|
+
},
|
21
|
+
create: function(context) {
|
22
|
+
// Stores any type imports (import type {} from ...).
|
23
|
+
// The key is the literal import path ("../foo.js");
|
24
|
+
const typeImports = new Map();
|
25
|
+
// Stores any value imports (import {} from ...).
|
26
|
+
// The key is the literal import path ("../foo.js");
|
27
|
+
const valueImports = new Map();
|
28
|
+
|
29
|
+
// Takes the node that represents an import ("Foo", "Foo as Bar") and
|
30
|
+
// return the literal text.
|
31
|
+
function getTextForImportSpecifier(specifier) {
|
32
|
+
// import {Foo as Bar} from 'foo';
|
33
|
+
// Foo = imported name
|
34
|
+
// Bar = local name
|
35
|
+
const localName = specifier.local.name;
|
36
|
+
const importedName = specifier.imported.name;
|
37
|
+
if (localName === importedName) {
|
38
|
+
// No `X as Y`, so just use either name.
|
39
|
+
return localName;
|
40
|
+
}
|
41
|
+
return `${importedName} as ${localName}`;
|
42
|
+
}
|
43
|
+
|
44
|
+
function mergeImports(fixer, typeImportNode, valueImportNode) {
|
45
|
+
// Get all the references from the type import node that we need to add to the value import node.
|
46
|
+
const typeImportSpecifiers = typeImportNode.specifiers.map(spec => {
|
47
|
+
return getTextForImportSpecifier(spec);
|
48
|
+
});
|
49
|
+
|
50
|
+
// Find the last value specifier, which we will then insert the type imports to.
|
51
|
+
const lastValueSpecifier = valueImportNode.specifiers[valueImportNode.specifiers.length - 1];
|
52
|
+
|
53
|
+
// Remember that we don't need to concern ourselves with indentation: in
|
54
|
+
// PRESUBMIT clang-format runs _after_ ESLint, so we can let Clang tidy
|
55
|
+
// up any rough edges.
|
56
|
+
const textToImport = ', ' +
|
57
|
+
typeImportSpecifiers
|
58
|
+
.map(spec => `type ${spec}`)
|
59
|
+
.join(', ');
|
60
|
+
|
61
|
+
return [
|
62
|
+
// Remove the type import
|
63
|
+
fixer.remove(typeImportNode),
|
64
|
+
// Add the type imports to the existing import
|
65
|
+
fixer.insertTextAfter(lastValueSpecifier, textToImport)
|
66
|
+
];
|
67
|
+
}
|
68
|
+
|
69
|
+
function inlineTypeImportKeyword(fixer, typeImportNode) {
|
70
|
+
// We need to remove the " type" text after "import".
|
71
|
+
const importStart = typeImportNode.range[0];
|
72
|
+
const typeImportStart = importStart + 6; // 6 here = length of "import"
|
73
|
+
const typeImportEnd = typeImportStart + 5; // 5 here = length of "type" + 1 to remove the space after it.
|
74
|
+
|
75
|
+
const addTypeToSpecifiersFixers = typeImportNode.specifiers.map(spec => {
|
76
|
+
const newText = getTextForImportSpecifier(spec);
|
77
|
+
|
78
|
+
return fixer.replaceText(spec, `type ${newText}`);
|
79
|
+
});
|
80
|
+
|
81
|
+
return [
|
82
|
+
...addTypeToSpecifiersFixers,
|
83
|
+
fixer.removeRange([typeImportStart, typeImportEnd]),
|
84
|
+
];
|
85
|
+
}
|
86
|
+
|
87
|
+
return {
|
88
|
+
ImportDeclaration(node) {
|
89
|
+
// Note that we only care about named imports: import {} from 'foo.js'.
|
90
|
+
// This is because:
|
91
|
+
// 1: if we have `import type * as SDK from '../` that means we know we
|
92
|
+
// aren't using `SDK` for any values, otherwise we wouldn't have the
|
93
|
+
// `type` modifier.
|
94
|
+
// 2: similarly, `import type Foo from './foo'` follows (1). We also
|
95
|
+
// don't use this pattern in DevTools, but even if we did we don't have
|
96
|
+
// to worry about it.
|
97
|
+
// 3: Any side-effect imports (import './foo.js') are irrelevant.
|
98
|
+
|
99
|
+
if (!node.specifiers || node.specifiers.length < 1) {
|
100
|
+
// import './foo.js';
|
101
|
+
return;
|
102
|
+
}
|
103
|
+
|
104
|
+
if (node.specifiers[0].type === 'ImportDefaultSpecifier') {
|
105
|
+
// import Foo from './foo.js';
|
106
|
+
return;
|
107
|
+
}
|
108
|
+
|
109
|
+
if (node.specifiers[0].type === 'ImportNamespaceSpecifier') {
|
110
|
+
// import * as Foo from './foo.js';
|
111
|
+
return;
|
112
|
+
}
|
113
|
+
|
114
|
+
// Store the import
|
115
|
+
const importFilePath = node.source.value;
|
116
|
+
if (node.importKind === 'type') {
|
117
|
+
typeImports.set(importFilePath, node);
|
118
|
+
} else if (node.importKind === 'value') {
|
119
|
+
valueImports.set(importFilePath, node);
|
120
|
+
}
|
121
|
+
},
|
122
|
+
'Program:exit'() {
|
123
|
+
// Loop over the type imports and see if there are any matching value
|
124
|
+
// imports.
|
125
|
+
// Looping this way means if there are any value imports without a
|
126
|
+
// matching type import, we leave them alone.
|
127
|
+
for (const [typeImportFilePath, typeImportNode] of typeImports) {
|
128
|
+
const valueImportNodeForFilePath = valueImports.get(typeImportFilePath);
|
129
|
+
if (valueImportNodeForFilePath) {
|
130
|
+
// If we've got here, we have two imports for the same file-path, one
|
131
|
+
// for types, and one for values, so let's merge them.
|
132
|
+
context.report({
|
133
|
+
node: typeImportNode,
|
134
|
+
messageId: 'inlineTypeImport',
|
135
|
+
fix(fixer) {
|
136
|
+
return mergeImports(fixer, typeImportNode, valueImportNodeForFilePath);
|
137
|
+
}
|
138
|
+
});
|
139
|
+
continue;
|
140
|
+
}
|
141
|
+
|
142
|
+
// At this point we have just a type import and no matching file
|
143
|
+
// import, but we still want to convert the import so that each
|
144
|
+
// imported reference uses the type modifier:
|
145
|
+
// BEFORE: import type {A, B} from '...';
|
146
|
+
// AFTER: import {type A, type B} from '...';
|
147
|
+
context.report({
|
148
|
+
node: typeImportNode,
|
149
|
+
messageId: 'convertTypeImport',
|
150
|
+
fix(fixer) {
|
151
|
+
return inlineTypeImportKeyword(fixer, typeImportNode);
|
152
|
+
}
|
153
|
+
});
|
154
|
+
}
|
155
|
+
},
|
156
|
+
};
|
157
|
+
}
|
158
|
+
};
|
@@ -0,0 +1,106 @@
|
|
1
|
+
// Copyright 2022 The Chromium Authors. All rights reserved.
|
2
|
+
// Use of this source code is governed by a BSD-style license that can be
|
3
|
+
// found in the LICENSE file.
|
4
|
+
'use strict';
|
5
|
+
|
6
|
+
const rule = require('../lib/inline_type_imports.js');
|
7
|
+
const ruleTester = new (require('eslint').RuleTester)({
|
8
|
+
parserOptions: {ecmaVersion: 9, sourceType: 'module'},
|
9
|
+
parser: require.resolve('@typescript-eslint/parser'),
|
10
|
+
});
|
11
|
+
|
12
|
+
ruleTester.run('inline_type_imports', rule, {
|
13
|
+
valid: [
|
14
|
+
{
|
15
|
+
code: 'import \'./foo.js\'',
|
16
|
+
},
|
17
|
+
{
|
18
|
+
code: 'import type * as Foo from \'./foo.js\'',
|
19
|
+
},
|
20
|
+
{
|
21
|
+
code: 'import * as Foo from \'./foo.js\'',
|
22
|
+
},
|
23
|
+
{
|
24
|
+
code: 'import Foo from \'./foo.js\'',
|
25
|
+
},
|
26
|
+
{
|
27
|
+
code: 'import type Foo from \'./foo.js\'',
|
28
|
+
},
|
29
|
+
{
|
30
|
+
code: 'import {type Foo} from \'./foo.js\'',
|
31
|
+
},
|
32
|
+
{
|
33
|
+
code: 'import {type Foo as Foo2} from \'./foo.js\'',
|
34
|
+
},
|
35
|
+
{
|
36
|
+
code: 'import {SomeValue, type Foo as Foo2} from \'./foo.js\'',
|
37
|
+
},
|
38
|
+
{
|
39
|
+
code: 'import {type Bar, type Foo as Foo2} from \'./foo.js\'',
|
40
|
+
},
|
41
|
+
],
|
42
|
+
invalid: [
|
43
|
+
{
|
44
|
+
code: `import type {AType} from './foo.js';
|
45
|
+
import {AValue} from './foo.js';`,
|
46
|
+
output: `
|
47
|
+
import {AValue, type AType} from './foo.js';`,
|
48
|
+
filename: 'front_end/components/test.ts',
|
49
|
+
errors: [{messageId: 'inlineTypeImport'}],
|
50
|
+
},
|
51
|
+
{
|
52
|
+
code: `import type {AType} from './foo.js';
|
53
|
+
import {AValue} from './foo.js';
|
54
|
+
import type {Foo} from './blah.js'`,
|
55
|
+
output: `
|
56
|
+
import {AValue, type AType} from './foo.js';
|
57
|
+
import {type Foo} from './blah.js'`,
|
58
|
+
filename: 'front_end/components/test.ts',
|
59
|
+
errors: [{messageId: 'inlineTypeImport'}, {messageId: 'convertTypeImport'}],
|
60
|
+
},
|
61
|
+
{
|
62
|
+
code: `import type {AType} from './foo.js';
|
63
|
+
import {AValue} from './foo.js';
|
64
|
+
import {Foo} from './blah.js'`,
|
65
|
+
output: `
|
66
|
+
import {AValue, type AType} from './foo.js';
|
67
|
+
import {Foo} from './blah.js'`,
|
68
|
+
filename: 'front_end/components/test.ts',
|
69
|
+
errors: [{messageId: 'inlineTypeImport'}],
|
70
|
+
},
|
71
|
+
{
|
72
|
+
code: 'import type {AType} from \'./foo.js\';',
|
73
|
+
output: 'import {type AType} from \'./foo.js\';',
|
74
|
+
filename: 'front_end/components/test.ts',
|
75
|
+
errors: [{messageId: 'convertTypeImport'}],
|
76
|
+
},
|
77
|
+
{
|
78
|
+
code: 'import type {Foo as Bar} from \'./foo.js\';',
|
79
|
+
output: 'import {type Foo as Bar} from \'./foo.js\';',
|
80
|
+
filename: 'front_end/components/test.ts',
|
81
|
+
errors: [{messageId: 'convertTypeImport'}],
|
82
|
+
},
|
83
|
+
{
|
84
|
+
code: `import {SomeValue} from './foo.js';
|
85
|
+
import type {Foo as Bar} from './foo.js';`,
|
86
|
+
output: 'import {SomeValue, type Foo as Bar} from \'./foo.js\';\n',
|
87
|
+
filename: 'front_end/components/test.ts',
|
88
|
+
errors: [{messageId: 'inlineTypeImport'}],
|
89
|
+
},
|
90
|
+
{
|
91
|
+
code: `import {SomeValue} from './foo.js';
|
92
|
+
import type {Foo as Bar, Baz} from './foo.js';`,
|
93
|
+
output: 'import {SomeValue, type Foo as Bar, type Baz} from \'./foo.js\';\n',
|
94
|
+
filename: 'front_end/components/test.ts',
|
95
|
+
errors: [{messageId: 'inlineTypeImport'}],
|
96
|
+
},
|
97
|
+
{
|
98
|
+
code: `import type {SomeValue} from './baz.js';
|
99
|
+
import type {Foo as Bar, Baz} from './foo.js';`,
|
100
|
+
output: `import {type SomeValue} from './baz.js';
|
101
|
+
import {type Foo as Bar, type Baz} from './foo.js';`,
|
102
|
+
filename: 'front_end/components/test.ts',
|
103
|
+
errors: [{messageId: 'convertTypeImport'}, {messageId: 'convertTypeImport'}],
|
104
|
+
},
|
105
|
+
]
|
106
|
+
});
|
@@ -4,7 +4,6 @@
|
|
4
4
|
|
5
5
|
import * as fs from 'fs';
|
6
6
|
import glob from 'glob';
|
7
|
-
import * as path from 'path';
|
8
7
|
import ts from 'typescript';
|
9
8
|
import * as WebIDL2 from 'webidl2';
|
10
9
|
|
@@ -45,7 +44,7 @@ for (const file of files) {
|
|
45
44
|
if (file.includes('testing')) {
|
46
45
|
continue;
|
47
46
|
}
|
48
|
-
const data = fs.readFileSync(
|
47
|
+
const data = fs.readFileSync(file, 'utf8');
|
49
48
|
const lines = data.split('\n');
|
50
49
|
const newLines = [];
|
51
50
|
for (const line of lines) {
|