62861 lines
2.2 MiB
62861 lines
2.2 MiB
/*
|
|
* Copyright (C) 2007-2021 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2008 Matt Lilek. All rights reserved.
|
|
* Copyright (C) 2008-2009 Anthony Ricaud <rik@webkit.org>
|
|
* Copyright (C) 2009-2010 Joseph Pecoraro. All rights reserved.
|
|
* Copyright (C) 2009-2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2009 280 North Inc. All Rights Reserved.
|
|
* Copyright (C) 2010 Nikita Vasilyev. All rights reserved.
|
|
* Copyright (C) 2011 Brian Grinstead All rights reserved.
|
|
* Copyright (C) 2013 Matt Holden <jftholden@yahoo.com>
|
|
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
|
|
* Copyright (C) 2013 Seokju Kwon (seokju.kwon@gmail.com)
|
|
* Copyright (C) 2013 Adobe Systems Inc. All rights reserved.
|
|
* Copyright (C) 2013-2015 University of Washington. All rights reserved.
|
|
* Copyright (C) 2014-2015 Saam Barati <saambarati1@gmail.com>
|
|
* Copyright (C) 2014 Antoine Quint
|
|
* Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
|
|
* Copyright (C) 2015-2017 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
* Copyright (C) 2017 The Chromium Authors
|
|
* Copyright (C) 2017-2018 Sony Interactive Entertainment Inc.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
/* Base/WebInspector.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
var WI = {}; // Namespace
|
|
var WebKitAdditions = {};
|
|
|
|
/* Base/Platform.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Platform = {
|
|
name: InspectorFrontendHost.platform,
|
|
version: {
|
|
name: InspectorFrontendHost.platformVersionName,
|
|
}
|
|
};
|
|
|
|
/* Base/Debouncer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// Debouncer wraps a function and continues to delay its invocation as long as
|
|
// clients continue to delay its firing. The most recent delay call overrides
|
|
// previous calls. Delays may be timeouts, animation frames, or microtasks.
|
|
//
|
|
// Example:
|
|
//
|
|
// let debouncer = new Debouncer(() => { this.refresh() });
|
|
// element.addEventListener("keydown", (event) => { debouncer.delayForTime(100); });
|
|
//
|
|
// Will ensure `refresh` will not happen until no keyevent has happened in 100ms:
|
|
//
|
|
// 0ms 100ms 200ms 300ms 400ms
|
|
// time: |-------------|-------------|-------------|-------------|
|
|
// delay: ^ ^ ^ ^ ^ ^ ^ ^
|
|
// refreshes: * (1)
|
|
//
|
|
// When the wrapped function is actually called, it will be given the most recent set of arguments.
|
|
|
|
class Debouncer
|
|
{
|
|
constructor(callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
this._callback = callback;
|
|
this._lastArguments = [];
|
|
|
|
this._timeoutIdentifier = undefined;
|
|
this._animationFrameIdentifier = undefined;
|
|
this._promiseIdentifier = undefined;
|
|
}
|
|
|
|
// Public
|
|
|
|
force()
|
|
{
|
|
this._lastArguments = arguments;
|
|
this._execute();
|
|
}
|
|
|
|
delayForTime(time, ...args)
|
|
{
|
|
console.assert(time >= 0);
|
|
|
|
this.cancel();
|
|
|
|
this._lastArguments = args;
|
|
|
|
this._timeoutIdentifier = setTimeout(() => {
|
|
this._execute();
|
|
}, time);
|
|
}
|
|
|
|
delayForFrame()
|
|
{
|
|
this.cancel();
|
|
|
|
this._lastArguments = arguments;
|
|
|
|
this._animationFrameIdentifier = requestAnimationFrame(() => {
|
|
this._execute();
|
|
});
|
|
}
|
|
|
|
delayForMicrotask()
|
|
{
|
|
this.cancel();
|
|
|
|
this._lastArguments = arguments;
|
|
|
|
let promiseIdentifier = Symbol("next-microtask");
|
|
|
|
this._promiseIdentifier = promiseIdentifier;
|
|
|
|
queueMicrotask(() => {
|
|
if (this._promiseIdentifier === promiseIdentifier)
|
|
this._execute();
|
|
});
|
|
}
|
|
|
|
cancel()
|
|
{
|
|
this._lastArguments = [];
|
|
|
|
if (this._timeoutIdentifier) {
|
|
clearTimeout(this._timeoutIdentifier);
|
|
this._timeoutIdentifier = undefined;
|
|
}
|
|
|
|
if (this._animationFrameIdentifier) {
|
|
cancelAnimationFrame(this._animationFrameIdentifier);
|
|
this._animationFrameIdentifier = undefined;
|
|
}
|
|
|
|
if (this._promiseIdentifier)
|
|
this._promiseIdentifier = undefined;
|
|
}
|
|
|
|
// Private
|
|
|
|
_execute()
|
|
{
|
|
let args = this._lastArguments;
|
|
|
|
this.cancel();
|
|
|
|
this._callback.apply(undefined, args);
|
|
}
|
|
}
|
|
|
|
/* Base/DebuggableType.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DebuggableType = {
|
|
ITML: "itml",
|
|
JavaScript: "javascript",
|
|
Page: "page",
|
|
ServiceWorker: "service-worker",
|
|
WebPage: "web-page",
|
|
};
|
|
|
|
WI.DebuggableType.fromString = function(type) {
|
|
switch (type) {
|
|
case "itml":
|
|
return WI.DebuggableType.ITML;
|
|
case "javascript":
|
|
return WI.DebuggableType.JavaScript;
|
|
case "page":
|
|
return WI.DebuggableType.Page;
|
|
case "service-worker":
|
|
return WI.DebuggableType.ServiceWorker;
|
|
case "web-page":
|
|
return WI.DebuggableType.WebPage;
|
|
}
|
|
|
|
console.assert(false, "Unknown debuggable type", type);
|
|
return null;
|
|
};
|
|
|
|
WI.DebuggableType.supportedTargetTypes = function(debuggableType) {
|
|
let targetTypes = new Set;
|
|
|
|
switch (debuggableType) {
|
|
case WI.DebuggableType.ITML:
|
|
targetTypes.add(WI.TargetType.ITML);
|
|
break;
|
|
|
|
case WI.DebuggableType.JavaScript:
|
|
targetTypes.add(WI.TargetType.JavaScript);
|
|
break;
|
|
|
|
case WI.DebuggableType.Page:
|
|
targetTypes.add(WI.TargetType.Page);
|
|
targetTypes.add(WI.TargetType.Worker);
|
|
break;
|
|
|
|
case WI.DebuggableType.ServiceWorker:
|
|
targetTypes.add(WI.TargetType.ServiceWorker);
|
|
break;
|
|
|
|
case WI.DebuggableType.WebPage:
|
|
targetTypes.add(WI.TargetType.Page);
|
|
targetTypes.add(WI.TargetType.WebPage);
|
|
targetTypes.add(WI.TargetType.Worker);
|
|
break;
|
|
}
|
|
|
|
console.assert(targetTypes.size, "Unknown debuggable type", debuggableType);
|
|
return targetTypes;
|
|
};
|
|
|
|
/* Base/IterableWeakSet.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
class IterableWeakSet
|
|
{
|
|
constructor(items = [])
|
|
{
|
|
this._wrappers = new Set;
|
|
this._wrapperForItem = new WeakMap;
|
|
|
|
for (let item of items)
|
|
this.add(item);
|
|
}
|
|
|
|
// Public
|
|
|
|
get size()
|
|
{
|
|
let size = 0;
|
|
for (let wrapper of this._wrappers) {
|
|
if (wrapper.deref())
|
|
++size;
|
|
}
|
|
return size;
|
|
}
|
|
|
|
has(item)
|
|
{
|
|
let result = this._wrapperForItem.has(item);
|
|
console.assert(Array.from(this._wrappers).some((wrapper) => wrapper.deref() === item) === result, this, item);
|
|
return result;
|
|
}
|
|
|
|
add(item)
|
|
{
|
|
console.assert(typeof item === "object", item);
|
|
console.assert(item !== null, item);
|
|
|
|
if (this.has(item))
|
|
return;
|
|
|
|
let wrapper = new WeakRef(item);
|
|
this._wrappers.add(wrapper);
|
|
this._wrapperForItem.set(item, wrapper);
|
|
this._finalizationRegistry.register(item, {weakThis: new WeakRef(this), wrapper}, wrapper);
|
|
}
|
|
|
|
delete(item)
|
|
{
|
|
return !!this.take(item);
|
|
}
|
|
|
|
take(item)
|
|
{
|
|
let wrapper = this._wrapperForItem.get(item);
|
|
if (!wrapper)
|
|
return undefined;
|
|
|
|
let itemDeleted = this._wrapperForItem.delete(item);
|
|
console.assert(itemDeleted, this, item);
|
|
|
|
let wrapperDeleted = this._wrappers.delete(wrapper);
|
|
console.assert(wrapperDeleted, this, item);
|
|
|
|
this._finalizationRegistry.unregister(wrapper);
|
|
|
|
console.assert(wrapper.deref() === item, this, item);
|
|
return item;
|
|
}
|
|
|
|
clear()
|
|
{
|
|
for (let wrapper of this._wrappers) {
|
|
this._wrapperForItem.delete(wrapper);
|
|
this._finalizationRegistry.unregister(wrapper);
|
|
}
|
|
|
|
this._wrappers.clear();
|
|
}
|
|
|
|
keys()
|
|
{
|
|
return this.values();
|
|
}
|
|
|
|
*values()
|
|
{
|
|
for (let wrapper of this._wrappers) {
|
|
let item = wrapper.deref();
|
|
console.assert(!item === !this._wrapperForItem.has(item), this, item);
|
|
if (item)
|
|
yield item;
|
|
}
|
|
}
|
|
|
|
[Symbol.iterator]()
|
|
{
|
|
return this.values();
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new IterableWeakSet(this.toJSON());
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return Array.from(this);
|
|
}
|
|
|
|
// Private
|
|
|
|
get _finalizationRegistry()
|
|
{
|
|
return IterableWeakSet._finalizationRegistry ??= new FinalizationRegistry(function(heldValue) {
|
|
heldValue.weakThis.deref()?._wrappers.delete(heldValue.wrapper);
|
|
});
|
|
}
|
|
}
|
|
|
|
/* Base/Multimap.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018-2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
class Multimap
|
|
{
|
|
constructor(items = [])
|
|
{
|
|
this._map = new Map;
|
|
|
|
for (let [key, value] of items)
|
|
this.add(key, value);
|
|
}
|
|
|
|
// Public
|
|
|
|
get size()
|
|
{
|
|
return this._map.size;
|
|
}
|
|
|
|
has(key, value)
|
|
{
|
|
let valueSet = this._map.get(key);
|
|
if (!valueSet)
|
|
return false;
|
|
return value === undefined || valueSet.has(value);
|
|
}
|
|
|
|
get(key)
|
|
{
|
|
return this._map.get(key);
|
|
}
|
|
|
|
add(key, value)
|
|
{
|
|
let valueSet = this._map.get(key);
|
|
if (!valueSet) {
|
|
valueSet = new Set;
|
|
this._map.set(key, valueSet);
|
|
}
|
|
valueSet.add(value);
|
|
|
|
return this;
|
|
}
|
|
|
|
delete(key, value)
|
|
{
|
|
// Allow an entire key to be removed by not passing a value.
|
|
if (arguments.length === 1)
|
|
return this._map.delete(key);
|
|
|
|
let valueSet = this._map.get(key);
|
|
if (!valueSet)
|
|
return false;
|
|
|
|
let deleted = valueSet.delete(value);
|
|
|
|
if (!valueSet.size)
|
|
this._map.delete(key);
|
|
|
|
return deleted;
|
|
}
|
|
|
|
take(key, value)
|
|
{
|
|
// Allow an entire key to be removed by not passing a value.
|
|
if (arguments.length === 1)
|
|
return this._map.take(key);
|
|
|
|
let valueSet = this._map.get(key);
|
|
if (!valueSet)
|
|
return undefined;
|
|
|
|
let result = valueSet.take(value);
|
|
if (!valueSet.size)
|
|
this._map.delete(key);
|
|
return result;
|
|
}
|
|
|
|
clear()
|
|
{
|
|
this._map.clear();
|
|
}
|
|
|
|
keys()
|
|
{
|
|
return this._map.keys();
|
|
}
|
|
|
|
*values()
|
|
{
|
|
for (let valueSet of this._map.values()) {
|
|
for (let value of valueSet)
|
|
yield value;
|
|
}
|
|
}
|
|
|
|
sets()
|
|
{
|
|
return this._map.entries();
|
|
}
|
|
|
|
*[Symbol.iterator]()
|
|
{
|
|
for (let [key, valueSet] of this._map) {
|
|
for (let value of valueSet)
|
|
yield [key, value];
|
|
}
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new Multimap(this.toJSON());
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return Array.from(this);
|
|
}
|
|
}
|
|
|
|
/* Base/Object.js */
|
|
|
|
/*
|
|
* Copyright (C) 2008, 2013 Apple Inc. All Rights Reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
|
|
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
|
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
|
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Object = class WebInspectorObject
|
|
{
|
|
constructor()
|
|
{
|
|
this._listeners = null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static addEventListener(eventType, listener, thisObject)
|
|
{
|
|
console.assert(typeof eventType === "string", this, eventType, listener, thisObject);
|
|
console.assert(typeof listener === "function", this, eventType, listener, thisObject);
|
|
console.assert(typeof thisObject === "object" || window.InspectorTest || window.ProtocolTest, this, eventType, listener, thisObject);
|
|
|
|
thisObject ??= this;
|
|
|
|
let data = {
|
|
listener,
|
|
thisObjectWeakRef: new WeakRef(thisObject),
|
|
};
|
|
|
|
WI.Object._listenerThisObjectFinalizationRegistry.register(thisObject, {eventTargetWeakRef: new WeakRef(this), eventType, data}, data);
|
|
|
|
this._listeners ??= new Multimap;
|
|
this._listeners.add(eventType, data);
|
|
|
|
console.assert(Array.from(this._listeners.get(eventType)).filter((item) => item.listener === listener && item.thisObjectWeakRef.deref() === thisObject).length === 1, this, eventType, listener, thisObject);
|
|
|
|
return listener;
|
|
}
|
|
|
|
static singleFireEventListener(eventType, listener, thisObject)
|
|
{
|
|
let eventTargetWeakRef = new WeakRef(this);
|
|
return this.addEventListener(eventType, function wrappedCallback() {
|
|
eventTargetWeakRef.deref()?.removeEventListener(eventType, wrappedCallback, this);
|
|
listener.apply(this, arguments);
|
|
}, thisObject);
|
|
}
|
|
|
|
static awaitEvent(eventType, thisObject)
|
|
{
|
|
return new Promise((resolve, reject) => {
|
|
this.singleFireEventListener(eventType, resolve, thisObject);
|
|
});
|
|
}
|
|
|
|
static removeEventListener(eventType, listener, thisObject)
|
|
{
|
|
console.assert(this._listeners, this, eventType, listener, thisObject);
|
|
console.assert(typeof eventType === "string", this, eventType, listener, thisObject);
|
|
console.assert(typeof listener === "function", this, eventType, listener, thisObject);
|
|
console.assert(typeof thisObject === "object" || window.InspectorTest || window.ProtocolTest, this, eventType, listener, thisObject);
|
|
|
|
if (!this._listeners)
|
|
return;
|
|
|
|
thisObject ??= this;
|
|
|
|
let listenersForEventType = this._listeners.get(eventType);
|
|
console.assert(listenersForEventType, this, eventType, listener, thisObject);
|
|
if (!listenersForEventType)
|
|
return;
|
|
|
|
let didDelete = false;
|
|
for (let data of listenersForEventType) {
|
|
let unwrapped = data.thisObjectWeakRef.deref();
|
|
if (!unwrapped || unwrapped !== thisObject || data.listener !== listener)
|
|
continue;
|
|
|
|
if (this._listeners.delete(eventType, data))
|
|
didDelete = true;
|
|
WI.Object._listenerThisObjectFinalizationRegistry.unregister(data);
|
|
}
|
|
console.assert(didDelete, this, eventType, listener, thisObject);
|
|
}
|
|
|
|
// Public
|
|
|
|
addEventListener() { return WI.Object.addEventListener.apply(this, arguments); }
|
|
singleFireEventListener() { return WI.Object.singleFireEventListener.apply(this, arguments); }
|
|
awaitEvent() { return WI.Object.awaitEvent.apply(this, arguments); }
|
|
removeEventListener() { return WI.Object.removeEventListener.apply(this, arguments); }
|
|
|
|
dispatchEventToListeners(eventType, eventData)
|
|
{
|
|
let event = new WI.Event(this, eventType, eventData);
|
|
|
|
function dispatch(object)
|
|
{
|
|
if (!object || event._stoppedPropagation)
|
|
return;
|
|
|
|
let listeners = object._listeners;
|
|
if (!listeners || !object.hasOwnProperty("_listeners") || !listeners.size)
|
|
return;
|
|
|
|
let listenersForEventType = listeners.get(eventType);
|
|
if (!listenersForEventType)
|
|
return;
|
|
|
|
// Copy the set of listeners so we don't have to worry about mutating while iterating.
|
|
for (let data of Array.from(listenersForEventType)) {
|
|
let unwrapped = data.thisObjectWeakRef.deref();
|
|
if (!unwrapped)
|
|
continue;
|
|
|
|
data.listener.call(unwrapped, event);
|
|
|
|
if (event._stoppedPropagation)
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Dispatch to listeners of this specific object.
|
|
dispatch(this);
|
|
|
|
// Allow propagation again so listeners on the constructor always have a crack at the event.
|
|
event._stoppedPropagation = false;
|
|
|
|
// Dispatch to listeners on all constructors up the prototype chain, including the immediate constructor.
|
|
let constructor = this.constructor;
|
|
while (constructor) {
|
|
dispatch(constructor);
|
|
|
|
if (!constructor.prototype.__proto__)
|
|
break;
|
|
|
|
constructor = constructor.prototype.__proto__.constructor;
|
|
}
|
|
|
|
return event.defaultPrevented;
|
|
}
|
|
|
|
// Test
|
|
|
|
static hasEventListeners(eventType)
|
|
{
|
|
console.assert(window.InspectorTest || window.ProtocolTest);
|
|
return this._listeners?.has(eventType);
|
|
}
|
|
|
|
static activelyListeningObjectsWithPrototype(proto)
|
|
{
|
|
console.assert(window.InspectorTest || window.ProtocolTest);
|
|
let results = new Set;
|
|
if (this._listeners) {
|
|
for (let data of this._listeners.values()) {
|
|
let unwrapped = data.thisObjectWeakRef.deref();
|
|
if (unwrapped instanceof proto)
|
|
results.add(unwrapped);
|
|
}
|
|
}
|
|
return results;
|
|
}
|
|
|
|
hasEventListeners() { return WI.Object.hasEventListeners.apply(this, arguments); }
|
|
activelyListeningObjectsWithPrototype() { return WI.Object.activelyListeningObjectsWithPrototype.apply(this, arguments); }
|
|
};
|
|
|
|
WI.Object._listenerThisObjectFinalizationRegistry = new FinalizationRegistry((heldValue) => {
|
|
heldValue.eventTargetWeakRef.deref()?._listeners.delete(heldValue.eventType, heldValue.data);
|
|
});
|
|
|
|
WI.Event = class Event
|
|
{
|
|
constructor(target, type, data)
|
|
{
|
|
this.target = target;
|
|
this.type = type;
|
|
this.data = data;
|
|
this.defaultPrevented = false;
|
|
this._stoppedPropagation = false;
|
|
}
|
|
|
|
stopPropagation()
|
|
{
|
|
this._stoppedPropagation = true;
|
|
}
|
|
|
|
preventDefault()
|
|
{
|
|
this.defaultPrevented = true;
|
|
}
|
|
};
|
|
|
|
WI.notifications = new WI.Object;
|
|
|
|
WI.Notification = {
|
|
GlobalModifierKeysDidChange: "global-modifiers-did-change",
|
|
PageArchiveStarted: "page-archive-started",
|
|
PageArchiveEnded: "page-archive-ended",
|
|
ExtraDomainsActivated: "extra-domains-activated", // COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
VisibilityStateDidChange: "visibility-state-did-change",
|
|
TransitionPageTarget: "transition-page-target",
|
|
};
|
|
|
|
/* Base/ReferencePage.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ReferencePage = class ReferencePage {
|
|
constructor(page, {topic} = {})
|
|
{
|
|
console.assert(page instanceof WI.ReferencePage || typeof page === "string", page);
|
|
console.assert(!(page instanceof WI.ReferencePage && page._page instanceof WI.ReferencePage), page);
|
|
console.assert(!(page instanceof WI.ReferencePage && page.topic), page);
|
|
console.assert(!topic || typeof topic === "string", topic);
|
|
|
|
if (page instanceof WI.ReferencePage)
|
|
page = page.page;
|
|
this._page = page;
|
|
|
|
this._topic = topic || "";
|
|
}
|
|
|
|
// Public
|
|
|
|
get page() { return this._page; }
|
|
get topic() { return this._topic; }
|
|
|
|
createLinkElement()
|
|
{
|
|
let url = "https://webkit.org/web-inspector/" + this._page + "/";
|
|
if (this._topic)
|
|
url += "#" + this._topic;
|
|
|
|
let wrapper = document.createElement("span");
|
|
wrapper.className = "reference-page-link-container";
|
|
|
|
let link = wrapper.appendChild(document.createElement("a"));
|
|
link.className = "reference-page-link";
|
|
link.href = link.title = url;
|
|
link.textContent = "?";
|
|
link.addEventListener("click", (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
WI.openURL(link.href, {alwaysOpenExternally: true});
|
|
});
|
|
|
|
return wrapper;
|
|
}
|
|
};
|
|
|
|
WI.ReferencePage.AuditTab = new WI.ReferencePage("audit-tab");
|
|
WI.ReferencePage.AuditTab.AuditResults = new WI.ReferencePage(WI.ReferencePage.AuditTab, {topic: "audit-results"});
|
|
WI.ReferencePage.AuditTab.CreatingAudits = new WI.ReferencePage(WI.ReferencePage.AuditTab, {topic: "creating-audits"});
|
|
WI.ReferencePage.AuditTab.EditingAudits = new WI.ReferencePage(WI.ReferencePage.AuditTab, {topic: "editing-audits"});
|
|
WI.ReferencePage.AuditTab.RunningAudits = new WI.ReferencePage(WI.ReferencePage.AuditTab, {topic: "running-audits"});
|
|
|
|
WI.ReferencePage.DOMBreakpoints = new WI.ReferencePage("dom-breakpoints");
|
|
WI.ReferencePage.DOMBreakpoints.Configuration = new WI.ReferencePage(WI.ReferencePage.DOMBreakpoints, {topic: "configuration"});
|
|
|
|
WI.ReferencePage.DeviceSettings = new WI.ReferencePage("device-settings");
|
|
WI.ReferencePage.DeviceSettings.Configuration = new WI.ReferencePage(WI.ReferencePage.DeviceSettings, {topic: "configuration"});
|
|
|
|
WI.ReferencePage.ElementsTab = new WI.ReferencePage("elements-tab");
|
|
WI.ReferencePage.ElementsTab.DOMTree = new WI.ReferencePage(WI.ReferencePage.ElementsTab, {topic: "dom-tree"});
|
|
|
|
WI.ReferencePage.EventBreakpoints = new WI.ReferencePage("event-breakpoints");
|
|
WI.ReferencePage.EventBreakpoints.Configuration = new WI.ReferencePage(WI.ReferencePage.EventBreakpoints, {topic: "configuration"});
|
|
|
|
WI.ReferencePage.JavaScriptBreakpoints = new WI.ReferencePage("javascript-breakpoints");
|
|
WI.ReferencePage.JavaScriptBreakpoints.Configuration = new WI.ReferencePage(WI.ReferencePage.JavaScriptBreakpoints, {topic: "configuration"});
|
|
|
|
WI.ReferencePage.LayersTab = new WI.ReferencePage("layers-tab");
|
|
|
|
WI.ReferencePage.LocalOverrides = new WI.ReferencePage("local-overrides");
|
|
WI.ReferencePage.LocalOverrides.ConfiguringLocalOverrides = new WI.ReferencePage(WI.ReferencePage.LocalOverrides, {topic: "configuring-local-overrides"});
|
|
|
|
WI.ReferencePage.NetworkTab = new WI.ReferencePage("network-tab");
|
|
WI.ReferencePage.NetworkTab.CookiesPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "cookies-pane"});
|
|
WI.ReferencePage.NetworkTab.HeadersPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "headers-pane"});
|
|
WI.ReferencePage.NetworkTab.PreviewPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "preview-pane"});
|
|
WI.ReferencePage.NetworkTab.SecurityPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "security-pane"});
|
|
WI.ReferencePage.NetworkTab.SizesPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "sizes-pane"});
|
|
WI.ReferencePage.NetworkTab.TimingPane = new WI.ReferencePage(WI.ReferencePage.NetworkTab, {topic: "timing-pane"});
|
|
|
|
WI.ReferencePage.SymbolicBreakpoints = new WI.ReferencePage("symbolic-breakpoints");
|
|
WI.ReferencePage.SymbolicBreakpoints.Configuration = new WI.ReferencePage(WI.ReferencePage.SymbolicBreakpoints, {topic: "configuration"});
|
|
|
|
WI.ReferencePage.TimelinesTab = new WI.ReferencePage("timelines-tab");
|
|
WI.ReferencePage.TimelinesTab.CPUTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "cpu-timeline"});
|
|
WI.ReferencePage.TimelinesTab.EventsView = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "events-view"});
|
|
WI.ReferencePage.TimelinesTab.FramesView = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "frames-view"});
|
|
WI.ReferencePage.TimelinesTab.JavaScriptAllocationsTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "javascript-allocations-timeline"});
|
|
WI.ReferencePage.TimelinesTab.JavaScriptAndEventsTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "javascript-events-timeline"});
|
|
WI.ReferencePage.TimelinesTab.LayoutAndRenderingTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "layout-rendering-timeline"});
|
|
WI.ReferencePage.TimelinesTab.MediaAndAnimationsTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "media-animations-timeline"});
|
|
WI.ReferencePage.TimelinesTab.MemoryTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "memory-timeline"});
|
|
WI.ReferencePage.TimelinesTab.NetworkRequestsTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "network-timeline"});
|
|
WI.ReferencePage.TimelinesTab.ScreenshotsTimeline = new WI.ReferencePage(WI.ReferencePage.TimelinesTab, {topic: "screenshots-timeline"});
|
|
|
|
WI.ReferencePage.URLBreakpoints = new WI.ReferencePage("url-breakpoints");
|
|
WI.ReferencePage.URLBreakpoints.Configuration = new WI.ReferencePage(WI.ReferencePage.URLBreakpoints, {topic: "configuration"});
|
|
|
|
/* Base/TargetType.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TargetType = {
|
|
ITML: "itml",
|
|
JavaScript: "javascript",
|
|
Page: "page",
|
|
ServiceWorker: "service-worker",
|
|
WebPage: "web-page",
|
|
Worker: "worker",
|
|
};
|
|
|
|
WI.TargetType.all = Object.values(WI.TargetType);
|
|
|
|
/* Base/Throttler.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// Throttler wraps a function and ensures it doesn't get called more than once every `delay` ms.
|
|
// The first fire will always trigger synchronous, and subsequent calls that need to be throttled
|
|
// will happen later asynchronously.
|
|
//
|
|
// Example:
|
|
//
|
|
// let throttler = new Throttler(250, () => { this.refresh() });
|
|
// element.addEventListener("keydown", (event) => { throttler.fire(); });
|
|
//
|
|
// Will ensure `refresh` happens no more than once every 250ms no matter how often `fire` is called:
|
|
//
|
|
// 0ms 250ms 500ms
|
|
// time: |---------------------------|---------------------------|
|
|
// fire: ^ ^ ^ ^ ^ ^ ^ ^
|
|
// refreshes: * (1) * (2) * (3)
|
|
//
|
|
// When the wrapped function is actually called, it will be given the most recent set of arguments.
|
|
|
|
class Throttler
|
|
{
|
|
constructor(callback, delay)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
console.assert(delay >= 0);
|
|
|
|
this._callback = callback;
|
|
this._delay = delay;
|
|
|
|
this._lastArguments = [];
|
|
|
|
this._timeoutIdentifier = undefined;
|
|
this._lastFireTime = -this._delay;
|
|
}
|
|
|
|
// Public
|
|
|
|
force()
|
|
{
|
|
this._lastArguments = arguments;
|
|
this._execute();
|
|
}
|
|
|
|
fire()
|
|
{
|
|
this._lastArguments = arguments;
|
|
|
|
let remaining = this._delay - (Date.now() - this._lastFireTime);
|
|
if (remaining <= 0) {
|
|
this._execute();
|
|
return;
|
|
}
|
|
|
|
if (this._timeoutIdentifier)
|
|
return;
|
|
|
|
this._timeoutIdentifier = setTimeout(() => {
|
|
this._execute();
|
|
}, remaining);
|
|
}
|
|
|
|
cancel()
|
|
{
|
|
this._lastArguments = [];
|
|
|
|
if (this._timeoutIdentifier) {
|
|
clearTimeout(this._timeoutIdentifier);
|
|
this._timeoutIdentifier = undefined;
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
_execute()
|
|
{
|
|
this._lastFireTime = Date.now();
|
|
|
|
let args = this._lastArguments;
|
|
|
|
this.cancel();
|
|
|
|
this._callback.apply(undefined, args);
|
|
}
|
|
}
|
|
|
|
/* Test/TestHarness.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015-2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
TestHarness = class TestHarness extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._logCount = 0;
|
|
this._failureObjects = new Map;
|
|
this._failureObjectIdentifier = 1;
|
|
|
|
// Options that are set per-test for debugging purposes.
|
|
this.forceDebugLogging = false;
|
|
|
|
// Options that are set per-test to ensure deterministic output.
|
|
this.suppressStackTraces = false;
|
|
}
|
|
|
|
completeTest()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
addResult()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
debugLog()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
// If 'callback' is a function, it will be with the arguments
|
|
// callback(error, result, wasThrown). Otherwise, a promise is
|
|
// returned that resolves with 'result' or rejects with 'error'.
|
|
|
|
// The options object accepts the following keys and values:
|
|
// 'remoteObjectOnly': if true, do not unwrap the result payload to a
|
|
// primitive value even if possible. Useful if testing WI.RemoteObject directly.
|
|
evaluateInPage(string, callback, options={})
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
debug()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
createAsyncSuite(name)
|
|
{
|
|
return new AsyncTestSuite(this, name);
|
|
}
|
|
|
|
createSyncSuite(name)
|
|
{
|
|
return new SyncTestSuite(this, name);
|
|
}
|
|
|
|
get logCount()
|
|
{
|
|
return this._logCount;
|
|
}
|
|
|
|
log(message)
|
|
{
|
|
++this._logCount;
|
|
|
|
if (this.forceDebugLogging)
|
|
this.debugLog(message);
|
|
else
|
|
this.addResult(message);
|
|
}
|
|
|
|
newline()
|
|
{
|
|
this.log("");
|
|
}
|
|
|
|
json(object, filter)
|
|
{
|
|
this.log(JSON.stringify(object, filter || null, 2));
|
|
}
|
|
|
|
assert(condition, message)
|
|
{
|
|
if (condition)
|
|
return;
|
|
|
|
let stringifiedMessage = TestHarness.messageAsString(message);
|
|
this.log("ASSERT: " + stringifiedMessage);
|
|
}
|
|
|
|
expectThat(actual, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.True, !!actual, message, actual);
|
|
}
|
|
|
|
expectTrue(actual, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.True, !!actual, message, actual);
|
|
}
|
|
|
|
expectFalse(actual, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.False, !actual, message, actual);
|
|
}
|
|
|
|
expectEmpty(actual, message)
|
|
{
|
|
if (Array.isArray(actual) || typeof actual === "string") {
|
|
this._expect(TestHarness.ExpectationType.Empty, !actual.length, message, actual);
|
|
return;
|
|
}
|
|
|
|
if (actual instanceof Set || actual instanceof Map) {
|
|
this._expect(TestHarness.ExpectationType.Empty, !actual.size, message, actual);
|
|
return;
|
|
}
|
|
|
|
if (typeof actual === "object") {
|
|
this._expect(TestHarness.ExpectationType.Empty, isEmptyObject(actual), message, actual);
|
|
return;
|
|
}
|
|
|
|
this.fail("expectEmpty should not be called with a non-object:\n Actual: " + this._expectationValueAsString(actual));
|
|
}
|
|
|
|
expectNotEmpty(actual, message)
|
|
{
|
|
if (Array.isArray(actual) || typeof actual === "string") {
|
|
this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.length, message, actual);
|
|
return;
|
|
}
|
|
|
|
if (actual instanceof Set || actual instanceof Map) {
|
|
this._expect(TestHarness.ExpectationType.NotEmpty, !!actual.size, message, actual);
|
|
return;
|
|
}
|
|
|
|
if (typeof actual === "object") {
|
|
this._expect(TestHarness.ExpectationType.NotEmpty, !isEmptyObject(actual), message, actual);
|
|
return;
|
|
}
|
|
|
|
this.fail("expectNotEmpty should not be called with a non-object:\n Actual: " + this._expectationValueAsString(actual));
|
|
}
|
|
|
|
expectNull(actual, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.Null, actual === null, message, actual, null);
|
|
}
|
|
|
|
expectNotNull(actual, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.NotNull, actual !== null, message, actual);
|
|
}
|
|
|
|
expectEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.Equal, expected === actual, message, actual, expected);
|
|
}
|
|
|
|
expectNotEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.NotEqual, expected !== actual, message, actual, expected);
|
|
}
|
|
|
|
expectShallowEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.ShallowEqual, Object.shallowEqual(actual, expected), message, actual, expected);
|
|
}
|
|
|
|
expectNotShallowEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.NotShallowEqual, !Object.shallowEqual(actual, expected), message, actual, expected);
|
|
}
|
|
|
|
expectEqualWithAccuracy(actual, expected, accuracy, message)
|
|
{
|
|
console.assert(typeof expected === "number");
|
|
console.assert(typeof actual === "number");
|
|
|
|
this._expect(TestHarness.ExpectationType.EqualWithAccuracy, Math.abs(expected - actual) <= accuracy, message, actual, expected, accuracy);
|
|
}
|
|
|
|
expectLessThan(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.LessThan, actual < expected, message, actual, expected);
|
|
}
|
|
|
|
expectLessThanOrEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.LessThanOrEqual, actual <= expected, message, actual, expected);
|
|
}
|
|
|
|
expectGreaterThan(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.GreaterThan, actual > expected, message, actual, expected);
|
|
}
|
|
|
|
expectGreaterThanOrEqual(actual, expected, message)
|
|
{
|
|
this._expect(TestHarness.ExpectationType.GreaterThanOrEqual, actual >= expected, message, actual, expected);
|
|
}
|
|
|
|
pass(message)
|
|
{
|
|
let stringifiedMessage = TestHarness.messageAsString(message);
|
|
this.log("PASS: " + stringifiedMessage);
|
|
}
|
|
|
|
fail(message)
|
|
{
|
|
let stringifiedMessage = TestHarness.messageAsString(message);
|
|
this.log("FAIL: " + stringifiedMessage);
|
|
}
|
|
|
|
passOrFail(condition, message)
|
|
{
|
|
if (condition)
|
|
this.pass(message);
|
|
else
|
|
this.fail(message);
|
|
}
|
|
|
|
// Use this to expect an exception. To further examine the exception,
|
|
// chain onto the result with .then() and add your own test assertions.
|
|
// The returned promise is rejected if an exception was not thrown.
|
|
expectException(work)
|
|
{
|
|
if (typeof work !== "function")
|
|
throw new Error("Invalid argument to catchException: work must be a function.");
|
|
|
|
let expectAndDumpError = (e, resolvedValue) => {
|
|
this.expectNotNull(e, "Should produce an exception.");
|
|
if (!e) {
|
|
this.expectEqual(resolvedValue, undefined, "Exception-producing work should not return a value");
|
|
return;
|
|
}
|
|
|
|
if (e instanceof Error || !(e instanceof Object))
|
|
this.log(e.toString());
|
|
else {
|
|
try {
|
|
this.json(e);
|
|
} catch {
|
|
this.log(e.constructor.name);
|
|
}
|
|
}
|
|
}
|
|
|
|
let error = null;
|
|
let result = null;
|
|
try {
|
|
result = work();
|
|
} catch (caughtError) {
|
|
error = caughtError;
|
|
} finally {
|
|
// If 'work' returns a promise, it will settle (resolve or reject) by itself.
|
|
// Invert the promise's settled state to match the expectation of the caller.
|
|
if (result instanceof Promise) {
|
|
return result.then((resolvedValue) => {
|
|
expectAndDumpError(null, resolvedValue);
|
|
return Promise.reject(resolvedValue);
|
|
}, (e) => { // Don't chain the .catch as it will log the value we just rejected.
|
|
expectAndDumpError(e);
|
|
return Promise.resolve(e);
|
|
});
|
|
}
|
|
|
|
// If a promise is not produced, turn the exception into a resolved promise, and a
|
|
// resolved value into a rejected value (since an exception was expected).
|
|
expectAndDumpError(error);
|
|
return error ? Promise.resolve(error) : Promise.reject(result);
|
|
}
|
|
}
|
|
|
|
// Protected
|
|
|
|
static messageAsString(message)
|
|
{
|
|
if (message instanceof Element)
|
|
return message.textContent;
|
|
|
|
return typeof message !== "string" ? JSON.stringify(message) : message;
|
|
}
|
|
|
|
static sanitizeURL(url)
|
|
{
|
|
if (!url)
|
|
return "(unknown)";
|
|
|
|
let lastPathSeparator = Math.max(url.lastIndexOf("/"), url.lastIndexOf("\\"));
|
|
let location = lastPathSeparator > 0 ? url.substr(lastPathSeparator + 1) : url;
|
|
if (!location.length)
|
|
location = "(unknown)";
|
|
|
|
// Clean up the location so it is bracketed or in parenthesis.
|
|
if (url.indexOf("[native code]") !== -1)
|
|
location = "[native code]";
|
|
|
|
return location;
|
|
}
|
|
|
|
static sanitizeStackFrame(frame, i)
|
|
{
|
|
// Most frames are of the form "functionName@file:///foo/bar/File.js:345".
|
|
// But, some frames do not have a functionName. Get rid of the file path.
|
|
let nameAndURLSeparator = frame.indexOf("@");
|
|
let frameName = nameAndURLSeparator > 0 ? frame.substr(0, nameAndURLSeparator) : "(anonymous)";
|
|
|
|
let lastPathSeparator = Math.max(frame.lastIndexOf("/"), frame.lastIndexOf("\\"));
|
|
let frameLocation = lastPathSeparator > 0 ? frame.substr(lastPathSeparator + 1) : frame;
|
|
if (!frameLocation.length)
|
|
frameLocation = "unknown";
|
|
|
|
// Clean up the location so it is bracketed or in parenthesis.
|
|
if (frame.indexOf("[native code]") !== -1)
|
|
frameLocation = "[native code]";
|
|
else
|
|
frameLocation = "(" + frameLocation + ")";
|
|
|
|
return `#${i}: ${frameName} ${frameLocation}`;
|
|
}
|
|
|
|
sanitizeStack(stack)
|
|
{
|
|
if (this.suppressStackTraces)
|
|
return "(suppressed)";
|
|
|
|
if (!stack || typeof stack !== "string")
|
|
return "(unknown)";
|
|
|
|
return stack.split("\n").map(TestHarness.sanitizeStackFrame).join("\n");
|
|
}
|
|
|
|
// Private
|
|
|
|
_expect(type, condition, message, ...values)
|
|
{
|
|
console.assert(values.length > 0, "Should have an 'actual' value.");
|
|
|
|
if (!message || !condition) {
|
|
values = values.map(this._expectationValueAsString.bind(this));
|
|
message = message || this._expectationMessageFormat(type).format(...values);
|
|
}
|
|
|
|
if (condition) {
|
|
this.pass(message);
|
|
return;
|
|
}
|
|
|
|
message += "\n Expected: " + this._expectedValueFormat(type).format(...values.slice(1));
|
|
message += "\n Actual: " + values[0];
|
|
|
|
this.fail(message);
|
|
}
|
|
|
|
_expectationValueAsString(value)
|
|
{
|
|
let instanceIdentifier = (object) => {
|
|
let id = this._failureObjects.get(object);
|
|
if (!id) {
|
|
id = this._failureObjectIdentifier++;
|
|
this._failureObjects.set(object, id);
|
|
}
|
|
return "#" + id;
|
|
};
|
|
|
|
const maximumValueStringLength = 200;
|
|
const defaultValueString = String(new Object); // [object Object]
|
|
|
|
// Special case for numbers, since JSON.stringify converts Infinity and NaN to null.
|
|
if (typeof value === "number")
|
|
return value;
|
|
|
|
try {
|
|
let valueString = JSON.stringify(value);
|
|
if (valueString.length <= maximumValueStringLength)
|
|
return valueString;
|
|
} catch { }
|
|
|
|
try {
|
|
let valueString = String(value);
|
|
if (valueString === defaultValueString && value.constructor && value.constructor.name !== "Object")
|
|
return value.constructor.name + " instance " + instanceIdentifier(value);
|
|
return valueString;
|
|
} catch {
|
|
return defaultValueString;
|
|
}
|
|
}
|
|
|
|
_expectationMessageFormat(type)
|
|
{
|
|
switch (type) {
|
|
case TestHarness.ExpectationType.True:
|
|
return "expectThat(%s)";
|
|
case TestHarness.ExpectationType.False:
|
|
return "expectFalse(%s)";
|
|
case TestHarness.ExpectationType.Empty:
|
|
return "expectEmpty(%s)";
|
|
case TestHarness.ExpectationType.NotEmpty:
|
|
return "expectNotEmpty(%s)";
|
|
case TestHarness.ExpectationType.Null:
|
|
return "expectNull(%s)";
|
|
case TestHarness.ExpectationType.NotNull:
|
|
return "expectNotNull(%s)";
|
|
case TestHarness.ExpectationType.Equal:
|
|
return "expectEqual(%s, %s)";
|
|
case TestHarness.ExpectationType.NotEqual:
|
|
return "expectNotEqual(%s, %s)";
|
|
case TestHarness.ExpectationType.ShallowEqual:
|
|
return "expectShallowEqual(%s, %s)";
|
|
case TestHarness.ExpectationType.NotShallowEqual:
|
|
return "expectNotShallowEqual(%s, %s)";
|
|
case TestHarness.ExpectationType.EqualWithAccuracy:
|
|
return "expectEqualWithAccuracy(%s, %s, %s)";
|
|
case TestHarness.ExpectationType.LessThan:
|
|
return "expectLessThan(%s, %s)";
|
|
case TestHarness.ExpectationType.LessThanOrEqual:
|
|
return "expectLessThanOrEqual(%s, %s)";
|
|
case TestHarness.ExpectationType.GreaterThan:
|
|
return "expectGreaterThan(%s, %s)";
|
|
case TestHarness.ExpectationType.GreaterThanOrEqual:
|
|
return "expectGreaterThanOrEqual(%s, %s)";
|
|
default:
|
|
console.error("Unknown TestHarness.ExpectationType type: " + type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
_expectedValueFormat(type)
|
|
{
|
|
switch (type) {
|
|
case TestHarness.ExpectationType.True:
|
|
return "truthy";
|
|
case TestHarness.ExpectationType.False:
|
|
return "falsey";
|
|
case TestHarness.ExpectationType.Empty:
|
|
return "empty";
|
|
case TestHarness.ExpectationType.NotEmpty:
|
|
return "not empty";
|
|
case TestHarness.ExpectationType.NotNull:
|
|
return "not null";
|
|
case TestHarness.ExpectationType.NotEqual:
|
|
case TestHarness.ExpectationType.NotShallowEqual:
|
|
return "not %s";
|
|
case TestHarness.ExpectationType.EqualWithAccuracy:
|
|
return "%s +/- %s";
|
|
case TestHarness.ExpectationType.LessThan:
|
|
return "less than %s";
|
|
case TestHarness.ExpectationType.LessThanOrEqual:
|
|
return "less than or equal to %s";
|
|
case TestHarness.ExpectationType.GreaterThan:
|
|
return "greater than %s";
|
|
case TestHarness.ExpectationType.GreaterThanOrEqual:
|
|
return "greater than or equal to %s";
|
|
default:
|
|
return "%s";
|
|
}
|
|
}
|
|
};
|
|
|
|
TestHarness.ExpectationType = {
|
|
True: Symbol("expect-true"),
|
|
False: Symbol("expect-false"),
|
|
Empty: Symbol("expect-empty"),
|
|
NotEmpty: Symbol("expect-not-empty"),
|
|
Null: Symbol("expect-null"),
|
|
NotNull: Symbol("expect-not-null"),
|
|
Equal: Symbol("expect-equal"),
|
|
NotEqual: Symbol("expect-not-equal"),
|
|
ShallowEqual: Symbol("expect-shallow-equal"),
|
|
NotShallowEqual: Symbol("expect-not-shallow-equal"),
|
|
EqualWithAccuracy: Symbol("expect-equal-with-accuracy"),
|
|
LessThan: Symbol("expect-less-than"),
|
|
LessThanOrEqual: Symbol("expect-less-than-or-equal"),
|
|
GreaterThan: Symbol("expect-greater-than"),
|
|
GreaterThanOrEqual: Symbol("expect-greater-than-or-equal"),
|
|
};
|
|
|
|
/* Test/FrontendTestHarness.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
FrontendTestHarness = class FrontendTestHarness extends TestHarness
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._results = [];
|
|
this._testPageHasLoaded = false;
|
|
|
|
// Options that are set per-test for debugging purposes.
|
|
this.dumpActivityToSystemConsole = false;
|
|
}
|
|
|
|
// TestHarness Overrides
|
|
|
|
completeTest()
|
|
{
|
|
if (this.dumpActivityToSystemConsole)
|
|
InspectorFrontendHost.unbufferedLog("completeTest()");
|
|
|
|
// Wait for results to be resent before requesting completeTest(). Otherwise, messages will be
|
|
// queued after pending dispatches run to zero and the test page will quit before processing them.
|
|
if (this._testPageIsReloading) {
|
|
this._completeTestAfterReload = true;
|
|
return;
|
|
}
|
|
|
|
InspectorBackend.runAfterPendingDispatches(this.evaluateInPage.bind(this, "TestPage.completeTest()"));
|
|
}
|
|
|
|
addResult(message)
|
|
{
|
|
let stringifiedMessage = TestHarness.messageAsString(message);
|
|
|
|
// Save the stringified message, since message may be a DOM element that won't survive reload.
|
|
this._results.push(stringifiedMessage);
|
|
|
|
if (this.dumpActivityToSystemConsole)
|
|
InspectorFrontendHost.unbufferedLog(stringifiedMessage);
|
|
|
|
if (!this._testPageIsReloading)
|
|
this.evaluateInPage(`TestPage.addResult(unescape("${escape(stringifiedMessage)}"))`);
|
|
}
|
|
|
|
debugLog(message)
|
|
{
|
|
let stringifiedMessage = TestHarness.messageAsString(message);
|
|
|
|
if (this.dumpActivityToSystemConsole)
|
|
InspectorFrontendHost.unbufferedLog(stringifiedMessage);
|
|
|
|
this.evaluateInPage(`TestPage.debugLog(unescape("${escape(stringifiedMessage)}"));`);
|
|
}
|
|
|
|
evaluateInPage(expression, callback, options = {})
|
|
{
|
|
let remoteObjectOnly = !!options.remoteObjectOnly;
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// If we load this page outside of the inspector, or hit an early error when loading
|
|
// the test frontend, then defer evaluating the commands (indefinitely in the former case).
|
|
if (this._originalConsole && (!target || !target.hasDomain("Runtime"))) {
|
|
this._originalConsole["error"]("Tried to evaluate in test page, but connection not yet established:", expression);
|
|
return;
|
|
}
|
|
|
|
// Return primitive values directly, otherwise return a WI.RemoteObject instance.
|
|
function translateResult(result) {
|
|
let remoteObject = WI.RemoteObject.fromPayload(result);
|
|
return (!remoteObjectOnly && remoteObject.hasValue()) ? remoteObject.value : remoteObject;
|
|
}
|
|
|
|
let response = target.RuntimeAgent.evaluate.invoke({expression, objectGroup: "test", includeCommandLineAPI: false});
|
|
if (callback && typeof callback === "function") {
|
|
response = response.then(({result, wasThrown}) => callback(null, translateResult(result), wasThrown));
|
|
response = response.catch((error) => callback(error, null, false));
|
|
} else {
|
|
// Turn a thrown Error result into a promise rejection.
|
|
return response.then(({result, wasThrown}) => {
|
|
result = translateResult(result);
|
|
if (result && wasThrown)
|
|
return Promise.reject(new Error(result.description));
|
|
return Promise.resolve(result);
|
|
});
|
|
}
|
|
}
|
|
|
|
debug()
|
|
{
|
|
this.dumpActivityToSystemConsole = true;
|
|
InspectorBackend.dumpInspectorProtocolMessages = true;
|
|
}
|
|
|
|
// Frontend test-specific methods.
|
|
|
|
expectNoError(error)
|
|
{
|
|
if (error) {
|
|
InspectorTest.log("PROTOCOL ERROR: " + error);
|
|
InspectorTest.completeTest();
|
|
throw "PROTOCOL ERROR";
|
|
}
|
|
}
|
|
|
|
deferOutputUntilTestPageIsReloaded()
|
|
{
|
|
console.assert(!this._testPageIsReloading);
|
|
this._testPageIsReloading = true;
|
|
}
|
|
|
|
testPageDidLoad()
|
|
{
|
|
if (this.dumpActivityToSystemConsole)
|
|
InspectorFrontendHost.unbufferedLog("testPageDidLoad()");
|
|
|
|
this._testPageIsReloading = false;
|
|
if (this._testPageHasLoaded)
|
|
this._resendResults();
|
|
else
|
|
this._testPageHasLoaded = true;
|
|
|
|
this.dispatchEventToListeners(FrontendTestHarness.Event.TestPageDidLoad);
|
|
|
|
if (this._completeTestAfterReload)
|
|
this.completeTest();
|
|
}
|
|
|
|
reloadPage(options = {})
|
|
{
|
|
console.assert(!this._testPageIsReloading);
|
|
console.assert(!this._testPageReloadedOnce);
|
|
|
|
this._testPageIsReloading = true;
|
|
|
|
let {ignoreCache, revalidateAllResources} = options;
|
|
ignoreCache = !!ignoreCache;
|
|
revalidateAllResources = !!revalidateAllResources;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.PageAgent.reload.invoke({ignoreCache, revalidateAllResources})
|
|
.then(() => {
|
|
this._testPageReloadedOnce = true;
|
|
|
|
return Promise.resolve(null);
|
|
});
|
|
}
|
|
|
|
redirectRequestAnimationFrame()
|
|
{
|
|
console.assert(!this._originalRequestAnimationFrame);
|
|
if (this._originalRequestAnimationFrame)
|
|
return;
|
|
|
|
this._originalRequestAnimationFrame = window.requestAnimationFrame;
|
|
this._requestAnimationFrameCallbacks = new Map;
|
|
this._nextRequestIdentifier = 1;
|
|
|
|
window.requestAnimationFrame = (callback) => {
|
|
let requestIdentifier = this._nextRequestIdentifier++;
|
|
this._requestAnimationFrameCallbacks.set(requestIdentifier, callback);
|
|
if (this._requestAnimationFrameTimer)
|
|
return requestIdentifier;
|
|
|
|
let dispatchCallbacks = () => {
|
|
let callbacks = this._requestAnimationFrameCallbacks;
|
|
this._requestAnimationFrameCallbacks = new Map;
|
|
this._requestAnimationFrameTimer = undefined;
|
|
let timestamp = window.performance.now();
|
|
for (let callback of callbacks.values())
|
|
callback(timestamp);
|
|
};
|
|
|
|
this._requestAnimationFrameTimer = setTimeout(dispatchCallbacks, 0);
|
|
return requestIdentifier;
|
|
};
|
|
|
|
window.cancelAnimationFrame = (requestIdentifier) => {
|
|
if (!this._requestAnimationFrameCallbacks.delete(requestIdentifier))
|
|
return;
|
|
|
|
if (!this._requestAnimationFrameCallbacks.size) {
|
|
clearTimeout(this._requestAnimationFrameTimer);
|
|
this._requestAnimationFrameTimer = undefined;
|
|
}
|
|
};
|
|
}
|
|
|
|
redirectConsoleToTestOutput()
|
|
{
|
|
// We can't use arrow functions here because of 'arguments'. It might
|
|
// be okay once rest parameters work.
|
|
let self = this;
|
|
function createProxyConsoleHandler(type) {
|
|
return function() {
|
|
self.addResult(`${type}: ` + Array.from(arguments).join(" "));
|
|
};
|
|
}
|
|
|
|
function createProxyConsoleTraceHandler(){
|
|
return function() {
|
|
try {
|
|
throw new Exception();
|
|
} catch (e) {
|
|
// Skip the first frame which is added by this function.
|
|
let frames = e.stack.split("\n").slice(1);
|
|
let sanitizedFrames = frames.map(TestHarness.sanitizeStackFrame);
|
|
self.addResult("TRACE: " + Array.from(arguments).join(" "));
|
|
self.addResult(sanitizedFrames.join("\n"));
|
|
}
|
|
};
|
|
}
|
|
|
|
let redirectedMethods = {};
|
|
for (let key in window.console)
|
|
redirectedMethods[key] = window.console[key];
|
|
|
|
for (let type of ["log", "error", "info", "warn"])
|
|
redirectedMethods[type] = createProxyConsoleHandler(type.toUpperCase());
|
|
|
|
redirectedMethods["trace"] = createProxyConsoleTraceHandler();
|
|
|
|
this._originalConsole = window.console;
|
|
window.console = redirectedMethods;
|
|
}
|
|
|
|
reportUnhandledRejection(error)
|
|
{
|
|
let message = error.message;
|
|
let stack = error.stack;
|
|
let result = `Unhandled promise rejection in inspector page: ${message}\n`;
|
|
if (stack) {
|
|
let sanitizedStack = this.sanitizeStack(stack);
|
|
result += `\nStack Trace: ${sanitizedStack}\n`;
|
|
}
|
|
|
|
// If the connection to the test page is not set up, then just dump to console and give up.
|
|
// Errors encountered this early can be debugged by loading Test.html in a normal browser page.
|
|
if (this._originalConsole && !this._testPageHasLoaded)
|
|
this._originalConsole["error"](result);
|
|
|
|
this.addResult(result);
|
|
this.completeTest();
|
|
|
|
// Stop default handler so we can empty InspectorBackend's message queue.
|
|
return true;
|
|
}
|
|
|
|
reportUncaughtExceptionFromEvent(message, url, lineNumber, columnNumber)
|
|
{
|
|
// An exception thrown from a timer callback does not report a URL.
|
|
if (url === "undefined")
|
|
url = "global";
|
|
|
|
return this.reportUncaughtException({message, url, lineNumber, columnNumber});
|
|
}
|
|
|
|
reportUncaughtException({message, url, lineNumber, columnNumber, stack, code})
|
|
{
|
|
let result;
|
|
let sanitizedURL = TestHarness.sanitizeURL(url);
|
|
let sanitizedStack = this.sanitizeStack(stack);
|
|
if (url || lineNumber || columnNumber)
|
|
result = `Uncaught exception in Inspector page: ${message} [${sanitizedURL}:${lineNumber}:${columnNumber}]\n`;
|
|
else
|
|
result = `Uncaught exception in Inspector page: ${message}\n`;
|
|
|
|
if (stack)
|
|
result += `\nStack Trace:\n${sanitizedStack}\n`;
|
|
if (code)
|
|
result += `\nEvaluated Code:\n${code}`;
|
|
|
|
// If the connection to the test page is not set up, then just dump to console and give up.
|
|
// Errors encountered this early can be debugged by loading Test.html in a normal browser page.
|
|
if (this._originalConsole && !this._testPageHasLoaded)
|
|
this._originalConsole["error"](result);
|
|
|
|
this.addResult(result);
|
|
this.completeTest();
|
|
// Stop default handler so we can empty InspectorBackend's message queue.
|
|
return true;
|
|
}
|
|
|
|
// Private
|
|
|
|
_resendResults()
|
|
{
|
|
console.assert(this._testPageHasLoaded);
|
|
|
|
if (this.dumpActivityToSystemConsole)
|
|
InspectorFrontendHost.unbufferedLog("_resendResults()");
|
|
|
|
this.evaluateInPage("TestPage.clearOutput()");
|
|
for (let result of this._results)
|
|
this.evaluateInPage(`TestPage.addResult(unescape("${escape(result)}"))`);
|
|
}
|
|
};
|
|
|
|
FrontendTestHarness.Event = {
|
|
TestPageDidLoad: "frontend-test-test-page-did-load"
|
|
};
|
|
|
|
/* Test/TestSuite.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
TestSuite = class TestSuite
|
|
{
|
|
constructor(harness, name) {
|
|
if (!(harness instanceof TestHarness))
|
|
throw new Error("Must pass the test's harness as the first argument.");
|
|
|
|
if (typeof name !== "string" || !name.trim().length)
|
|
throw new Error("Tried to create TestSuite without string suite name.");
|
|
|
|
this.name = name;
|
|
this._harness = harness;
|
|
|
|
this.testcases = [];
|
|
this.runCount = 0;
|
|
this.failCount = 0;
|
|
}
|
|
|
|
// Use this if the test file only has one suite, and no handling
|
|
// of the value returned by runTestCases() is needed.
|
|
runTestCasesAndFinish()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
runTestCases()
|
|
{
|
|
throw new Error("Must be implemented by subclasses.");
|
|
}
|
|
|
|
get passCount()
|
|
{
|
|
return this.runCount - (this.failCount - this.skipCount);
|
|
}
|
|
|
|
get skipCount()
|
|
{
|
|
if (this.failCount)
|
|
return this.testcases.length - this.runCount;
|
|
else
|
|
return 0;
|
|
}
|
|
|
|
addTestCase(testcase)
|
|
{
|
|
if (!testcase || !(testcase instanceof Object))
|
|
throw new Error("Tried to add non-object test case.");
|
|
|
|
if (typeof testcase.name !== "string" || !testcase.name.trim().length)
|
|
throw new Error("Tried to add test case without a name.");
|
|
|
|
if (typeof testcase.test !== "function")
|
|
throw new Error("Tried to add test case without `test` function.");
|
|
|
|
if (testcase.setup && typeof testcase.setup !== "function")
|
|
throw new Error("Tried to add test case with invalid `setup` parameter (must be a function).");
|
|
|
|
if (testcase.teardown && typeof testcase.teardown !== "function")
|
|
throw new Error("Tried to add test case with invalid `teardown` parameter (must be a function).");
|
|
|
|
this.testcases.push(testcase);
|
|
}
|
|
|
|
// Protected
|
|
|
|
logThrownObject(e)
|
|
{
|
|
let message = e;
|
|
let stack = "(unknown)";
|
|
if (e instanceof Error) {
|
|
message = e.message;
|
|
if (e.stack)
|
|
stack = e.stack;
|
|
}
|
|
|
|
if (typeof message !== "string")
|
|
message = JSON.stringify(message);
|
|
|
|
let sanitizedStack = this._harness.sanitizeStack(stack);
|
|
|
|
let result = `!! EXCEPTION: ${message}`;
|
|
if (stack)
|
|
result += `\nStack Trace: ${sanitizedStack}`;
|
|
|
|
this._harness.log(result);
|
|
}
|
|
};
|
|
|
|
AsyncTestSuite = class AsyncTestSuite extends TestSuite
|
|
{
|
|
runTestCasesAndFinish()
|
|
{
|
|
let finish = () => { this._harness.completeTest(); };
|
|
|
|
this.runTestCases()
|
|
.then(finish)
|
|
.catch(finish);
|
|
}
|
|
|
|
runTestCases()
|
|
{
|
|
if (!this.testcases.length)
|
|
throw new Error("Tried to call runTestCases() for suite with no test cases");
|
|
if (this._startedRunning)
|
|
throw new Error("Tried to call runTestCases() more than once.");
|
|
|
|
this._startedRunning = true;
|
|
|
|
this._harness.log("");
|
|
this._harness.log(`== Running test suite: ${this.name}`);
|
|
|
|
// Avoid adding newlines if nothing was logged.
|
|
let priorLogCount = this._harness.logCount;
|
|
|
|
return Promise.resolve().then(() => Promise.chain(this.testcases.map((testcase, i) => () => new Promise(async (resolve, reject) => {
|
|
if (i > 0 && priorLogCount < this._harness.logCount)
|
|
this._harness.log("");
|
|
priorLogCount = this._harness.logCount;
|
|
|
|
let hasTimeout = testcase.timeout !== -1;
|
|
let timeoutId = undefined;
|
|
if (hasTimeout) {
|
|
let delay = testcase.timeout || 10000;
|
|
timeoutId = setTimeout(() => {
|
|
if (!timeoutId)
|
|
return;
|
|
|
|
timeoutId = undefined;
|
|
|
|
this.failCount++;
|
|
this._harness.log(`!! TIMEOUT: took longer than ${delay}ms`);
|
|
|
|
resolve();
|
|
}, delay);
|
|
}
|
|
|
|
try {
|
|
if (testcase.setup) {
|
|
this._harness.log("-- Running test setup.");
|
|
priorLogCount++;
|
|
|
|
if (testcase.setup[Symbol.toStringTag] === "AsyncFunction")
|
|
await testcase.setup();
|
|
else
|
|
await new Promise(testcase.setup);
|
|
}
|
|
|
|
this.runCount++;
|
|
|
|
this._harness.log(`-- Running test case: ${testcase.name}`);
|
|
priorLogCount++;
|
|
|
|
if (testcase.test[Symbol.toStringTag] === "AsyncFunction")
|
|
await testcase.test();
|
|
else
|
|
await new Promise(testcase.test);
|
|
|
|
if (testcase.teardown) {
|
|
this._harness.log("-- Running test teardown.");
|
|
priorLogCount++;
|
|
|
|
if (testcase.teardown[Symbol.toStringTag] === "AsyncFunction")
|
|
await testcase.teardown();
|
|
else
|
|
await new Promise(testcase.teardown);
|
|
}
|
|
} catch (e) {
|
|
this.failCount++;
|
|
this.logThrownObject(e);
|
|
}
|
|
|
|
if (!hasTimeout || timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
timeoutId = undefined;
|
|
|
|
resolve();
|
|
}
|
|
})))
|
|
// Clear result value.
|
|
.then(() => {}));
|
|
}
|
|
};
|
|
|
|
SyncTestSuite = class SyncTestSuite extends TestSuite
|
|
{
|
|
addTestCase(testcase)
|
|
{
|
|
if ([testcase.setup, testcase.teardown, testcase.test].some((fn) => fn && fn[Symbol.toStringTag] === "AsyncFunction"))
|
|
throw new Error("Tried to pass a test case with an async `setup`, `test`, or `teardown` function, but this is a synchronous test suite.");
|
|
|
|
super.addTestCase(testcase);
|
|
}
|
|
|
|
runTestCasesAndFinish()
|
|
{
|
|
this.runTestCases();
|
|
this._harness.completeTest();
|
|
}
|
|
|
|
runTestCases()
|
|
{
|
|
if (!this.testcases.length)
|
|
throw new Error("Tried to call runTestCases() for suite with no test cases");
|
|
if (this._startedRunning)
|
|
throw new Error("Tried to call runTestCases() more than once.");
|
|
|
|
this._startedRunning = true;
|
|
|
|
this._harness.log("");
|
|
this._harness.log(`== Running test suite: ${this.name}`);
|
|
|
|
let priorLogCount = this._harness.logCount;
|
|
for (let i = 0; i < this.testcases.length; i++) {
|
|
let testcase = this.testcases[i];
|
|
|
|
if (i > 0 && priorLogCount < this._harness.logCount)
|
|
this._harness.log("");
|
|
priorLogCount = this._harness.logCount;
|
|
|
|
try {
|
|
// Run the setup action, if one was provided.
|
|
if (testcase.setup) {
|
|
this._harness.log("-- Running test setup.");
|
|
priorLogCount++;
|
|
|
|
let setupResult = testcase.setup();
|
|
if (setupResult === false) {
|
|
this._harness.log("!! SETUP FAILED");
|
|
this.failCount++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
this.runCount++;
|
|
|
|
this._harness.log(`-- Running test case: ${testcase.name}`);
|
|
priorLogCount++;
|
|
|
|
let testResult = testcase.test();
|
|
if (testResult === false) {
|
|
this.failCount++;
|
|
continue;
|
|
}
|
|
|
|
// Run the teardown action, if one was provided.
|
|
if (testcase.teardown) {
|
|
this._harness.log("-- Running test teardown.");
|
|
priorLogCount++;
|
|
|
|
let teardownResult = testcase.teardown();
|
|
if (teardownResult === false) {
|
|
this._harness.log("!! TEARDOWN FAILED");
|
|
this.failCount++;
|
|
continue;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.failCount++;
|
|
this.logThrownObject(e);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
|
|
/* Test/Test.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.loaded = function()
|
|
{
|
|
// Register observers for events from the InspectorBackend.
|
|
// The initialization order should match the same in Main.js.
|
|
InspectorBackend.registerAnimationDispatcher(WI.AnimationObserver);
|
|
InspectorBackend.registerApplicationCacheDispatcher(WI.ApplicationCacheObserver);
|
|
InspectorBackend.registerBrowserDispatcher(WI.BrowserObserver);
|
|
InspectorBackend.registerCPUProfilerDispatcher(WI.CPUProfilerObserver);
|
|
InspectorBackend.registerCSSDispatcher(WI.CSSObserver);
|
|
InspectorBackend.registerCanvasDispatcher(WI.CanvasObserver);
|
|
InspectorBackend.registerConsoleDispatcher(WI.ConsoleObserver);
|
|
InspectorBackend.registerDOMDispatcher(WI.DOMObserver);
|
|
InspectorBackend.registerDOMStorageDispatcher(WI.DOMStorageObserver);
|
|
InspectorBackend.registerDatabaseDispatcher(WI.DatabaseObserver);
|
|
InspectorBackend.registerDebuggerDispatcher(WI.DebuggerObserver);
|
|
InspectorBackend.registerHeapDispatcher(WI.HeapObserver);
|
|
InspectorBackend.registerInspectorDispatcher(WI.InspectorObserver);
|
|
InspectorBackend.registerLayerTreeDispatcher(WI.LayerTreeObserver);
|
|
InspectorBackend.registerMemoryDispatcher(WI.MemoryObserver);
|
|
InspectorBackend.registerNetworkDispatcher(WI.NetworkObserver);
|
|
InspectorBackend.registerPageDispatcher(WI.PageObserver);
|
|
InspectorBackend.registerRuntimeDispatcher(WI.RuntimeObserver);
|
|
InspectorBackend.registerScriptProfilerDispatcher(WI.ScriptProfilerObserver);
|
|
InspectorBackend.registerTargetDispatcher(WI.TargetObserver);
|
|
InspectorBackend.registerTimelineDispatcher(WI.TimelineObserver);
|
|
InspectorBackend.registerWorkerDispatcher(WI.WorkerObserver);
|
|
|
|
// Instantiate controllers used by tests.
|
|
WI.managers = [
|
|
WI.browserManager = new WI.BrowserManager,
|
|
WI.targetManager = new WI.TargetManager,
|
|
WI.networkManager = new WI.NetworkManager,
|
|
WI.domStorageManager = new WI.DOMStorageManager,
|
|
WI.databaseManager = new WI.DatabaseManager,
|
|
WI.indexedDBManager = new WI.IndexedDBManager,
|
|
WI.domManager = new WI.DOMManager,
|
|
WI.cssManager = new WI.CSSManager,
|
|
WI.consoleManager = new WI.ConsoleManager,
|
|
WI.runtimeManager = new WI.RuntimeManager,
|
|
WI.heapManager = new WI.HeapManager,
|
|
WI.memoryManager = new WI.MemoryManager,
|
|
WI.applicationCacheManager = new WI.ApplicationCacheManager,
|
|
WI.timelineManager = new WI.TimelineManager,
|
|
WI.auditManager = new WI.AuditManager,
|
|
WI.debuggerManager = new WI.DebuggerManager,
|
|
WI.layerTreeManager = new WI.LayerTreeManager,
|
|
WI.workerManager = new WI.WorkerManager,
|
|
WI.domDebuggerManager = new WI.DOMDebuggerManager,
|
|
WI.canvasManager = new WI.CanvasManager,
|
|
WI.animationManager = new WI.AnimationManager,
|
|
];
|
|
|
|
// Register for events.
|
|
document.addEventListener("DOMContentLoaded", WI.contentLoaded);
|
|
WI.browserManager.enable();
|
|
|
|
// Targets.
|
|
WI.backendTarget = null;
|
|
WI._backendTargetAvailablePromise = new WI.WrappedPromise;
|
|
|
|
WI.pageTarget = null;
|
|
WI._pageTargetAvailablePromise = new WI.WrappedPromise;
|
|
|
|
if (InspectorBackend.hasDomain("Target"))
|
|
WI.targetManager.createMultiplexingBackendTarget();
|
|
else
|
|
WI.targetManager.createDirectBackendTarget();
|
|
};
|
|
|
|
WI.contentLoaded = function()
|
|
{
|
|
// Things that would normally get called by the UI, that we still want to do in tests.
|
|
WI.animationManager.enable();
|
|
WI.applicationCacheManager.enable();
|
|
WI.canvasManager.enable();
|
|
WI.databaseManager.enable();
|
|
WI.domStorageManager.enable();
|
|
WI.heapManager.enable();
|
|
WI.indexedDBManager.enable();
|
|
WI.memoryManager.enable();
|
|
WI.timelineManager.enable();
|
|
|
|
// Signal that the frontend is now ready to receive messages.
|
|
WI._backendTargetAvailablePromise.promise.then(() => {
|
|
InspectorFrontendAPI.loadCompleted();
|
|
});
|
|
|
|
// Tell the InspectorFrontendHost we loaded, which causes the window to display
|
|
// and pending InspectorFrontendAPI commands to be sent.
|
|
InspectorFrontendHost.loaded();
|
|
};
|
|
|
|
WI.performOneTimeFrontendInitializationsUsingTarget = function(target)
|
|
{
|
|
if (!WI.__didPerformConsoleInitialization && target.hasDomain("Console")) {
|
|
WI.__didPerformConsoleInitialization = true;
|
|
WI.consoleManager.initializeLogChannels(target);
|
|
}
|
|
|
|
// FIXME: This slows down test debug logging considerably.
|
|
if (!WI.__didPerformCSSInitialization && target.hasDomain("CSS")) {
|
|
WI.__didPerformCSSInitialization = true;
|
|
WI.cssManager.initializeCSSPropertyNameCompletions(target);
|
|
}
|
|
};
|
|
|
|
WI.initializeTarget = function(target)
|
|
{
|
|
};
|
|
|
|
WI.targetsAvailable = function()
|
|
{
|
|
return WI._pageTargetAvailablePromise.settled;
|
|
};
|
|
|
|
WI.whenTargetsAvailable = function()
|
|
{
|
|
return WI._pageTargetAvailablePromise.promise;
|
|
};
|
|
|
|
Object.defineProperty(WI, "mainTarget",
|
|
{
|
|
get() { return WI.pageTarget || WI.backendTarget; }
|
|
});
|
|
|
|
Object.defineProperty(WI, "targets",
|
|
{
|
|
get() { return WI.targetManager.targets; }
|
|
});
|
|
|
|
WI.assumingMainTarget = () => WI.mainTarget;
|
|
|
|
WI.isDebugUIEnabled = () => false;
|
|
|
|
WI.isEngineeringBuild = false;
|
|
|
|
WI.engineeringSettingsAllowed = () => WI.isEngineeringBuild;
|
|
|
|
WI.unlocalizedString = (string) => string;
|
|
WI.UIString = (string, key, comment) => string;
|
|
|
|
WI.indentString = () => " ";
|
|
|
|
WI.LayoutDirection = {
|
|
System: "system",
|
|
LTR: "ltr",
|
|
RTL: "rtl",
|
|
};
|
|
|
|
WI.resolvedLayoutDirection = () => { return InspectorFrontendHost.userInterfaceLayoutDirection(); };
|
|
|
|
// Add stubs that are called by the frontend API.
|
|
WI.updateDockedState = () => {};
|
|
WI.updateDockingAvailability = () => {};
|
|
WI.updateVisibilityState = () => {};
|
|
WI.updateFindString = () => {};
|
|
|
|
// FIXME: <https://webkit.org/b/201149> Web Inspector: replace all uses of `window.*Agent` with a target-specific call
|
|
(function() {
|
|
function makeAgentGetter(domainName) {
|
|
Object.defineProperty(window, domainName + "Agent",
|
|
{
|
|
get() { return WI.mainTarget._agents[domainName]; },
|
|
});
|
|
}
|
|
makeAgentGetter("Animation");
|
|
makeAgentGetter("Audit");
|
|
makeAgentGetter("ApplicationCache");
|
|
makeAgentGetter("CPUProfiler");
|
|
makeAgentGetter("CSS");
|
|
makeAgentGetter("Canvas");
|
|
makeAgentGetter("Console");
|
|
makeAgentGetter("DOM");
|
|
makeAgentGetter("DOMDebugger");
|
|
makeAgentGetter("DOMStorage");
|
|
makeAgentGetter("Database");
|
|
makeAgentGetter("Debugger");
|
|
makeAgentGetter("Heap");
|
|
makeAgentGetter("IndexedDB");
|
|
makeAgentGetter("Inspector");
|
|
makeAgentGetter("LayerTree");
|
|
makeAgentGetter("Memory");
|
|
makeAgentGetter("Network");
|
|
makeAgentGetter("Page");
|
|
makeAgentGetter("Recording");
|
|
makeAgentGetter("Runtime");
|
|
makeAgentGetter("ScriptProfiler");
|
|
makeAgentGetter("ServiceWorker");
|
|
makeAgentGetter("Target");
|
|
makeAgentGetter("Timeline");
|
|
makeAgentGetter("Worker");
|
|
})();
|
|
|
|
window.InspectorTest = new FrontendTestHarness();
|
|
|
|
InspectorTest.redirectConsoleToTestOutput();
|
|
|
|
WI.reportInternalError = (e) => { console.error(e); };
|
|
|
|
window.reportUnhandledRejection = InspectorTest.reportUnhandledRejection.bind(InspectorTest);
|
|
window.onerror = InspectorTest.reportUncaughtExceptionFromEvent.bind(InspectorTest);
|
|
|
|
/* Controllers/AppControllerBase.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.NotImplementedError = class NotImplementedError extends Error
|
|
{
|
|
constructor(message = "This method is not implemented.")
|
|
{
|
|
super(message);
|
|
}
|
|
|
|
static subclassMustOverride()
|
|
{
|
|
return new WI.NotImplementedError("This method must be overridden by a subclass.");
|
|
}
|
|
};
|
|
|
|
WI.AppControllerBase = class AppControllerBase
|
|
{
|
|
constructor()
|
|
{
|
|
this._initialized = false;
|
|
|
|
this._extensionController = new WI.WebInspectorExtensionController;
|
|
}
|
|
|
|
// Public
|
|
|
|
get debuggableType() { throw WI.NotImplementedError.subclassMustOverride(); }
|
|
get extensionController() { return this._extensionController; }
|
|
|
|
// Since various members of the app controller depend on the global singleton to exist,
|
|
// some initialization needs to happen after the app controller has been constructed.
|
|
initialize()
|
|
{
|
|
if (this._initialized)
|
|
throw new Error("App controller is already initialized.");
|
|
|
|
this._initialized = true;
|
|
|
|
// FIXME: eventually all code within WI.loaded should be distributed elsewhere.
|
|
WI.loaded();
|
|
}
|
|
|
|
isWebDebuggable()
|
|
{
|
|
return this.debuggableType === WI.DebuggableType.Page
|
|
|| this.debuggableType === WI.DebuggableType.WebPage;
|
|
}
|
|
};
|
|
|
|
/* Test/TestAppController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TestAppController = class TestAppController extends WI.AppControllerBase
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._debuggableType = WI.DebuggableType.fromString(InspectorFrontendHost.debuggableInfo.debuggableType);
|
|
console.assert(this._debuggableType);
|
|
}
|
|
|
|
// Public
|
|
|
|
get debuggableType() { return this._debuggableType; }
|
|
};
|
|
|
|
/* Test/TestUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
//
|
|
// This can be used to get a promise for any function that takes a callback.
|
|
//
|
|
// For example:
|
|
//
|
|
// object.getValues(arg1, arg2, (callbackArg1, callbackArg2) => {
|
|
// ...
|
|
// });
|
|
//
|
|
// Can be promisified like so:
|
|
//
|
|
// promisify((cb) => { object.getValues(arg1, arg2, cb); }).then([callbackArg1, callbackArg2]) {
|
|
// ...
|
|
// });
|
|
//
|
|
// Or more naturally with await:
|
|
//
|
|
// let [callbackArg1, callbackArg2] = await promisify((cb) => { object.getValues(arg1, arg2, cb); });
|
|
//
|
|
|
|
function promisify(func) {
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
func((...args) => { resolve(args); });
|
|
} catch (e) {
|
|
reject(e);
|
|
}
|
|
});
|
|
}
|
|
|
|
function sanitizeURL(url) {
|
|
return url.replace(/^.*?LayoutTests\//, "");
|
|
}
|
|
|
|
/* Base/BlobUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BlobUtilities = class BlobUtilities {
|
|
static blobForContent(content, base64Encoded, mimeType)
|
|
{
|
|
if (base64Encoded)
|
|
return BlobUtilities.decodeBase64ToBlob(content, mimeType);
|
|
return BlobUtilities.textToBlob(content, mimeType);
|
|
}
|
|
|
|
static decodeBase64ToBlob(base64Data, mimeType)
|
|
{
|
|
mimeType = mimeType || "";
|
|
|
|
const sliceSize = 1024;
|
|
let byteCharacters = atob(base64Data);
|
|
let bytesLength = byteCharacters.length;
|
|
let slicesCount = Math.ceil(bytesLength / sliceSize);
|
|
let byteArrays = new Array(slicesCount);
|
|
|
|
for (let sliceIndex = 0; sliceIndex < slicesCount; ++sliceIndex) {
|
|
let begin = sliceIndex * sliceSize;
|
|
let end = Math.min(begin + sliceSize, bytesLength);
|
|
|
|
let bytes = new Array(end - begin);
|
|
for (let offset = begin, i = 0; offset < end; ++i, ++offset)
|
|
bytes[i] = byteCharacters[offset].charCodeAt(0);
|
|
|
|
byteArrays[sliceIndex] = new Uint8Array(bytes);
|
|
}
|
|
|
|
return new Blob(byteArrays, {type: mimeType});
|
|
}
|
|
|
|
static textToBlob(text, mimeType)
|
|
{
|
|
return new Blob([text], {type: mimeType});
|
|
}
|
|
|
|
static blobAsText(blob, callback)
|
|
{
|
|
console.assert(blob instanceof Blob);
|
|
let fileReader = new FileReader;
|
|
fileReader.addEventListener("loadend", () => { callback(fileReader.result); });
|
|
fileReader.readAsText(blob);
|
|
}
|
|
};
|
|
|
|
/* Base/DOMUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2008 Matt Lilek <webkit@mattlilek.com>
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.roleSelectorForNode = function(node)
|
|
{
|
|
// This is proposed syntax for CSS 4 computed role selector :role(foo) and subject to change.
|
|
// See http://lists.w3.org/Archives/Public/www-style/2013Jul/0104.html
|
|
var title = "";
|
|
var role = node.computedRole();
|
|
if (role)
|
|
title = ":role(" + role + ")";
|
|
return title;
|
|
};
|
|
|
|
WI.linkifyAccessibilityNodeReference = function(node)
|
|
{
|
|
if (!node)
|
|
return null;
|
|
// Same as linkifyNodeReference except the link text has the classnames removed...
|
|
// ...for list brevity, and both text and title have roleSelectorForNode appended.
|
|
var link = WI.linkifyNodeReference(node);
|
|
var tagIdSelector = link.title;
|
|
var classSelectorIndex = tagIdSelector.indexOf(".");
|
|
if (classSelectorIndex > -1)
|
|
tagIdSelector = tagIdSelector.substring(0, classSelectorIndex);
|
|
var roleSelector = WI.roleSelectorForNode(node);
|
|
link.textContent = tagIdSelector + roleSelector;
|
|
link.title += roleSelector;
|
|
return link;
|
|
};
|
|
|
|
WI.linkifyStyleable = function(styleable)
|
|
{
|
|
console.assert(styleable instanceof WI.DOMStyleable, styleable);
|
|
let displayName = styleable.displayName;
|
|
let link = document.createElement("span");
|
|
link.append(displayName);
|
|
return WI.linkifyNodeReferenceElement(styleable.node, link, {displayName});
|
|
};
|
|
|
|
WI.linkifyNodeReference = function(node, options = {})
|
|
{
|
|
let displayName = node.displayName;
|
|
if (!isNaN(options.maxLength))
|
|
displayName = displayName.truncate(options.maxLength);
|
|
|
|
let link = document.createElement("span");
|
|
link.append(displayName);
|
|
return WI.linkifyNodeReferenceElement(node, link, {...options, displayName});
|
|
};
|
|
|
|
WI.linkifyNodeReferenceElement = function(node, element, options = {})
|
|
{
|
|
element.setAttribute("role", "link");
|
|
element.title = options.displayName || node.displayName;
|
|
|
|
let nodeType = node.nodeType();
|
|
if (!options.ignoreClick && (nodeType !== Node.DOCUMENT_NODE || node.parentNode) && nodeType !== Node.TEXT_NODE)
|
|
element.classList.add("node-link");
|
|
|
|
WI.bindInteractionsForNodeToElement(node, element, options);
|
|
|
|
return element;
|
|
};
|
|
|
|
WI.bindInteractionsForNodeToElement = function(node, element, options = {}) {
|
|
if (!options.ignoreClick) {
|
|
element.addEventListener("click", (event) => {
|
|
WI.domManager.inspectElement(node.id, {
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.LinkClick,
|
|
});
|
|
});
|
|
}
|
|
|
|
element.addEventListener("mouseover", (event) => {
|
|
node.highlight();
|
|
});
|
|
|
|
element.addEventListener("mouseout", (event) => {
|
|
WI.domManager.hideDOMNodeHighlight();
|
|
});
|
|
|
|
element.addEventListener("contextmenu", (event) => {
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
WI.appendContextMenuItemsForDOMNode(contextMenu, node, options);
|
|
});
|
|
};
|
|
|
|
function createSVGElement(tagName)
|
|
{
|
|
return document.createElementNS("http://www.w3.org/2000/svg", tagName);
|
|
}
|
|
|
|
WI.cssPath = function(node, options = {})
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
if (node.nodeType() !== Node.ELEMENT_NODE)
|
|
return "";
|
|
|
|
let suffix = "";
|
|
if (node.isPseudoElement()) {
|
|
suffix = "::" + node.pseudoType();
|
|
node = node.parentNode;
|
|
}
|
|
|
|
let components = [];
|
|
while (node) {
|
|
let component = WI.cssPathComponent(node, options);
|
|
if (!component)
|
|
break;
|
|
components.push(component);
|
|
if (component.done)
|
|
break;
|
|
node = node.parentNode;
|
|
}
|
|
|
|
components.reverse();
|
|
return components.map((x) => x.value).join(" > ") + suffix;
|
|
};
|
|
|
|
WI.cssPathComponent = function(node, options = {})
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
console.assert(!node.isPseudoElement());
|
|
if (node.nodeType() !== Node.ELEMENT_NODE)
|
|
return null;
|
|
|
|
let nodeName = node.nodeNameInCorrectCase();
|
|
|
|
// Root node does not have siblings.
|
|
if (!node.parentNode || node.parentNode.nodeType() === Node.DOCUMENT_NODE)
|
|
return {value: nodeName, done: true};
|
|
|
|
if (options.full) {
|
|
function getUniqueAttributes(domNode) {
|
|
let uniqueAttributes = new Map;
|
|
for (let attribute of domNode.attributes()) {
|
|
let values = [attribute.value];
|
|
if (attribute.name === "id" || attribute.name === "class")
|
|
values = attribute.value.split(/\s+/);
|
|
uniqueAttributes.set(attribute.name, new Set(values));
|
|
}
|
|
return uniqueAttributes;
|
|
}
|
|
|
|
let nodeIndex = 0;
|
|
let needsNthChild = false;
|
|
let uniqueAttributes = getUniqueAttributes(node);
|
|
node.parentNode.children.forEach((child, i) => {
|
|
if (child.nodeType() !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
if (child === node) {
|
|
nodeIndex = i;
|
|
return;
|
|
}
|
|
|
|
if (needsNthChild || child.nodeNameInCorrectCase() !== nodeName)
|
|
return;
|
|
|
|
let childUniqueAttributes = getUniqueAttributes(child);
|
|
let subsetCount = 0;
|
|
for (let [name, values] of uniqueAttributes) {
|
|
let childValues = childUniqueAttributes.get(name);
|
|
if (childValues && values.size <= childValues.size && values.isSubsetOf(childValues))
|
|
++subsetCount;
|
|
}
|
|
|
|
if (subsetCount === uniqueAttributes.size)
|
|
needsNthChild = true;
|
|
});
|
|
|
|
function selectorForAttribute(values, prefix = "", shouldCSSEscape = false) {
|
|
if (!values || !values.size)
|
|
return "";
|
|
values = Array.from(values);
|
|
values = values.filter((value) => value && value.length);
|
|
if (!values.length)
|
|
return "";
|
|
values = values.map((value) => shouldCSSEscape ? CSS.escape(value) : value.escapeCharacters("\""));
|
|
return prefix + values.join(prefix);
|
|
}
|
|
|
|
let selector = nodeName;
|
|
selector += selectorForAttribute(uniqueAttributes.get("id"), "#", true);
|
|
selector += selectorForAttribute(uniqueAttributes.get("class"), ".", true);
|
|
for (let [attribute, values] of uniqueAttributes) {
|
|
if (attribute !== "id" && attribute !== "class")
|
|
selector += `[${attribute}="${selectorForAttribute(values)}"]`;
|
|
}
|
|
|
|
if (needsNthChild)
|
|
selector += `:nth-child(${nodeIndex + 1})`;
|
|
|
|
return {value: selector, done: false};
|
|
}
|
|
|
|
let lowerNodeName = node.nodeName().toLowerCase();
|
|
|
|
// html, head, and body are unique nodes.
|
|
if (lowerNodeName === "body" || lowerNodeName === "head" || lowerNodeName === "html")
|
|
return {value: nodeName, done: true};
|
|
|
|
// #id is unique.
|
|
let id = node.getAttribute("id");
|
|
if (id)
|
|
return {value: node.escapedIdSelector, done: true};
|
|
|
|
// Find uniqueness among siblings.
|
|
// - look for a unique className
|
|
// - look for a unique tagName
|
|
// - fallback to nth-child()
|
|
|
|
function classNames(node) {
|
|
let classAttribute = node.getAttribute("class");
|
|
return classAttribute ? classAttribute.trim().split(/\s+/) : [];
|
|
}
|
|
|
|
let nthChildIndex = -1;
|
|
let hasUniqueTagName = true;
|
|
let uniqueClasses = new Set(classNames(node));
|
|
|
|
let siblings = node.parentNode.children;
|
|
let elementIndex = 0;
|
|
for (let sibling of siblings) {
|
|
if (sibling.nodeType() !== Node.ELEMENT_NODE)
|
|
continue;
|
|
|
|
elementIndex++;
|
|
if (sibling === node) {
|
|
nthChildIndex = elementIndex;
|
|
continue;
|
|
}
|
|
|
|
if (sibling.nodeNameInCorrectCase() === nodeName)
|
|
hasUniqueTagName = false;
|
|
|
|
if (uniqueClasses.size) {
|
|
let siblingClassNames = classNames(sibling);
|
|
for (let className of siblingClassNames)
|
|
uniqueClasses.delete(className);
|
|
}
|
|
}
|
|
|
|
let selector = nodeName;
|
|
if (lowerNodeName === "input" && node.getAttribute("type") && !uniqueClasses.size)
|
|
selector += `[type="${node.getAttribute("type")}"]`;
|
|
if (!hasUniqueTagName) {
|
|
if (uniqueClasses.size)
|
|
selector += node.escapedClassSelector;
|
|
else
|
|
selector += `:nth-child(${nthChildIndex})`;
|
|
}
|
|
|
|
return {value: selector, done: false};
|
|
};
|
|
|
|
WI.xpath = function(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
|
|
if (node.nodeType() === Node.DOCUMENT_NODE)
|
|
return "/";
|
|
|
|
let components = [];
|
|
while (node) {
|
|
let component = WI.xpathComponent(node);
|
|
if (!component)
|
|
break;
|
|
components.push(component);
|
|
if (component.done)
|
|
break;
|
|
node = node.parentNode;
|
|
}
|
|
|
|
components.reverse();
|
|
|
|
let prefix = components.length && components[0].done ? "" : "/";
|
|
return prefix + components.map((x) => x.value).join("/");
|
|
};
|
|
|
|
WI.xpathComponent = function(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, "Expected a DOMNode.");
|
|
|
|
let index = WI.xpathIndex(node);
|
|
if (index === -1)
|
|
return null;
|
|
|
|
let value;
|
|
|
|
switch (node.nodeType()) {
|
|
case Node.DOCUMENT_NODE:
|
|
return {value: "", done: true};
|
|
case Node.ELEMENT_NODE:
|
|
var id = node.getAttribute("id");
|
|
if (id)
|
|
return {value: `//*[@id="${id}"]`, done: true};
|
|
value = node.localName();
|
|
break;
|
|
case Node.ATTRIBUTE_NODE:
|
|
value = `@${node.nodeName()}`;
|
|
break;
|
|
case Node.TEXT_NODE:
|
|
case Node.CDATA_SECTION_NODE:
|
|
value = "text()";
|
|
break;
|
|
case Node.COMMENT_NODE:
|
|
value = "comment()";
|
|
break;
|
|
case Node.PROCESSING_INSTRUCTION_NODE:
|
|
value = "processing-instruction()";
|
|
break;
|
|
default:
|
|
value = "";
|
|
break;
|
|
}
|
|
|
|
if (index > 0)
|
|
value += `[${index}]`;
|
|
|
|
return {value, done: false};
|
|
};
|
|
|
|
WI.xpathIndex = function(node)
|
|
{
|
|
// Root node.
|
|
if (!node.parentNode)
|
|
return 0;
|
|
|
|
// No siblings.
|
|
let siblings = node.parentNode.children;
|
|
if (siblings.length <= 1)
|
|
return 0;
|
|
|
|
// Find uniqueness among siblings.
|
|
// - look for a unique localName
|
|
// - fallback to index
|
|
|
|
function isSimiliarNode(a, b) {
|
|
if (a === b)
|
|
return true;
|
|
|
|
let aType = a.nodeType();
|
|
let bType = b.nodeType();
|
|
|
|
if (aType === Node.ELEMENT_NODE && bType === Node.ELEMENT_NODE)
|
|
return a.localName() === b.localName();
|
|
|
|
// XPath CDATA and text() are the same.
|
|
if (aType === Node.CDATA_SECTION_NODE)
|
|
return aType === Node.TEXT_NODE;
|
|
if (bType === Node.CDATA_SECTION_NODE)
|
|
return bType === Node.TEXT_NODE;
|
|
|
|
return aType === bType;
|
|
}
|
|
|
|
let unique = true;
|
|
let xPathIndex = -1;
|
|
|
|
let xPathIndexCounter = 1; // XPath indices start at 1.
|
|
for (let sibling of siblings) {
|
|
if (!isSimiliarNode(node, sibling))
|
|
continue;
|
|
|
|
if (node === sibling) {
|
|
xPathIndex = xPathIndexCounter;
|
|
if (!unique)
|
|
return xPathIndex;
|
|
} else {
|
|
unique = false;
|
|
if (xPathIndex !== -1)
|
|
return xPathIndex;
|
|
}
|
|
|
|
xPathIndexCounter++;
|
|
}
|
|
|
|
if (unique)
|
|
return 0;
|
|
|
|
console.assert(xPathIndex > 0, "Should have found the node.");
|
|
return xPathIndex;
|
|
};
|
|
|
|
/* Base/FileUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FileUtilities = class FileUtilities {
|
|
static screenshotString()
|
|
{
|
|
let date = new Date;
|
|
let values = [
|
|
date.getFullYear(),
|
|
Number.zeroPad(date.getMonth() + 1, 2),
|
|
Number.zeroPad(date.getDate(), 2),
|
|
Number.zeroPad(date.getHours(), 2),
|
|
Number.zeroPad(date.getMinutes(), 2),
|
|
Number.zeroPad(date.getSeconds(), 2),
|
|
];
|
|
return WI.UIString("Screen Shot %s-%s-%s at %s.%s.%s").format(...values);
|
|
}
|
|
|
|
static sanitizeFilename(filename)
|
|
{
|
|
return filename.replace(/:+/g, "-");
|
|
}
|
|
|
|
static inspectorURLForFilename(filename)
|
|
{
|
|
return "web-inspector:///" + encodeURIComponent(FileUtilities.sanitizeFilename(filename));
|
|
}
|
|
|
|
static longestCommonPrefix(files, {directory} = {})
|
|
{
|
|
let longestCommonPrefix = files[0].getPath();
|
|
|
|
for (let i = 1; i < files.length; ++i) {
|
|
let path = files[i].getPath();
|
|
for (let j = 0; j < longestCommonPrefix.length; ++j) {
|
|
if (longestCommonPrefix[j] !== path[j]) {
|
|
longestCommonPrefix = longestCommonPrefix.substring(0, j);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((directory || files.length > 1) && !longestCommonPrefix.endsWith("/")) {
|
|
let lastSlashIndex = longestCommonPrefix.lastIndexOf("/");
|
|
console.assert(lastSlashIndex);
|
|
if (lastSlashIndex)
|
|
longestCommonPrefix = longestCommonPrefix.substring(0, lastSlashIndex);
|
|
longestCommonPrefix += "/";
|
|
}
|
|
|
|
return longestCommonPrefix;
|
|
}
|
|
|
|
static canSave(saveMode)
|
|
{
|
|
console.assert(Object.values(WI.FileUtilities.SaveMode).includes(saveMode), saveMode);
|
|
return InspectorFrontendHost.canSave(saveMode);
|
|
}
|
|
|
|
static async save(saveMode, fileVariants, forceSaveAs)
|
|
{
|
|
console.assert(WI.FileUtilities.canSave(saveMode), saveMode);
|
|
|
|
console.assert(fileVariants);
|
|
if (!fileVariants) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
let isFileVariantsMode = saveMode === WI.FileUtilities.SaveMode.FileVariants;
|
|
if (isFileVariantsMode)
|
|
forceSaveAs = true;
|
|
|
|
if (typeof fileVariants.customSaveHandler === "function") {
|
|
fileVariants.customSaveHandler(forceSaveAs);
|
|
return;
|
|
}
|
|
|
|
if (!isFileVariantsMode && !Array.isArray(fileVariants))
|
|
fileVariants = [fileVariants];
|
|
|
|
console.assert(Array.isArray(fileVariants), fileVariants);
|
|
if (!Array.isArray(fileVariants)) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
let promises = fileVariants.map((fileVariant) => {
|
|
let content = fileVariant.content;
|
|
console.assert(content, fileVariant);
|
|
if (!content)
|
|
return null;
|
|
|
|
let displayType = fileVariant.displayType || "";
|
|
console.assert(!isFileVariantsMode || fileVariant.displayType, fileVariant);
|
|
if (!fileVariant.displayType && isFileVariantsMode)
|
|
return null;
|
|
|
|
let suggestedName = fileVariant.suggestedName;
|
|
if (!suggestedName) {
|
|
let url = fileVariant.url || "";
|
|
suggestedName = parseURL(url).lastPathComponent;
|
|
if (!suggestedName) {
|
|
suggestedName = WI.UIString("Untitled");
|
|
let dataURLTypeMatch = /^data:([^;]+)/.exec(url);
|
|
if (dataURLTypeMatch) {
|
|
let fileExtension = WI.fileExtensionForMIMEType(dataURLTypeMatch[1]);
|
|
if (fileExtension)
|
|
suggestedName += "." + fileExtension;
|
|
}
|
|
}
|
|
}
|
|
let url = WI.FileUtilities.inspectorURLForFilename(suggestedName);
|
|
|
|
if (typeof content === "string") {
|
|
return Promise.resolve({
|
|
displayType,
|
|
url,
|
|
content,
|
|
base64Encoded: !!fileVariant.base64Encoded,
|
|
});
|
|
}
|
|
|
|
let wrappedPromise = new WI.WrappedPromise;
|
|
let fileReader = new FileReader;
|
|
fileReader.addEventListener("loadend", () => {
|
|
wrappedPromise.resolve({
|
|
displayType,
|
|
url,
|
|
content: parseDataURL(fileReader.result).data,
|
|
base64Encoded: true,
|
|
});
|
|
});
|
|
fileReader.readAsDataURL(content);
|
|
return wrappedPromise.promise;
|
|
});
|
|
if (promises.includes(null)) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
let saveDatas = await Promise.all(promises);
|
|
|
|
console.assert(isFileVariantsMode || saveDatas.length === 1, saveDatas);
|
|
console.assert(!isFileVariantsMode || new Set(saveDatas.map((saveData) => saveData.displayType)).size === saveDatas.length, saveDatas);
|
|
console.assert(!isFileVariantsMode || new Set(saveDatas.map((saveData) => WI.urlWithoutExtension(saveData.url))).size === 1, saveDatas);
|
|
|
|
InspectorFrontendHost.save(saveDatas, !!forceSaveAs);
|
|
}
|
|
|
|
static import(callback, {multiple, directory} = {})
|
|
{
|
|
let inputElement = document.createElement("input");
|
|
inputElement.type = "file";
|
|
inputElement.value = null;
|
|
inputElement.multiple = !!multiple || !!directory;
|
|
inputElement.webkitdirectory = !!directory;
|
|
inputElement.addEventListener("change", (event) => {
|
|
callback(inputElement.files);
|
|
});
|
|
|
|
inputElement.click();
|
|
|
|
// Cache the last used import element so that it doesn't get GCd while the native file
|
|
// picker is shown, which would prevent the "change" event listener from firing.
|
|
FileUtilities.importInputElement = inputElement;
|
|
}
|
|
|
|
static importText(callback, options = {})
|
|
{
|
|
FileUtilities.import((files) => {
|
|
FileUtilities.readText(files, callback);
|
|
}, options);
|
|
}
|
|
|
|
static importJSON(callback, options = {})
|
|
{
|
|
FileUtilities.import((files) => {
|
|
FileUtilities.readJSON(files, callback);
|
|
}, options);
|
|
}
|
|
|
|
static importData(callback, options = {})
|
|
{
|
|
FileUtilities.import((files) => {
|
|
FileUtilities.readData(files, callback);
|
|
}, options);
|
|
}
|
|
|
|
static async readText(fileOrList, callback)
|
|
{
|
|
await FileUtilities._read(fileOrList, async (file, result) => {
|
|
await new Promise((resolve, reject) => {
|
|
let reader = new FileReader;
|
|
reader.addEventListener("loadend", (event) => {
|
|
result.text = reader.result;
|
|
resolve(event);
|
|
});
|
|
reader.addEventListener("error", reject);
|
|
reader.readAsText(file);
|
|
});
|
|
}, callback);
|
|
}
|
|
|
|
static async readJSON(fileOrList, callback)
|
|
{
|
|
await WI.FileUtilities.readText(fileOrList, async (result) => {
|
|
if (result.text && !result.error) {
|
|
try {
|
|
result.json = JSON.parse(result.text);
|
|
} catch (e) {
|
|
result.error = e;
|
|
}
|
|
}
|
|
|
|
await callback(result);
|
|
});
|
|
}
|
|
|
|
static async readData(fileOrList, callback)
|
|
{
|
|
await FileUtilities._read(fileOrList, async (file, result) => {
|
|
await new Promise((resolve, reject) => {
|
|
let reader = new FileReader;
|
|
reader.addEventListener("loadend", (event) => {
|
|
let {mimeType, base64, data} = parseDataURL(reader.result);
|
|
|
|
// In case no mime type was determined, try to derive one from the file extension.
|
|
if (!mimeType || mimeType === "text/plain") {
|
|
let extension = WI.fileExtensionForFilename(result.filename);
|
|
if (extension)
|
|
mimeType = WI.mimeTypeForFileExtension(extension);
|
|
}
|
|
|
|
result.mimeType = mimeType;
|
|
result.base64Encoded = base64;
|
|
result.content = data;
|
|
|
|
resolve(event);
|
|
});
|
|
reader.addEventListener("error", reject);
|
|
reader.readAsDataURL(file);
|
|
});
|
|
}, callback);
|
|
}
|
|
|
|
// Private
|
|
|
|
static async _read(fileOrList, operation, callback)
|
|
{
|
|
console.assert(fileOrList instanceof File || fileOrList instanceof FileList);
|
|
|
|
let files = [];
|
|
if (fileOrList instanceof File)
|
|
files.push(fileOrList);
|
|
else if (fileOrList instanceof FileList)
|
|
files = Array.from(fileOrList);
|
|
|
|
for (let file of files) {
|
|
let result = {
|
|
filename: file.name,
|
|
};
|
|
|
|
try {
|
|
await operation(file, result);
|
|
} catch (e) {
|
|
result.error = e;
|
|
}
|
|
|
|
await callback(result);
|
|
}
|
|
}
|
|
};
|
|
|
|
// Keep in sync with `InspectorFrontendClient::SaveMode` and `InspectorFrontendHost::SaveMode`.
|
|
WI.FileUtilities.SaveMode = {
|
|
SingleFile: "single-file",
|
|
FileVariants: "file-variants",
|
|
};
|
|
|
|
/* Base/HTTPUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HTTPUtilities = class HTTPUtilities {
|
|
static statusTextForStatusCode(code)
|
|
{
|
|
console.assert(typeof code === "number");
|
|
|
|
switch (code) {
|
|
case 0: return "OK";
|
|
case 100: return "Continue";
|
|
case 101: return "Switching Protocols";
|
|
case 200: return "OK";
|
|
case 201: return "Created";
|
|
case 202: return "Accepted";
|
|
case 203: return "Non-Authoritative Information";
|
|
case 204: return "No Content";
|
|
case 205: return "Reset Content";
|
|
case 206: return "Partial Content";
|
|
case 207: return "Multi-Status";
|
|
case 300: return "Multiple Choices";
|
|
case 301: return "Moved Permanently";
|
|
case 302: return "Found";
|
|
case 303: return "See Other";
|
|
case 304: return "Not Modified";
|
|
case 305: return "Use Proxy";
|
|
case 307: return "Temporary Redirect";
|
|
case 308: return "Permanent Redirect";
|
|
case 400: return "Bad Request";
|
|
case 401: return "Unauthorized";
|
|
case 402: return "Payment Required";
|
|
case 403: return "Forbidden";
|
|
case 404: return "Not Found";
|
|
case 405: return "Method Not Allowed";
|
|
case 406: return "Not Acceptable";
|
|
case 407: return "Proxy Authentication Required";
|
|
case 408: return "Request Time-out";
|
|
case 409: return "Conflict";
|
|
case 410: return "Gone";
|
|
case 411: return "Length Required";
|
|
case 412: return "Precondition Failed";
|
|
case 413: return "Request Entity Too Large";
|
|
case 414: return "Request-URI Too Large";
|
|
case 415: return "Unsupported Media Type";
|
|
case 416: return "Requested range not satisfiable";
|
|
case 417: return "Expectation Failed";
|
|
case 500: return "Internal Server Error";
|
|
case 501: return "Not Implemented";
|
|
case 502: return "Bad Gateway";
|
|
case 503: return "Service Unavailable";
|
|
case 504: return "Gateway Time-out";
|
|
case 505: return "HTTP Version not supported";
|
|
}
|
|
|
|
if (code < 200)
|
|
return "Continue";
|
|
if (code < 300)
|
|
return "OK";
|
|
if (code < 400)
|
|
return "Multiple Choices";
|
|
if (code < 500)
|
|
return "Bad Request";
|
|
|
|
return "Internal Server Error";
|
|
}
|
|
};
|
|
|
|
WI.HTTPUtilities.RequestMethod = {
|
|
CONNECT: "CONNECT",
|
|
DELETE: "DELETE",
|
|
GET: "GET",
|
|
HEAD: "HEAD",
|
|
OPTIONS: "OPTIONS",
|
|
PATCH: "PATCH",
|
|
POST: "POST",
|
|
PUT: "PUT",
|
|
TRACE: "TRACE",
|
|
};
|
|
|
|
WI.HTTPUtilities.RequestMethodsWithBody = new Set([
|
|
WI.HTTPUtilities.RequestMethod.DELETE,
|
|
WI.HTTPUtilities.RequestMethod.PATCH,
|
|
WI.HTTPUtilities.RequestMethod.POST,
|
|
WI.HTTPUtilities.RequestMethod.PUT,
|
|
]);
|
|
|
|
/* Base/ImageUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2017 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ImageUtilities = class ImageUtilities {
|
|
static useSVGSymbol(url, className, title)
|
|
{
|
|
const svgNamespace = "http://www.w3.org/2000/svg";
|
|
const xlinkNamespace = "http://www.w3.org/1999/xlink";
|
|
|
|
let svgElement = document.createElementNS(svgNamespace, "svg");
|
|
svgElement.style.width = "100%";
|
|
svgElement.style.height = "100%";
|
|
|
|
// URL must contain a fragment reference to a graphical element, like a symbol. If none is given
|
|
// append #root which all of our SVGs have on the top level <svg> element.
|
|
if (!url.includes("#"))
|
|
url += "#root";
|
|
|
|
let useElement = document.createElementNS(svgNamespace, "use");
|
|
useElement.setAttributeNS(xlinkNamespace, "xlink:href", url);
|
|
svgElement.appendChild(useElement);
|
|
|
|
let wrapper = document.createElement("div");
|
|
wrapper.appendChild(svgElement);
|
|
|
|
if (className)
|
|
wrapper.className = className;
|
|
if (title)
|
|
wrapper.title = title;
|
|
|
|
return wrapper;
|
|
}
|
|
|
|
static promisifyLoad(src)
|
|
{
|
|
return new Promise((resolve, reject) => {
|
|
let image = new Image;
|
|
let resolveWithImage = () => { resolve(image); };
|
|
image.addEventListener("load", resolveWithImage);
|
|
image.addEventListener("error", resolveWithImage);
|
|
image.src = src;
|
|
});
|
|
}
|
|
|
|
static scratchCanvasContext2D(callback)
|
|
{
|
|
if (!WI.ImageUtilities._scratchContext2D)
|
|
WI.ImageUtilities._scratchContext2D = document.createElement("canvas").getContext("2d");
|
|
|
|
let context = WI.ImageUtilities._scratchContext2D;
|
|
|
|
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
|
|
context.save();
|
|
callback(context);
|
|
context.restore();
|
|
}
|
|
|
|
static imageFromImageBitmap(data)
|
|
{
|
|
console.assert(data instanceof ImageBitmap);
|
|
|
|
let image = null;
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
context.canvas.width = data.width;
|
|
context.canvas.height = data.height;
|
|
context.drawImage(data, 0, 0);
|
|
|
|
image = new Image;
|
|
image.src = context.canvas.toDataURL();
|
|
});
|
|
return image;
|
|
}
|
|
|
|
static imageFromImageData(data)
|
|
{
|
|
console.assert(data instanceof ImageData);
|
|
|
|
let image = null;
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
context.canvas.width = data.width;
|
|
context.canvas.height = data.height;
|
|
context.putImageData(data, 0, 0);
|
|
|
|
image = new Image;
|
|
image.src = context.canvas.toDataURL();
|
|
});
|
|
return image;
|
|
}
|
|
|
|
static imageFromCanvasGradient(gradient, width, height)
|
|
{
|
|
console.assert(gradient instanceof CanvasGradient);
|
|
|
|
let image = null;
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
context.canvas.width = width;
|
|
context.canvas.height = height;
|
|
context.fillStyle = gradient;
|
|
context.fillRect(0, 0, width, height);
|
|
|
|
image = new Image;
|
|
image.src = context.canvas.toDataURL();
|
|
});
|
|
return image;
|
|
}
|
|
};
|
|
|
|
WI.ImageUtilities._scratchContext2D = null;
|
|
|
|
/* Base/MIMETypeUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.fileExtensionForFilename = function(filename)
|
|
{
|
|
if (!filename)
|
|
return null;
|
|
|
|
let index = filename.lastIndexOf(".");
|
|
if (index === -1)
|
|
return null;
|
|
|
|
if (index === filename.length - 1)
|
|
return null;
|
|
|
|
return filename.substr(index + 1);
|
|
};
|
|
|
|
WI.fileExtensionForURL = function(url)
|
|
{
|
|
let lastPathComponent = parseURL(url).lastPathComponent;
|
|
return WI.fileExtensionForFilename(lastPathComponent);
|
|
};
|
|
|
|
WI.mimeTypeForFileExtension = function(extension)
|
|
{
|
|
const extensionToMIMEType = {
|
|
// Document types.
|
|
"html": "text/html",
|
|
"xhtml": "application/xhtml+xml",
|
|
"xml": "text/xml",
|
|
|
|
// Script types.
|
|
"js": "text/javascript",
|
|
"mjs": "text/javascript",
|
|
"json": "application/json",
|
|
"clj": "text/x-clojure",
|
|
"coffee": "text/x-coffeescript",
|
|
"ls": "text/x-livescript",
|
|
"ts": "text/typescript",
|
|
"ps": "application/postscript",
|
|
"jsx": "text/jsx",
|
|
|
|
// Stylesheet types.
|
|
"css": "text/css",
|
|
"less": "text/x-less",
|
|
"sass": "text/x-sass",
|
|
"scss": "text/x-scss",
|
|
|
|
// Image types.
|
|
"avif": "image/avif",
|
|
"bmp": "image/bmp",
|
|
"gif": "image/gif",
|
|
"ico": "image/x-icon",
|
|
"jp2": "image/jp2",
|
|
"jpeg": "image/jpeg",
|
|
"jpg": "image/jpeg",
|
|
"jxl": "image/jxl",
|
|
"pdf": "application/pdf",
|
|
"png": "image/png",
|
|
"tif": "image/tiff",
|
|
"tiff": "image/tiff",
|
|
"webp": "image/webp",
|
|
"xbm": "image/x-xbitmap",
|
|
|
|
"ogx": "application/ogg",
|
|
"ogg": "audio/ogg",
|
|
"oga": "audio/ogg",
|
|
"ogv": "video/ogg",
|
|
|
|
// Annodex
|
|
"anx": "application/annodex",
|
|
"axa": "audio/annodex",
|
|
"axv": "video/annodex",
|
|
"spx": "audio/speex",
|
|
|
|
// WebM
|
|
"webm": "video/webm",
|
|
|
|
// MPEG
|
|
"m1a": "audio/mpeg",
|
|
"m2a": "audio/mpeg",
|
|
"mpg": "video/mpeg",
|
|
"m15": "video/mpeg",
|
|
"m1s": "video/mpeg",
|
|
"m1v": "video/mpeg",
|
|
"m75": "video/mpeg",
|
|
"mpa": "video/mpeg",
|
|
"mpeg": "video/mpeg",
|
|
"mpm": "video/mpeg",
|
|
"mpv": "video/mpeg",
|
|
|
|
// MPEG playlist
|
|
"m3u8": "application/x-mpegurl",
|
|
"m3url": "audio/x-mpegurl",
|
|
"m3u": "audio/x-mpegurl",
|
|
|
|
// MPEG-4
|
|
"m4v": "video/x-m4v",
|
|
"m4a": "audio/x-m4a",
|
|
"m4b": "audio/x-m4b",
|
|
"m4p": "audio/x-m4p",
|
|
|
|
// MP3
|
|
"mp3": "audio/mp3",
|
|
|
|
// MPEG-2
|
|
"mp2": "video/x-mpeg2",
|
|
"vob": "video/mpeg2",
|
|
"mod": "video/mpeg2",
|
|
"m2ts": "video/m2ts",
|
|
"m2t": "video/x-m2ts",
|
|
|
|
// 3GP/3GP2
|
|
"3gpp": "audio/3gpp",
|
|
"3g2": "audio/3gpp2",
|
|
"amc": "application/x-mpeg",
|
|
|
|
// AAC
|
|
"aac": "audio/aac",
|
|
"adts": "audio/aac",
|
|
"m4r": "audio/x-aac",
|
|
|
|
// CoreAudio File
|
|
"caf": "audio/x-caf",
|
|
"gsm": "audio/x-gsm",
|
|
|
|
// ADPCM
|
|
"wav": "audio/x-wav",
|
|
|
|
// Text Track
|
|
"vtt": "text/vtt",
|
|
|
|
// Font
|
|
"woff": "font/woff",
|
|
"woff2": "font/woff2",
|
|
"otf": "font/otf",
|
|
"ttf": "font/ttf",
|
|
"sfnt": "font/sfnt",
|
|
|
|
// Miscellaneous types.
|
|
"svg": "image/svg+xml",
|
|
"txt": "text/plain",
|
|
"xsl": "text/xsl"
|
|
};
|
|
|
|
return extensionToMIMEType[extension] || null;
|
|
};
|
|
|
|
WI.fileExtensionForMIMEType = function(mimeType)
|
|
{
|
|
if (!mimeType)
|
|
return null;
|
|
|
|
const mimeTypeToExtension = {
|
|
// Document types.
|
|
"text/html": "html",
|
|
"application/xhtml+xml": "xhtml",
|
|
"application/xml": "xml",
|
|
"text/xml": "xml",
|
|
|
|
// Script types.
|
|
"application/ecmascript": "js",
|
|
"application/javascript": "js",
|
|
"application/x-ecmascript": "js",
|
|
"application/x-javascript": "js",
|
|
"text/ecmascript": "js",
|
|
"text/javascript": "js",
|
|
"text/javascript1.0": "js",
|
|
"text/javascript1.1": "js",
|
|
"text/javascript1.2": "js",
|
|
"text/javascript1.3": "js",
|
|
"text/javascript1.4": "js",
|
|
"text/javascript1.5": "js",
|
|
"text/jscript": "js",
|
|
"text/x-ecmascript": "js",
|
|
"text/x-javascript": "js",
|
|
"application/json": "json",
|
|
"text/x-clojure": "clj",
|
|
"text/x-coffeescript": "coffee",
|
|
"text/livescript": "ls",
|
|
"text/x-livescript": "ls",
|
|
"text/typescript": "ts",
|
|
"application/postscript": "ps",
|
|
"text/jsx": "jsx",
|
|
|
|
// Stylesheet types.
|
|
"text/css": "css",
|
|
"text/x-less": "less",
|
|
"text/x-sass": "sass",
|
|
"text/x-scss": "scss",
|
|
|
|
// Image types.
|
|
"image/avif": "avif",
|
|
"image/bmp": "bmp",
|
|
"image/gif": "gif",
|
|
"image/vnd.microsoft.icon": "ico",
|
|
"image/x-icon": "ico",
|
|
"image/jp2": "jp2",
|
|
"image/jpeg": "jpg",
|
|
"image/jxl": "jxl",
|
|
"application/pdf": "pdf",
|
|
"text/pdf": "pdf",
|
|
"image/png": "png",
|
|
"image/tiff": "tiff",
|
|
"image/webp": "webp",
|
|
"image/x-xbitmap": "xbm",
|
|
|
|
// Ogg
|
|
"application/ogg": "ogx",
|
|
"audio/ogg": "ogg",
|
|
|
|
// Annodex
|
|
"application/annodex": "anx",
|
|
"audio/annodex": "axa",
|
|
"video/annodex": "axv",
|
|
"audio/speex": "spx",
|
|
|
|
// WebM
|
|
"video/webm": "webm",
|
|
"audio/webm": "webm",
|
|
|
|
// MPEG
|
|
"video/mpeg": "mpeg",
|
|
|
|
// MPEG playlist
|
|
"application/vnd.apple.mpegurl": "m3u8",
|
|
"application/mpegurl": "m3u8",
|
|
"application/x-mpegurl": "m3u8",
|
|
"audio/mpegurl": "m3u",
|
|
"audio/x-mpegurl": "m3u",
|
|
|
|
// MPEG-4
|
|
"video/x-m4v": "m4v",
|
|
"audio/x-m4a": "m4a",
|
|
"audio/x-m4b": "m4b",
|
|
"audio/x-m4p": "m4p",
|
|
"audio/mp4": "m4a",
|
|
|
|
// MP3
|
|
"audio/mp3": "mp3",
|
|
"audio/x-mp3": "mp3",
|
|
"audio/x-mpeg": "mp3",
|
|
|
|
// MPEG-2
|
|
"video/x-mpeg2": "mp2",
|
|
"video/mpeg2": "vob",
|
|
"video/m2ts": "m2ts",
|
|
"video/x-m2ts": "m2t",
|
|
|
|
// 3GP/3GP2
|
|
"audio/3gpp": "3gpp",
|
|
"audio/3gpp2": "3g2",
|
|
"application/x-mpeg": "amc",
|
|
|
|
// AAC
|
|
"audio/aac": "aac",
|
|
"audio/x-aac": "m4r",
|
|
|
|
// CoreAudio File
|
|
"audio/x-caf": "caf",
|
|
"audio/x-gsm": "gsm",
|
|
|
|
// ADPCM
|
|
"audio/x-wav": "wav",
|
|
"audio/vnd.wave": "wav",
|
|
|
|
// Text Track
|
|
"text/vtt": "vtt",
|
|
|
|
// Font
|
|
"font/woff": "woff",
|
|
"font/woff2": "woff2",
|
|
"font/otf": "otf",
|
|
"font/ttf": "ttf",
|
|
"font/sfnt": "sfnt",
|
|
|
|
// Miscellaneous types.
|
|
"image/svg+xml": "svg",
|
|
"text/plain": "txt",
|
|
"text/xsl": "xsl",
|
|
};
|
|
|
|
let extension = mimeTypeToExtension[mimeType];
|
|
if (extension)
|
|
return extension;
|
|
|
|
if (mimeType.endsWith("+json"))
|
|
return "json";
|
|
if (mimeType.endsWith("+xml"))
|
|
return "xml";
|
|
|
|
return null;
|
|
};
|
|
|
|
WI.shouldTreatMIMETypeAsText = function(mimeType)
|
|
{
|
|
if (!mimeType)
|
|
return false;
|
|
|
|
if (mimeType.startsWith("text/"))
|
|
return true;
|
|
|
|
if (mimeType.endsWith("+json") || mimeType.endsWith("+xml"))
|
|
return true;
|
|
|
|
let extension = WI.fileExtensionForMIMEType(mimeType);
|
|
if (extension === "xml")
|
|
return true;
|
|
|
|
// Various script and JSON mime types.
|
|
if (extension === "js" || extension === "json")
|
|
return true;
|
|
|
|
// Various media text mime types.
|
|
if (extension === "m3u8" || extension === "m3u")
|
|
return true;
|
|
|
|
if (mimeType.startsWith("application/"))
|
|
return mimeType.endsWith("script") || mimeType.endsWith("json") || mimeType.endsWith("xml");
|
|
|
|
return false;
|
|
};
|
|
|
|
/* Base/ObjectStore.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ObjectStore = class ObjectStore
|
|
{
|
|
constructor(name, options = {})
|
|
{
|
|
this._name = name;
|
|
this._options = options;
|
|
}
|
|
|
|
// Static
|
|
|
|
static supported()
|
|
{
|
|
return (!window.InspectorTest || WI.ObjectStore.__testObjectStore) && window.indexedDB;
|
|
}
|
|
|
|
static async reset()
|
|
{
|
|
if (WI.ObjectStore._database)
|
|
WI.ObjectStore._database.close();
|
|
|
|
await window.indexedDB.deleteDatabase(ObjectStore._databaseName);
|
|
}
|
|
|
|
static get _databaseName()
|
|
{
|
|
let inspectionLevel = InspectorFrontendHost ? InspectorFrontendHost.inspectionLevel : 1;
|
|
let levelString = (inspectionLevel > 1) ? "-" + inspectionLevel : "";
|
|
return "com.apple.WebInspector" + levelString;
|
|
}
|
|
|
|
static _open(callback)
|
|
{
|
|
if (WI.ObjectStore._database) {
|
|
callback(WI.ObjectStore._database);
|
|
return;
|
|
}
|
|
|
|
if (Array.isArray(WI.ObjectStore._databaseCallbacks)) {
|
|
WI.ObjectStore._databaseCallbacks.push(callback);
|
|
return;
|
|
}
|
|
|
|
WI.ObjectStore._databaseCallbacks = [callback];
|
|
|
|
const version = 8; // Increment this for every edit to `WI.objectStores`.
|
|
|
|
let databaseRequest = window.indexedDB.open(WI.ObjectStore._databaseName, version);
|
|
databaseRequest.addEventListener("upgradeneeded", (event) => {
|
|
let database = databaseRequest.result;
|
|
|
|
let objectStores = Object.values(WI.objectStores);
|
|
if (WI.ObjectStore.__testObjectStore)
|
|
objectStores.push(WI.ObjectStore.__testObjectStore);
|
|
|
|
let existingNames = new Set;
|
|
for (let objectStore of objectStores) {
|
|
if (!database.objectStoreNames.contains(objectStore._name))
|
|
database.createObjectStore(objectStore._name, objectStore._options);
|
|
|
|
existingNames.add(objectStore._name);
|
|
}
|
|
|
|
for (let objectStoreName of database.objectStoreNames) {
|
|
if (!existingNames.has(objectStoreName))
|
|
database.deleteObjectStore(objectStoreName);
|
|
}
|
|
});
|
|
databaseRequest.addEventListener("success", (successEvent) => {
|
|
WI.ObjectStore._database = databaseRequest.result;
|
|
WI.ObjectStore._database.addEventListener("close", (closeEvent) => {
|
|
WI.ObjectStore._database = null;
|
|
});
|
|
|
|
for (let databaseCallback of WI.ObjectStore._databaseCallbacks)
|
|
databaseCallback(WI.ObjectStore._database);
|
|
|
|
WI.ObjectStore._databaseCallbacks = null;
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get keyPath()
|
|
{
|
|
return (this._options || {}).keyPath;
|
|
}
|
|
|
|
associateObject(object, key, value)
|
|
{
|
|
if (typeof value === "object")
|
|
value = this._resolveKeyPath(value, key).value;
|
|
|
|
let resolved = this._resolveKeyPath(object, key);
|
|
resolved.object[resolved.key] = value;
|
|
}
|
|
|
|
async get(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
return this._operation("readonly", (objectStore) => objectStore.get(...args));
|
|
}
|
|
|
|
async getAll(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return [];
|
|
|
|
return this._operation("readonly", (objectStore) => objectStore.getAll(...args));
|
|
}
|
|
|
|
async getAllKeys(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return [];
|
|
|
|
return this._operation("readonly", (objectStore) => objectStore.getAllKeys(...args));
|
|
}
|
|
|
|
async put(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
return this._operation("readwrite", (objectStore) => objectStore.put(...args));
|
|
}
|
|
|
|
async putObject(object, ...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
console.assert(typeof object.toJSON === "function", "ObjectStore cannot store an object without JSON serialization", object.constructor.name);
|
|
let result = await this.put(object.toJSON(WI.ObjectStore.toJSONSymbol), ...args);
|
|
this.associateObject(object, args[0], result);
|
|
return result;
|
|
}
|
|
|
|
async delete(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
return this._operation("readwrite", (objectStore) => objectStore.delete(...args));
|
|
}
|
|
|
|
async deleteObject(object, ...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
return this.delete(this._resolveKeyPath(object).value, ...args);
|
|
}
|
|
|
|
async clear(...args)
|
|
{
|
|
if (!WI.ObjectStore.supported())
|
|
return undefined;
|
|
|
|
return this._operation("readwrite", (objectStore) => objectStore.clear(...args));
|
|
}
|
|
|
|
// Private
|
|
|
|
_resolveKeyPath(object, keyPath)
|
|
{
|
|
keyPath = keyPath || this._options.keyPath || "";
|
|
|
|
let parts = keyPath.split(".");
|
|
let key = parts.splice(-1, 1);
|
|
while (parts.length) {
|
|
if (!object.hasOwnProperty(parts[0]))
|
|
break;
|
|
object = object[parts.shift()];
|
|
}
|
|
|
|
if (parts.length)
|
|
key = parts.join(".") + "." + key;
|
|
|
|
return {
|
|
object,
|
|
key,
|
|
value: object[key],
|
|
};
|
|
}
|
|
|
|
async _operation(mode, func)
|
|
{
|
|
// IndexedDB transactions will auto-close if there are no active operations at the end of a
|
|
// microtask, so we need to do everything using event listeners instead of promises.
|
|
return new Promise((resolve, reject) => {
|
|
WI.ObjectStore._open((database) => {
|
|
let transaction = database.transaction([this._name], mode);
|
|
let objectStore = transaction.objectStore(this._name);
|
|
let request = null;
|
|
|
|
try {
|
|
request = func(objectStore);
|
|
} catch (e) {
|
|
reject(e);
|
|
return;
|
|
}
|
|
|
|
function listener(event) {
|
|
transaction.removeEventListener("complete", listener);
|
|
transaction.removeEventListener("error", listener);
|
|
request.removeEventListener("success", listener);
|
|
request.removeEventListener("error", listener);
|
|
|
|
if (request.error) {
|
|
reject(request.error);
|
|
return;
|
|
}
|
|
|
|
resolve(request.result);
|
|
}
|
|
transaction.addEventListener("complete", listener, {once: true});
|
|
transaction.addEventListener("error", listener, {once: true});
|
|
request.addEventListener("success", listener, {once: true});
|
|
request.addEventListener("error", listener, {once: true});
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.ObjectStore._database = null;
|
|
WI.ObjectStore._databaseCallbacks = null;
|
|
|
|
WI.ObjectStore.toJSONSymbol = Symbol("ObjectStore-toJSON");
|
|
|
|
// Be sure to update the `version` above when making changes.
|
|
WI.objectStores = {
|
|
// Version 1
|
|
audits: new WI.ObjectStore("audit-manager-tests", {keyPath: "__id", autoIncrement: true}),
|
|
|
|
// Version 2
|
|
breakpoints: new WI.ObjectStore("debugger-breakpoints", {keyPath: "__id"}),
|
|
|
|
// Version 3
|
|
domBreakpoints: new WI.ObjectStore("dom-debugger-dom-breakpoints", {keyPath: "__id"}),
|
|
eventBreakpoints: new WI.ObjectStore("dom-debugger-event-breakpoints", {keyPath: "__id"}),
|
|
urlBreakpoints: new WI.ObjectStore("dom-debugger-url-breakpoints", {keyPath: "__id"}),
|
|
|
|
// Version 4
|
|
localResourceOverrides: new WI.ObjectStore("local-resource-overrides", {keyPath: "__id"}),
|
|
|
|
// Version 5
|
|
general: new WI.ObjectStore("general"),
|
|
|
|
// Version 6
|
|
cssPropertyNameCounts: new WI.ObjectStore("css-property-name-counts"),
|
|
|
|
// Version 7
|
|
symbolicBreakpoints: new WI.ObjectStore("debugger-symbolic-breakpoints", {keyPath: "__id"}),
|
|
|
|
// Version 8
|
|
consoleSnippets: new WI.ObjectStore("console-snippets", {keyPath: "__id"}),
|
|
};
|
|
|
|
/* Base/URLUtilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
function removeURLFragment(url)
|
|
{
|
|
var hashIndex = url.indexOf("#");
|
|
if (hashIndex >= 0)
|
|
return url.substring(0, hashIndex);
|
|
return url;
|
|
}
|
|
|
|
function relativePath(path, basePath)
|
|
{
|
|
console.assert(path.charAt(0) === "/");
|
|
console.assert(basePath.charAt(0) === "/");
|
|
|
|
var pathComponents = path.split("/");
|
|
var baseComponents = basePath.replace(/\/$/, "").split("/");
|
|
var finalComponents = [];
|
|
|
|
var index = 1;
|
|
for (; index < pathComponents.length && index < baseComponents.length; ++index) {
|
|
if (pathComponents[index] !== baseComponents[index])
|
|
break;
|
|
}
|
|
|
|
for (var i = index; i < baseComponents.length; ++i)
|
|
finalComponents.push("..");
|
|
|
|
for (var i = index; i < pathComponents.length; ++i)
|
|
finalComponents.push(pathComponents[i]);
|
|
|
|
return finalComponents.join("/");
|
|
}
|
|
|
|
function parseSecurityOrigin(securityOrigin)
|
|
{
|
|
securityOrigin = securityOrigin ? securityOrigin.trim() : "";
|
|
|
|
let match = securityOrigin.match(/^(?<scheme>[^:]+):\/\/(?<host>[^\/:]*)(?::(?<port>[\d]+))?$/i);
|
|
if (!match)
|
|
return {scheme: null, host: null, port: null};
|
|
|
|
let scheme = match.groups.scheme.toLowerCase();
|
|
let host = match.groups.host.toLowerCase();
|
|
let port = Number(match.groups.port) || null;
|
|
|
|
return {scheme, host, port};
|
|
}
|
|
|
|
function parseDataURL(url)
|
|
{
|
|
if (!url.startsWith("data:"))
|
|
return null;
|
|
|
|
// data:[<media type>][;charset=<character set>][;base64],<data>
|
|
let match = url.match(/^data:(?<mime>[^;,]*)?(?:;charset=(?<charset>[^;,]*?))?(?<base64>;base64)?,(?<data>.*)$/);
|
|
if (!match)
|
|
return null;
|
|
|
|
let scheme = "data";
|
|
let mimeType = match.groups.mime || "text/plain";
|
|
let charset = match.groups.charset || "US-ASCII";
|
|
let base64 = !!match.groups.base64;
|
|
let data = decodeURIComponent(match.groups.data);
|
|
|
|
return {scheme, mimeType, charset, base64, data};
|
|
}
|
|
|
|
function parseURL(url)
|
|
{
|
|
let result = {
|
|
scheme: null,
|
|
userinfo: null,
|
|
host: null,
|
|
port: null,
|
|
origin: null,
|
|
path: null,
|
|
queryString: null,
|
|
fragment: null,
|
|
lastPathComponent: null,
|
|
};
|
|
|
|
// dataURLs should be handled by `parseDataURL`.
|
|
if (url && url.startsWith("data:")) {
|
|
result.scheme = "data";
|
|
return result;
|
|
}
|
|
|
|
// Internal sourceURLs will fail in URL constructor anyways.
|
|
if (isWebKitInternalScript(url))
|
|
return result;
|
|
|
|
let parsed = null;
|
|
try {
|
|
parsed = new URL(url);
|
|
} catch {
|
|
return result;
|
|
}
|
|
|
|
result.scheme = parsed.protocol.slice(0, -1); // remove trailing ":"
|
|
|
|
if (parsed.username)
|
|
result.userinfo = parsed.username;
|
|
if (parsed.password)
|
|
result.userinfo = (result.userinfo || "") + ":" + parsed.password;
|
|
|
|
if (parsed.hostname)
|
|
result.host = parsed.hostname;
|
|
|
|
if (parsed.port)
|
|
result.port = Number(parsed.port);
|
|
|
|
if (parsed.origin && parsed.origin !== "null")
|
|
result.origin = parsed.origin;
|
|
else if (result.scheme && result.host) {
|
|
result.origin = result.scheme + "://" + result.host;
|
|
if (result.port)
|
|
result.origin += ":" + result.port;
|
|
}
|
|
|
|
if (parsed.pathname)
|
|
result.path = parsed.pathname;
|
|
|
|
if (parsed.search)
|
|
result.queryString = parsed.search.substring(1); // remove leading "?"
|
|
|
|
if (parsed.hash)
|
|
result.fragment = parsed.hash.substring(1); // remove leading "#"
|
|
|
|
// Find last path component.
|
|
if (result.path && result.path !== "/") {
|
|
// Skip the trailing slash if there is one.
|
|
let endOffset = result.path.endsWith("/") ? 1 : 0;
|
|
let lastSlashIndex = result.path.lastIndexOf("/", result.path.length - 1 - endOffset);
|
|
if (lastSlashIndex !== -1)
|
|
result.lastPathComponent = result.path.substring(lastSlashIndex + 1, result.path.length - endOffset);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function absoluteURL(partialURL, baseURL)
|
|
{
|
|
partialURL = partialURL ? partialURL.trim() : "";
|
|
|
|
// Return data and javascript URLs as-is.
|
|
if (partialURL.startsWith("data:") || partialURL.startsWith("javascript:") || partialURL.startsWith("mailto:"))
|
|
return partialURL;
|
|
|
|
// If the URL has a scheme it is already a full URL, so return it.
|
|
if (parseURL(partialURL).scheme)
|
|
return partialURL;
|
|
|
|
// If there is no partial URL, just return the base URL.
|
|
if (!partialURL)
|
|
return baseURL || null;
|
|
|
|
var baseURLComponents = parseURL(baseURL);
|
|
|
|
// The base URL needs to be an absolute URL. Return null if it isn't.
|
|
if (!baseURLComponents.scheme)
|
|
return null;
|
|
|
|
// A URL that starts with "//" is a full URL without the scheme. Use the base URL scheme.
|
|
if (partialURL[0] === "/" && partialURL[1] === "/")
|
|
return baseURLComponents.scheme + ":" + partialURL;
|
|
|
|
// The path can be null for URLs that have just a scheme and host (like "http://apple.com"). So make the path be "/".
|
|
if (!baseURLComponents.path)
|
|
baseURLComponents.path = "/";
|
|
|
|
// Generate the base URL prefix that is used in the rest of the cases.
|
|
var baseURLPrefix = baseURLComponents.scheme + "://" + baseURLComponents.host + (baseURLComponents.port ? (":" + baseURLComponents.port) : "");
|
|
|
|
// A URL that starts with "?" is just a query string that gets applied to the base URL (replacing the base URL query string and fragment).
|
|
if (partialURL[0] === "?")
|
|
return baseURLPrefix + baseURLComponents.path + partialURL;
|
|
|
|
// A URL that starts with "/" is an absolute path that gets applied to the base URL (replacing the base URL path, query string and fragment).
|
|
if (partialURL[0] === "/")
|
|
return baseURLPrefix + resolveDotsInPath(partialURL);
|
|
|
|
// A URL that starts with "#" is just a fragment that gets applied to the base URL (replacing the base URL fragment, maintaining the query string).
|
|
if (partialURL[0] === "#") {
|
|
let queryStringComponent = baseURLComponents.queryString ? "?" + baseURLComponents.queryString : "";
|
|
return baseURLPrefix + baseURLComponents.path + queryStringComponent + partialURL;
|
|
}
|
|
|
|
// Generate the base path that is used in the final case by removing everything after the last "/" from the base URL's path.
|
|
var basePath = baseURLComponents.path.substring(0, baseURLComponents.path.lastIndexOf("/")) + "/";
|
|
return baseURLPrefix + resolveDotsInPath(basePath + partialURL);
|
|
}
|
|
|
|
function parseQueryString(queryString, arrayResult)
|
|
{
|
|
if (!queryString)
|
|
return arrayResult ? [] : {};
|
|
|
|
function decode(string)
|
|
{
|
|
try {
|
|
// Replace "+" with " " then decode percent encoded values.
|
|
return decodeURIComponent(string.replace(/\+/g, " "));
|
|
} catch {
|
|
return string;
|
|
}
|
|
}
|
|
|
|
var parameters = arrayResult ? [] : {};
|
|
for (let parameterString of queryString.split("&")) {
|
|
let index = parameterString.indexOf("=");
|
|
if (index === -1)
|
|
index = parameterString.length;
|
|
|
|
let name = decode(parameterString.substring(0, index));
|
|
let value = decode(parameterString.substring(index + 1));
|
|
|
|
if (arrayResult)
|
|
parameters.push({name, value});
|
|
else
|
|
parameters[name] = value;
|
|
}
|
|
|
|
return parameters;
|
|
}
|
|
|
|
WI.displayNameForURL = function(url, urlComponents, options = {})
|
|
{
|
|
if (url.startsWith("data:"))
|
|
return WI.truncateURL(url);
|
|
|
|
if (!urlComponents)
|
|
urlComponents = parseURL(url);
|
|
|
|
var displayName;
|
|
try {
|
|
displayName = decodeURIComponent(urlComponents.lastPathComponent || "");
|
|
} catch {
|
|
displayName = urlComponents.lastPathComponent;
|
|
}
|
|
|
|
if (options.allowDirectoryAsName && (urlComponents.path === "/" || (displayName && urlComponents.path.endsWith(displayName + "/"))))
|
|
displayName = "/";
|
|
|
|
return displayName || WI.displayNameForHost(urlComponents.host) || url;
|
|
};
|
|
|
|
WI.truncateURL = function(url, multiline = false, dataURIMaxSize = 6)
|
|
{
|
|
if (!url.startsWith("data:"))
|
|
return url;
|
|
|
|
const dataIndex = url.indexOf(",") + 1;
|
|
let header = url.slice(0, dataIndex);
|
|
if (multiline)
|
|
header += "\n";
|
|
|
|
const data = url.slice(dataIndex);
|
|
if (data.length < dataURIMaxSize)
|
|
return header + data;
|
|
|
|
const firstChunk = data.slice(0, Math.ceil(dataURIMaxSize / 2));
|
|
const ellipsis = "\u2026";
|
|
const middleChunk = multiline ? `\n${ellipsis}\n` : ellipsis;
|
|
const lastChunk = data.slice(-Math.floor(dataURIMaxSize / 2));
|
|
return header + firstChunk + middleChunk + lastChunk;
|
|
};
|
|
|
|
WI.urlWithoutExtension = function(urlString)
|
|
{
|
|
let url = null;
|
|
try {
|
|
url = new URL(urlString);
|
|
} catch { }
|
|
if (!url)
|
|
return urlString;
|
|
|
|
let firstDotInLastPathComponentIndex = url.pathname.indexOf(".", url.pathname.lastIndexOf("/"));
|
|
if (firstDotInLastPathComponentIndex !== -1)
|
|
url.pathname = url.pathname.substring(0, firstDotInLastPathComponentIndex);
|
|
|
|
url.search = "";
|
|
url.hash = "";
|
|
return url.toString();
|
|
};
|
|
|
|
WI.urlWithoutFragment = function(urlString)
|
|
{
|
|
try {
|
|
let url = new URL(urlString);
|
|
if (url.hash) {
|
|
url.hash = "";
|
|
return url.toString();
|
|
}
|
|
|
|
// URL.toString with an empty hash leaves the hash symbol, so we strip it.
|
|
let result = url.toString();
|
|
if (result.endsWith("#"))
|
|
return result.substring(0, result.length - 1);
|
|
|
|
return result;
|
|
} catch { }
|
|
|
|
return urlString;
|
|
};
|
|
|
|
WI.urlWithoutUserQueryOrFragment = function(urlString)
|
|
{
|
|
try {
|
|
let url = new URL(urlString);
|
|
|
|
if (url.username) {
|
|
url.username = "";
|
|
}
|
|
if (url.password) {
|
|
url.password = "";
|
|
}
|
|
if (url.search) {
|
|
url.search = "";
|
|
}
|
|
if (url.hash) {
|
|
url.hash = "";
|
|
}
|
|
|
|
let result = url.toString();
|
|
if (result.endsWith("#"))
|
|
return result.substring(0, result.length - 1);
|
|
|
|
return result;
|
|
|
|
} catch { }
|
|
|
|
return urlString;
|
|
}
|
|
|
|
WI.displayNameForHost = function(host)
|
|
{
|
|
let extensionName = WI.browserManager.extensionNameForId(host);
|
|
if (extensionName)
|
|
return extensionName;
|
|
|
|
// FIXME <rdar://problem/11237413>: This should decode punycode hostnames.
|
|
|
|
return host;
|
|
};
|
|
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
|
WI.h2Authority = function(components)
|
|
{
|
|
let {scheme, userinfo, host, port} = components;
|
|
let result = host || "";
|
|
|
|
// The authority MUST NOT include the deprecated "userinfo"
|
|
// subcomponent for "http" or "https" schemed URIs.
|
|
if (userinfo && (scheme !== "http" && scheme !== "https"))
|
|
result = userinfo + "@" + result;
|
|
if (port)
|
|
result += ":" + port;
|
|
|
|
return result;
|
|
};
|
|
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
|
WI.h2Path = function(components)
|
|
{
|
|
let {scheme, path, queryString} = components;
|
|
let result = path || "";
|
|
|
|
// The ":path" pseudo-header field includes the path and query parts
|
|
// of the target URI. [...] This pseudo-header field MUST NOT be empty
|
|
// for "http" or "https" URIs; "http" or "https" URIs that do not contain
|
|
// a path component MUST include a value of '/'.
|
|
if (!path && (scheme === "http" || scheme === "https"))
|
|
result = "/";
|
|
if (queryString)
|
|
result += "?" + queryString;
|
|
|
|
return result;
|
|
};
|
|
|
|
/* Base/Utilities.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
var emDash = "\u2014";
|
|
var enDash = "\u2013";
|
|
var figureDash = "\u2012";
|
|
var ellipsis = "\u2026";
|
|
var zeroWidthSpace = "\u200b";
|
|
var multiplicationSign = "\u00d7";
|
|
|
|
function xor(a, b)
|
|
{
|
|
if (a)
|
|
return b ? false : a;
|
|
return b || false;
|
|
}
|
|
|
|
function nullish(value)
|
|
{
|
|
return value === null || value === undefined;
|
|
}
|
|
|
|
Object.defineProperty(Object, "shallowCopy",
|
|
{
|
|
value(object)
|
|
{
|
|
// Make a new object and copy all the key/values. The values are not copied.
|
|
var copy = {};
|
|
var keys = Object.keys(object);
|
|
for (var i = 0; i < keys.length; ++i)
|
|
copy[keys[i]] = object[keys[i]];
|
|
return copy;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Object, "shallowEqual",
|
|
{
|
|
value(a, b)
|
|
{
|
|
// Checks if two objects have the same top-level properties.
|
|
|
|
if (!(a instanceof Object) || !(b instanceof Object))
|
|
return false;
|
|
|
|
if (a === b)
|
|
return true;
|
|
|
|
if (Array.shallowEqual(a, b))
|
|
return true;
|
|
|
|
if (a.constructor !== b.constructor)
|
|
return false;
|
|
|
|
let aKeys = Object.keys(a);
|
|
let bKeys = Object.keys(b);
|
|
if (aKeys.length !== bKeys.length)
|
|
return false;
|
|
|
|
for (let aKey of aKeys) {
|
|
if (!(aKey in b))
|
|
return false;
|
|
|
|
let aValue = a[aKey];
|
|
let bValue = b[aKey];
|
|
if (aValue !== bValue && !Array.shallowEqual(aValue, bValue))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Object, "filter",
|
|
{
|
|
value(object, callback)
|
|
{
|
|
let filtered = {};
|
|
for (let key in object) {
|
|
if (callback(key, object[key]))
|
|
filtered[key] = object[key];
|
|
}
|
|
return filtered;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Object.prototype, "valueForCaseInsensitiveKey",
|
|
{
|
|
value(key)
|
|
{
|
|
if (this.hasOwnProperty(key))
|
|
return this[key];
|
|
|
|
var lowerCaseKey = key.toLowerCase();
|
|
for (var currentKey in this) {
|
|
if (currentKey.toLowerCase() === lowerCaseKey)
|
|
return this[currentKey];
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Map, "fromObject",
|
|
{
|
|
value(object)
|
|
{
|
|
let map = new Map;
|
|
for (let key in object)
|
|
map.set(key, object[key]);
|
|
return map;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Map.prototype, "take",
|
|
{
|
|
value(key)
|
|
{
|
|
let deletedValue = this.get(key);
|
|
this.delete(key);
|
|
return deletedValue;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Map.prototype, "getOrInitialize",
|
|
{
|
|
value(key, initialValue)
|
|
{
|
|
console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined.");
|
|
|
|
let value = this.get(key);
|
|
if (value)
|
|
return value;
|
|
|
|
if (typeof initialValue === "function")
|
|
initialValue = initialValue();
|
|
|
|
console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined.");
|
|
|
|
this.set(key, initialValue);
|
|
return initialValue;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(WeakMap.prototype, "getOrInitialize",
|
|
{
|
|
value(key, initialValue)
|
|
{
|
|
console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined.");
|
|
|
|
let value = this.get(key);
|
|
if (value)
|
|
return value;
|
|
|
|
if (typeof initialValue === "function")
|
|
initialValue = initialValue();
|
|
|
|
console.assert(initialValue !== undefined, "getOrInitialize should not be used with undefined.");
|
|
|
|
this.set(key, initialValue);
|
|
return initialValue;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "find",
|
|
{
|
|
value(predicate)
|
|
{
|
|
for (let item of this) {
|
|
if (predicate(item, this))
|
|
return item;
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "filter",
|
|
{
|
|
value(callback, thisArg)
|
|
{
|
|
let filtered = new Set;
|
|
for (let item of this) {
|
|
if (callback.call(thisArg, item, item, this))
|
|
filtered.add(item);
|
|
}
|
|
return filtered;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "some",
|
|
{
|
|
value(predicate, thisArg)
|
|
{
|
|
for (let item of this) {
|
|
if (predicate.call(thisArg, item, item, this))
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "addAll",
|
|
{
|
|
value(iterable)
|
|
{
|
|
for (let item of iterable)
|
|
this.add(item);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "take",
|
|
{
|
|
value(key)
|
|
{
|
|
if (this.has(key)) {
|
|
this.delete(key);
|
|
return key;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "equals",
|
|
{
|
|
value(other)
|
|
{
|
|
return this.size === other.size && this.isSubsetOf(other);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "difference",
|
|
{
|
|
value(other)
|
|
{
|
|
if (other === this)
|
|
return new Set;
|
|
|
|
let result = new Set;
|
|
for (let item of this) {
|
|
if (!other.has(item))
|
|
result.add(item);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "firstValue",
|
|
{
|
|
get()
|
|
{
|
|
return this.values().next().value;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "lastValue",
|
|
{
|
|
get()
|
|
{
|
|
return Array.from(this.values()).lastValue;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "intersects",
|
|
{
|
|
value(other)
|
|
{
|
|
if (!this.size || !other.size)
|
|
return false;
|
|
|
|
for (let item of this) {
|
|
if (other.has(item))
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Set.prototype, "isSubsetOf",
|
|
{
|
|
value(other)
|
|
{
|
|
for (let item of this) {
|
|
if (!other.has(item))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Node.prototype, "traverseNextNode",
|
|
{
|
|
value(stayWithin)
|
|
{
|
|
var node = this.firstChild;
|
|
if (node)
|
|
return node;
|
|
|
|
if (stayWithin && this === stayWithin)
|
|
return null;
|
|
|
|
node = this.nextSibling;
|
|
if (node)
|
|
return node;
|
|
|
|
node = this;
|
|
while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
|
|
node = node.parentNode;
|
|
if (!node)
|
|
return null;
|
|
|
|
return node.nextSibling;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Node.prototype, "traversePreviousNode",
|
|
{
|
|
value(stayWithin)
|
|
{
|
|
if (stayWithin && this === stayWithin)
|
|
return null;
|
|
var node = this.previousSibling;
|
|
while (node && node.lastChild)
|
|
node = node.lastChild;
|
|
if (node)
|
|
return node;
|
|
return this.parentNode;
|
|
}
|
|
});
|
|
|
|
|
|
Object.defineProperty(Node.prototype, "rangeOfWord",
|
|
{
|
|
value(offset, stopCharacters, stayWithinNode, direction)
|
|
{
|
|
var startNode;
|
|
var startOffset = 0;
|
|
var endNode;
|
|
var endOffset = 0;
|
|
|
|
if (!stayWithinNode)
|
|
stayWithinNode = this;
|
|
|
|
if (!direction || direction === "backward" || direction === "both") {
|
|
var node = this;
|
|
while (node) {
|
|
if (node === stayWithinNode) {
|
|
if (!startNode)
|
|
startNode = stayWithinNode;
|
|
break;
|
|
}
|
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
let start = node === this ? (offset - 1) : (node.nodeValue.length - 1);
|
|
for (var i = start; i >= 0; --i) {
|
|
if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
|
|
startNode = node;
|
|
startOffset = i + 1;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (startNode)
|
|
break;
|
|
|
|
node = node.traversePreviousNode(stayWithinNode);
|
|
}
|
|
|
|
if (!startNode) {
|
|
startNode = stayWithinNode;
|
|
startOffset = 0;
|
|
}
|
|
} else {
|
|
startNode = this;
|
|
startOffset = offset;
|
|
}
|
|
|
|
if (!direction || direction === "forward" || direction === "both") {
|
|
node = this;
|
|
while (node) {
|
|
if (node === stayWithinNode) {
|
|
if (!endNode)
|
|
endNode = stayWithinNode;
|
|
break;
|
|
}
|
|
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
let start = node === this ? offset : 0;
|
|
for (var i = start; i < node.nodeValue.length; ++i) {
|
|
if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
|
|
endNode = node;
|
|
endOffset = i;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (endNode)
|
|
break;
|
|
|
|
node = node.traverseNextNode(stayWithinNode);
|
|
}
|
|
|
|
if (!endNode) {
|
|
endNode = stayWithinNode;
|
|
endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
|
|
}
|
|
} else {
|
|
endNode = this;
|
|
endOffset = offset;
|
|
}
|
|
|
|
var result = this.ownerDocument.createRange();
|
|
result.setStart(startNode, startOffset);
|
|
result.setEnd(endNode, endOffset);
|
|
|
|
return result;
|
|
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "realOffsetWidth",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().width;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "realOffsetHeight",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().height;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "totalOffsetLeft",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().left;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "totalOffsetRight",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().right;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "totalOffsetTop",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().top;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "totalOffsetBottom",
|
|
{
|
|
get()
|
|
{
|
|
return this.getBoundingClientRect().bottom;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "removeChildren",
|
|
{
|
|
value()
|
|
{
|
|
// This has been tested to be the fastest removal method.
|
|
if (this.firstChild)
|
|
this.textContent = "";
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "isInsertionCaretInside",
|
|
{
|
|
value()
|
|
{
|
|
var selection = window.getSelection();
|
|
if (!selection.rangeCount || !selection.isCollapsed)
|
|
return false;
|
|
var selectionRange = selection.getRangeAt(0);
|
|
return selectionRange.startContainer === this || this.contains(selectionRange.startContainer);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "createChild",
|
|
{
|
|
value(elementName, className)
|
|
{
|
|
var element = this.ownerDocument.createElement(elementName);
|
|
if (className)
|
|
element.className = className;
|
|
this.appendChild(element);
|
|
return element;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "isScrolledToBottom",
|
|
{
|
|
value()
|
|
{
|
|
// This code works only for 0-width border
|
|
return this.scrollTop + this.clientHeight === this.scrollHeight;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "recalculateStyles",
|
|
{
|
|
value()
|
|
{
|
|
this.ownerDocument.defaultView.getComputedStyle(this);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Element.prototype, "getComputedCSSPropertyNumberValue", {
|
|
value(property) {
|
|
let result = undefined;
|
|
result ??= this.computedStyleMap?.().get(property)?.value;
|
|
result ??= window.getComputedStyle(this).getPropertyCSSValue(property)?.getFloatValue(CSSPrimitiveValue.CSS_PX);
|
|
return result;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(DocumentFragment.prototype, "createChild",
|
|
{
|
|
value: Element.prototype.createChild
|
|
});
|
|
|
|
(function() {
|
|
const fontSymbol = Symbol("font");
|
|
|
|
Object.defineProperty(HTMLInputElement.prototype, "autosize",
|
|
{
|
|
value(extra = 0)
|
|
{
|
|
extra += 6; // UserAgent styles add 1px padding and 2px border.
|
|
if (this.type === "number")
|
|
extra += 13; // Number input inner spin button width.
|
|
extra += 2; // Add extra pixels for the cursor.
|
|
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
this[fontSymbol] ||= window.getComputedStyle(this).font;
|
|
|
|
context.font = this[fontSymbol];
|
|
let textMetrics = context.measureText(this.value || this.placeholder);
|
|
this.style.setProperty("width", (textMetrics.width + extra) + "px");
|
|
});
|
|
},
|
|
});
|
|
})();
|
|
|
|
Object.defineProperty(HTMLCollection.prototype, "indexOf",
|
|
{
|
|
value(element)
|
|
{
|
|
let length = this.length;
|
|
for (let i = 0; i < length; ++i) {
|
|
if (this[i] === element)
|
|
return i;
|
|
}
|
|
return -1;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Event.prototype, "stop",
|
|
{
|
|
value()
|
|
{
|
|
this.stopImmediatePropagation();
|
|
this.preventDefault();
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(KeyboardEvent.prototype, "commandOrControlKey",
|
|
{
|
|
get()
|
|
{
|
|
return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(MouseEvent.prototype, "commandOrControlKey",
|
|
{
|
|
get()
|
|
{
|
|
return WI.Platform.name === "mac" ? this.metaKey : this.ctrlKey;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array, "isTypedArray",
|
|
{
|
|
value(array)
|
|
{
|
|
if (!array)
|
|
return false;
|
|
|
|
let constructor = array.constructor;
|
|
return constructor === Int8Array
|
|
|| constructor === Int16Array
|
|
|| constructor === Int32Array
|
|
|| constructor === Uint8Array
|
|
|| constructor === Uint8ClampedArray
|
|
|| constructor === Uint16Array
|
|
|| constructor === Uint32Array
|
|
|| constructor === Float32Array
|
|
|| constructor === Float64Array;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array, "shallowEqual",
|
|
{
|
|
value(a, b)
|
|
{
|
|
function isArrayLike(x) {
|
|
return Array.isArray(x) || Array.isTypedArray(x);
|
|
}
|
|
|
|
if (!isArrayLike(a) || !isArrayLike(b))
|
|
return false;
|
|
|
|
if (a === b)
|
|
return true;
|
|
|
|
let length = a.length;
|
|
|
|
if (length !== b.length)
|
|
return false;
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
if (a[i] === b[i])
|
|
continue;
|
|
|
|
if (!Object.shallowEqual(a[i], b[i]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array, "diffArrays",
|
|
{
|
|
value(initialArray, currentArray, onEach, comparator)
|
|
{
|
|
"use strict";
|
|
|
|
function defaultComparator(initial, current) {
|
|
return initial === current;
|
|
}
|
|
comparator = comparator || defaultComparator;
|
|
|
|
// Find the shortest prefix of matching items in both arrays.
|
|
//
|
|
// initialArray = ["a", "b", "b", "c"]
|
|
// currentArray = ["c", "b", "b", "a"]
|
|
// findShortestEdit() // [1, 1]
|
|
//
|
|
function findShortestEdit() {
|
|
let deletionCount = initialArray.length;
|
|
let additionCount = currentArray.length;
|
|
let editCount = deletionCount + additionCount;
|
|
for (let i = 0; i < initialArray.length; ++i) {
|
|
if (i > editCount) {
|
|
// Break since any possible edits at this point are going to be longer than the one already found.
|
|
break;
|
|
}
|
|
|
|
for (let j = 0; j < currentArray.length; ++j) {
|
|
let newEditCount = i + j;
|
|
if (newEditCount > editCount) {
|
|
// Break since any possible edits at this point are going to be longer than the one already found.
|
|
break;
|
|
}
|
|
|
|
if (comparator(initialArray[i], currentArray[j])) {
|
|
// A candidate for the shortest edit found.
|
|
if (newEditCount < editCount) {
|
|
editCount = newEditCount;
|
|
deletionCount = i;
|
|
additionCount = j;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return [deletionCount, additionCount];
|
|
}
|
|
|
|
function commonPrefixLength(listA, listB) {
|
|
let shorterListLength = Math.min(listA.length, listB.length);
|
|
let i = 0;
|
|
while (i < shorterListLength) {
|
|
if (!comparator(listA[i], listB[i]))
|
|
break;
|
|
++i;
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function fireOnEach(count, diffAction, array) {
|
|
for (let i = 0; i < count; ++i)
|
|
onEach(array[i], diffAction);
|
|
}
|
|
|
|
while (initialArray.length || currentArray.length) {
|
|
// Remove common prefix.
|
|
let prefixLength = commonPrefixLength(initialArray, currentArray);
|
|
if (prefixLength) {
|
|
fireOnEach(prefixLength, 0, currentArray);
|
|
initialArray = initialArray.slice(prefixLength);
|
|
currentArray = currentArray.slice(prefixLength);
|
|
}
|
|
|
|
if (!initialArray.length && !currentArray.length)
|
|
break;
|
|
|
|
let [deletionCount, additionCount] = findShortestEdit();
|
|
fireOnEach(deletionCount, -1, initialArray);
|
|
fireOnEach(additionCount, 1, currentArray);
|
|
initialArray = initialArray.slice(deletionCount);
|
|
currentArray = currentArray.slice(additionCount);
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "firstValue",
|
|
{
|
|
get()
|
|
{
|
|
return this[0];
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "lastValue",
|
|
{
|
|
get()
|
|
{
|
|
if (!this.length)
|
|
return undefined;
|
|
return this[this.length - 1];
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "adjacencies",
|
|
{
|
|
value: function*() {
|
|
for (let i = 1; i < this.length; ++i)
|
|
yield [this[i - 1], this[i]];
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "remove",
|
|
{
|
|
value(value)
|
|
{
|
|
for (let i = 0; i < this.length; ++i) {
|
|
if (this[i] === value) {
|
|
this.splice(i, 1);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "removeAll",
|
|
{
|
|
value(value)
|
|
{
|
|
for (let i = this.length - 1; i >= 0; --i) {
|
|
if (this[i] === value)
|
|
this.splice(i, 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "toggleIncludes",
|
|
{
|
|
value(value, force)
|
|
{
|
|
let exists = this.includes(value);
|
|
|
|
if (force !== undefined && exists === !!force)
|
|
return;
|
|
|
|
if (exists)
|
|
this.remove(value);
|
|
else
|
|
this.push(value);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "insertAtIndex",
|
|
{
|
|
value(value, index)
|
|
{
|
|
this.splice(index, 0, value);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "pushAll",
|
|
{
|
|
value(iterable)
|
|
{
|
|
for (let item of iterable)
|
|
this.push(item);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "partition",
|
|
{
|
|
value(callback)
|
|
{
|
|
let positive = [];
|
|
let negative = [];
|
|
for (let i = 0; i < this.length; ++i) {
|
|
let value = this[i];
|
|
if (callback(value))
|
|
positive.push(value);
|
|
else
|
|
negative.push(value);
|
|
}
|
|
return [positive, negative];
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "isLowerCase",
|
|
{
|
|
value()
|
|
{
|
|
return /^[a-z]+$/.test(this);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "isUpperCase",
|
|
{
|
|
value()
|
|
{
|
|
return /^[A-Z]+$/.test(this);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "isJSON",
|
|
{
|
|
value(predicate)
|
|
{
|
|
try {
|
|
let json = JSON.parse(this);
|
|
return !predicate || predicate(json);
|
|
} catch { }
|
|
return false;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "truncateStart",
|
|
{
|
|
value(maxLength)
|
|
{
|
|
"use strict";
|
|
|
|
if (this.length <= maxLength)
|
|
return this;
|
|
return ellipsis + this.substr(this.length - maxLength + 1);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "truncateMiddle",
|
|
{
|
|
value(maxLength)
|
|
{
|
|
"use strict";
|
|
|
|
if (this.length <= maxLength)
|
|
return this;
|
|
var leftHalf = maxLength >> 1;
|
|
var rightHalf = maxLength - leftHalf - 1;
|
|
return this.substr(0, leftHalf) + ellipsis + this.substr(this.length - rightHalf, rightHalf);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "truncateEnd",
|
|
{
|
|
value(maxLength)
|
|
{
|
|
"use strict";
|
|
|
|
if (this.length <= maxLength)
|
|
return this;
|
|
return this.substr(0, maxLength - 1) + ellipsis;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "truncate",
|
|
{
|
|
value(maxLength)
|
|
{
|
|
"use strict";
|
|
|
|
if (this.length <= maxLength)
|
|
return this;
|
|
|
|
let clipped = this.slice(0, maxLength);
|
|
let indexOfLastWhitespace = clipped.search(/\s\S*$/);
|
|
if (indexOfLastWhitespace > Math.floor(maxLength / 2))
|
|
clipped = clipped.slice(0, indexOfLastWhitespace - 1);
|
|
|
|
return clipped + ellipsis;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "collapseWhitespace",
|
|
{
|
|
value()
|
|
{
|
|
return this.replace(/[\s\xA0]+/g, " ");
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "removeWhitespace",
|
|
{
|
|
value()
|
|
{
|
|
return this.replace(/[\s\xA0]+/g, "");
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "escapeCharacters",
|
|
{
|
|
value(charactersToEscape)
|
|
{
|
|
if (!charactersToEscape)
|
|
return this.valueOf();
|
|
|
|
let charactersToEscapeSet = new Set(charactersToEscape);
|
|
|
|
let foundCharacter = false;
|
|
for (let c of this) {
|
|
if (!charactersToEscapeSet.has(c))
|
|
continue;
|
|
foundCharacter = true;
|
|
break;
|
|
}
|
|
|
|
if (!foundCharacter)
|
|
return this.valueOf();
|
|
|
|
let result = "";
|
|
for (let c of this) {
|
|
if (charactersToEscapeSet.has(c))
|
|
result += "\\";
|
|
result += c;
|
|
}
|
|
|
|
return result.valueOf();
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "escapeForRegExp",
|
|
{
|
|
value()
|
|
{
|
|
return this.escapeCharacters("^[]{}()\\.$*+?|");
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "capitalize",
|
|
{
|
|
value()
|
|
{
|
|
return this.charAt(0).toUpperCase() + this.slice(1);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "extendedLocaleCompare",
|
|
{
|
|
value(other)
|
|
{
|
|
return this.localeCompare(other, undefined, {numeric: true});
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String, "tokenizeFormatString",
|
|
{
|
|
value(format)
|
|
{
|
|
var tokens = [];
|
|
var substitutionIndex = 0;
|
|
|
|
function addStringToken(str)
|
|
{
|
|
tokens.push({type: "string", value: str});
|
|
}
|
|
|
|
function addSpecifierToken(specifier, precision, substitutionIndex)
|
|
{
|
|
tokens.push({type: "specifier", specifier, precision, substitutionIndex});
|
|
}
|
|
|
|
var index = 0;
|
|
for (var precentIndex = format.indexOf("%", index); precentIndex !== -1; precentIndex = format.indexOf("%", index)) {
|
|
addStringToken(format.substring(index, precentIndex));
|
|
index = precentIndex + 1;
|
|
|
|
if (format[index] === "%") {
|
|
addStringToken("%");
|
|
++index;
|
|
continue;
|
|
}
|
|
|
|
if (!isNaN(format[index])) {
|
|
// The first character is a number, it might be a substitution index.
|
|
var number = parseInt(format.substring(index), 10);
|
|
while (!isNaN(format[index]))
|
|
++index;
|
|
|
|
// If the number is greater than zero and ends with a "$",
|
|
// then this is a substitution index.
|
|
if (number > 0 && format[index] === "$") {
|
|
substitutionIndex = (number - 1);
|
|
++index;
|
|
}
|
|
}
|
|
|
|
const defaultPrecision = 6;
|
|
|
|
let precision = defaultPrecision;
|
|
if (format[index] === ".") {
|
|
// This is a precision specifier. If no digit follows the ".",
|
|
// then use the default precision of six digits (ISO C99 specification).
|
|
++index;
|
|
|
|
precision = parseInt(format.substring(index), 10);
|
|
if (isNaN(precision))
|
|
precision = defaultPrecision;
|
|
|
|
while (!isNaN(format[index]))
|
|
++index;
|
|
}
|
|
|
|
addSpecifierToken(format[index], precision, substitutionIndex);
|
|
|
|
++substitutionIndex;
|
|
++index;
|
|
}
|
|
|
|
addStringToken(format.substring(index));
|
|
|
|
return tokens;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "lineCount",
|
|
{
|
|
get()
|
|
{
|
|
"use strict";
|
|
|
|
let lineCount = 1;
|
|
let index = 0;
|
|
while (true) {
|
|
index = this.indexOf("\n", index);
|
|
if (index === -1)
|
|
return lineCount;
|
|
|
|
index += "\n".length;
|
|
lineCount++;
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "lastLine",
|
|
{
|
|
get()
|
|
{
|
|
"use strict";
|
|
|
|
let index = this.lastIndexOf("\n");
|
|
if (index === -1)
|
|
return this;
|
|
|
|
return this.slice(index + "\n".length);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "hash",
|
|
{
|
|
get()
|
|
{
|
|
// Matches the wtf/Hasher.h (SuperFastHash) algorithm.
|
|
|
|
// Arbitrary start value to avoid mapping all 0's to all 0's.
|
|
const stringHashingStartValue = 0x9e3779b9;
|
|
|
|
var result = stringHashingStartValue;
|
|
var pendingCharacter = null;
|
|
for (var i = 0; i < this.length; ++i) {
|
|
var currentCharacter = this[i].charCodeAt(0);
|
|
if (pendingCharacter === null) {
|
|
pendingCharacter = currentCharacter;
|
|
continue;
|
|
}
|
|
|
|
result += pendingCharacter;
|
|
result = (result << 16) ^ ((currentCharacter << 11) ^ result);
|
|
result += result >> 11;
|
|
|
|
pendingCharacter = null;
|
|
}
|
|
|
|
// Handle the last character in odd length strings.
|
|
if (pendingCharacter !== null) {
|
|
result += pendingCharacter;
|
|
result ^= result << 11;
|
|
result += result >> 17;
|
|
}
|
|
|
|
// Force "avalanching" of final 31 bits.
|
|
result ^= result << 3;
|
|
result += result >> 5;
|
|
result ^= result << 2;
|
|
result += result >> 15;
|
|
result ^= result << 10;
|
|
|
|
// Prevent 0 and negative results.
|
|
return (0xffffffff + result + 1).toString(36);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String, "standardFormatters",
|
|
{
|
|
value: {
|
|
d: function(substitution)
|
|
{
|
|
return parseInt(substitution).toLocaleString();
|
|
},
|
|
|
|
f: function(substitution, token)
|
|
{
|
|
let value = parseFloat(substitution);
|
|
if (isNaN(value))
|
|
return NaN;
|
|
|
|
let options = {
|
|
minimumFractionDigits: token.precision,
|
|
maximumFractionDigits: token.precision,
|
|
useGrouping: false
|
|
};
|
|
return value.toLocaleString(undefined, options);
|
|
},
|
|
|
|
s: function(substitution)
|
|
{
|
|
return substitution;
|
|
}
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String, "format",
|
|
{
|
|
value(format, substitutions, formatters, initialValue, append)
|
|
{
|
|
if (!format || !substitutions || !substitutions.length)
|
|
return {formattedResult: append(initialValue, format), unusedSubstitutions: substitutions};
|
|
|
|
function prettyFunctionName()
|
|
{
|
|
return "String.format(\"" + format + "\", \"" + Array.from(substitutions).join("\", \"") + "\")";
|
|
}
|
|
|
|
function warn(msg)
|
|
{
|
|
console.warn(prettyFunctionName() + ": " + msg);
|
|
}
|
|
|
|
function error(msg)
|
|
{
|
|
console.error(prettyFunctionName() + ": " + msg);
|
|
}
|
|
|
|
var result = initialValue;
|
|
var tokens = String.tokenizeFormatString(format);
|
|
var usedSubstitutionIndexes = {};
|
|
let ignoredUnknownSpecifierCount = 0;
|
|
|
|
for (var i = 0; i < tokens.length; ++i) {
|
|
var token = tokens[i];
|
|
|
|
if (token.type === "string") {
|
|
result = append(result, token.value);
|
|
continue;
|
|
}
|
|
|
|
if (token.type !== "specifier") {
|
|
error("Unknown token type \"" + token.type + "\" found.");
|
|
continue;
|
|
}
|
|
|
|
let substitutionIndex = token.substitutionIndex - ignoredUnknownSpecifierCount;
|
|
if (substitutionIndex >= substitutions.length) {
|
|
// If there are not enough substitutions for the current substitutionIndex
|
|
// just output the format specifier literally and move on.
|
|
error("not enough substitution arguments. Had " + substitutions.length + " but needed " + (substitutionIndex + 1) + ", so substitution was skipped.");
|
|
result = append(result, "%" + (token.precision > -1 ? token.precision : "") + token.specifier);
|
|
continue;
|
|
}
|
|
|
|
if (!(token.specifier in formatters)) {
|
|
warn(`Unsupported format specifier "%${token.specifier}" will be ignored.`);
|
|
result = append(result, "%" + token.specifier);
|
|
++ignoredUnknownSpecifierCount;
|
|
continue;
|
|
}
|
|
|
|
usedSubstitutionIndexes[substitutionIndex] = true;
|
|
result = append(result, formatters[token.specifier](substitutions[substitutionIndex], token));
|
|
}
|
|
|
|
var unusedSubstitutions = [];
|
|
for (var i = 0; i < substitutions.length; ++i) {
|
|
if (i in usedSubstitutionIndexes)
|
|
continue;
|
|
unusedSubstitutions.push(substitutions[i]);
|
|
}
|
|
|
|
return {formattedResult: result, unusedSubstitutions};
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "format",
|
|
{
|
|
value()
|
|
{
|
|
return String.format(this, arguments, String.standardFormatters, "", function(a, b) { return a + b; }).formattedResult;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "insertWordBreakCharacters",
|
|
{
|
|
value()
|
|
{
|
|
// Add zero width spaces after characters that are good to break after.
|
|
// Otherwise a string with no spaces will not break and overflow its container.
|
|
// This is mainly used on URL strings, so the characters are tailored for URLs.
|
|
return this.replace(/([\/;:\)\]\}&?])/g, "$1\u200b");
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "removeWordBreakCharacters",
|
|
{
|
|
value()
|
|
{
|
|
// Undoes what insertWordBreakCharacters did.
|
|
return this.replace(/\u200b/g, "");
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "levenshteinDistance",
|
|
{
|
|
value(s)
|
|
{
|
|
var m = this.length;
|
|
var n = s.length;
|
|
var d = new Array(m + 1);
|
|
|
|
for (var i = 0; i <= m; ++i) {
|
|
d[i] = new Array(n + 1);
|
|
d[i][0] = i;
|
|
}
|
|
|
|
for (var j = 0; j <= n; ++j)
|
|
d[0][j] = j;
|
|
|
|
for (var j = 1; j <= n; ++j) {
|
|
for (var i = 1; i <= m; ++i) {
|
|
if (this[i - 1] === s[j - 1])
|
|
d[i][j] = d[i - 1][j - 1];
|
|
else {
|
|
var deletion = d[i - 1][j] + 1;
|
|
var insertion = d[i][j - 1] + 1;
|
|
var substitution = d[i - 1][j - 1] + 1;
|
|
d[i][j] = Math.min(deletion, insertion, substitution);
|
|
}
|
|
}
|
|
}
|
|
|
|
return d[m][n];
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "toCamelCase",
|
|
{
|
|
value()
|
|
{
|
|
return this.toLowerCase().replace(/[^\w]+(\w)/g, (match, group) => group.toUpperCase());
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(String.prototype, "hasMatchingEscapedQuotes",
|
|
{
|
|
value()
|
|
{
|
|
return /^\"(?:[^\"\\]|\\.)*\"$/.test(this) || /^\'(?:[^\'\\]|\\.)*\'$/.test(this);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Math, "roundTo",
|
|
{
|
|
value(num, step)
|
|
{
|
|
return Math.round(num / step) * step;
|
|
}
|
|
});
|
|
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/WebGL_API/Matrix_math_for_the_web#Multiplying_a_matrix_and_a_point
|
|
Object.defineProperty(Math, "multiplyMatrixByVector",
|
|
{
|
|
value(matrix, vector)
|
|
{
|
|
let height = matrix.length;
|
|
let width = matrix[0].length;
|
|
console.assert(width === vector.length);
|
|
|
|
let result = Array(width).fill(0);
|
|
for (let i = 0; i < width; ++i) {
|
|
for (let rowIndex = 0; rowIndex < height; ++rowIndex)
|
|
result[i] += vector[rowIndex] * matrix[i][rowIndex];
|
|
}
|
|
|
|
return result;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "constrain",
|
|
{
|
|
value(num, min, max)
|
|
{
|
|
if (isNaN(num) || max < min)
|
|
return min;
|
|
|
|
if (num < min)
|
|
num = min;
|
|
else if (num > max)
|
|
num = max;
|
|
return num;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "percentageString",
|
|
{
|
|
value(fraction, precision = 1)
|
|
{
|
|
return fraction.toLocaleString(undefined, {minimumFractionDigits: precision, style: "percent"});
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "secondsToMillisecondsString",
|
|
{
|
|
value(seconds, higherResolution)
|
|
{
|
|
let ms = seconds * 1000;
|
|
|
|
if (higherResolution)
|
|
return WI.UIString("%.2fms").format(ms);
|
|
return WI.UIString("%.1fms").format(ms);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "secondsToString",
|
|
{
|
|
value(seconds, higherResolution)
|
|
{
|
|
const epsilon = 0.0001;
|
|
|
|
let ms = seconds * 1000;
|
|
if (ms < epsilon)
|
|
return WI.UIString("%.0fms").format(0);
|
|
|
|
if (Math.abs(ms) < (10 + epsilon)) {
|
|
if (higherResolution)
|
|
return WI.UIString("%.3fms").format(ms);
|
|
return WI.UIString("%.2fms").format(ms);
|
|
}
|
|
|
|
if (Math.abs(ms) < (100 + epsilon)) {
|
|
if (higherResolution)
|
|
return WI.UIString("%.2fms").format(ms);
|
|
return WI.UIString("%.1fms").format(ms);
|
|
}
|
|
|
|
if (Math.abs(ms) < (1000 + epsilon)) {
|
|
if (higherResolution)
|
|
return WI.UIString("%.1fms").format(ms);
|
|
return WI.UIString("%.0fms").format(ms);
|
|
}
|
|
|
|
// Do not go over seconds when in high resolution mode.
|
|
if (higherResolution || Math.abs(seconds) < 60)
|
|
return WI.UIString("%.2fs").format(seconds);
|
|
|
|
let minutes = seconds / 60;
|
|
if (Math.abs(minutes) < 60)
|
|
return WI.UIString("%.1fmin").format(minutes);
|
|
|
|
let hours = minutes / 60;
|
|
if (Math.abs(hours) < 24)
|
|
return WI.UIString("%.1fhrs").format(hours);
|
|
|
|
let days = hours / 24;
|
|
return WI.UIString("%.1f days").format(days);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "bytesToString",
|
|
{
|
|
value(bytes, higherResolution, bytesThreshold)
|
|
{
|
|
higherResolution ??= true;
|
|
bytesThreshold ??= 1000;
|
|
|
|
if (Math.abs(bytes) < bytesThreshold)
|
|
return WI.UIString("%.0f B").format(bytes);
|
|
|
|
let kilobytes = bytes / 1000;
|
|
if (Math.abs(kilobytes) < 1000) {
|
|
if (higherResolution || Math.abs(kilobytes) < 10)
|
|
return WI.UIString("%.2f KB").format(kilobytes);
|
|
return WI.UIString("%.1f KB").format(kilobytes);
|
|
}
|
|
|
|
let megabytes = kilobytes / 1000;
|
|
if (Math.abs(megabytes) < 1000) {
|
|
if (higherResolution || Math.abs(megabytes) < 10)
|
|
return WI.UIString("%.2f MB").format(megabytes);
|
|
return WI.UIString("%.1f MB").format(megabytes);
|
|
}
|
|
|
|
let gigabytes = megabytes / 1000;
|
|
if (higherResolution || Math.abs(gigabytes) < 10)
|
|
return WI.UIString("%.2f GB").format(gigabytes);
|
|
return WI.UIString("%.1f GB").format(gigabytes);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "abbreviate",
|
|
{
|
|
value(num)
|
|
{
|
|
if (num < 1000)
|
|
return num.toLocaleString();
|
|
|
|
if (num < 1_000_000)
|
|
return WI.UIString("%.1fK").format(Math.round(num / 100) / 10);
|
|
|
|
if (num < 1_000_000_000)
|
|
return WI.UIString("%.1fM").format(Math.round(num / 100_000) / 10);
|
|
|
|
return WI.UIString("%.1fB").format(Math.round(num / 100_000_000) / 10);
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number, "zeroPad",
|
|
{
|
|
value(num, length)
|
|
{
|
|
let string = num.toLocaleString();
|
|
return string.padStart(length, "0");
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Number, "countDigits",
|
|
{
|
|
value(num)
|
|
{
|
|
if (num === 0)
|
|
return 1;
|
|
|
|
num = Math.abs(num);
|
|
return Math.floor(Math.log(num) * Math.LOG10E) + 1;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Number.prototype, "maxDecimals",
|
|
{
|
|
value(decimals)
|
|
{
|
|
let power = 10 ** decimals;
|
|
return Math.round(this * power) / power;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Uint32Array, "isLittleEndian",
|
|
{
|
|
value()
|
|
{
|
|
if ("_isLittleEndian" in this)
|
|
return this._isLittleEndian;
|
|
|
|
var buffer = new ArrayBuffer(4);
|
|
var longData = new Uint32Array(buffer);
|
|
var data = new Uint8Array(buffer);
|
|
|
|
longData[0] = 0x0a0b0c0d;
|
|
|
|
this._isLittleEndian = data[0] === 0x0d && data[1] === 0x0c && data[2] === 0x0b && data[3] === 0x0a;
|
|
|
|
return this._isLittleEndian;
|
|
}
|
|
});
|
|
|
|
function isEmptyObject(object)
|
|
{
|
|
for (var property in object)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
function isEnterKey(event)
|
|
{
|
|
// Check if this is an IME event.
|
|
return event.keyCode !== 229 && event.keyIdentifier === "Enter";
|
|
}
|
|
|
|
function resolveDotsInPath(path)
|
|
{
|
|
if (!path)
|
|
return path;
|
|
|
|
if (path.indexOf("./") === -1)
|
|
return path;
|
|
|
|
console.assert(path.charAt(0) === "/");
|
|
|
|
var result = [];
|
|
|
|
var components = path.split("/");
|
|
for (var i = 0; i < components.length; ++i) {
|
|
var component = components[i];
|
|
|
|
// Skip over "./".
|
|
if (component === ".")
|
|
continue;
|
|
|
|
// Rewind one component for "../".
|
|
if (component === "..") {
|
|
if (result.length === 1)
|
|
continue;
|
|
result.pop();
|
|
continue;
|
|
}
|
|
|
|
result.push(component);
|
|
}
|
|
|
|
return result.join("/");
|
|
}
|
|
|
|
function parseMIMEType(fullMimeType)
|
|
{
|
|
if (!fullMimeType)
|
|
return {type: fullMimeType, boundary: null, encoding: null};
|
|
|
|
var typeParts = fullMimeType.split(/\s*;\s*/);
|
|
console.assert(typeParts.length >= 1);
|
|
|
|
var type = typeParts[0];
|
|
var boundary = null;
|
|
var encoding = null;
|
|
|
|
for (var i = 1; i < typeParts.length; ++i) {
|
|
var subparts = typeParts[i].split(/\s*=\s*/);
|
|
if (subparts.length !== 2)
|
|
continue;
|
|
|
|
if (subparts[0].toLowerCase() === "boundary")
|
|
boundary = subparts[1];
|
|
else if (subparts[0].toLowerCase() === "charset")
|
|
encoding = subparts[1].replace("^\"|\"$", ""); // Trim quotes.
|
|
}
|
|
|
|
return {type, boundary: boundary || null, encoding: encoding || null};
|
|
}
|
|
|
|
function simpleGlobStringToRegExp(globString, regExpFlags)
|
|
{
|
|
// Only supports "*" globs.
|
|
|
|
if (!globString)
|
|
return null;
|
|
|
|
// Escape everything from String.prototype.escapeForRegExp except "*".
|
|
var regexString = globString.escapeCharacters("^[]{}()\\.$+?|");
|
|
|
|
// Unescape all doubly escaped backslashes in front of escaped asterisks.
|
|
// So "\\*" will become "\*" again, undoing escapeCharacters escaping of "\".
|
|
// This makes "\*" match a literal "*" instead of using the "*" for globbing.
|
|
regexString = regexString.replace(/\\\\\*/g, "\\*");
|
|
|
|
// The following regex doesn't match an asterisk that has a backslash in front.
|
|
// It also catches consecutive asterisks so they collapse down when replaced.
|
|
var unescapedAsteriskRegex = /(^|[^\\])\*+/g;
|
|
if (unescapedAsteriskRegex.test(globString)) {
|
|
// Replace all unescaped asterisks with ".*".
|
|
regexString = regexString.replace(unescapedAsteriskRegex, "$1.*");
|
|
|
|
// Match edge boundaries when there is an asterisk to better meet the expectations
|
|
// of the user. When someone types "*.js" they don't expect "foo.json" to match. They
|
|
// would only expect that if they type "*.js*". We use \b (instead of ^ and $) to allow
|
|
// matches inside paths or URLs, so "ba*.js" will match "foo/bar.js" but not "boo/bbar.js".
|
|
// When there isn't an asterisk the regexString is just a substring search.
|
|
regexString = "\\b" + regexString + "\\b";
|
|
}
|
|
|
|
return new RegExp(regexString, regExpFlags);
|
|
}
|
|
|
|
Object.defineProperty(Array.prototype, "min",
|
|
{
|
|
value(comparator)
|
|
{
|
|
return this[this.minIndex(comparator)];
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "minIndex",
|
|
{
|
|
value(comparator)
|
|
{
|
|
function defaultComparator(a, b)
|
|
{
|
|
return a - b;
|
|
}
|
|
comparator = comparator || defaultComparator;
|
|
|
|
let minIndex = -1;
|
|
for (let i = 0; i < this.length; ++i) {
|
|
if (minIndex === -1 || comparator(this[minIndex], this[i]) > 0)
|
|
minIndex = i;
|
|
}
|
|
return minIndex;
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "lowerBound",
|
|
{
|
|
// Return index of the leftmost element that is equal or greater
|
|
// than the specimen object. If there's no such element (i.e. all
|
|
// elements are smaller than the specimen) returns array.length.
|
|
// The function works for sorted array.
|
|
value(object, comparator)
|
|
{
|
|
function defaultComparator(a, b)
|
|
{
|
|
return a - b;
|
|
}
|
|
comparator = comparator || defaultComparator;
|
|
var l = 0;
|
|
var r = this.length;
|
|
while (l < r) {
|
|
var m = (l + r) >> 1;
|
|
if (comparator(object, this[m]) > 0)
|
|
l = m + 1;
|
|
else
|
|
r = m;
|
|
}
|
|
return r;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "upperBound",
|
|
{
|
|
// Return index of the leftmost element that is greater
|
|
// than the specimen object. If there's no such element (i.e. all
|
|
// elements are smaller than the specimen) returns array.length.
|
|
// The function works for sorted array.
|
|
value(object, comparator)
|
|
{
|
|
function defaultComparator(a, b)
|
|
{
|
|
return a - b;
|
|
}
|
|
comparator = comparator || defaultComparator;
|
|
var l = 0;
|
|
var r = this.length;
|
|
while (l < r) {
|
|
var m = (l + r) >> 1;
|
|
if (comparator(object, this[m]) >= 0)
|
|
l = m + 1;
|
|
else
|
|
r = m;
|
|
}
|
|
return r;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Array.prototype, "binaryIndexOf",
|
|
{
|
|
value(value, comparator)
|
|
{
|
|
function defaultComparator(a, b)
|
|
{
|
|
return a - b;
|
|
}
|
|
comparator = comparator || defaultComparator;
|
|
|
|
var index = this.lowerBound(value, comparator);
|
|
return index < this.length && comparator(value, this[index]) === 0 ? index : -1;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Promise, "chain",
|
|
{
|
|
async value(callbacks, initialValue)
|
|
{
|
|
let results = [];
|
|
for (let i = 0; i < callbacks.length; ++i)
|
|
results.push(await callbacks[i](results.lastValue || initialValue || null, i));
|
|
return results;
|
|
}
|
|
});
|
|
|
|
Object.defineProperty(Promise, "delay",
|
|
{
|
|
value(delay)
|
|
{
|
|
return new Promise((resolve) => setTimeout(resolve, delay || 0));
|
|
}
|
|
});
|
|
|
|
function appendWebInspectorSourceURL(string)
|
|
{
|
|
if (string.includes("//# sourceURL"))
|
|
return string;
|
|
return "\n//# sourceURL=__WebInspectorInternal__\n" + string;
|
|
}
|
|
|
|
function appendWebInspectorConsoleEvaluationSourceURL(string)
|
|
{
|
|
if (string.includes("//# sourceURL"))
|
|
return string;
|
|
return "\n//# sourceURL=__WebInspectorConsoleEvaluation__\n" + string;
|
|
}
|
|
|
|
function isWebInspectorBootstrapScript(url)
|
|
{
|
|
return url === WI.NetworkManager.bootstrapScriptURL;
|
|
}
|
|
|
|
function isWebInspectorInternalScript(url)
|
|
{
|
|
return url === "__WebInspectorInternal__";
|
|
}
|
|
|
|
function isWebInspectorConsoleEvaluationScript(url)
|
|
{
|
|
return url === "__WebInspectorConsoleEvaluation__";
|
|
}
|
|
|
|
function isWebKitInjectedScript(url)
|
|
{
|
|
return url && url.startsWith("__InjectedScript_") && url.endsWith(".js");
|
|
}
|
|
|
|
function isWebKitInternalScript(url)
|
|
{
|
|
if (isWebInspectorConsoleEvaluationScript(url))
|
|
return false;
|
|
|
|
if (isWebKitInjectedScript(url))
|
|
return true;
|
|
|
|
return url && url.startsWith("__Web") && url.endsWith("__");
|
|
}
|
|
|
|
function isFunctionStringNativeCode(str)
|
|
{
|
|
return str.endsWith("{\n [native code]\n}");
|
|
}
|
|
|
|
function whitespaceRatio(content, start, end)
|
|
{
|
|
let whitespaceScore = 0;
|
|
let size = end - start;
|
|
|
|
for (let i = start; i < end; i++) {
|
|
let char = content[i];
|
|
if (char === " ")
|
|
whitespaceScore++;
|
|
else if (char === "\t")
|
|
whitespaceScore += 4;
|
|
else if (char === "\n")
|
|
whitespaceScore += 8;
|
|
}
|
|
|
|
let ratio = whitespaceScore / size;
|
|
return ratio;
|
|
}
|
|
|
|
function isTextLikelyMinified(content)
|
|
{
|
|
const autoFormatMaxCharactersToCheck = 2500;
|
|
const autoFormatWhitespaceRatio = 0.2;
|
|
|
|
if (content.length <= autoFormatMaxCharactersToCheck) {
|
|
let ratio = whitespaceRatio(content, 0, content.length);
|
|
return ratio < autoFormatWhitespaceRatio;
|
|
}
|
|
|
|
let startRatio = whitespaceRatio(content, 0, autoFormatMaxCharactersToCheck);
|
|
if (startRatio < autoFormatWhitespaceRatio)
|
|
return true;
|
|
|
|
let endRatio = whitespaceRatio(content, content.length - autoFormatMaxCharactersToCheck, content.length);
|
|
if (endRatio < autoFormatWhitespaceRatio)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
function doubleQuotedString(str)
|
|
{
|
|
return JSON.stringify(str);
|
|
}
|
|
|
|
function insertionIndexForObjectInListSortedByFunction(object, list, comparator, insertionIndexAfter)
|
|
{
|
|
if (insertionIndexAfter) {
|
|
return list.upperBound(object, comparator);
|
|
} else {
|
|
return list.lowerBound(object, comparator);
|
|
}
|
|
}
|
|
|
|
function insertObjectIntoSortedArray(object, array, comparator)
|
|
{
|
|
array.splice(insertionIndexForObjectInListSortedByFunction(object, array, comparator), 0, object);
|
|
}
|
|
|
|
async function retryUntil(predicate, {delay, retries} = {})
|
|
{
|
|
retries ??= 100;
|
|
delay ??= 100;
|
|
|
|
for (let i = 0; i < retries; ++i) {
|
|
let result = predicate();
|
|
if (result)
|
|
return result;
|
|
|
|
await Promise.delay(delay);
|
|
}
|
|
|
|
console.assert(false, "retryUntil exceeded the maximum number of retries.", predicate, retries);
|
|
return null;
|
|
}
|
|
|
|
WI.setReentrantCheck = function(object, key)
|
|
{
|
|
key = "__checkReentrant_" + key;
|
|
object[key] = (object[key] || 0) + 1;
|
|
return object[key] === 1;
|
|
};
|
|
|
|
WI.clearReentrantCheck = function(object, key)
|
|
{
|
|
key = "__checkReentrant_" + key;
|
|
object[key] = (object[key] || 0) - 1;
|
|
return object[key] === 0;
|
|
};
|
|
|
|
/* Base/Setting.js */
|
|
|
|
/*
|
|
* Copyright (C) 2009 Google Inc. All rights reserved.
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Setting = class Setting extends WI.Object
|
|
{
|
|
constructor(name, defaultValue)
|
|
{
|
|
super();
|
|
|
|
this._name = name;
|
|
|
|
this._defaultValue = defaultValue;
|
|
}
|
|
|
|
// Static
|
|
|
|
static migrateValue(key)
|
|
{
|
|
let localStorageKey = WI.Setting._localStorageKeyPrefix + key;
|
|
|
|
let value = undefined;
|
|
if (!window.InspectorTest && window.localStorage) {
|
|
let item = window.localStorage.getItem(localStorageKey);
|
|
if (item !== null) {
|
|
try {
|
|
value = JSON.parse(item);
|
|
} catch { }
|
|
|
|
window.localStorage.removeItem(localStorageKey);
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
static reset()
|
|
{
|
|
let prefix = WI.Setting._localStorageKeyPrefix;
|
|
|
|
let keysToRemove = [];
|
|
for (let i = 0; i < window.localStorage.length; ++i) {
|
|
let key = window.localStorage.key(i);
|
|
if (key.startsWith(prefix))
|
|
keysToRemove.push(key);
|
|
}
|
|
|
|
for (let key of keysToRemove)
|
|
window.localStorage.removeItem(key);
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get defaultValue() { return this._defaultValue; }
|
|
|
|
get value()
|
|
{
|
|
if ("_value" in this)
|
|
return this._value;
|
|
|
|
// Make a copy of the default value so changes to object values don't modify the default value.
|
|
this._value = JSON.parse(JSON.stringify(this._defaultValue));
|
|
|
|
if (!window.InspectorTest && window.localStorage) {
|
|
let key = WI.Setting._localStorageKeyPrefix + this._name;
|
|
let item = window.localStorage.getItem(key);
|
|
if (item !== null) {
|
|
try {
|
|
this._value = JSON.parse(item);
|
|
} catch {
|
|
window.localStorage.removeItem(key);
|
|
}
|
|
}
|
|
}
|
|
|
|
return this._value;
|
|
}
|
|
|
|
set value(value)
|
|
{
|
|
if (this._value === value)
|
|
return;
|
|
|
|
this._value = value;
|
|
|
|
this.save();
|
|
}
|
|
|
|
save()
|
|
{
|
|
if (!window.InspectorTest && window.localStorage) {
|
|
let key = WI.Setting._localStorageKeyPrefix + this._name;
|
|
try {
|
|
if (Object.shallowEqual(this._value, this._defaultValue))
|
|
window.localStorage.removeItem(key);
|
|
else
|
|
window.localStorage.setItem(key, JSON.stringify(this._value));
|
|
} catch {
|
|
console.error("Error saving setting with name: " + this._name);
|
|
}
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Setting.Event.Changed, this._value, {name: this._name});
|
|
}
|
|
|
|
reset()
|
|
{
|
|
// Make a copy of the default value so changes to object values don't modify the default value.
|
|
this.value = JSON.parse(JSON.stringify(this._defaultValue));
|
|
}
|
|
};
|
|
|
|
WI.Setting._localStorageKeyPrefix = (function() {
|
|
let inspectionLevel = InspectorFrontendHost ? InspectorFrontendHost.inspectionLevel : 1;
|
|
let levelString = inspectionLevel > 1 ? "-" + inspectionLevel : "";
|
|
return `com.apple.WebInspector${levelString}.`;
|
|
})();
|
|
|
|
WI.Setting.isFirstLaunch = !!window.InspectorTest || (window.localStorage && Object.keys(window.localStorage).every((key) => !key.startsWith(WI.Setting._localStorageKeyPrefix)));
|
|
|
|
WI.Setting.Event = {
|
|
Changed: "setting-changed"
|
|
};
|
|
|
|
WI.EngineeringSetting = class EngineeringSetting extends WI.Setting
|
|
{
|
|
get value()
|
|
{
|
|
if (WI.engineeringSettingsAllowed())
|
|
return super.value;
|
|
return this.defaultValue;
|
|
}
|
|
|
|
set value(value)
|
|
{
|
|
console.assert(WI.engineeringSettingsAllowed());
|
|
if (WI.engineeringSettingsAllowed())
|
|
super.value = value;
|
|
}
|
|
};
|
|
|
|
WI.DebugSetting = class DebugSetting extends WI.Setting
|
|
{
|
|
get value()
|
|
{
|
|
if (WI.isDebugUIEnabled())
|
|
return super.value;
|
|
return this.defaultValue;
|
|
}
|
|
|
|
set value(value)
|
|
{
|
|
console.assert(WI.isDebugUIEnabled());
|
|
if (WI.isDebugUIEnabled())
|
|
super.value = value;
|
|
}
|
|
};
|
|
|
|
WI.settings = {
|
|
blackboxBreakpointEvaluations: new WI.Setting("blackbox-breakpoint-evaluations", true),
|
|
canvasRecordingAutoCaptureEnabled: new WI.Setting("canvas-recording-auto-capture-enabled", false),
|
|
canvasRecordingAutoCaptureFrameCount: new WI.Setting("canvas-recording-auto-capture-frame-count", 1),
|
|
consoleAutoExpandTrace: new WI.Setting("console-auto-expand-trace", true),
|
|
consoleSavedResultAlias: new WI.Setting("console-saved-result-alias", ""),
|
|
cssChangesPerNode: new WI.Setting("css-changes-per-node", false),
|
|
clearLogOnNavigate: new WI.Setting("clear-log-on-navigate", true),
|
|
clearNetworkOnNavigate: new WI.Setting("clear-network-on-navigate", true),
|
|
cpuTimelineThreadDetailsExpanded: new WI.Setting("cpu-timeline-thread-details-expanded", false),
|
|
domTreeDeemphasizesNodesThatAreNotRendered: new WI.Setting("dom-tree-deemphasizes-nodes-that-are-not-rendered", true),
|
|
emulateInUserGesture: new WI.Setting("emulate-in-user-gesture", false),
|
|
enableControlFlowProfiler: new WI.Setting("enable-control-flow-profiler", false),
|
|
enableElementsTabIndependentStylesDetailsSidebarPanel: new WI.Setting("elements-tab-independent-styles-details-panel", true),
|
|
enableLineWrapping: new WI.Setting("enable-line-wrapping", true),
|
|
enableNarrowLayoutMode: new WI.Setting("enable-narrow-layout-mode", true),
|
|
flexOverlayShowOrderNumbers: new WI.Setting("flex-overlay-show-order-numbers", false),
|
|
frontendAppearance: new WI.Setting("frontend-appearance", "system"),
|
|
gridOverlayShowAreaNames: new WI.Setting("grid-overlay-show-area-names", false),
|
|
gridOverlayShowExtendedGridLines: new WI.Setting("grid-overlay-show-extended-grid-lines", false),
|
|
gridOverlayShowLineNames: new WI.Setting("grid-overlay-show-line-names", false),
|
|
gridOverlayShowLineNumbers: new WI.Setting("grid-overlay-show-line-numbers", true),
|
|
gridOverlayShowTrackSizes: new WI.Setting("grid-overlay-show-track-sizes", true),
|
|
groupMediaRequestsByDOMNode: new WI.Setting("group-media-requests-by-dom-node", WI.Setting.migrateValue("group-by-dom-node") || false),
|
|
indentUnit: new WI.Setting("indent-unit", 4),
|
|
indentWithTabs: new WI.Setting("indent-with-tabs", false),
|
|
resourceCachingDisabled: new WI.Setting("disable-resource-caching", false),
|
|
searchCaseSensitive: new WI.Setting("search-case-sensitive", false),
|
|
searchFromSelection: new WI.Setting("search-from-selection", false),
|
|
searchRegularExpression: new WI.Setting("search-regular-expression", false),
|
|
selectedNetworkDetailContentViewIdentifier: new WI.Setting("network-detail-content-view-identifier", "preview"),
|
|
sourceMapsEnabled: new WI.Setting("source-maps-enabled", true),
|
|
showConsoleMessageTimestamps: new WI.Setting("show-console-message-timestamps", false),
|
|
showCSSPropertySyntaxInDocumentationPopover: new WI.Setting("show-css-property-syntax-in-documentation-popover", false),
|
|
showCanvasPath: new WI.Setting("show-canvas-path", false),
|
|
showFlexOverlayDuringElementSelection: new WI.Setting("show-grid-overlay-during-element-selection", true),
|
|
showGridOverlayDuringElementSelection: new WI.Setting("show-flex-overlay-during-element-selection", true),
|
|
showImageGrid: new WI.Setting("show-image-grid", true),
|
|
showInvisibleCharacters: new WI.Setting("show-invisible-characters", !!WI.Setting.migrateValue("show-invalid-characters")),
|
|
showJavaScriptTypeInformation: new WI.Setting("show-javascript-type-information", false),
|
|
showRulers: new WI.Setting("show-rulers", false),
|
|
showRulersDuringElementSelection: new WI.Setting("show-rulers-during-element-selection", true),
|
|
showScopeChainOnPause: new WI.Setting("show-scope-chain-sidebar", true),
|
|
showWhitespaceCharacters: new WI.Setting("show-whitespace-characters", false),
|
|
tabSize: new WI.Setting("tab-size", 4),
|
|
timelinesAutoStop: new WI.Setting("timelines-auto-stop", true),
|
|
timelineOverviewGroupBySourceCode: new WI.Setting("timeline-overview-group-by-source-code", true),
|
|
zoomFactor: new WI.Setting("zoom-factor", 1),
|
|
|
|
// Experimental
|
|
experimentalEnableStylesJumpToEffective: new WI.Setting("experimental-styles-jump-to-effective", false),
|
|
experimentalEnableStylesJumpToVariableDeclaration: new WI.Setting("experimental-styles-jump-to-variable-declaration", false),
|
|
experimentalAllowInspectingInspector: new WI.Setting("experimental-allow-inspecting-inspector", false),
|
|
experimentalCSSSortPropertyNameAutocompletionByUsage: new WI.Setting("experimental-css-sort-property-name-autocompletion-by-usage", true),
|
|
experimentalEnableNetworkEmulatedCondition: new WI.Setting("experimental-enable-network-emulated-condition", false),
|
|
experimentalGroupSourceMapErrors: new WI.Setting("experimental-group-source-map-errors", true),
|
|
experimentalLimitSourceCodeHighlighting: new WI.Setting("engineering-limit-source-code-highlighting", false),
|
|
|
|
// Protocol
|
|
protocolLogAsText: new WI.Setting("protocol-log-as-text", false),
|
|
protocolAutoLogMessages: new WI.Setting("protocol-auto-log-messages", false),
|
|
protocolAutoLogTimeStats: new WI.Setting("protocol-auto-log-time-stats", false),
|
|
protocolFilterMultiplexingBackendMessages: new WI.Setting("protocol-filter-multiplexing-backend-messages", true),
|
|
|
|
// Engineering
|
|
engineeringShowInternalExecutionContexts: new WI.EngineeringSetting("engineering-show-internal-execution-contexts", false),
|
|
engineeringShowInternalScripts: new WI.EngineeringSetting("engineering-show-internal-scripts", false),
|
|
engineeringPauseForInternalScripts: new WI.EngineeringSetting("engineering-pause-for-internal-scripts", false),
|
|
engineeringShowInternalObjectsInHeapSnapshot: new WI.EngineeringSetting("engineering-show-internal-objects-in-heap-snapshot", false),
|
|
engineeringShowPrivateSymbolsInHeapSnapshot: new WI.EngineeringSetting("engineering-show-private-symbols-in-heap-snapshot", false),
|
|
engineeringAllowEditingUserAgentShadowTrees: new WI.EngineeringSetting("engineering-allow-editing-user-agent-shadow-trees", false),
|
|
|
|
// Debug
|
|
debugShowConsoleEvaluations: new WI.DebugSetting("debug-show-console-evaluations", false),
|
|
debugOutlineFocusedElement: new WI.DebugSetting("debug-outline-focused-element", false),
|
|
debugEnableLayoutFlashing: new WI.DebugSetting("debug-enable-layout-flashing", false),
|
|
debugEnableStyleEditingDebugMode: new WI.DebugSetting("debug-enable-style-editing-debug-mode", false),
|
|
debugEnableUncaughtExceptionReporter: new WI.DebugSetting("debug-enable-uncaught-exception-reporter", true),
|
|
debugEnableDiagnosticLogging: new WI.DebugSetting("debug-enable-diagnostic-logging", true),
|
|
debugAutoLogDiagnosticEvents: new WI.DebugSetting("debug-auto-log-diagnostic-events", false),
|
|
debugLayoutDirection: new WI.DebugSetting("debug-layout-direction-override", "system"),
|
|
debugShowMockWebExtensionTab: new WI.DebugSetting("debug-show-mock-web-extension-tab", false),
|
|
};
|
|
|
|
/* Base/YieldableTask.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.YieldableTask = class YieldableTask
|
|
{
|
|
constructor(delegate, items, options = {})
|
|
{
|
|
let {workInterval, idleInterval} = options;
|
|
console.assert(!workInterval || workInterval > 0, workInterval);
|
|
console.assert(!idleInterval || idleInterval > 0, idleInterval);
|
|
|
|
console.assert(delegate && typeof delegate.yieldableTaskWillProcessItem === "function", "Delegate provide an implementation of method 'yieldableTaskWillProcessItem'.");
|
|
|
|
console.assert(items instanceof Object && Symbol.iterator in items, "Argument `items` must subclass Object and be iterable.", items);
|
|
|
|
// Milliseconds to run before the task should yield.
|
|
this._workInterval = workInterval || 10;
|
|
// Milliseconds to idle before asynchronously resuming the task.
|
|
this._idleInterval = idleInterval || 0;
|
|
|
|
this._delegate = delegate;
|
|
|
|
this._items = items;
|
|
this._idleTimeoutIdentifier = undefined;
|
|
this._processing = false;
|
|
this._processing = false;
|
|
this._cancelled = false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get processing() { return this._processing; }
|
|
get cancelled() { return this._cancelled; }
|
|
|
|
get idleInterval() { return this._idleInterval; }
|
|
get workInterval() { return this._workInterval; }
|
|
|
|
start()
|
|
{
|
|
console.assert(!this._processing);
|
|
if (this._processing)
|
|
return;
|
|
|
|
console.assert(!this._cancelled);
|
|
if (this._cancelled)
|
|
return;
|
|
|
|
function* createIteratorForProcessingItems()
|
|
{
|
|
let startTime = Date.now();
|
|
let processedItems = [];
|
|
|
|
for (let item of this._items) {
|
|
if (this._cancelled)
|
|
break;
|
|
|
|
this._delegate.yieldableTaskWillProcessItem(this, item);
|
|
processedItems.push(item);
|
|
|
|
// Calling out to the delegate may cause the task to be cancelled.
|
|
if (this._cancelled)
|
|
break;
|
|
|
|
let elapsedTime = Date.now() - startTime;
|
|
if (elapsedTime > this._workInterval) {
|
|
let returnedItems = processedItems.slice();
|
|
processedItems = [];
|
|
this._willYield(returnedItems, elapsedTime);
|
|
|
|
yield;
|
|
|
|
startTime = Date.now();
|
|
}
|
|
}
|
|
|
|
// The task sends a fake yield notification to the delegate so that
|
|
// the delegate receives notification of all processed items before finishing.
|
|
if (processedItems.length)
|
|
this._willYield(processedItems, Date.now() - startTime);
|
|
}
|
|
|
|
this._processing = true;
|
|
this._pendingItemsIterator = createIteratorForProcessingItems.call(this);
|
|
this._processPendingItems();
|
|
}
|
|
|
|
cancel()
|
|
{
|
|
if (!this._processing)
|
|
return;
|
|
|
|
this._cancelled = true;
|
|
}
|
|
|
|
// Private
|
|
|
|
_processPendingItems()
|
|
{
|
|
console.assert(this._processing);
|
|
|
|
if (this._cancelled)
|
|
return;
|
|
|
|
if (!this._pendingItemsIterator.next().done) {
|
|
this._idleTimeoutIdentifier = setTimeout(() => { this._processPendingItems(); }, this._idleInterval);
|
|
return;
|
|
}
|
|
|
|
this._didFinish();
|
|
}
|
|
|
|
_willYield(processedItems, elapsedTime)
|
|
{
|
|
if (typeof this._delegate.yieldableTaskDidYield === "function")
|
|
this._delegate.yieldableTaskDidYield(this, processedItems, elapsedTime);
|
|
}
|
|
|
|
_didFinish()
|
|
{
|
|
this._processing = false;
|
|
this._pendingItemsIterator = null;
|
|
|
|
if (this._idleTimeoutIdentifier) {
|
|
clearTimeout(this._idleTimeoutIdentifier);
|
|
this._idleTimeoutIdentifier = undefined;
|
|
}
|
|
|
|
if (typeof this._delegate.yieldableTaskDidFinish === "function")
|
|
this._delegate.yieldableTaskDidFinish(this);
|
|
}
|
|
};
|
|
|
|
|
|
/* Base/IDLExtensions.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// The following should only be used for response local override file mapping.
|
|
|
|
Object.defineProperty(File.prototype, "getPath", {
|
|
value() {
|
|
return InspectorFrontendHost.getPath(this);
|
|
},
|
|
});
|
|
|
|
// The following should only be used for rendering (and interacting with) canvas 2D recordings.
|
|
|
|
Object.defineProperty(CanvasRenderingContext2D.prototype, "currentX", {
|
|
get() {
|
|
return InspectorFrontendHost.getCurrentX(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(CanvasRenderingContext2D.prototype, "currentY", {
|
|
get() {
|
|
return InspectorFrontendHost.getCurrentY(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(CanvasRenderingContext2D.prototype, "getPath", {
|
|
value() {
|
|
return InspectorFrontendHost.getPath(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(CanvasRenderingContext2D.prototype, "setPath", {
|
|
value(path) {
|
|
return InspectorFrontendHost.setPath(this, path);
|
|
},
|
|
});
|
|
|
|
if (window.OffscreenCanvasRenderingContext2D) {
|
|
Object.defineProperty(OffscreenCanvasRenderingContext2D.prototype, "currentX", {
|
|
get() {
|
|
return InspectorFrontendHost.getCurrentX(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(OffscreenCanvasRenderingContext2D.prototype, "currentY", {
|
|
get() {
|
|
return InspectorFrontendHost.getCurrentY(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(OffscreenCanvasRenderingContext2D.prototype, "getPath", {
|
|
value() {
|
|
return InspectorFrontendHost.getPath(this);
|
|
},
|
|
});
|
|
|
|
Object.defineProperty(OffscreenCanvasRenderingContext2D.prototype, "setPath", {
|
|
value(path) {
|
|
return InspectorFrontendHost.setPath(this, path);
|
|
},
|
|
});
|
|
}
|
|
|
|
/* Protocol/ProtocolTracer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProtocolTracer = class ProtocolTracer
|
|
{
|
|
// Public
|
|
|
|
logStarted()
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logFrontendException(connection, message, exception)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logFrontendRequest(connection, message)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logWillHandleResponse(connection, message)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logDidHandleResponse(connection, message, timings = null)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logWillHandleEvent(connection, message)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logDidHandleEvent(connection, message, timings = null)
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
|
|
logFinished()
|
|
{
|
|
// To be overridden by subclasses.
|
|
}
|
|
};
|
|
|
|
/* Protocol/LoggingProtocolTracer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LoggingProtocolTracer = class LoggingProtocolTracer extends WI.ProtocolTracer
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._dumpMessagesToConsole = false;
|
|
this._dumpTimingDataToConsole = false;
|
|
this._filterMultiplexingBackend = true;
|
|
this._logToConsole = window.InspectorTest ? InspectorFrontendHost.unbufferedLog.bind(InspectorFrontendHost) : console.log;
|
|
}
|
|
|
|
// Public
|
|
|
|
set dumpMessagesToConsole(value)
|
|
{
|
|
this._dumpMessagesToConsole = !!value;
|
|
}
|
|
|
|
get dumpMessagesToConsole()
|
|
{
|
|
return this._dumpMessagesToConsole;
|
|
}
|
|
|
|
set dumpTimingDataToConsole(value)
|
|
{
|
|
this._dumpTimingDataToConsole = !!value;
|
|
}
|
|
|
|
get dumpTimingDataToConsole()
|
|
{
|
|
return this._dumpTimingDataToConsole;
|
|
}
|
|
|
|
set filterMultiplexingBackend(value)
|
|
{
|
|
this._filterMultiplexingBackend = !!value;
|
|
}
|
|
|
|
get filterMultiplexingBackend()
|
|
{
|
|
return this._filterMultiplexingBackend;
|
|
}
|
|
|
|
logFrontendException(connection, message, exception)
|
|
{
|
|
this._processEntry({type: "exception", connection, message, exception});
|
|
}
|
|
|
|
logFrontendRequest(connection, message)
|
|
{
|
|
this._processEntry({type: "request", connection, message});
|
|
}
|
|
|
|
logWillHandleResponse(connection, message)
|
|
{
|
|
let entry = {type: "response", connection, message};
|
|
this._processEntry(entry);
|
|
}
|
|
|
|
logDidHandleResponse(connection, message, timings = null)
|
|
{
|
|
let entry = {type: "response", connection, message};
|
|
if (timings)
|
|
entry.timings = Object.shallowCopy(timings);
|
|
|
|
this._processEntry(entry);
|
|
}
|
|
|
|
logWillHandleEvent(connection, message)
|
|
{
|
|
let entry = {type: "event", connection, message};
|
|
this._processEntry(entry);
|
|
}
|
|
|
|
logDidHandleEvent(connection, message, timings = null)
|
|
{
|
|
let entry = {type: "event", connection, message};
|
|
if (timings)
|
|
entry.timings = Object.shallowCopy(timings);
|
|
|
|
this._processEntry(entry);
|
|
}
|
|
|
|
_processEntry(entry)
|
|
{
|
|
if (this._dumpTimingDataToConsole && entry.timings) {
|
|
if (entry.timings.rtt && entry.timings.dispatch)
|
|
this._logToConsole(`time-stats: Handling: ${entry.timings.dispatch || NaN}ms; RTT: ${entry.timings.rtt}ms`);
|
|
else if (entry.timings.dispatch)
|
|
this._logToConsole(`time-stats: Handling: ${entry.timings.dispatch || NaN}ms`);
|
|
} else if (this._dumpMessagesToConsole && !entry.timings) {
|
|
let connection = entry.connection;
|
|
let targetId = connection && connection.target ? connection.target.identifier : "unknown";
|
|
if (this._filterMultiplexingBackend && targetId === "multi")
|
|
return;
|
|
|
|
let prefix = `${entry.type} (${targetId})`;
|
|
if (!window.InspectorTest && InspectorFrontendHost.isBeingInspected() && !WI.settings.protocolLogAsText.value) {
|
|
if (entry.type === "request" || entry.type === "exception")
|
|
console.trace(prefix, entry.message);
|
|
else
|
|
this._logToConsole(prefix, entry.message);
|
|
} else
|
|
this._logToConsole(`${prefix}: ${JSON.stringify(entry.message)}`);
|
|
|
|
if (entry.exception) {
|
|
this._logToConsole(entry.exception);
|
|
if (entry.exception.stack)
|
|
this._logToConsole(entry.exception.stack);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Protocol/InspectorBackend.js */
|
|
|
|
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2013, 2015, 2016 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2014 University of Washington.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
InspectorBackendClass = class InspectorBackendClass
|
|
{
|
|
constructor()
|
|
{
|
|
this._registeredDomains = {};
|
|
this._activeDomains = {};
|
|
|
|
this._customTracer = null;
|
|
this._defaultTracer = new WI.LoggingProtocolTracer;
|
|
this._activeTracers = [this._defaultTracer];
|
|
|
|
// FIXME: <https://webkit.org/b/213632> Web Inspector: release unused backend domains/events/commands once the debuggable type is known
|
|
this._supportedDomainsForTargetType = new Multimap;
|
|
this._supportedCommandParameters = new Map;
|
|
this._supportedEventParameters = new Map;
|
|
|
|
WI.settings.protocolAutoLogMessages.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this);
|
|
WI.settings.protocolAutoLogTimeStats.addEventListener(WI.Setting.Event.Changed, this._startOrStopAutomaticTracing, this);
|
|
this._startOrStopAutomaticTracing();
|
|
|
|
this.currentDispatchState = {
|
|
event: null,
|
|
request: null,
|
|
response: null,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
// This should only be used for getting enum values (`InspectorBackend.Enum.Domain.Type.Item`).
|
|
// Domain/Command/Event feature checking should use one of the `has*` functions below.
|
|
get Enum()
|
|
{
|
|
// Enums should not be conditionally enabled by debuggable and/or target type.
|
|
return this._registeredDomains;
|
|
}
|
|
|
|
// It's still possible to set this flag on InspectorBackend to just
|
|
// dump protocol traffic as it happens. For more complex uses of
|
|
// protocol data, install a subclass of WI.ProtocolTracer.
|
|
set dumpInspectorProtocolMessages(value)
|
|
{
|
|
// Implicitly cause automatic logging to start if it's allowed.
|
|
WI.settings.protocolAutoLogMessages.value = value;
|
|
|
|
this._defaultTracer.dumpMessagesToConsole = value;
|
|
}
|
|
|
|
get dumpInspectorProtocolMessages()
|
|
{
|
|
return WI.settings.protocolAutoLogMessages.value;
|
|
}
|
|
|
|
set dumpInspectorTimeStats(value)
|
|
{
|
|
WI.settings.protocolAutoLogTimeStats.value = value;
|
|
|
|
if (!this.dumpInspectorProtocolMessages)
|
|
this.dumpInspectorProtocolMessages = true;
|
|
|
|
this._defaultTracer.dumpTimingDataToConsole = value;
|
|
}
|
|
|
|
get dumpInspectorTimeStats()
|
|
{
|
|
return WI.settings.protocolAutoLogTimeStats.value;
|
|
}
|
|
|
|
set filterMultiplexingBackendInspectorProtocolMessages(value)
|
|
{
|
|
WI.settings.protocolFilterMultiplexingBackendMessages.value = value;
|
|
|
|
this._defaultTracer.filterMultiplexingBackend = value;
|
|
}
|
|
|
|
get filterMultiplexingBackendInspectorProtocolMessages()
|
|
{
|
|
return WI.settings.protocolFilterMultiplexingBackendMessages.value;
|
|
}
|
|
|
|
set customTracer(tracer)
|
|
{
|
|
console.assert(!tracer || tracer instanceof WI.ProtocolTracer, tracer);
|
|
console.assert(!tracer || tracer !== this._defaultTracer, tracer);
|
|
|
|
// Bail early if no state change is to be made.
|
|
if (!tracer && !this._customTracer)
|
|
return;
|
|
|
|
if (tracer === this._customTracer)
|
|
return;
|
|
|
|
if (tracer === this._defaultTracer)
|
|
return;
|
|
|
|
if (this._customTracer)
|
|
this._customTracer.logFinished();
|
|
|
|
this._customTracer = tracer;
|
|
this._activeTracers = [this._defaultTracer];
|
|
|
|
if (this._customTracer) {
|
|
this._customTracer.logStarted();
|
|
this._activeTracers.push(this._customTracer);
|
|
}
|
|
}
|
|
|
|
get activeTracers()
|
|
{
|
|
return this._activeTracers;
|
|
}
|
|
|
|
registerDomain(domainName, targetTypes)
|
|
{
|
|
targetTypes = targetTypes || WI.TargetType.all;
|
|
for (let targetType of targetTypes)
|
|
this._supportedDomainsForTargetType.add(targetType, domainName);
|
|
|
|
this._registeredDomains[domainName] = new InspectorBackend.Domain(domainName);
|
|
}
|
|
|
|
registerVersion(domainName, version)
|
|
{
|
|
let domain = this._registeredDomains[domainName];
|
|
domain.VERSION = version;
|
|
}
|
|
|
|
registerEnum(qualifiedName, enumValues)
|
|
{
|
|
let [domainName, enumName] = qualifiedName.split(".");
|
|
let domain = this._registeredDomains[domainName];
|
|
domain._addEnum(enumName, enumValues);
|
|
}
|
|
|
|
registerCommand(qualifiedName, targetTypes, callSignature, replySignature)
|
|
{
|
|
let [domainName, commandName] = qualifiedName.split(".");
|
|
let domain = this._registeredDomains[domainName];
|
|
domain._addCommand(targetTypes, new InspectorBackend.Command(qualifiedName, commandName, callSignature, replySignature));
|
|
}
|
|
|
|
registerEvent(qualifiedName, targetTypes, signature)
|
|
{
|
|
let [domainName, eventName] = qualifiedName.split(".");
|
|
let domain = this._registeredDomains[domainName];
|
|
domain._addEvent(targetTypes, new InspectorBackend.Event(qualifiedName, eventName, signature));
|
|
}
|
|
|
|
registerDispatcher(domainName, dispatcher)
|
|
{
|
|
let domain = this._registeredDomains[domainName];
|
|
domain._addDispatcher(dispatcher);
|
|
}
|
|
|
|
activateDomain(domainName, debuggableTypes)
|
|
{
|
|
// FIXME: <https://webkit.org/b/201150> Web Inspector: remove "extra domains" concept now that domains can be added based on the debuggable type
|
|
|
|
// Ask `WI.sharedApp` (if it exists) as it may have a different debuggable type if extra
|
|
// domains were activated, which is the only other time this will be called.
|
|
let currentDebuggableType = WI.sharedApp?.debuggableType || InspectorFrontendHost.debuggableInfo.debuggableType;
|
|
|
|
if (debuggableTypes && !debuggableTypes.includes(currentDebuggableType))
|
|
return;
|
|
|
|
console.assert(domainName in this._registeredDomains);
|
|
console.assert(!(domainName in this._activeDomains));
|
|
|
|
let domain = this._registeredDomains[domainName];
|
|
this._activeDomains[domainName] = domain;
|
|
|
|
let supportedTargetTypes = WI.DebuggableType.supportedTargetTypes(currentDebuggableType);
|
|
|
|
for (let [targetType, command] of domain._supportedCommandsForTargetType) {
|
|
if (!supportedTargetTypes.has(targetType))
|
|
continue;
|
|
|
|
let parameters = this._supportedCommandParameters.get(command._qualifiedName);
|
|
if (!parameters) {
|
|
parameters = new Set;
|
|
this._supportedCommandParameters.set(command._qualifiedName, parameters);
|
|
}
|
|
parameters.addAll(command._callSignature.map((item) => item.name));
|
|
}
|
|
|
|
for (let [targetType, event] of domain._supportedEventsForTargetType) {
|
|
if (!supportedTargetTypes.has(targetType))
|
|
continue;
|
|
|
|
let parameters = this._supportedEventParameters.get(event._qualifiedName);
|
|
if (!parameters) {
|
|
parameters = new Set;
|
|
this._supportedEventParameters.set(event._qualifiedName, parameters);
|
|
}
|
|
parameters.addAll(event._parameterNames);
|
|
}
|
|
}
|
|
|
|
dispatch(message)
|
|
{
|
|
InspectorBackend.backendConnection.dispatch(message);
|
|
}
|
|
|
|
runAfterPendingDispatches(callback)
|
|
{
|
|
if (!WI.mainTarget) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// FIXME: Should this respect pending dispatches in all connections?
|
|
WI.mainTarget.connection.runAfterPendingDispatches(callback);
|
|
}
|
|
|
|
supportedDomainsForTargetType(type)
|
|
{
|
|
console.assert(WI.TargetType.all.includes(type), "Unknown target type", type);
|
|
|
|
return this._supportedDomainsForTargetType.get(type) || new Set;
|
|
}
|
|
|
|
hasDomain(domainName)
|
|
{
|
|
console.assert(!domainName.includes(".") && !domainName.endsWith("Agent"));
|
|
|
|
return domainName in this._activeDomains;
|
|
}
|
|
|
|
hasCommand(qualifiedName, parameterName)
|
|
{
|
|
console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));
|
|
|
|
let parameters = this._supportedCommandParameters.get(qualifiedName);
|
|
if (!parameters)
|
|
return false;
|
|
|
|
return parameterName === undefined || parameters.has(parameterName);
|
|
}
|
|
|
|
hasEvent(qualifiedName, parameterName)
|
|
{
|
|
console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));
|
|
|
|
let parameters = this._supportedEventParameters.get(qualifiedName);
|
|
if (!parameters)
|
|
return false;
|
|
|
|
return parameterName === undefined || parameters.has(parameterName);
|
|
}
|
|
|
|
getVersion(domainName)
|
|
{
|
|
console.assert(!domainName.includes(".") && !domainName.endsWith("Agent"));
|
|
|
|
let domain = this._activeDomains[domainName];
|
|
if (domain && "VERSION" in domain)
|
|
return domain.VERSION;
|
|
|
|
return -Infinity;
|
|
}
|
|
|
|
invokeCommand(qualifiedName, targetType, connection, commandArguments, callback)
|
|
{
|
|
let [domainName, commandName] = qualifiedName.split(".");
|
|
|
|
let domain = this._activeDomains[domainName];
|
|
return domain._invokeCommand(commandName, targetType, connection, commandArguments, callback);
|
|
}
|
|
|
|
// Private
|
|
|
|
_makeAgent(domainName, target)
|
|
{
|
|
let domain = this._activeDomains[domainName];
|
|
return domain._makeAgent(target);
|
|
}
|
|
|
|
_startOrStopAutomaticTracing()
|
|
{
|
|
this._defaultTracer.dumpMessagesToConsole = this.dumpInspectorProtocolMessages;
|
|
this._defaultTracer.dumpTimingDataToConsole = this.dumpTimingDataToConsole;
|
|
this._defaultTracer.filterMultiplexingBackend = this.filterMultiplexingBackendInspectorProtocolMessages;
|
|
}
|
|
};
|
|
|
|
InspectorBackend = new InspectorBackendClass;
|
|
|
|
InspectorBackend.Domain = class InspectorBackendDomain
|
|
{
|
|
constructor(domainName)
|
|
{
|
|
this._domainName = domainName;
|
|
|
|
// Enums are stored directly on the Domain instance using their unqualified
|
|
// type name as the property. Thus, callers can write: Domain.EnumType.
|
|
|
|
this._dispatcher = null;
|
|
|
|
// FIXME: <https://webkit.org/b/213632> Web Inspector: release unused backend domains/events/commands once the debuggable type is known
|
|
this._supportedCommandsForTargetType = new Multimap;
|
|
this._supportedEventsForTargetType = new Multimap;
|
|
}
|
|
|
|
// Private
|
|
|
|
_addEnum(enumName, enumValues)
|
|
{
|
|
console.assert(!(enumName in this));
|
|
this[enumName] = enumValues;
|
|
}
|
|
|
|
_addCommand(targetTypes, command)
|
|
{
|
|
targetTypes = targetTypes || WI.TargetType.all;
|
|
for (let type of targetTypes)
|
|
this._supportedCommandsForTargetType.add(type, command);
|
|
}
|
|
|
|
_addEvent(targetTypes, event)
|
|
{
|
|
targetTypes = targetTypes || WI.TargetType.all;
|
|
for (let type of targetTypes)
|
|
this._supportedEventsForTargetType.add(type, event);
|
|
}
|
|
|
|
_addDispatcher(dispatcher)
|
|
{
|
|
console.assert(!this._dispatcher);
|
|
this._dispatcher = dispatcher;
|
|
}
|
|
|
|
_makeAgent(target)
|
|
{
|
|
let commands = this._supportedCommandsForTargetType.get(target.type) || new Set;
|
|
let events = this._supportedEventsForTargetType.get(target.type) || new Set;
|
|
return new InspectorBackend.Agent(target, commands, events, this._dispatcher);
|
|
}
|
|
|
|
_invokeCommand(commandName, targetType, connection, commandArguments, callback)
|
|
{
|
|
let commands = this._supportedCommandsForTargetType.get(targetType);
|
|
for (let command of commands) {
|
|
if (command._commandName === commandName)
|
|
return command._makeCallable(connection).invoke(commandArguments, callback);
|
|
}
|
|
|
|
console.assert();
|
|
}
|
|
};
|
|
|
|
InspectorBackend.Agent = class InspectorBackendAgent
|
|
{
|
|
constructor(target, commands, events, dispatcher)
|
|
{
|
|
// Commands are stored directly on the Agent instance using their unqualified
|
|
// method name as the property. Thus, callers can write: DomainAgent.commandName().
|
|
for (let command of commands) {
|
|
this[command._commandName] = command._makeCallable(target.connection);
|
|
target._supportedCommandParameters.set(command._qualifiedName, command);
|
|
}
|
|
|
|
this._events = {};
|
|
for (let event of events) {
|
|
this._events[event._eventName] = event;
|
|
target._supportedEventParameters.set(event._qualifiedName, event);
|
|
}
|
|
|
|
this._dispatcher = dispatcher ? new dispatcher(target) : null;
|
|
}
|
|
};
|
|
|
|
InspectorBackend.Dispatcher = class InspectorBackendDispatcher
|
|
{
|
|
constructor(target)
|
|
{
|
|
console.assert(target instanceof WI.Target);
|
|
|
|
this._target = target;
|
|
}
|
|
};
|
|
|
|
InspectorBackend.Command = class InspectorBackendCommand
|
|
{
|
|
constructor(qualifiedName, commandName, callSignature, replySignature)
|
|
{
|
|
this._qualifiedName = qualifiedName;
|
|
this._commandName = commandName;
|
|
this._callSignature = callSignature || [];
|
|
this._replySignature = replySignature || [];
|
|
}
|
|
|
|
// Private
|
|
|
|
_hasParameter(parameterName)
|
|
{
|
|
return this._callSignature.some((item) => item.name === parameterName);
|
|
}
|
|
|
|
_makeCallable(connection)
|
|
{
|
|
let instance = new InspectorBackend.Callable(this, connection);
|
|
|
|
function callable() {
|
|
console.assert(this instanceof InspectorBackend.Agent);
|
|
return instance._invokeWithArguments.call(instance, Array.from(arguments));
|
|
}
|
|
callable._instance = instance;
|
|
Object.setPrototypeOf(callable, InspectorBackend.Callable.prototype);
|
|
return callable;
|
|
}
|
|
};
|
|
|
|
InspectorBackend.Event = class InspectorBackendEvent
|
|
{
|
|
constructor(qualifiedName, eventName, parameterNames)
|
|
{
|
|
this._qualifiedName = qualifiedName;
|
|
this._eventName = eventName;
|
|
this._parameterNames = parameterNames || [];
|
|
}
|
|
|
|
// Private
|
|
|
|
_hasParameter(parameterName)
|
|
{
|
|
return this._parameterNames.includes(parameterName);
|
|
}
|
|
};
|
|
|
|
// InspectorBackend.Callable can't use ES6 classes because of its trampoline nature.
|
|
// But we can use strict mode to get stricter handling of the code inside its functions.
|
|
InspectorBackend.Callable = function(command, connection)
|
|
{
|
|
"use strict";
|
|
|
|
this._command = command;
|
|
this._connection = connection;
|
|
|
|
this._instance = this;
|
|
};
|
|
|
|
// As part of the workaround to make commands callable, these functions use `this._instance`.
|
|
// `this` could refer to the callable trampoline, or the InspectorBackend.Callable instance.
|
|
InspectorBackend.Callable.prototype = {
|
|
__proto__: Function.prototype,
|
|
|
|
// Public
|
|
|
|
invoke(commandArguments, callback)
|
|
{
|
|
"use strict";
|
|
|
|
let command = this._instance._command;
|
|
let connection = this._instance._connection;
|
|
|
|
function deliverFailure(message) {
|
|
console.error(`Protocol Error: ${message}`);
|
|
if (callback)
|
|
setTimeout(callback.bind(null, message), 0);
|
|
else
|
|
return Promise.reject(new Error(message));
|
|
}
|
|
|
|
if (typeof commandArguments !== "object")
|
|
return deliverFailure(`invoke expects an object for command arguments but its type is '${typeof commandArguments}'.`);
|
|
|
|
let parameters = {};
|
|
for (let {name, type, optional} of command._callSignature) {
|
|
if (!(name in commandArguments) && !optional)
|
|
return deliverFailure(`Missing argument '${name}' for command '${command._qualifiedName}'.`);
|
|
|
|
let value = commandArguments[name];
|
|
if (optional && value === undefined)
|
|
continue;
|
|
|
|
if (typeof value !== type)
|
|
return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`);
|
|
|
|
parameters[name] = value;
|
|
}
|
|
|
|
if (typeof callback === "function")
|
|
connection._sendCommandToBackendWithCallback(command, parameters, callback);
|
|
else
|
|
return connection._sendCommandToBackendExpectingPromise(command, parameters);
|
|
},
|
|
|
|
// Private
|
|
|
|
_invokeWithArguments(commandArguments)
|
|
{
|
|
"use strict";
|
|
|
|
let command = this._instance._command;
|
|
let connection = this._instance._connection;
|
|
let callback = typeof commandArguments.lastValue === "function" ? commandArguments.pop() : null;
|
|
|
|
function deliverFailure(message) {
|
|
console.error(`Protocol Error: ${message}`);
|
|
if (callback)
|
|
setTimeout(callback.bind(null, message), 0);
|
|
else
|
|
return Promise.reject(new Error(message));
|
|
}
|
|
|
|
let parameters = {};
|
|
for (let {name, type, optional} of command._callSignature) {
|
|
if (!commandArguments.length && !optional)
|
|
return deliverFailure(`Invalid number of arguments for command '${command._qualifiedName}'.`);
|
|
|
|
let value = commandArguments.shift();
|
|
if (optional && value === undefined)
|
|
continue;
|
|
|
|
if (typeof value !== type)
|
|
return deliverFailure(`Invalid type of argument '${name}' for command '${command._qualifiedName}' call. It must be '${type}' but it is '${typeof value}'.`);
|
|
|
|
parameters[name] = value;
|
|
}
|
|
|
|
if (!callback && commandArguments.length === 1 && commandArguments[0] !== undefined)
|
|
return deliverFailure(`Protocol Error: Optional callback argument for command '${command._qualifiedName}' call must be a function but its type is '${typeof commandArguments[0]}'.`);
|
|
|
|
if (callback)
|
|
connection._sendCommandToBackendWithCallback(command, parameters, callback);
|
|
else
|
|
return connection._sendCommandToBackendExpectingPromise(command, parameters);
|
|
}
|
|
};
|
|
|
|
/* Protocol/Connection.js */
|
|
|
|
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2013-2016 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2014 University of Washington.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
InspectorBackend.globalSequenceId = 1;
|
|
|
|
InspectorBackend.Connection = class InspectorBackendConnection
|
|
{
|
|
constructor()
|
|
{
|
|
this._pendingResponses = new Map;
|
|
this._deferredCallbacks = [];
|
|
this._target = null;
|
|
this._provisionalMessages = [];
|
|
}
|
|
|
|
// Public
|
|
|
|
get target()
|
|
{
|
|
return this._target;
|
|
}
|
|
|
|
set target(target)
|
|
{
|
|
console.assert(!this._target);
|
|
this._target = target;
|
|
}
|
|
|
|
addProvisionalMessage(message)
|
|
{
|
|
console.assert(this.target && this.target.isProvisional);
|
|
this._provisionalMessages.push(message);
|
|
}
|
|
|
|
dispatchProvisionalMessages()
|
|
{
|
|
console.assert(this.target && !this.target.isProvisional);
|
|
for (let message of this._provisionalMessages)
|
|
this.dispatch(message);
|
|
this._provisionalMessages = [];
|
|
}
|
|
|
|
dispatch(message)
|
|
{
|
|
let messageObject = typeof message === "string" ? JSON.parse(message) : message;
|
|
|
|
if ("id" in messageObject)
|
|
this._dispatchResponse(messageObject);
|
|
else
|
|
this._dispatchEvent(messageObject);
|
|
}
|
|
|
|
runAfterPendingDispatches(callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
if (!this._pendingResponses.size)
|
|
callback.call(this);
|
|
else
|
|
this._deferredCallbacks.push(callback);
|
|
}
|
|
|
|
// Protected
|
|
|
|
sendMessageToBackend(message)
|
|
{
|
|
throw new Error("Should be implemented by a InspectorBackend.Connection subclass");
|
|
}
|
|
|
|
// Private
|
|
|
|
_dispatchResponse(messageObject)
|
|
{
|
|
console.assert(this._pendingResponses.size >= 0);
|
|
|
|
if (messageObject.error && messageObject.error.code !== -32_000)
|
|
console.error("Request with id = " + messageObject["id"] + " failed. " + JSON.stringify(messageObject.error));
|
|
|
|
let sequenceId = messageObject["id"];
|
|
console.assert(this._pendingResponses.has(sequenceId), sequenceId, this._target ? this._target.identifier : "(unknown)", this._pendingResponses);
|
|
|
|
let responseData = this._pendingResponses.take(sequenceId) || {};
|
|
let {request, command, callback, promise} = responseData;
|
|
|
|
let processingStartTimestamp = performance.now();
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logWillHandleResponse(this, messageObject);
|
|
|
|
InspectorBackend.currentDispatchState.request = request;
|
|
InspectorBackend.currentDispatchState.response = messageObject;
|
|
|
|
if (typeof callback === "function")
|
|
this._dispatchResponseToCallback(command, request, messageObject, callback);
|
|
else if (typeof promise === "object")
|
|
this._dispatchResponseToPromise(command, messageObject, promise);
|
|
else
|
|
console.error("Received a command response without a corresponding callback or promise.", messageObject, command);
|
|
|
|
InspectorBackend.currentDispatchState.request = null;
|
|
InspectorBackend.currentDispatchState.response = null;
|
|
|
|
let processingTime = (performance.now() - processingStartTimestamp).toFixed(3);
|
|
let roundTripTime = (processingStartTimestamp - responseData.sendRequestTimestamp).toFixed(3);
|
|
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logDidHandleResponse(this, messageObject, {rtt: roundTripTime, dispatch: processingTime});
|
|
|
|
if (this._deferredCallbacks.length && !this._pendingResponses.size)
|
|
this._flushPendingScripts();
|
|
}
|
|
|
|
_dispatchResponseToCallback(command, requestObject, responseObject, callback)
|
|
{
|
|
let callbackArguments = [];
|
|
callbackArguments.push(responseObject["error"] ? responseObject["error"].message : null);
|
|
|
|
if (responseObject["result"]) {
|
|
for (let parameterName of command._replySignature)
|
|
callbackArguments.push(responseObject["result"][parameterName]);
|
|
}
|
|
|
|
try {
|
|
callback.apply(null, callbackArguments);
|
|
} catch (e) {
|
|
WI.reportInternalError(e, {"cause": `An uncaught exception was thrown while dispatching response callback for command ${command._qualifiedName}.`});
|
|
}
|
|
}
|
|
|
|
_dispatchResponseToPromise(command, messageObject, promise)
|
|
{
|
|
let {resolve, reject} = promise;
|
|
if (messageObject["error"])
|
|
reject(new Error(messageObject["error"].message));
|
|
else
|
|
resolve(messageObject["result"]);
|
|
}
|
|
|
|
_dispatchEvent(messageObject)
|
|
{
|
|
let qualifiedName = messageObject.method;
|
|
let [domainName, eventName] = qualifiedName.split(".");
|
|
|
|
// COMPATIBILITY (iOS 12.2 and iOS 13): because the multiplexing target isn't created until
|
|
// `Target.exists` returns, any `Target.targetCreated` won't have a dispatcher for the
|
|
// message, so create a multiplexing target here to force this._target._agents.Target.
|
|
if (!this._target && this === InspectorBackend.backendConnection && WI.sharedApp.debuggableType === WI.DebuggableType.WebPage && qualifiedName === "Target.targetCreated")
|
|
WI.targetManager.createMultiplexingBackendTarget();
|
|
|
|
let agent = this._target._agents[domainName];
|
|
if (!agent) {
|
|
console.error(`Protocol Error: Attempted to dispatch method '${qualifiedName}' for non-existing domain '${domainName}'`, messageObject);
|
|
return;
|
|
}
|
|
|
|
let dispatcher = agent._dispatcher;
|
|
if (!dispatcher) {
|
|
console.error(`Protocol Error: Missing dispatcher for domain '${domainName}', for event '${qualifiedName}'`, messageObject);
|
|
return;
|
|
}
|
|
|
|
let event = agent._events[eventName];
|
|
if (!event) {
|
|
console.error(`Protocol Error: Attempted to dispatch an unspecified method '${qualifiedName}'`, messageObject);
|
|
return;
|
|
}
|
|
|
|
let handler = dispatcher[eventName];
|
|
if (!handler) {
|
|
console.error(`Protocol Error: Attempted to dispatch an unimplemented method '${qualifiedName}'`, messageObject);
|
|
return;
|
|
}
|
|
|
|
let processingStartTimestamp = performance.now();
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logWillHandleEvent(this, messageObject);
|
|
|
|
InspectorBackend.currentDispatchState.event = messageObject;
|
|
|
|
try {
|
|
let params = messageObject.params || {};
|
|
handler.apply(dispatcher, event._parameterNames.map((name) => params[name]));
|
|
} catch (e) {
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logFrontendException(this, messageObject, e);
|
|
|
|
WI.reportInternalError(e, {"cause": `An uncaught exception was thrown while handling event: ${qualifiedName}`});
|
|
}
|
|
|
|
InspectorBackend.currentDispatchState.event = null;
|
|
|
|
let processingDuration = (performance.now() - processingStartTimestamp).toFixed(3);
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logDidHandleEvent(this, messageObject, {dispatch: processingDuration});
|
|
}
|
|
|
|
_sendCommandToBackendWithCallback(command, parameters, callback)
|
|
{
|
|
let sequenceId = InspectorBackend.globalSequenceId++;
|
|
|
|
let messageObject = {
|
|
"id": sequenceId,
|
|
"method": command._qualifiedName,
|
|
};
|
|
|
|
if (!isEmptyObject(parameters))
|
|
messageObject["params"] = parameters;
|
|
|
|
let responseData = {command, request: messageObject, callback};
|
|
|
|
if (InspectorBackend.activeTracer)
|
|
responseData.sendRequestTimestamp = performance.now();
|
|
|
|
this._pendingResponses.set(sequenceId, responseData);
|
|
this._sendMessageToBackend(messageObject);
|
|
}
|
|
|
|
_sendCommandToBackendExpectingPromise(command, parameters)
|
|
{
|
|
let sequenceId = InspectorBackend.globalSequenceId++;
|
|
|
|
let messageObject = {
|
|
"id": sequenceId,
|
|
"method": command._qualifiedName,
|
|
};
|
|
|
|
if (!isEmptyObject(parameters))
|
|
messageObject["params"] = parameters;
|
|
|
|
let responseData = {command, request: messageObject};
|
|
|
|
if (InspectorBackend.activeTracer)
|
|
responseData.sendRequestTimestamp = performance.now();
|
|
|
|
let responsePromise = new Promise(function(resolve, reject) {
|
|
responseData.promise = {resolve, reject};
|
|
});
|
|
|
|
this._pendingResponses.set(sequenceId, responseData);
|
|
this._sendMessageToBackend(messageObject);
|
|
|
|
return responsePromise;
|
|
}
|
|
|
|
_sendMessageToBackend(messageObject)
|
|
{
|
|
for (let tracer of InspectorBackend.activeTracers)
|
|
tracer.logFrontendRequest(this, messageObject);
|
|
|
|
this.sendMessageToBackend(JSON.stringify(messageObject));
|
|
}
|
|
|
|
_flushPendingScripts()
|
|
{
|
|
console.assert(this._pendingResponses.size === 0);
|
|
|
|
let scriptsToRun = this._deferredCallbacks;
|
|
this._deferredCallbacks = [];
|
|
for (let script of scriptsToRun)
|
|
script.call(this);
|
|
}
|
|
};
|
|
|
|
InspectorBackend.BackendConnection = class InspectorBackendBackendConnection extends InspectorBackend.Connection
|
|
{
|
|
sendMessageToBackend(message)
|
|
{
|
|
InspectorFrontendHost.sendMessageToBackend(message);
|
|
}
|
|
};
|
|
|
|
InspectorBackend.WorkerConnection = class InspectorBackendWorkerConnection extends InspectorBackend.Connection
|
|
{
|
|
sendMessageToBackend(message)
|
|
{
|
|
let workerId = this.target.identifier;
|
|
this.target.parentTarget.WorkerAgent.sendMessageToWorker(workerId, message).catch((error) => {
|
|
// Ignore errors if a worker went away quickly.
|
|
if (this.target.isDestroyed)
|
|
return;
|
|
WI.reportInternalError(error);
|
|
});
|
|
}
|
|
};
|
|
|
|
InspectorBackend.TargetConnection = class InspectorBackendTargetConnection extends InspectorBackend.Connection
|
|
{
|
|
sendMessageToBackend(message)
|
|
{
|
|
let targetId = this.target.identifier;
|
|
this.target.parentTarget.TargetAgent.sendMessageToTarget(targetId, message).catch((error) => {
|
|
// Ignore errors if the target was destroyed after the command was dispatched.
|
|
if (this.target.isDestroyed)
|
|
return;
|
|
WI.reportInternalError(error);
|
|
});
|
|
}
|
|
};
|
|
|
|
InspectorBackend.backendConnection = new InspectorBackend.BackendConnection;
|
|
|
|
/* Protocol/InspectorFrontendAPI.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
InspectorFrontendAPI = {
|
|
_loaded: false,
|
|
_pendingCommands: [],
|
|
|
|
isTimelineProfilingEnabled: function()
|
|
{
|
|
return WI.timelineManager.isCapturing();
|
|
},
|
|
|
|
setTimelineProfilingEnabled: function(enabled)
|
|
{
|
|
if (!WI.targetsAvailable()) {
|
|
WI.whenTargetsAvailable().then(() => {
|
|
InspectorFrontendAPI.setTimelineProfilingEnabled(enabled);
|
|
});
|
|
return;
|
|
}
|
|
|
|
WI.showTimelineTab({
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI
|
|
});
|
|
|
|
if (WI.timelineManager.isCapturing() === enabled)
|
|
return;
|
|
|
|
if (enabled)
|
|
WI.timelineManager.startCapturing();
|
|
else
|
|
WI.timelineManager.stopCapturing();
|
|
},
|
|
|
|
setElementSelectionEnabled: function(enabled)
|
|
{
|
|
if (!WI.targetsAvailable()) {
|
|
WI.whenTargetsAvailable().then(() => {
|
|
InspectorFrontendAPI.setElementSelectionEnabled(enabled);
|
|
});
|
|
return;
|
|
}
|
|
|
|
WI.domManager.inspectModeEnabled = enabled;
|
|
},
|
|
|
|
setDockingUnavailable: function(unavailable)
|
|
{
|
|
WI.updateDockingAvailability(!unavailable);
|
|
},
|
|
|
|
setDockSide: function(side)
|
|
{
|
|
WI.updateDockedState(side);
|
|
},
|
|
|
|
setIsVisible: function(visible)
|
|
{
|
|
WI.updateVisibilityState(visible);
|
|
},
|
|
|
|
updateFindString: function(findString)
|
|
{
|
|
WI.updateFindString(findString);
|
|
},
|
|
|
|
setDiagnosticLoggingAvailable: function(available)
|
|
{
|
|
if (WI.diagnosticController)
|
|
WI.diagnosticController.diagnosticLoggingAvailable = available;
|
|
},
|
|
|
|
showConsole: function()
|
|
{
|
|
WI.showConsoleTab({
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI,
|
|
});
|
|
|
|
WI.quickConsole.prompt.focus();
|
|
|
|
// If the page is still loading, focus the quick console again after tabindex autofocus.
|
|
if (document.readyState !== "complete")
|
|
document.addEventListener("readystatechange", this);
|
|
if (document.visibilityState !== "visible")
|
|
document.addEventListener("visibilitychange", this);
|
|
},
|
|
|
|
handleEvent: function(event)
|
|
{
|
|
console.assert(event.type === "readystatechange" || event.type === "visibilitychange");
|
|
|
|
if (document.readyState === "complete" && document.visibilityState === "visible") {
|
|
WI.quickConsole.prompt.focus();
|
|
document.removeEventListener("readystatechange", this);
|
|
document.removeEventListener("visibilitychange", this);
|
|
}
|
|
},
|
|
|
|
showResources: function()
|
|
{
|
|
WI.showSourcesTab({
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI,
|
|
});
|
|
},
|
|
|
|
// COMPATIBILITY (iOS 13): merged into InspectorFrontendAPI.setTimelineProfilingEnabled.
|
|
showTimelines: function()
|
|
{
|
|
WI.showTimelineTab({
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI
|
|
});
|
|
},
|
|
|
|
showMainResourceForFrame: function(frameIdentifier)
|
|
{
|
|
WI.showSourceCodeForFrame(frameIdentifier, {
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI,
|
|
});
|
|
},
|
|
|
|
contextMenuItemSelected: function(id)
|
|
{
|
|
WI.ContextMenu.contextMenuItemSelected(id);
|
|
},
|
|
|
|
contextMenuCleared: function()
|
|
{
|
|
WI.ContextMenu.contextMenuCleared();
|
|
},
|
|
|
|
dispatchMessageAsync: function(messageObject)
|
|
{
|
|
WI.dispatchMessageFromBackend(messageObject);
|
|
},
|
|
|
|
dispatchMessage: function(messageObject)
|
|
{
|
|
InspectorBackend.dispatch(messageObject);
|
|
},
|
|
|
|
dispatch: function(signature)
|
|
{
|
|
if (!InspectorFrontendAPI._loaded) {
|
|
InspectorFrontendAPI._pendingCommands.push(signature);
|
|
return null;
|
|
}
|
|
|
|
var methodName = signature.shift();
|
|
console.assert(InspectorFrontendAPI[methodName], "Unexpected InspectorFrontendAPI method name: " + methodName);
|
|
if (!InspectorFrontendAPI[methodName])
|
|
return null;
|
|
|
|
return InspectorFrontendAPI[methodName].apply(InspectorFrontendAPI, signature);
|
|
},
|
|
|
|
loadCompleted: function()
|
|
{
|
|
InspectorFrontendAPI._loaded = true;
|
|
|
|
for (var i = 0; i < InspectorFrontendAPI._pendingCommands.length; ++i)
|
|
InspectorFrontendAPI.dispatch(InspectorFrontendAPI._pendingCommands[i]);
|
|
|
|
delete InspectorFrontendAPI._pendingCommands;
|
|
},
|
|
|
|
// Returns a WI.WebInspectorExtension.ErrorCode if an error occurred, otherwise nothing.
|
|
registerExtension(extensionID, extensionBundleIdentifier, displayName)
|
|
{
|
|
return WI.sharedApp.extensionController.registerExtension(extensionID, extensionBundleIdentifier, displayName);
|
|
},
|
|
|
|
// Returns a WI.WebInspectorExtension.ErrorCode if an error occurred, otherwise nothing.
|
|
unregisterExtension(extensionID)
|
|
{
|
|
return WI.sharedApp.extensionController.unregisterExtension(extensionID);
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred that prevented creating a tab.
|
|
// Returns a Promise that is resolved if the evaluation completes and rejected if there was an internal error.
|
|
// When the promise is fulfilled, it will be either:
|
|
// - resolved with an object containing a 'result' key and value that is the tab identifier for the new tab.
|
|
// - rejected with an object containing an 'error' key and value that is the exception that was thrown while evaluating script.
|
|
createTabForExtension(extensionID, tabName, tabIconURL, sourceURL)
|
|
{
|
|
return WI.sharedApp.extensionController.createTabForExtension(extensionID, tabName, tabIconURL, sourceURL);
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred that prevented evaluation.
|
|
// Returns a Promise that is resolved if the evaluation completes and rejected if there was an internal error.
|
|
// When the promise is fulfilled, it will be either:
|
|
// - resolved with an object containing a 'result' key and value that is the result of the script evaluation.
|
|
// - rejected with an object containing an 'error' key and value that is the exception that was thrown while evaluating script.
|
|
evaluateScriptForExtension(extensionID, scriptSource, {frameURL, contextSecurityOrigin, useContentScriptContext} = {})
|
|
{
|
|
return WI.sharedApp.extensionController.evaluateScriptForExtension(extensionID, scriptSource, {frameURL, contextSecurityOrigin, useContentScriptContext});
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred that prevented reloading.
|
|
reloadForExtension(extensionID, {ignoreCache, userAgent, injectedScript} = {})
|
|
{
|
|
return WI.sharedApp.extensionController.reloadForExtension(extensionID, {ignoreCache, userAgent, injectedScript});
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred before attempting to switch tabs.
|
|
// Returns a Promise that is resolved if the tab could be shown and rejected if the tab could not be shown.
|
|
// When the promise is fulfilled, it will be either:
|
|
// - resolved with no value.
|
|
// - rejected with an object containing an 'error' key and value that is the exception that was thrown while showing the tab.
|
|
showExtensionTab(extensionTabID)
|
|
{
|
|
return WI.sharedApp.extensionController.showExtensionTab(extensionTabID);
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred that prevented evaluation.
|
|
// Returns a Promise that is resolved if the evaluation completes and rejected if there was an internal error.
|
|
// When the promise is fulfilled, it will be either:
|
|
// - resolved with an object containing a 'result' key and value that is the result of the script evaluation.
|
|
// - rejected with an object containing an 'error' key and value that is the exception that was thrown while evaluating script.
|
|
evaluateScriptInExtensionTab(extensionTabID, scriptSource)
|
|
{
|
|
return WI.sharedApp.extensionController.evaluateScriptInExtensionTab(extensionTabID, scriptSource);
|
|
},
|
|
|
|
// Returns a string (WI.WebInspectorExtension.ErrorCode) if an error occurred.
|
|
navigateTabForExtension(extensionTabID, sourceURL)
|
|
{
|
|
return WI.sharedApp.extensionController.navigateTabForExtension(extensionTabID, sourceURL);
|
|
},
|
|
};
|
|
|
|
/* Protocol/LoadInspectorBackendCommands.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
(function() {
|
|
let backendCommandsURL = InspectorFrontendHost.backendCommandsURL || "Protocol/InspectorBackendCommands.js";
|
|
document.write("<script src=\"" + backendCommandsURL + "\"></script>");
|
|
})();
|
|
|
|
/* Protocol/MessageDispatcher.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2014 University of Washington
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI._messagesToDispatch = [];
|
|
|
|
WI.dispatchNextQueuedMessageFromBackend = function()
|
|
{
|
|
const startCount = WI._messagesToDispatch.length;
|
|
const startTimestamp = performance.now();
|
|
const timeLimitPerRunLoop = 10; // milliseconds
|
|
|
|
let i = 0;
|
|
for (; i < WI._messagesToDispatch.length; ++i) {
|
|
// Defer remaining messages if we have taken too long. In practice, single
|
|
// messages like Page.getResourceContent blow through the time budget.
|
|
if (performance.now() - startTimestamp > timeLimitPerRunLoop)
|
|
break;
|
|
|
|
InspectorBackend.dispatch(WI._messagesToDispatch[i]);
|
|
}
|
|
|
|
if (i === WI._messagesToDispatch.length) {
|
|
WI._messagesToDispatch = [];
|
|
WI._dispatchTimeout = null;
|
|
} else {
|
|
WI._messagesToDispatch = WI._messagesToDispatch.slice(i);
|
|
WI._dispatchTimeout = setTimeout(WI.dispatchNextQueuedMessageFromBackend, 0);
|
|
}
|
|
|
|
if (InspectorBackend.dumpInspectorTimeStats) {
|
|
let messageDuration = (performance.now() - startTimestamp).toFixed(3);
|
|
let dispatchedCount = startCount - WI._messagesToDispatch.length;
|
|
let remainingCount = WI._messagesToDispatch.length;
|
|
console.log(`time-stats: --- RunLoop duration: ${messageDuration}ms; dispatched: ${dispatchedCount}; remaining: ${remainingCount}`);
|
|
}
|
|
};
|
|
|
|
WI.dispatchMessageFromBackend = function(message)
|
|
{
|
|
// Enforce asynchronous interaction between the backend and the frontend by queueing messages.
|
|
// The messages are dequeued on a zero delay timeout.
|
|
|
|
this._messagesToDispatch.push(message);
|
|
|
|
// If something has gone wrong and the uncaught exception sheet is showing,
|
|
// then don't try to dispatch more messages. Dispatching causes spurious uncaught
|
|
// exceptions and cause the sheet to overflow with hundreds of logged exceptions.
|
|
if (window.__uncaughtExceptions && window.__uncaughtExceptions.length)
|
|
return;
|
|
|
|
if (this._dispatchTimeout)
|
|
return;
|
|
|
|
this._dispatchTimeout = setTimeout(this.dispatchNextQueuedMessageFromBackend, 0);
|
|
};
|
|
|
|
/* Protocol/Target.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Target = class Target extends WI.Object
|
|
{
|
|
constructor(parentTarget, identifier, name, type, connection, {isPaused, isProvisional} = {})
|
|
{
|
|
console.assert(parentTarget === null || parentTarget instanceof WI.Target);
|
|
console.assert(!isPaused || parentTarget.hasCommand("Target.setPauseOnStart"));
|
|
super();
|
|
|
|
this._parentTarget = parentTarget;
|
|
this._identifier = identifier;
|
|
this._name = name;
|
|
this._type = type;
|
|
this._connection = connection;
|
|
this._isPaused = !!isPaused;
|
|
this._isProvisional = !!isProvisional;
|
|
this._executionContext = null;
|
|
this._mainResource = null;
|
|
this._resourceCollection = new WI.ResourceCollection;
|
|
this._extraScriptCollection = new WI.ScriptCollection;
|
|
|
|
this._supportedCommandParameters = new Map;
|
|
this._supportedEventParameters = new Map;
|
|
|
|
// Restrict the agents to the list of supported agents for this target type.
|
|
// This makes it so `target.FooAgent` only exists if the "Foo" domain is
|
|
// supported by the target.
|
|
this._agents = {};
|
|
for (let domainName of InspectorBackend.supportedDomainsForTargetType(this._type))
|
|
this._agents[domainName] = InspectorBackend._makeAgent(domainName, this);
|
|
|
|
this._connection.target = this;
|
|
|
|
// Agents we always expect in every target.
|
|
console.assert(this.hasDomain("Target") || this.hasDomain("Runtime"));
|
|
console.assert(this.hasDomain("Target") || this.hasDomain("Debugger"));
|
|
console.assert(this.hasDomain("Target") || this.hasDomain("Console"));
|
|
}
|
|
|
|
// Target
|
|
|
|
initialize()
|
|
{
|
|
// Intentionally initialize InspectorAgent first if it is available.
|
|
// This may not be strictly necessary anymore, but is historical.
|
|
if (this.hasDomain("Inspector"))
|
|
this.InspectorAgent.enable();
|
|
|
|
// Initialize agents.
|
|
for (let manager of WI.managers) {
|
|
if (manager.initializeTarget)
|
|
manager.initializeTarget(this);
|
|
}
|
|
|
|
// Non-manager specific initialization.
|
|
WI.initializeTarget(this);
|
|
|
|
// Intentionally defer ConsoleAgent initialization to the end. We do this so that any
|
|
// previous initialization messages will have their responses arrive before a stream
|
|
// of console message added events come in after enabling Console.
|
|
this.ConsoleAgent.enable();
|
|
|
|
setTimeout(() => {
|
|
// Use this opportunity to run any one time frontend initialization
|
|
// now that we have a target with potentially new capabilities.
|
|
WI.performOneTimeFrontendInitializationsUsingTarget(this);
|
|
});
|
|
|
|
console.assert(Target._initializationPromises.length || Target._completedInitializationPromiseCount);
|
|
Promise.all(Target._initializationPromises).then(() => {
|
|
// Tell the backend we are initialized after all our initialization messages have been sent.
|
|
// This allows an automatically paused backend to resume execution, but we want to ensure
|
|
// our breakpoints were already sent to that backend.
|
|
if (this.hasDomain("Inspector"))
|
|
this.InspectorAgent.initialized();
|
|
});
|
|
|
|
this._resumeIfPaused();
|
|
}
|
|
|
|
_resumeIfPaused()
|
|
{
|
|
if (this._isPaused) {
|
|
console.assert(this._parentTarget.hasCommand("Target.resume"));
|
|
this._parentTarget.TargetAgent.resume(this._identifier, (error) => {
|
|
if (error) {
|
|
// Ignore errors if the target was destroyed after the command was sent.
|
|
if (!this.isDestroyed)
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._isPaused = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
activateExtraDomain(domainName)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
this._agents[domainName] = InspectorBackend._makeAgent(domainName, this);
|
|
}
|
|
|
|
// Agents
|
|
|
|
get AnimationAgent() { return this._agents.Animation; }
|
|
get ApplicationCacheAgent() { return this._agents.ApplicationCache; }
|
|
get AuditAgent() { return this._agents.Audit; }
|
|
get BrowserAgent() { return this._agents.Browser; }
|
|
get CPUProfilerAgent() { return this._agents.CPUProfiler; }
|
|
get CSSAgent() { return this._agents.CSS; }
|
|
get CanvasAgent() { return this._agents.Canvas; }
|
|
get ConsoleAgent() { return this._agents.Console; }
|
|
get DOMAgent() { return this._agents.DOM; }
|
|
get DOMDebuggerAgent() { return this._agents.DOMDebugger; }
|
|
get DOMStorageAgent() { return this._agents.DOMStorage; }
|
|
get DatabaseAgent() { return this._agents.Database; }
|
|
get DebuggerAgent() { return this._agents.Debugger; }
|
|
get HeapAgent() { return this._agents.Heap; }
|
|
get IndexedDBAgent() { return this._agents.IndexedDB; }
|
|
get InspectorAgent() { return this._agents.Inspector; }
|
|
get LayerTreeAgent() { return this._agents.LayerTree; }
|
|
get MemoryAgent() { return this._agents.Memory; }
|
|
get NetworkAgent() { return this._agents.Network; }
|
|
get PageAgent() { return this._agents.Page; }
|
|
get RecordingAgent() { return this._agents.Recording; }
|
|
get RuntimeAgent() { return this._agents.Runtime; }
|
|
get ScriptProfilerAgent() { return this._agents.ScriptProfiler; }
|
|
get ServiceWorkerAgent() { return this._agents.ServiceWorker; }
|
|
get TargetAgent() { return this._agents.Target; }
|
|
get TimelineAgent() { return this._agents.Timeline; }
|
|
get WorkerAgent() { return this._agents.Worker; }
|
|
|
|
// Static
|
|
|
|
static registerInitializationPromise(promise)
|
|
{
|
|
// This can be called for work that has to be done before `Inspector.initialized` is called.
|
|
// Should only be called before the first target is created.
|
|
console.assert(!Target._completedInitializationPromiseCount);
|
|
|
|
Target._initializationPromises.push(promise);
|
|
|
|
promise.then(() => {
|
|
++Target._completedInitializationPromiseCount;
|
|
Target._initializationPromises.remove(promise);
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get parentTarget() { return this._parentTarget; }
|
|
|
|
get rootTarget()
|
|
{
|
|
if (this._type === WI.TargetType.Page)
|
|
return this;
|
|
if (this._parentTarget)
|
|
return this._parentTarget.rootTarget;
|
|
return this;
|
|
}
|
|
|
|
get identifier() { return this._identifier; }
|
|
set identifier(identifier) { this._identifier = identifier; }
|
|
|
|
get name() { return this._name; }
|
|
set name(name) { this._name = name; }
|
|
|
|
get type() { return this._type; }
|
|
get connection() { return this._connection; }
|
|
get executionContext() { return this._executionContext; }
|
|
|
|
get resourceCollection() { return this._resourceCollection; }
|
|
get extraScriptCollection() { return this._extraScriptCollection; }
|
|
|
|
get isProvisional() { return this._isProvisional; }
|
|
get isPaused() { return this._isPaused; }
|
|
get isDestroyed() { return this._isDestroyed; }
|
|
|
|
get displayName() { return this._name; }
|
|
|
|
get mainResource()
|
|
{
|
|
return this._mainResource;
|
|
}
|
|
|
|
set mainResource(resource)
|
|
{
|
|
console.assert(!this._mainResource);
|
|
|
|
this._mainResource = resource;
|
|
|
|
this.dispatchEventToListeners(WI.Target.Event.MainResourceAdded, {resource});
|
|
}
|
|
|
|
addResource(resource)
|
|
{
|
|
this._resourceCollection.add(resource);
|
|
|
|
this.dispatchEventToListeners(WI.Target.Event.ResourceAdded, {resource});
|
|
}
|
|
|
|
adoptResource(resource)
|
|
{
|
|
resource._target = this;
|
|
|
|
this.addResource(resource);
|
|
}
|
|
|
|
addScript(script)
|
|
{
|
|
this._extraScriptCollection.add(script);
|
|
|
|
this.dispatchEventToListeners(WI.Target.Event.ScriptAdded, {script});
|
|
}
|
|
|
|
didCommitProvisionalTarget()
|
|
{
|
|
console.assert(this._isProvisional);
|
|
this._isProvisional = false;
|
|
}
|
|
|
|
destroy()
|
|
{
|
|
this._isDestroyed = true;
|
|
}
|
|
|
|
hasDomain(domainName)
|
|
{
|
|
console.assert(!domainName.includes(".") && !domainName.endsWith("Agent"));
|
|
return domainName in this._agents;
|
|
}
|
|
|
|
hasCommand(qualifiedName, parameterName)
|
|
{
|
|
console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));
|
|
|
|
let command = this._supportedCommandParameters.get(qualifiedName);
|
|
if (!command)
|
|
return false;
|
|
|
|
return parameterName === undefined || command._hasParameter(parameterName);
|
|
}
|
|
|
|
hasEvent(qualifiedName, parameterName)
|
|
{
|
|
console.assert(qualifiedName.includes(".") && !qualifiedName.includes("Agent."));
|
|
|
|
let event = this._supportedEventParameters.get(qualifiedName);
|
|
if (!event)
|
|
return false;
|
|
|
|
return parameterName === undefined || event._hasParameter(parameterName);
|
|
}
|
|
};
|
|
|
|
WI.Target.Event = {
|
|
MainResourceAdded: "target-main-resource-added",
|
|
ResourceAdded: "target-resource-added",
|
|
ScriptAdded: "target-script-added",
|
|
};
|
|
|
|
WI.Target._initializationPromises = [];
|
|
WI.Target._completedInitializationPromiseCount = 0;
|
|
|
|
/* Protocol/DirectBackendTarget.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// This class is used when connecting directly to a single target.
|
|
// The main connection is a direct connection to a target.
|
|
|
|
WI.DirectBackendTarget = class DirectBackendTarget extends WI.Target
|
|
{
|
|
constructor()
|
|
{
|
|
const parentTarget = null;
|
|
const targetId = "direct";
|
|
let {type, displayName} = DirectBackendTarget.connectionInfoForDebuggable();
|
|
super(parentTarget, targetId, displayName, type, InspectorBackend.backendConnection);
|
|
|
|
this._executionContext = new WI.ExecutionContext(this, WI.RuntimeManager.TopLevelContextExecutionIdentifier, WI.ExecutionContext.Type.Normal, displayName);
|
|
this._mainResource = null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static connectionInfoForDebuggable()
|
|
{
|
|
switch (WI.sharedApp.debuggableType) {
|
|
case WI.DebuggableType.ITML:
|
|
return {
|
|
type: WI.TargetType.ITML,
|
|
displayName: WI.UIString("ITML Context"),
|
|
};
|
|
case WI.DebuggableType.JavaScript:
|
|
return {
|
|
type: WI.TargetType.JavaScript,
|
|
displayName: WI.UIString("JavaScript Context"),
|
|
};
|
|
case WI.DebuggableType.Page:
|
|
return {
|
|
type: WI.TargetType.Page,
|
|
displayName: WI.UIString("Page"),
|
|
};
|
|
case WI.DebuggableType.ServiceWorker:
|
|
return {
|
|
type: WI.TargetType.ServiceWorker,
|
|
displayName: WI.UIString("ServiceWorker"),
|
|
};
|
|
case WI.DebuggableType.WebPage:
|
|
return {
|
|
type: WI.TargetType.WebPage,
|
|
displayName: WI.UIString("Page"),
|
|
};
|
|
default:
|
|
console.error("Unexpected debuggable type: ", WI.sharedApp.debuggableType);
|
|
return {
|
|
type: WI.TargetType.JavaScript,
|
|
displayName: WI.UIString("JavaScript Context"),
|
|
};
|
|
}
|
|
}
|
|
|
|
// Protected (Target)
|
|
|
|
get mainResource()
|
|
{
|
|
if (this._mainResource)
|
|
return this._mainResource;
|
|
|
|
let mainFrame = WI.networkManager.mainFrame;
|
|
return mainFrame ? mainFrame.mainResource : null;
|
|
}
|
|
|
|
set mainResource(resource)
|
|
{
|
|
this._mainResource = resource;
|
|
}
|
|
};
|
|
|
|
/* Protocol/MultiplexingBackendTarget.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// This class is used when connecting to a target which multiplexes to other targets.
|
|
|
|
WI.MultiplexingBackendTarget = class MultiplexingBackendTarget extends WI.Target
|
|
{
|
|
constructor()
|
|
{
|
|
const parentTarget = null;
|
|
const targetId = "multi";
|
|
super(parentTarget, targetId, WI.UIString("Web Page"), WI.TargetType.WebPage, InspectorBackend.backendConnection);
|
|
|
|
console.assert(Array.shallowEqual(Object.keys(this._agents), ["Browser", "Target"]));
|
|
}
|
|
|
|
// Target
|
|
|
|
initialize()
|
|
{
|
|
// Only initialize with the managers that are known to support a multiplexing target.
|
|
WI.browserManager.initializeTarget(this);
|
|
WI.targetManager.initializeTarget(this);
|
|
}
|
|
|
|
// Protected (Target)
|
|
|
|
get name()
|
|
{
|
|
console.error("Called name on a MultiplexingBackendTarget");
|
|
return WI.UIString("Page");
|
|
}
|
|
|
|
get executionContext()
|
|
{
|
|
console.error("Called executionContext on a MultiplexingBackendTarget");
|
|
return null;
|
|
}
|
|
|
|
get mainResource()
|
|
{
|
|
console.error("Called mainResource on a MultiplexingBackendTarget");
|
|
return null;
|
|
}
|
|
};
|
|
|
|
/* Protocol/PageTarget.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.PageTarget = class PageTarget extends WI.Target
|
|
{
|
|
constructor(parentTarget, targetId, name, connection, options = {})
|
|
{
|
|
super(parentTarget, targetId, name, WI.TargetType.Page, connection, options);
|
|
|
|
this._executionContext = new WI.ExecutionContext(this, WI.RuntimeManager.TopLevelContextExecutionIdentifier, WI.ExecutionContext.Type.Normal, this.displayName);
|
|
}
|
|
};
|
|
|
|
/* Protocol/WorkerTarget.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WorkerTarget = class WorkerTarget extends WI.Target
|
|
{
|
|
constructor(parentTarget, workerId, url, displayName, connection, options = {})
|
|
{
|
|
super(parentTarget, workerId, url, WI.TargetType.Worker, connection, options);
|
|
|
|
this._displayName = displayName;
|
|
|
|
this._executionContext = new WI.ExecutionContext(this, WI.RuntimeManager.TopLevelContextExecutionIdentifier, WI.ExecutionContext.Type.Normal, this.displayName);
|
|
}
|
|
|
|
// Protected (Target)
|
|
|
|
get customName()
|
|
{
|
|
return this._displayName;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
return this._displayName || this.displayURL;
|
|
}
|
|
|
|
get displayURL()
|
|
{
|
|
return WI.displayNameForURL(this._name);
|
|
}
|
|
};
|
|
|
|
/* Protocol/InspectorObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.InspectorObserver = class InspectorObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Inspector" domain.
|
|
|
|
evaluateForTestInFrontend(script)
|
|
{
|
|
if (!InspectorFrontendHost.isUnderTest())
|
|
return;
|
|
|
|
InspectorBackend.runAfterPendingDispatches(function() {
|
|
window.eval(script);
|
|
});
|
|
}
|
|
|
|
inspect(payload, hints)
|
|
{
|
|
let remoteObject = WI.RemoteObject.fromPayload(payload, WI.mainTarget);
|
|
if (remoteObject.subtype === "node") {
|
|
WI.domManager.inspectNodeObject(remoteObject);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "function") {
|
|
remoteObject.findFunctionSourceCodeLocation().then((sourceCodeLocation) => {
|
|
if (sourceCodeLocation instanceof WI.SourceCodeLocation) {
|
|
WI.showSourceCodeLocation(sourceCodeLocation, {
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
});
|
|
}
|
|
});
|
|
remoteObject.release();
|
|
return;
|
|
}
|
|
|
|
if (hints.databaseId)
|
|
WI.databaseManager.inspectDatabase(hints.databaseId);
|
|
else if (hints.domStorageId)
|
|
WI.domStorageManager.inspectDOMStorage(hints.domStorageId);
|
|
|
|
remoteObject.release();
|
|
}
|
|
|
|
activateExtraDomains(domains)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
WI.sharedApp.activateExtraDomains(domains);
|
|
}
|
|
};
|
|
|
|
/* Protocol/AnimationObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AnimationObserver = class AnimationObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Animation" domain.
|
|
|
|
animationCreated(animation)
|
|
{
|
|
WI.animationManager.animationCreated(animation);
|
|
}
|
|
|
|
nameChanged(animationId, name)
|
|
{
|
|
WI.animationManager.nameChanged(animationId, name);
|
|
}
|
|
|
|
effectChanged(animationId, effect)
|
|
{
|
|
WI.animationManager.effectChanged(animationId, effect);
|
|
}
|
|
|
|
targetChanged(animationId)
|
|
{
|
|
WI.animationManager.targetChanged(animationId);
|
|
}
|
|
|
|
animationDestroyed(animationId)
|
|
{
|
|
WI.animationManager.animationDestroyed(animationId);
|
|
}
|
|
|
|
trackingStart(timestamp)
|
|
{
|
|
WI.timelineManager.animationTrackingStarted(timestamp);
|
|
}
|
|
|
|
trackingUpdate(timestamp, event)
|
|
{
|
|
WI.timelineManager.animationTrackingUpdated(timestamp, event);
|
|
}
|
|
|
|
trackingComplete(timestamp)
|
|
{
|
|
WI.timelineManager.animationTrackingCompleted(timestamp);
|
|
}
|
|
};
|
|
|
|
/* Protocol/ApplicationCacheObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ApplicationCacheObserver = class ApplicationCacheObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "ApplicationCache" domain.
|
|
|
|
applicationCacheStatusUpdated(frameId, manifestURL, status)
|
|
{
|
|
WI.applicationCacheManager.applicationCacheStatusUpdated(frameId, manifestURL, status);
|
|
}
|
|
|
|
networkStateUpdated(isNowOnline)
|
|
{
|
|
WI.applicationCacheManager.networkStateUpdated(isNowOnline);
|
|
}
|
|
};
|
|
|
|
/* Protocol/BrowserObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BrowserObserver = class BrowserObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Browser" domain.
|
|
|
|
extensionsEnabled(extensions)
|
|
{
|
|
WI.browserManager.extensionsEnabled(extensions);
|
|
}
|
|
|
|
extensionsDisabled(extensionIds)
|
|
{
|
|
WI.browserManager.extensionsDisabled(extensionIds);
|
|
}
|
|
};
|
|
|
|
/* Protocol/CPUProfilerObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CPUProfilerObserver = class CPUProfilerObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "CPUProfiler" domain.
|
|
|
|
trackingStart(timestamp)
|
|
{
|
|
WI.timelineManager.cpuProfilerTrackingStarted(timestamp);
|
|
}
|
|
|
|
trackingUpdate(event)
|
|
{
|
|
WI.timelineManager.cpuProfilerTrackingUpdated(event);
|
|
}
|
|
|
|
trackingComplete(timestamp)
|
|
{
|
|
WI.timelineManager.cpuProfilerTrackingCompleted(timestamp);
|
|
}
|
|
};
|
|
|
|
/* Protocol/CSSObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSObserver = class CSSObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "CSS" domain.
|
|
|
|
mediaQueryResultChanged()
|
|
{
|
|
WI.cssManager.mediaQueryResultChanged();
|
|
}
|
|
|
|
styleSheetChanged(styleSheetId)
|
|
{
|
|
WI.cssManager.styleSheetChanged(styleSheetId);
|
|
}
|
|
|
|
styleSheetAdded(styleSheetInfo)
|
|
{
|
|
WI.cssManager.styleSheetAdded(styleSheetInfo);
|
|
}
|
|
|
|
styleSheetRemoved(id)
|
|
{
|
|
WI.cssManager.styleSheetRemoved(id);
|
|
}
|
|
|
|
nodeLayoutFlagsChanged(nodeId, layoutFlags)
|
|
{
|
|
WI.domManager.nodeLayoutFlagsChanged(nodeId, layoutFlags);
|
|
}
|
|
|
|
nodeLayoutContextTypeChanged(nodeId, layoutContextType)
|
|
{
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): CSS.nodeLayoutContextTypeChanged was renamed/expanded to CSS.nodeLayoutFlagsChanged.
|
|
WI.domManager.nodeLayoutFlagsChanged(nodeId, [WI.DOMNode.LayoutFlag.Rendered, layoutContextType]);
|
|
}
|
|
};
|
|
|
|
/* Protocol/CanvasObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CanvasObserver = class CanvasObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Canvas" domain.
|
|
|
|
canvasAdded(canvas)
|
|
{
|
|
WI.canvasManager.canvasAdded(this._target, canvas);
|
|
}
|
|
|
|
canvasRemoved(canvasId)
|
|
{
|
|
WI.canvasManager.canvasRemoved(this._target, canvasId);
|
|
}
|
|
|
|
canvasSizeChanged(canvasId, width, height)
|
|
{
|
|
WI.canvasManager.canvasSizeChanged(this._target, canvasId, width, height);
|
|
}
|
|
|
|
canvasMemoryChanged(canvasId, memoryCost)
|
|
{
|
|
WI.canvasManager.canvasMemoryChanged(this._target, canvasId, memoryCost);
|
|
}
|
|
|
|
clientNodesChanged(canvasId)
|
|
{
|
|
WI.canvasManager.clientNodesChanged(this._target, canvasId);
|
|
}
|
|
|
|
recordingStarted(canvasId, initiator)
|
|
{
|
|
WI.canvasManager.recordingStarted(this._target, canvasId, initiator);
|
|
}
|
|
|
|
recordingProgress(canvasId, frames, bufferUsed)
|
|
{
|
|
WI.canvasManager.recordingProgress(this._target, canvasId, frames, bufferUsed);
|
|
}
|
|
|
|
recordingFinished(canvasId, recording)
|
|
{
|
|
WI.canvasManager.recordingFinished(this._target, canvasId, recording);
|
|
}
|
|
|
|
extensionEnabled(canvasId, extension)
|
|
{
|
|
WI.canvasManager.extensionEnabled(this._target, canvasId, extension);
|
|
}
|
|
|
|
programCreated(shaderProgram)
|
|
{
|
|
// COMPATIBILITY (iOS 13.0): `shaderProgram` replaced `canvasId` and `programId`.
|
|
if (arguments.length === 2) {
|
|
shaderProgram = {
|
|
canvasId: arguments[0],
|
|
programId: arguments[1],
|
|
};
|
|
}
|
|
WI.canvasManager.programCreated(this._target, shaderProgram);
|
|
}
|
|
|
|
programDeleted(programId)
|
|
{
|
|
WI.canvasManager.programDeleted(this._target, programId);
|
|
}
|
|
|
|
// COMPATIBILITY (iOS 13): Canvas.events.cssCanvasClientNodesChanged was renamed to Canvas.events.clientNodesChanged.
|
|
cssCanvasClientNodesChanged(canvasId)
|
|
{
|
|
WI.canvasManager.clientNodesChanged(this._target, canvasId);
|
|
}
|
|
};
|
|
|
|
/* Protocol/ConsoleObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ConsoleObserver = class ConsoleObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Console" domain.
|
|
|
|
messageAdded(message)
|
|
{
|
|
if (message.source === "console-api" && message.type === "clear")
|
|
return;
|
|
|
|
if (message.type === "assert" && !message.text)
|
|
message.text = WI.UIString("Assertion");
|
|
|
|
WI.consoleManager.messageWasAdded(this._target, message.source, message.level, message.text, message.type, message.url, message.line, message.column || 0, message.repeatCount, message.parameters, message.stackTrace, message.networkRequestId, message.timestamp);
|
|
}
|
|
|
|
messageRepeatCountUpdated(count, timestamp)
|
|
{
|
|
WI.consoleManager.messageRepeatCountUpdated(count, timestamp);
|
|
}
|
|
|
|
messagesCleared(reason)
|
|
{
|
|
WI.consoleManager.messagesCleared(reason);
|
|
}
|
|
|
|
heapSnapshot(timestamp, snapshotStringData, title)
|
|
{
|
|
let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
|
|
workerProxy.createSnapshot(snapshotStringData, title || null, ({objectId, snapshot: serializedSnapshot}) => {
|
|
let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
|
|
snapshot.snapshotStringData = snapshotStringData;
|
|
WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Protocol/DOMObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMObserver = class DOMObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "DOM" domain.
|
|
|
|
documentUpdated()
|
|
{
|
|
WI.domManager._documentUpdated();
|
|
}
|
|
|
|
inspect(nodeId)
|
|
{
|
|
WI.domManager.inspectElement(nodeId);
|
|
}
|
|
|
|
setChildNodes(parentId, nodes)
|
|
{
|
|
WI.domManager._setChildNodes(parentId, nodes);
|
|
}
|
|
|
|
attributeModified(nodeId, name, value)
|
|
{
|
|
WI.domManager._attributeModified(nodeId, name, value);
|
|
}
|
|
|
|
attributeRemoved(nodeId, name)
|
|
{
|
|
WI.domManager._attributeRemoved(nodeId, name);
|
|
}
|
|
|
|
inlineStyleInvalidated(nodeIds)
|
|
{
|
|
WI.domManager._inlineStyleInvalidated(nodeIds);
|
|
}
|
|
|
|
characterDataModified(nodeId, characterData)
|
|
{
|
|
WI.domManager._characterDataModified(nodeId, characterData);
|
|
}
|
|
|
|
childNodeCountUpdated(nodeId, childNodeCount)
|
|
{
|
|
WI.domManager._childNodeCountUpdated(nodeId, childNodeCount);
|
|
}
|
|
|
|
childNodeInserted(parentNodeId, previousNodeId, node)
|
|
{
|
|
WI.domManager._childNodeInserted(parentNodeId, previousNodeId, node);
|
|
}
|
|
|
|
childNodeRemoved(parentNodeId, nodeId)
|
|
{
|
|
WI.domManager._childNodeRemoved(parentNodeId, nodeId);
|
|
}
|
|
|
|
willDestroyDOMNode(nodeId)
|
|
{
|
|
WI.domManager.willDestroyDOMNode(nodeId);
|
|
}
|
|
|
|
shadowRootPushed(hostId, root)
|
|
{
|
|
WI.domManager._childNodeInserted(hostId, 0, root);
|
|
}
|
|
|
|
shadowRootPopped(hostId, rootId)
|
|
{
|
|
WI.domManager._childNodeRemoved(hostId, rootId);
|
|
}
|
|
|
|
customElementStateChanged(nodeId, customElementState)
|
|
{
|
|
WI.domManager._customElementStateChanged(nodeId, customElementState);
|
|
}
|
|
|
|
pseudoElementAdded(parentNodeId, pseudoElement)
|
|
{
|
|
WI.domManager._pseudoElementAdded(parentNodeId, pseudoElement);
|
|
}
|
|
|
|
pseudoElementRemoved(parentNodeId, pseudoElementId)
|
|
{
|
|
WI.domManager._pseudoElementRemoved(parentNodeId, pseudoElementId);
|
|
}
|
|
|
|
didAddEventListener(nodeId)
|
|
{
|
|
WI.domManager.didAddEventListener(nodeId);
|
|
}
|
|
|
|
willRemoveEventListener(nodeId)
|
|
{
|
|
WI.domManager.willRemoveEventListener(nodeId);
|
|
}
|
|
|
|
didFireEvent(nodeId, eventName, timestamp, data)
|
|
{
|
|
WI.domManager.didFireEvent(nodeId, eventName, timestamp, data);
|
|
}
|
|
|
|
videoLowPowerChanged(nodeId, timestamp, isLowPower)
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): DOM.videoLowPowerChanged was renamed to DOM.powerEfficientPlaybackStateChanged.
|
|
WI.domManager.powerEfficientPlaybackStateChanged(nodeId, timestamp, isLowPower);
|
|
}
|
|
|
|
powerEfficientPlaybackStateChanged(nodeId, timestamp, isPowerEfficient)
|
|
{
|
|
WI.domManager.powerEfficientPlaybackStateChanged(nodeId, timestamp, isPowerEfficient);
|
|
}
|
|
};
|
|
|
|
/* Protocol/DOMStorageObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMStorageObserver = class DOMStorageObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "DOMStorage" domain.
|
|
|
|
domStorageItemsCleared(storageId)
|
|
{
|
|
WI.domStorageManager.itemsCleared(storageId);
|
|
}
|
|
|
|
domStorageItemRemoved(storageId, key)
|
|
{
|
|
WI.domStorageManager.itemRemoved(storageId, key);
|
|
}
|
|
|
|
domStorageItemAdded(storageId, key, value)
|
|
{
|
|
WI.domStorageManager.itemAdded(storageId, key, value);
|
|
}
|
|
|
|
domStorageItemUpdated(storageId, key, oldValue, newValue)
|
|
{
|
|
WI.domStorageManager.itemUpdated(storageId, key, oldValue, newValue);
|
|
}
|
|
};
|
|
|
|
/* Protocol/DatabaseObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DatabaseObserver = class DatabaseObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Database" domain.
|
|
|
|
addDatabase(database)
|
|
{
|
|
WI.databaseManager.databaseWasAdded(database.id, database.domain, database.name, database.version);
|
|
}
|
|
};
|
|
|
|
/* Protocol/DebuggerObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DebuggerObserver = class DebuggerObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
constructor(target)
|
|
{
|
|
super(target);
|
|
|
|
this._legacyScriptParsed = this._target.hasEvent("Debugger.scriptParsed", "hasSourceURL");
|
|
}
|
|
|
|
// Events defined by the "Debugger" domain.
|
|
|
|
globalObjectCleared()
|
|
{
|
|
WI.debuggerManager.globalObjectCleared(this._target);
|
|
}
|
|
|
|
scriptParsed(scriptId, url, startLine, startColumn, endLine, endColumn, isContentScript, sourceURL, sourceMapURL, isModule)
|
|
{
|
|
WI.debuggerManager.scriptDidParse(this._target, scriptId, url, startLine, startColumn, endLine, endColumn, isModule, isContentScript, sourceURL, sourceMapURL);
|
|
}
|
|
|
|
scriptFailedToParse(url, scriptSource, startLine, errorLine, errorMessage)
|
|
{
|
|
// NOTE: A Console.messageAdded event will handle the error message.
|
|
WI.debuggerManager.scriptDidFail(this._target, url, scriptSource);
|
|
}
|
|
|
|
breakpointResolved(breakpointId, location)
|
|
{
|
|
WI.debuggerManager.breakpointResolved(this._target, breakpointId, location);
|
|
}
|
|
|
|
paused(callFrames, reason, data, asyncStackTrace)
|
|
{
|
|
WI.debuggerManager.debuggerDidPause(this._target, callFrames, reason, data, asyncStackTrace);
|
|
}
|
|
|
|
resumed()
|
|
{
|
|
WI.debuggerManager.debuggerDidResume(this._target);
|
|
}
|
|
|
|
playBreakpointActionSound(breakpointActionIdentifier)
|
|
{
|
|
WI.debuggerManager.playBreakpointActionSound(breakpointActionIdentifier);
|
|
}
|
|
|
|
didSampleProbe(sample)
|
|
{
|
|
WI.debuggerManager.didSampleProbe(this._target, sample);
|
|
}
|
|
};
|
|
|
|
/* Protocol/HeapObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapObserver = class HeapObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Heap" domain.
|
|
|
|
garbageCollected(collection)
|
|
{
|
|
WI.heapManager.garbageCollected(this._target, collection);
|
|
}
|
|
|
|
trackingStart(timestamp, snapshotStringData)
|
|
{
|
|
let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
|
|
workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
|
|
let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
|
|
snapshot.snapshotStringData = snapshotStringData;
|
|
WI.timelineManager.heapTrackingStarted(timestamp, snapshot);
|
|
});
|
|
}
|
|
|
|
trackingComplete(timestamp, snapshotStringData)
|
|
{
|
|
let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
|
|
workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
|
|
let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
|
|
snapshot.snapshotStringData = snapshotStringData;
|
|
WI.timelineManager.heapTrackingCompleted(timestamp, snapshot);
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Protocol/InspectorObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.InspectorObserver = class InspectorObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Inspector" domain.
|
|
|
|
evaluateForTestInFrontend(script)
|
|
{
|
|
if (!InspectorFrontendHost.isUnderTest())
|
|
return;
|
|
|
|
InspectorBackend.runAfterPendingDispatches(function() {
|
|
window.eval(script);
|
|
});
|
|
}
|
|
|
|
inspect(payload, hints)
|
|
{
|
|
let remoteObject = WI.RemoteObject.fromPayload(payload, WI.mainTarget);
|
|
if (remoteObject.subtype === "node") {
|
|
WI.domManager.inspectNodeObject(remoteObject);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "function") {
|
|
remoteObject.findFunctionSourceCodeLocation().then((sourceCodeLocation) => {
|
|
if (sourceCodeLocation instanceof WI.SourceCodeLocation) {
|
|
WI.showSourceCodeLocation(sourceCodeLocation, {
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
});
|
|
}
|
|
});
|
|
remoteObject.release();
|
|
return;
|
|
}
|
|
|
|
if (hints.databaseId)
|
|
WI.databaseManager.inspectDatabase(hints.databaseId);
|
|
else if (hints.domStorageId)
|
|
WI.domStorageManager.inspectDOMStorage(hints.domStorageId);
|
|
|
|
remoteObject.release();
|
|
}
|
|
|
|
activateExtraDomains(domains)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
WI.sharedApp.activateExtraDomains(domains);
|
|
}
|
|
};
|
|
|
|
/* Protocol/LayerTreeObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OdF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LayerTreeObserver = class LayerTreeObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "LayerTree" domain.
|
|
|
|
layerTreeDidChange()
|
|
{
|
|
WI.layerTreeManager.layerTreeDidChange();
|
|
}
|
|
};
|
|
|
|
/* Protocol/MemoryObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryObserver = class MemoryObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Memory" domain.
|
|
|
|
memoryPressure(timestamp, severity)
|
|
{
|
|
WI.memoryManager.memoryPressure(timestamp, severity);
|
|
}
|
|
|
|
trackingStart(timestamp)
|
|
{
|
|
WI.timelineManager.memoryTrackingStarted(timestamp);
|
|
}
|
|
|
|
trackingUpdate(event)
|
|
{
|
|
WI.timelineManager.memoryTrackingUpdated(event);
|
|
}
|
|
|
|
trackingComplete(timestamp)
|
|
{
|
|
WI.timelineManager.memoryTrackingCompleted(timestamp);
|
|
}
|
|
};
|
|
|
|
/* Protocol/NetworkObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.NetworkObserver = class NetworkObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Network" domain.
|
|
|
|
requestWillBeSent(requestId, frameId, loaderId, documentURL, request, timestamp, walltime, initiator, redirectResponse, type, targetId)
|
|
{
|
|
WI.networkManager.resourceRequestWillBeSent(requestId, frameId, loaderId, request, type, redirectResponse, timestamp, walltime, initiator, targetId);
|
|
}
|
|
|
|
responseReceived(requestId, frameId, loaderId, timestamp, type, response)
|
|
{
|
|
WI.networkManager.resourceRequestDidReceiveResponse(requestId, frameId, loaderId, type, response, timestamp);
|
|
}
|
|
|
|
dataReceived(requestId, timestamp, dataLength, encodedDataLength)
|
|
{
|
|
WI.networkManager.resourceRequestDidReceiveData(requestId, dataLength, encodedDataLength, timestamp);
|
|
}
|
|
|
|
loadingFinished(requestId, timestamp, sourceMapURL, metrics)
|
|
{
|
|
WI.networkManager.resourceRequestDidFinishLoading(requestId, timestamp, sourceMapURL, metrics);
|
|
}
|
|
|
|
loadingFailed(requestId, timestamp, errorText, canceled)
|
|
{
|
|
WI.networkManager.resourceRequestDidFailLoading(requestId, canceled, timestamp, errorText);
|
|
}
|
|
|
|
requestServedFromMemoryCache(requestId, frameId, loaderId, documentURL, timestamp, initiator, resource)
|
|
{
|
|
WI.networkManager.resourceRequestWasServedFromMemoryCache(requestId, frameId, loaderId, resource, timestamp, initiator);
|
|
}
|
|
|
|
webSocketCreated(requestId, url)
|
|
{
|
|
WI.networkManager.webSocketCreated(requestId, url);
|
|
}
|
|
|
|
webSocketWillSendHandshakeRequest(requestId, timestamp, walltime, request)
|
|
{
|
|
WI.networkManager.webSocketWillSendHandshakeRequest(requestId, timestamp, walltime, request);
|
|
}
|
|
|
|
webSocketHandshakeResponseReceived(requestId, timestamp, response)
|
|
{
|
|
WI.networkManager.webSocketHandshakeResponseReceived(requestId, timestamp, response);
|
|
}
|
|
|
|
webSocketClosed(requestId, timestamp)
|
|
{
|
|
WI.networkManager.webSocketClosed(requestId, timestamp);
|
|
}
|
|
|
|
webSocketFrameReceived(requestId, timestamp, response)
|
|
{
|
|
WI.networkManager.webSocketFrameReceived(requestId, timestamp, response);
|
|
}
|
|
|
|
webSocketFrameSent(requestId, timestamp, response)
|
|
{
|
|
WI.networkManager.webSocketFrameSent(requestId, timestamp, response);
|
|
}
|
|
|
|
webSocketFrameError(requestId, timestamp, errorMessage)
|
|
{
|
|
// FIXME: Not implemented.
|
|
}
|
|
|
|
requestIntercepted(requestId, request)
|
|
{
|
|
WI.networkManager.requestIntercepted(this._target, requestId, request);
|
|
}
|
|
|
|
responseIntercepted(requestId, response)
|
|
{
|
|
WI.networkManager.responseIntercepted(this._target, requestId, response);
|
|
}
|
|
};
|
|
|
|
/* Protocol/PageObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.PageObserver = class PageObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Page" domain.
|
|
|
|
domContentEventFired(timestamp)
|
|
{
|
|
WI.timelineManager.pageDOMContentLoadedEventFired(timestamp);
|
|
}
|
|
|
|
loadEventFired(timestamp)
|
|
{
|
|
WI.timelineManager.pageLoadEventFired(timestamp);
|
|
}
|
|
|
|
frameNavigated(frame, loaderId)
|
|
{
|
|
WI.networkManager.frameDidNavigate(frame, loaderId);
|
|
}
|
|
|
|
frameDetached(frameId)
|
|
{
|
|
WI.networkManager.frameDidDetach(frameId);
|
|
}
|
|
|
|
// COMPATIBILITY (macOS 13, iOS 16.0): `Page.defaultAppearanceDidChange` was removed in favor of `Page.defaultUserPreferencesDidChange`
|
|
defaultAppearanceDidChange(appearance)
|
|
{
|
|
WI.cssManager.defaultAppearanceDidChange(appearance);
|
|
}
|
|
|
|
defaultUserPreferencesDidChange(userPreferences)
|
|
{
|
|
WI.cssManager.defaultUserPreferencesDidChange(userPreferences);
|
|
}
|
|
|
|
frameStartedLoading(frameId)
|
|
{
|
|
// Not handled yet.
|
|
}
|
|
|
|
frameStoppedLoading(frameId)
|
|
{
|
|
// Not handled yet.
|
|
}
|
|
|
|
frameScheduledNavigation(frameId, delay)
|
|
{
|
|
// Not handled yet.
|
|
}
|
|
|
|
frameClearedScheduledNavigation(frameId)
|
|
{
|
|
// Not handled yet.
|
|
}
|
|
};
|
|
|
|
/* Protocol/RemoteObject.js */
|
|
|
|
/*
|
|
* Copyright (C) 2009 Google Inc. All rights reserved.
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RemoteObject = class RemoteObject
|
|
{
|
|
constructor(target, objectId, type, subtype, value, description, size, classPrototype, className, preview)
|
|
{
|
|
console.assert(type);
|
|
console.assert(!preview || preview instanceof WI.ObjectPreview);
|
|
console.assert(!target || target instanceof WI.Target);
|
|
|
|
this._target = target || WI.mainTarget;
|
|
this._type = type;
|
|
this._subtype = subtype;
|
|
|
|
if (objectId) {
|
|
// Object, Function, or Symbol.
|
|
console.assert(!subtype || typeof subtype === "string");
|
|
console.assert(!description || typeof description === "string");
|
|
console.assert(!value);
|
|
|
|
this._objectId = objectId;
|
|
this._description = description || "";
|
|
this._hasChildren = type !== "symbol";
|
|
this._size = size;
|
|
this._classPrototype = classPrototype;
|
|
this._preview = preview;
|
|
|
|
if (subtype === "class") {
|
|
this._functionDescription = this._description;
|
|
this._description = "class " + className;
|
|
}
|
|
} else {
|
|
// Primitive, BigInt, or null.
|
|
console.assert(type !== "object" || value === null);
|
|
console.assert(!preview);
|
|
|
|
this._description = description || (value + "");
|
|
this._hasChildren = false;
|
|
this._value = value;
|
|
|
|
if (type === "bigint") {
|
|
console.assert(value === undefined);
|
|
console.assert(description.endsWith("n"));
|
|
if (window.BigInt)
|
|
this._value = BigInt(description.substring(0, description.length - 1));
|
|
else
|
|
this._value = `${description} [BigInt Not Enabled in Web Inspector]`;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static createFakeRemoteObject()
|
|
{
|
|
return new WI.RemoteObject(undefined, WI.RemoteObject.FakeRemoteObjectId, "object");
|
|
}
|
|
|
|
static fromPrimitiveValue(value)
|
|
{
|
|
return new WI.RemoteObject(undefined, undefined, typeof value, undefined, value, undefined, undefined, undefined, undefined);
|
|
}
|
|
|
|
static createBigIntFromDescriptionString(description)
|
|
{
|
|
console.assert(description.endsWith("n"));
|
|
|
|
return new WI.RemoteObject(undefined, undefined, "bigint", undefined, undefined, description, undefined, undefined, undefined);
|
|
}
|
|
|
|
static fromPayload(payload, target)
|
|
{
|
|
console.assert(typeof payload === "object", "Remote object payload should only be an object");
|
|
|
|
if (payload.classPrototype)
|
|
payload.classPrototype = WI.RemoteObject.fromPayload(payload.classPrototype, target);
|
|
|
|
if (payload.preview)
|
|
payload.preview = WI.ObjectPreview.fromPayload(payload.preview);
|
|
|
|
return new WI.RemoteObject(target, payload.objectId, payload.type, payload.subtype, payload.value, payload.description, payload.size, payload.classPrototype, payload.className, payload.preview);
|
|
}
|
|
|
|
static createCallArgument(valueOrObject)
|
|
{
|
|
if (valueOrObject instanceof WI.RemoteObject) {
|
|
if (valueOrObject.objectId)
|
|
return {objectId: valueOrObject.objectId};
|
|
return {value: valueOrObject.value};
|
|
}
|
|
|
|
return {value: valueOrObject};
|
|
}
|
|
|
|
static resolveNode(node, objectGroup)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, node);
|
|
|
|
if (node.destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.DOMAgent.resolveNode(node.id, objectGroup)
|
|
.then(({object}) => WI.RemoteObject.fromPayload(object, WI.mainTarget));
|
|
}
|
|
|
|
static resolveWebSocket(webSocketResource, objectGroup, callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.NetworkAgent.resolveWebSocket(webSocketResource.requestIdentifier, objectGroup, (error, object) => {
|
|
if (error || !object)
|
|
callback(null);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(object, webSocketResource.target));
|
|
});
|
|
}
|
|
|
|
static resolveCanvasContext(canvas, objectGroup)
|
|
{
|
|
let promise = null;
|
|
|
|
// COMPATIBILITY (iOS 13): Canvas.resolveCanvasContext was renamed to Canvas.resolveContext.
|
|
if (!canvas.target.hasCommand("Canvas.resolveContext"))
|
|
promise = canvas.target.CanvasAgent.resolveCanvasContext(canvas.identifier, objectGroup);
|
|
else
|
|
promise = canvas.target.CanvasAgent.resolveContext(canvas.identifier, objectGroup);
|
|
|
|
return promise.then(({object}) => WI.RemoteObject.fromPayload(object, canvas.target));
|
|
}
|
|
|
|
static resolveAnimation(animation, objectGroup, callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
function wrapCallback(error, object) {
|
|
if (error || !object)
|
|
callback(null);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(object, WI.mainTarget));
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (iOS 13.1): Animation.resolveAnimation did not exist yet.
|
|
console.assert(target.hasCommand("Animation.resolveAnimation"));
|
|
|
|
target.AnimationAgent.resolveAnimation(animation.animationId, objectGroup, wrapCallback);
|
|
}
|
|
|
|
// Public
|
|
|
|
get target()
|
|
{
|
|
return this._target;
|
|
}
|
|
|
|
get objectId()
|
|
{
|
|
return this._objectId;
|
|
}
|
|
|
|
get type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
get subtype()
|
|
{
|
|
return this._subtype;
|
|
}
|
|
|
|
get description()
|
|
{
|
|
return this._description;
|
|
}
|
|
|
|
get functionDescription()
|
|
{
|
|
console.assert(this.type === "function");
|
|
|
|
return this._functionDescription || this._description;
|
|
}
|
|
|
|
get hasChildren()
|
|
{
|
|
return this._hasChildren;
|
|
}
|
|
|
|
get value()
|
|
{
|
|
return this._value;
|
|
}
|
|
|
|
get size()
|
|
{
|
|
return this._size || 0;
|
|
}
|
|
|
|
get classPrototype()
|
|
{
|
|
return this._classPrototype;
|
|
}
|
|
|
|
get preview()
|
|
{
|
|
return this._preview;
|
|
}
|
|
|
|
hasSize()
|
|
{
|
|
return this.isArray() || this.isCollectionType();
|
|
}
|
|
|
|
hasValue()
|
|
{
|
|
return "_value" in this;
|
|
}
|
|
|
|
canLoadPreview()
|
|
{
|
|
if (this._failedToLoadPreview)
|
|
return false;
|
|
|
|
if (this._type !== "object")
|
|
return false;
|
|
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject())
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
updatePreview(callback)
|
|
{
|
|
if (!this.canLoadPreview()) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
if (!this._target.hasCommand("Runtime.getPreview")) {
|
|
this._failedToLoadPreview = true;
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this._target.RuntimeAgent.getPreview(this._objectId, (error, payload) => {
|
|
if (error) {
|
|
this._failedToLoadPreview = true;
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
this._preview = WI.ObjectPreview.fromPayload(payload);
|
|
callback(this._preview);
|
|
});
|
|
}
|
|
|
|
getPropertyDescriptors(callback, options = {})
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
this._getProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options);
|
|
}
|
|
|
|
getDisplayablePropertyDescriptors(callback, options = {})
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
this._getDisplayableProperties(this._getPropertyDescriptorsResolver.bind(this, callback), options);
|
|
}
|
|
|
|
setPropertyValue(name, value, callback)
|
|
{
|
|
if (!this._objectId || this._isSymbol() || this._isFakeObject()) {
|
|
callback("Can't set a property of non-object.");
|
|
return;
|
|
}
|
|
|
|
// FIXME: It doesn't look like setPropertyValue is used yet. This will need to be tested when it is again (editable ObjectTrees).
|
|
this._target.RuntimeAgent.evaluate.invoke({expression: appendWebInspectorSourceURL(value), doNotPauseOnExceptionsAndMuteConsole: true}, evaluatedCallback.bind(this));
|
|
|
|
function evaluatedCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown) {
|
|
callback(error || result.description);
|
|
return;
|
|
}
|
|
|
|
function setPropertyValue(propertyName, propertyValue)
|
|
{
|
|
this[propertyName] = propertyValue;
|
|
}
|
|
|
|
delete result.description; // Optimize on traffic.
|
|
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(setPropertyValue.toString()), [{value: name}, result], true, undefined, propertySetCallback.bind(this));
|
|
|
|
if (result._objectId)
|
|
this._target.RuntimeAgent.releaseObject(result._objectId);
|
|
}
|
|
|
|
function propertySetCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown) {
|
|
callback(error || result.description);
|
|
return;
|
|
}
|
|
|
|
callback();
|
|
}
|
|
}
|
|
|
|
isUndefined()
|
|
{
|
|
return this._type === "undefined";
|
|
}
|
|
|
|
isNode()
|
|
{
|
|
return this._subtype === "node";
|
|
}
|
|
|
|
isArray()
|
|
{
|
|
return this._subtype === "array";
|
|
}
|
|
|
|
isClass()
|
|
{
|
|
return this._subtype === "class";
|
|
}
|
|
|
|
isCollectionType()
|
|
{
|
|
return this._subtype === "map" || this._subtype === "set" || this._subtype === "weakmap" || this._subtype === "weakset";
|
|
}
|
|
|
|
isWeakCollection()
|
|
{
|
|
return this._subtype === "weakmap" || this._subtype === "weakset";
|
|
}
|
|
|
|
getCollectionEntries(callback, {fetchStart, fetchCount} = {})
|
|
{
|
|
console.assert(this.isCollectionType());
|
|
console.assert(typeof fetchStart === "undefined" || (typeof fetchStart === "number" && fetchStart >= 0), fetchStart);
|
|
console.assert(typeof fetchCount === "undefined" || (typeof fetchCount === "number" && fetchCount > 0), fetchCount);
|
|
|
|
// WeakMaps and WeakSets are not ordered. We should never send a non-zero start.
|
|
console.assert(!this.isWeakCollection() || typeof fetchStart === "undefined" || fetchStart === 0, fetchStart);
|
|
|
|
let objectGroup = this.isWeakCollection() ? this._weakCollectionObjectGroup() : "";
|
|
|
|
// COMPATIBILITY (iOS 13): `startIndex` and `numberToFetch` were renamed to `fetchStart` and `fetchCount` (but kept in the same position).
|
|
this._target.RuntimeAgent.getCollectionEntries(this._objectId, objectGroup, fetchStart, fetchCount, (error, entries) => {
|
|
callback(entries.map((x) => WI.CollectionEntry.fromPayload(x, this._target)));
|
|
});
|
|
}
|
|
|
|
releaseWeakCollectionEntries()
|
|
{
|
|
console.assert(this.isWeakCollection());
|
|
|
|
this._target.RuntimeAgent.releaseObjectGroup(this._weakCollectionObjectGroup());
|
|
}
|
|
|
|
pushNodeToFrontend(callback)
|
|
{
|
|
if (this._objectId && InspectorBackend.hasCommand("DOM.requestNode"))
|
|
WI.domManager.pushNodeToFrontend(this._objectId, callback);
|
|
else
|
|
callback(0);
|
|
}
|
|
|
|
async fetchProperties(propertyNames, resultObject = {})
|
|
{
|
|
let seenPropertyNames = new Set;
|
|
let requestedValues = [];
|
|
for (let propertyName of propertyNames) {
|
|
// Check this here, otherwise things like '{}' would be valid Set keys.
|
|
if (typeof propertyName !== "string" && typeof propertyName !== "number")
|
|
throw new Error(`Tried to get property using key is not a string or number: ${propertyName}`);
|
|
|
|
if (seenPropertyNames.has(propertyName))
|
|
continue;
|
|
|
|
seenPropertyNames.add(propertyName);
|
|
requestedValues.push(this.getProperty(propertyName));
|
|
}
|
|
|
|
// Return primitive values directly, otherwise return a WI.RemoteObject instance.
|
|
function maybeUnwrapValue(remoteObject) {
|
|
return remoteObject.hasValue() ? remoteObject.value : remoteObject;
|
|
}
|
|
|
|
// Request property values one by one, since returning an array of property
|
|
// values would then be subject to arbitrary object preview size limits.
|
|
let fetchedKeys = Array.from(seenPropertyNames);
|
|
let fetchedValues = await Promise.all(requestedValues);
|
|
for (let i = 0; i < fetchedKeys.length; ++i)
|
|
resultObject[fetchedKeys[i]] = maybeUnwrapValue(fetchedValues[i]);
|
|
|
|
return resultObject;
|
|
}
|
|
|
|
getProperty(propertyName, callback = null)
|
|
{
|
|
function inspectedPage_object_getProperty(property) {
|
|
if (typeof property !== "string" && typeof property !== "number")
|
|
throw new Error(`Tried to get property using key is not a string or number: ${property}`);
|
|
|
|
return this[property];
|
|
}
|
|
|
|
if (callback && typeof callback === "function")
|
|
this.callFunction(inspectedPage_object_getProperty, [propertyName], true, callback);
|
|
else
|
|
return this.callFunction(inspectedPage_object_getProperty, [propertyName], true);
|
|
}
|
|
|
|
callFunction(functionDeclaration, args, generatePreview, callback = null)
|
|
{
|
|
let translateResult = (result) => result ? WI.RemoteObject.fromPayload(result, this._target) : null;
|
|
|
|
if (args)
|
|
args = args.map(WI.RemoteObject.createCallArgument);
|
|
|
|
if (callback && typeof callback === "function") {
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview, (error, result, wasThrown) => {
|
|
callback(error, translateResult(result), wasThrown);
|
|
});
|
|
} else {
|
|
// Protocol errors and results that were thrown should cause promise rejection with the same.
|
|
return this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, undefined, !!generatePreview)
|
|
.then(({result, wasThrown}) => {
|
|
result = translateResult(result);
|
|
if (result && wasThrown)
|
|
return Promise.reject(result);
|
|
return Promise.resolve(result);
|
|
});
|
|
}
|
|
}
|
|
|
|
callFunctionJSON(functionDeclaration, args, callback = null)
|
|
{
|
|
if (callback && typeof callback === "function") {
|
|
this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, true, (error, result, wasThrown) => {
|
|
callback((error || wasThrown) ? null : result.value);
|
|
});
|
|
} else {
|
|
return this._target.RuntimeAgent.callFunctionOn(this._objectId, appendWebInspectorSourceURL(functionDeclaration.toString()), args, true, true)
|
|
.then(({result, wasThrown}) => {
|
|
return wasThrown ? null : result.value;
|
|
});
|
|
}
|
|
}
|
|
|
|
invokeGetter(getterRemoteObject, callback)
|
|
{
|
|
console.assert(getterRemoteObject instanceof WI.RemoteObject);
|
|
|
|
function backendInvokeGetter(getter)
|
|
{
|
|
return getter ? getter.call(this) : undefined;
|
|
}
|
|
|
|
this.callFunction(backendInvokeGetter, [getterRemoteObject], true, callback);
|
|
}
|
|
|
|
getOwnPropertyDescriptor(propertyName, callback)
|
|
{
|
|
function backendGetOwnPropertyDescriptor(propertyName)
|
|
{
|
|
return this[propertyName];
|
|
}
|
|
|
|
function wrappedCallback(error, result, wasThrown)
|
|
{
|
|
if (error || wasThrown || !(result instanceof WI.RemoteObject)) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
var fakeDescriptor = {name: propertyName, value: result, writable: true, configurable: true};
|
|
var fakePropertyDescriptor = new WI.PropertyDescriptor(fakeDescriptor, null, true, false, false, false);
|
|
callback(fakePropertyDescriptor);
|
|
}
|
|
|
|
// FIXME: Implement a real RuntimeAgent.getOwnPropertyDescriptor?
|
|
this.callFunction(backendGetOwnPropertyDescriptor, [propertyName], false, wrappedCallback.bind(this));
|
|
}
|
|
|
|
release()
|
|
{
|
|
if (this._objectId && !this._isFakeObject())
|
|
this._target.RuntimeAgent.releaseObject(this._objectId);
|
|
}
|
|
|
|
arrayLength()
|
|
{
|
|
if (this._subtype !== "array")
|
|
return 0;
|
|
|
|
var matches = this._description.match(/\[([0-9]+)\]/);
|
|
if (!matches)
|
|
return 0;
|
|
|
|
return parseInt(matches[1], 10);
|
|
}
|
|
|
|
asCallArgument()
|
|
{
|
|
return WI.RemoteObject.createCallArgument(this);
|
|
}
|
|
|
|
findFunctionSourceCodeLocation()
|
|
{
|
|
var result = new WI.WrappedPromise;
|
|
|
|
if (!this._isFunction() || !this._objectId) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.MissingObjectId);
|
|
return result.promise;
|
|
}
|
|
|
|
this._target.DebuggerAgent.getFunctionDetails(this._objectId, (error, response) => {
|
|
if (error) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound);
|
|
return;
|
|
}
|
|
|
|
var location = response.location;
|
|
var sourceCode = WI.debuggerManager.scriptForIdentifier(location.scriptId, this._target);
|
|
|
|
if (!sourceCode || (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(sourceCode.sourceURL))) {
|
|
result.resolve(WI.RemoteObject.SourceCodeLocationPromise.NoSourceFound);
|
|
return;
|
|
}
|
|
|
|
var sourceCodeLocation = sourceCode.createSourceCodeLocation(location.lineNumber, location.columnNumber || 0);
|
|
result.resolve(sourceCodeLocation);
|
|
});
|
|
|
|
return result.promise;
|
|
}
|
|
|
|
// Private
|
|
|
|
_isFakeObject()
|
|
{
|
|
return this._objectId === WI.RemoteObject.FakeRemoteObjectId;
|
|
}
|
|
|
|
_isSymbol()
|
|
{
|
|
return this._type === "symbol";
|
|
}
|
|
|
|
_isFunction()
|
|
{
|
|
return this._type === "function";
|
|
}
|
|
|
|
_weakCollectionObjectGroup()
|
|
{
|
|
return JSON.stringify(this._objectId) + "-" + this._subtype;
|
|
}
|
|
|
|
_getProperties(callback, {ownProperties, fetchStart, fetchCount, generatePreview} = {})
|
|
{
|
|
// COMPATIBILITY (iOS 13): `result` was renamed to `properties` (but kept in the same position).
|
|
this._target.RuntimeAgent.getProperties.invoke({
|
|
objectId: this._objectId,
|
|
ownProperties,
|
|
fetchStart,
|
|
fetchCount,
|
|
generatePreview,
|
|
}, callback);
|
|
}
|
|
|
|
_getDisplayableProperties(callback, {fetchStart, fetchCount, generatePreview} = {})
|
|
{
|
|
console.assert(this._target.hasCommand("Runtime.getDisplayableProperties"));
|
|
|
|
this._target.RuntimeAgent.getDisplayableProperties.invoke({
|
|
objectId: this._objectId,
|
|
fetchStart,
|
|
fetchCount,
|
|
generatePreview,
|
|
}, callback);
|
|
}
|
|
|
|
_getPropertyDescriptorsResolver(callback, error, properties, internalProperties)
|
|
{
|
|
if (error) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
let descriptors = properties.map((payload) => WI.PropertyDescriptor.fromPayload(payload, false, this._target));
|
|
|
|
if (internalProperties) {
|
|
for (let payload of internalProperties)
|
|
descriptors.push(WI.PropertyDescriptor.fromPayload(payload, true, this._target));
|
|
}
|
|
|
|
callback(descriptors);
|
|
}
|
|
};
|
|
|
|
WI.RemoteObject.FakeRemoteObjectId = "fake-remote-object";
|
|
|
|
WI.RemoteObject.SourceCodeLocationPromise = {
|
|
NoSourceFound: "remote-object-source-code-location-promise-no-source-found",
|
|
MissingObjectId: "remote-object-source-code-location-promise-missing-object-id"
|
|
};
|
|
|
|
/* Protocol/RuntimeObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RuntimeObserver = class RuntimeObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Runtime" domain.
|
|
|
|
executionContextCreated(contextPayload)
|
|
{
|
|
WI.networkManager.executionContextCreated(contextPayload);
|
|
}
|
|
};
|
|
|
|
/* Protocol/ScriptProfilerObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScriptProfilerObserver = class ScriptProfilerObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "ScriptProfiler" domain.
|
|
|
|
trackingStart(timestamp)
|
|
{
|
|
WI.timelineManager.scriptProfilerTrackingStarted(timestamp);
|
|
}
|
|
|
|
trackingUpdate(event)
|
|
{
|
|
WI.timelineManager.scriptProfilerTrackingUpdated(event);
|
|
}
|
|
|
|
trackingComplete(timestamp, samples)
|
|
{
|
|
WI.timelineManager.scriptProfilerTrackingCompleted(timestamp, samples);
|
|
}
|
|
|
|
programmaticCaptureStarted()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): ScriptProfiler.programmaticCaptureStarted was removed after iOS 12.2.
|
|
}
|
|
|
|
programmaticCaptureStopped()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): ScriptProfiler.programmaticCaptureStopped was removed after iOS 12.2.
|
|
}
|
|
};
|
|
|
|
/* Protocol/TargetObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TargetObserver = class TargetObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Target" domain.
|
|
|
|
targetCreated(targetInfo)
|
|
{
|
|
WI.targetManager.targetCreated(this._target, targetInfo);
|
|
}
|
|
|
|
didCommitProvisionalTarget(oldTargetId, newTargetId)
|
|
{
|
|
WI.targetManager.didCommitProvisionalTarget(this._target, oldTargetId, newTargetId);
|
|
}
|
|
|
|
targetDestroyed(targetId)
|
|
{
|
|
WI.targetManager.targetDestroyed(targetId);
|
|
}
|
|
|
|
dispatchMessageFromTarget(targetId, message)
|
|
{
|
|
WI.targetManager.dispatchMessageFromTarget(targetId, message);
|
|
}
|
|
};
|
|
|
|
/* Protocol/TimelineObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TimelineObserver = class TimelineObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Timeline" domain.
|
|
|
|
eventRecorded(record)
|
|
{
|
|
WI.timelineManager.eventRecorded(record);
|
|
}
|
|
|
|
recordingStarted(startTime)
|
|
{
|
|
WI.timelineManager.capturingStarted(startTime);
|
|
}
|
|
|
|
recordingStopped(endTime)
|
|
{
|
|
WI.timelineManager.capturingStopped(endTime);
|
|
}
|
|
|
|
autoCaptureStarted()
|
|
{
|
|
WI.timelineManager.autoCaptureStarted();
|
|
}
|
|
|
|
programmaticCaptureStarted()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): Timeline.programmaticCaptureStarted was removed after iOS 12.2.
|
|
}
|
|
|
|
programmaticCaptureStopped()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): Timeline.programmaticCaptureStopped was removed after iOS 12.2.
|
|
}
|
|
};
|
|
|
|
/* Protocol/WorkerObserver.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WorkerObserver = class WorkerObserver extends InspectorBackend.Dispatcher
|
|
{
|
|
// Events defined by the "Worker" domain.
|
|
|
|
workerCreated(workerId, url, name)
|
|
{
|
|
WI.workerManager.workerCreated(this._target, workerId, url, name);
|
|
}
|
|
|
|
workerTerminated(workerId)
|
|
{
|
|
WI.workerManager.workerTerminated(workerId);
|
|
}
|
|
|
|
dispatchMessageFromWorker(workerId, message)
|
|
{
|
|
WI.workerManager.dispatchMessageFromWorker(workerId, message);
|
|
}
|
|
};
|
|
|
|
/* Models/Breakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Breakpoint = class Breakpoint extends WI.Object
|
|
{
|
|
constructor({disabled, condition, actions, ignoreCount, autoContinue} = {})
|
|
{
|
|
console.assert(!disabled || typeof disabled === "boolean", disabled);
|
|
console.assert(!condition || typeof condition === "string", condition);
|
|
console.assert(!actions || Array.isArray(actions), actions);
|
|
console.assert(!ignoreCount || !isNaN(ignoreCount), ignoreCount);
|
|
console.assert(!autoContinue || typeof autoContinue === "boolean", autoContinue);
|
|
|
|
super();
|
|
|
|
// This class should not be instantiated directly. Create a concrete subclass instead.
|
|
console.assert(this.constructor !== WI.Breakpoint && this instanceof WI.Breakpoint);
|
|
console.assert(this.constructor.ReferencePage, "Should have a link to a reference page.");
|
|
|
|
this._disabled = disabled || false;
|
|
this._condition = condition || "";
|
|
this._ignoreCount = ignoreCount || 0;
|
|
this._autoContinue = autoContinue || false;
|
|
this._actions = actions || [];
|
|
|
|
for (let action of this._actions)
|
|
action.addEventListener(WI.BreakpointAction.Event.Modified, this._handleBreakpointActionModified, this);
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = {};
|
|
if (this._disabled)
|
|
json.disabled = this._disabled;
|
|
if (this.editable) {
|
|
if (this._condition)
|
|
json.condition = this._condition;
|
|
if (this._ignoreCount)
|
|
json.ignoreCount = this._ignoreCount;
|
|
if (this._actions.length)
|
|
json.actions = this._actions.map((action) => action.toJSON());
|
|
if (this._autoContinue)
|
|
json.autoContinue = this._autoContinue;
|
|
}
|
|
return json;
|
|
}
|
|
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get special()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return false;
|
|
}
|
|
|
|
get removable()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return true;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return false;
|
|
}
|
|
|
|
get resolved()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return WI.debuggerManager.breakpointsEnabled;
|
|
}
|
|
|
|
get disabled()
|
|
{
|
|
return this._disabled;
|
|
}
|
|
|
|
set disabled(disabled)
|
|
{
|
|
if (this._disabled === disabled)
|
|
return;
|
|
|
|
this._disabled = disabled || false;
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.DisabledStateDidChange);
|
|
}
|
|
|
|
get condition()
|
|
{
|
|
return this._condition;
|
|
}
|
|
|
|
set condition(condition)
|
|
{
|
|
console.assert(this.editable, this);
|
|
console.assert(typeof condition === "string");
|
|
|
|
if (this._condition === condition)
|
|
return;
|
|
|
|
this._condition = condition;
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.ConditionDidChange);
|
|
}
|
|
|
|
get ignoreCount()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
return this._ignoreCount;
|
|
}
|
|
|
|
set ignoreCount(ignoreCount)
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
console.assert(ignoreCount >= 0, "Ignore count cannot be negative.");
|
|
if (ignoreCount < 0)
|
|
return;
|
|
|
|
if (this._ignoreCount === ignoreCount)
|
|
return;
|
|
|
|
this._ignoreCount = ignoreCount;
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.IgnoreCountDidChange);
|
|
}
|
|
|
|
get autoContinue()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
return this._autoContinue;
|
|
}
|
|
|
|
set autoContinue(cont)
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
if (this._autoContinue === cont)
|
|
return;
|
|
|
|
this._autoContinue = cont;
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.AutoContinueDidChange);
|
|
}
|
|
|
|
get actions()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
return this._actions;
|
|
}
|
|
|
|
get probeActions()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
return this._actions.filter(function(action) {
|
|
return action.type === WI.BreakpointAction.Type.Probe;
|
|
});
|
|
}
|
|
|
|
addAction(action, {precedingAction} = {})
|
|
{
|
|
console.assert(this.editable, this);
|
|
console.assert(action instanceof WI.BreakpointAction, action);
|
|
|
|
action.addEventListener(WI.BreakpointAction.Event.Modified, this._handleBreakpointActionModified, this);
|
|
|
|
if (!precedingAction)
|
|
this._actions.push(action);
|
|
else {
|
|
var index = this._actions.indexOf(precedingAction);
|
|
console.assert(index !== -1);
|
|
if (index === -1)
|
|
this._actions.push(action);
|
|
else
|
|
this._actions.splice(index + 1, 0, action);
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.ActionsDidChange);
|
|
}
|
|
|
|
removeAction(action)
|
|
{
|
|
console.assert(this.editable, this);
|
|
console.assert(action instanceof WI.BreakpointAction, action);
|
|
|
|
var index = this._actions.indexOf(action);
|
|
console.assert(index !== -1);
|
|
if (index === -1)
|
|
return;
|
|
|
|
this._actions.splice(index, 1);
|
|
|
|
action.removeEventListener(WI.BreakpointAction.Event.Modified, this._handleBreakpointActionModified, this);
|
|
|
|
if (!this._actions.length)
|
|
this.autoContinue = false;
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.ActionsDidChange);
|
|
}
|
|
|
|
clearActions(type)
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
if (!type)
|
|
this._actions = [];
|
|
else
|
|
this._actions = this._actions.filter(function(action) { return action.type !== type; });
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.ActionsDidChange);
|
|
}
|
|
|
|
reset()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
this.condition = "";
|
|
this.ignoreCount = 0;
|
|
this.autoContinue = false;
|
|
this.clearActions();
|
|
}
|
|
|
|
remove()
|
|
{
|
|
console.assert(this.removable, this);
|
|
|
|
// Overridden by subclasses if needed.
|
|
}
|
|
|
|
optionsToProtocol()
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
let payload = {};
|
|
|
|
if (this._condition)
|
|
payload.condition = this._condition;
|
|
|
|
if (this._actions.length) {
|
|
payload.actions = this._actions.map((action) => action.toProtocol()).filter((action) => {
|
|
if (action.type !== WI.BreakpointAction.Type.Log)
|
|
return true;
|
|
|
|
if (!/\$\{.*?\}/.test(action.data))
|
|
return true;
|
|
|
|
let lexer = new WI.BreakpointLogMessageLexer;
|
|
let tokens = lexer.tokenize(action.data);
|
|
if (!tokens)
|
|
return false;
|
|
|
|
let templateLiteral = tokens.reduce((text, token) => {
|
|
if (token.type === WI.BreakpointLogMessageLexer.TokenType.PlainText)
|
|
return text + token.data.escapeCharacters("`\\");
|
|
if (token.type === WI.BreakpointLogMessageLexer.TokenType.Expression)
|
|
return text + "${" + token.data + "}";
|
|
return text;
|
|
}, "");
|
|
|
|
action.data = "console.log(`" + templateLiteral + "`)";
|
|
action.type = WI.BreakpointAction.Type.Evaluate;
|
|
return true;
|
|
});
|
|
}
|
|
|
|
if (this._autoContinue)
|
|
payload.autoContinue = this._autoContinue;
|
|
|
|
if (this._ignoreCount)
|
|
payload.ignoreCount = this._ignoreCount;
|
|
|
|
return !isEmptyObject(payload) ? payload : undefined;
|
|
}
|
|
|
|
// Private
|
|
|
|
_handleBreakpointActionModified(event)
|
|
{
|
|
console.assert(this.editable, this);
|
|
|
|
this.dispatchEventToListeners(WI.Breakpoint.Event.ActionsDidChange);
|
|
}
|
|
};
|
|
|
|
WI.Breakpoint.TypeIdentifier = "breakpoint";
|
|
|
|
WI.Breakpoint.Event = {
|
|
DisabledStateDidChange: "breakpoint-disabled-state-did-change",
|
|
ConditionDidChange: "breakpoint-condition-did-change",
|
|
IgnoreCountDidChange: "breakpoint-ignore-count-did-change",
|
|
ActionsDidChange: "breakpoint-actions-did-change",
|
|
AutoContinueDidChange: "breakpoint-auto-continue-did-change",
|
|
};
|
|
|
|
/* Models/BreakpointAction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BreakpointAction = class BreakpointAction extends WI.Object
|
|
{
|
|
constructor(type, {data, emulateUserGesture} = {})
|
|
{
|
|
console.assert(Object.values(WI.BreakpointAction.Type).includes(type), type);
|
|
console.assert(!data || typeof data === "string", data);
|
|
|
|
super();
|
|
|
|
this._type = type;
|
|
this._data = data || null;
|
|
this._id = WI.debuggerManager.nextBreakpointActionIdentifier();
|
|
this._emulateUserGesture = !!emulateUserGesture;
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsEmulateUserAction()
|
|
{
|
|
// COMPATIBILITY (iOS 14): the `emulateUserGesture` property of `Debugger.BreakpointAction` did not exist yet.
|
|
// Since support can't be tested directly, check for the `options` parameter of `Debugger.setPauseOnExceptions`.
|
|
// FIXME: Use explicit version checking once <https://webkit.org/b/148680> is fixed.
|
|
return WI.sharedApp.isWebDebuggable() && InspectorBackend.hasCommand("Debugger.setPauseOnExceptions", "options");
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.BreakpointAction(json.type, {
|
|
data: json.data,
|
|
emulateUserGesture: json.emulateUserGesture,
|
|
});
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = {
|
|
type: this._type,
|
|
};
|
|
if (this._data)
|
|
json.data = this._data;
|
|
if (this._emulateUserGesture)
|
|
json.emulateUserGesture = this._emulateUserGesture;
|
|
return json;
|
|
}
|
|
|
|
// Public
|
|
|
|
get id() { return this._id; }
|
|
|
|
get type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
set type(type)
|
|
{
|
|
console.assert(Object.values(WI.BreakpointAction.Type).includes(type), type);
|
|
|
|
if (type === this._type)
|
|
return;
|
|
|
|
this._type = type;
|
|
|
|
this.dispatchEventToListeners(WI.BreakpointAction.Event.Modified);
|
|
}
|
|
|
|
get data()
|
|
{
|
|
return this._data;
|
|
}
|
|
|
|
set data(data)
|
|
{
|
|
console.assert(!data || typeof data === "string", data);
|
|
|
|
if (this._data === data)
|
|
return;
|
|
|
|
this._data = data;
|
|
|
|
this.dispatchEventToListeners(WI.BreakpointAction.Event.Modified);
|
|
}
|
|
|
|
get emulateUserGesture()
|
|
{
|
|
return this._emulateUserGesture;
|
|
}
|
|
|
|
set emulateUserGesture(emulateUserGesture)
|
|
{
|
|
if (this._emulateUserGesture === emulateUserGesture)
|
|
return;
|
|
|
|
this._emulateUserGesture = emulateUserGesture;
|
|
|
|
this.dispatchEventToListeners(WI.BreakpointAction.Event.Modified);
|
|
}
|
|
|
|
toProtocol()
|
|
{
|
|
let json = this.toJSON();
|
|
json.id = this._id;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.BreakpointAction.Type = {
|
|
Log: "log",
|
|
Evaluate: "evaluate",
|
|
Sound: "sound",
|
|
Probe: "probe"
|
|
};
|
|
|
|
WI.BreakpointAction.Event = {
|
|
Modified: "breakpoint-action-modified",
|
|
};
|
|
|
|
/* Models/Collection.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Collection = class Collection extends WI.Object
|
|
{
|
|
constructor(items = [])
|
|
{
|
|
super();
|
|
|
|
this._items = new Set;
|
|
|
|
for (let item of items)
|
|
this.add(item);
|
|
}
|
|
|
|
// Public
|
|
|
|
get size()
|
|
{
|
|
return this._items.size;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
add(item)
|
|
{
|
|
let isValidType = this.objectIsRequiredType(item);
|
|
console.assert(isValidType);
|
|
if (!isValidType)
|
|
return;
|
|
|
|
console.assert(!this._items.has(item));
|
|
this._items.add(item);
|
|
|
|
this.itemAdded(item);
|
|
|
|
this.dispatchEventToListeners(WI.Collection.Event.ItemAdded, {item});
|
|
}
|
|
|
|
remove(item)
|
|
{
|
|
let wasRemoved = this._items.delete(item);
|
|
console.assert(wasRemoved);
|
|
|
|
this.itemRemoved(item);
|
|
|
|
this.dispatchEventToListeners(WI.Collection.Event.ItemRemoved, {item});
|
|
}
|
|
|
|
has(...args)
|
|
{
|
|
return this._items.has(...args);
|
|
}
|
|
|
|
clear()
|
|
{
|
|
let items = new Set(this._items);
|
|
|
|
this._items.clear();
|
|
|
|
this.itemsCleared(items);
|
|
|
|
for (let item of items)
|
|
this.dispatchEventToListeners(WI.Collection.Event.ItemRemoved, {item});
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return Array.from(this);
|
|
}
|
|
|
|
[Symbol.iterator]()
|
|
{
|
|
return this._items[Symbol.iterator]();
|
|
}
|
|
|
|
// Protected
|
|
|
|
itemAdded(item)
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
itemRemoved(item)
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
itemsCleared(items)
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
};
|
|
|
|
WI.Collection.Event = {
|
|
ItemAdded: "collection-item-added",
|
|
ItemRemoved: "collection-item-removed",
|
|
};
|
|
|
|
|
|
/* Models/ConsoleMessage.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ConsoleMessage = class ConsoleMessage
|
|
{
|
|
constructor(target, source, level, message, type, url, line, column, repeatCount, parameters, stackTrace, request, timestamp)
|
|
{
|
|
console.assert(target instanceof WI.Target);
|
|
console.assert(typeof source === "string");
|
|
console.assert(typeof level === "string");
|
|
console.assert(typeof message === "string");
|
|
console.assert(!type || Object.values(WI.ConsoleMessage.MessageType).includes(type));
|
|
console.assert(!parameters || parameters.every((x) => x instanceof WI.RemoteObject));
|
|
console.assert(!stackTrace || stackTrace instanceof WI.StackTrace, stackTrace);
|
|
console.assert(!timestamp || !isNaN(timestamp), timestamp);
|
|
|
|
this._target = target;
|
|
this._source = source;
|
|
this._level = level;
|
|
this._messageText = message;
|
|
this._type = type || WI.ConsoleMessage.MessageType.Log;
|
|
this._url = url || null;
|
|
this._line = line || 0;
|
|
this._column = column || 0;
|
|
this._sourceCodeLocation = undefined;
|
|
this._repeatCount = repeatCount || 0;
|
|
this._parameters = parameters;
|
|
this._stackTrace = stackTrace || null;
|
|
this._request = request;
|
|
this._timestamp = timestamp ?? NaN;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get source() { return this._source; }
|
|
get level() { return this._level; }
|
|
get messageText() { return this._messageText; }
|
|
get type() { return this._type; }
|
|
get url() { return this._url; }
|
|
get line() { return this._line; }
|
|
get column() { return this._column; }
|
|
get repeatCount() { return this._repeatCount; }
|
|
get parameters() { return this._parameters; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
get request() { return this._request; }
|
|
get timestamp() { return this._timestamp; }
|
|
|
|
get sourceCodeLocation()
|
|
{
|
|
if (this._sourceCodeLocation !== undefined)
|
|
return this._sourceCodeLocation;
|
|
|
|
// First try to get the location from the top frame of the stack trace.
|
|
let topCallFrame = this._stackTrace?.callFrames[0];
|
|
if (topCallFrame && topCallFrame.sourceCodeLocation) {
|
|
this._sourceCodeLocation = topCallFrame.sourceCodeLocation;
|
|
return this._sourceCodeLocation;
|
|
}
|
|
|
|
// If that doesn't exist try to get a location from the url/line/column in the ConsoleMessage.
|
|
// FIXME <http://webkit.org/b/76404>: Remove the string equality checks for undefined once we don't get that value anymore.
|
|
if (this._url && this._url !== "undefined") {
|
|
let sourceCode = WI.networkManager.resourcesForURL(this._url).firstValue;
|
|
if (sourceCode) {
|
|
let lineNumber = this._line > 0 ? this._line - 1 : 0;
|
|
let columnNumber = this._column > 0 ? this._column - 1 : 0;
|
|
this._sourceCodeLocation = new WI.SourceCodeLocation(sourceCode, lineNumber, columnNumber);
|
|
return this._sourceCodeLocation;
|
|
}
|
|
}
|
|
|
|
this._sourceCodeLocation = null;
|
|
return this._sourceCodeLocation;
|
|
}
|
|
};
|
|
|
|
WI.ConsoleMessage.MessageSource = {
|
|
HTML: "html",
|
|
XML: "xml",
|
|
JS: "javascript",
|
|
Network: "network",
|
|
ConsoleAPI: "console-api",
|
|
Storage: "storage",
|
|
Appcache: "appcache",
|
|
Rendering: "rendering",
|
|
CSS: "css",
|
|
Security: "security",
|
|
Media: "media",
|
|
MediaSource: "mediasource",
|
|
WebRTC: "webrtc",
|
|
ITPDebug: "itp-debug",
|
|
PrivateClickMeasurement: "private-click-measurement",
|
|
PaymentRequest: "payment-request",
|
|
Other: "other",
|
|
|
|
// COMPATIBILITY (iOS 14.0): `Console.ChannelSource.AdClickAttribution` was renamed to `Console.ChannelSource.PrivateClickMeasurement`.
|
|
AdClickAttribution: "ad-click-attribution",
|
|
};
|
|
|
|
WI.ConsoleMessage.MessageType = {
|
|
Log: "log",
|
|
Dir: "dir",
|
|
DirXML: "dirxml",
|
|
Table: "table",
|
|
Trace: "trace",
|
|
StartGroup: "startGroup",
|
|
StartGroupCollapsed: "startGroupCollapsed",
|
|
EndGroup: "endGroup",
|
|
Assert: "assert",
|
|
Timing: "timing",
|
|
Profile: "profile",
|
|
ProfileEnd: "profileEnd",
|
|
Image: "image",
|
|
Result: "result", // Frontend Only.
|
|
};
|
|
|
|
WI.ConsoleMessage.MessageLevel = {
|
|
Log: "log",
|
|
Info: "info",
|
|
Warning: "warning",
|
|
Error: "error",
|
|
Debug: "debug",
|
|
};
|
|
|
|
/* Models/Instrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Instrument = class Instrument
|
|
{
|
|
// Static
|
|
|
|
static createForTimelineType(type)
|
|
{
|
|
switch (type) {
|
|
case WI.TimelineRecord.Type.Network:
|
|
return new WI.NetworkInstrument;
|
|
case WI.TimelineRecord.Type.Layout:
|
|
return new WI.LayoutInstrument;
|
|
case WI.TimelineRecord.Type.Script:
|
|
return new WI.ScriptInstrument;
|
|
case WI.TimelineRecord.Type.RenderingFrame:
|
|
return new WI.FPSInstrument;
|
|
case WI.TimelineRecord.Type.CPU:
|
|
return new WI.CPUInstrument;
|
|
case WI.TimelineRecord.Type.Memory:
|
|
return new WI.MemoryInstrument;
|
|
case WI.TimelineRecord.Type.HeapAllocations:
|
|
return new WI.HeapAllocationsInstrument;
|
|
case WI.TimelineRecord.Type.Media:
|
|
return new WI.MediaInstrument;
|
|
case WI.TimelineRecord.Type.Screenshots:
|
|
return new WI.ScreenshotsInstrument;
|
|
default:
|
|
console.error("Unknown TimelineRecord.Type: " + type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static startLegacyTimelineAgent(initiatedByBackend)
|
|
{
|
|
console.assert(WI.timelineManager._enabled);
|
|
|
|
if (WI.Instrument._legacyTimelineAgentStarted)
|
|
return;
|
|
|
|
WI.Instrument._legacyTimelineAgentStarted = true;
|
|
|
|
if (initiatedByBackend)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.TimelineAgent.start();
|
|
}
|
|
|
|
static stopLegacyTimelineAgent(initiatedByBackend)
|
|
{
|
|
console.assert(WI.timelineManager._enabled);
|
|
|
|
if (!WI.Instrument._legacyTimelineAgentStarted)
|
|
return;
|
|
|
|
WI.Instrument._legacyTimelineAgentStarted = false;
|
|
|
|
if (initiatedByBackend)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.TimelineAgent.stop();
|
|
}
|
|
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return null; // Implemented by subclasses.
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
WI.Instrument.startLegacyTimelineAgent(initiatedByBackend);
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
WI.Instrument.stopLegacyTimelineAgent(initiatedByBackend);
|
|
}
|
|
};
|
|
|
|
WI.Instrument._legacyTimelineAgentStarted = false;
|
|
|
|
/* Models/SourceCode.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceCode = class SourceCode extends WI.Object
|
|
{
|
|
constructor(url)
|
|
{
|
|
super();
|
|
|
|
this._url = url;
|
|
this._urlComponents = null;
|
|
|
|
this._originalRevision = new WI.SourceCodeRevision(this);
|
|
this._currentRevision = this._originalRevision;
|
|
|
|
this._sourceMaps = null;
|
|
this._formatterSourceMap = null;
|
|
this._requestContentPromise = null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static generateSpecialContentForURL(url)
|
|
{
|
|
if (url === "about:blank") {
|
|
return Promise.resolve({
|
|
content: "",
|
|
message: WI.unlocalizedString("about:blank")
|
|
});
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
// Implemented by subclasses.
|
|
console.error("Needs to be implemented by a subclass.");
|
|
return "";
|
|
}
|
|
|
|
get originalRevision()
|
|
{
|
|
return this._originalRevision;
|
|
}
|
|
|
|
get currentRevision()
|
|
{
|
|
return this._currentRevision;
|
|
}
|
|
|
|
set currentRevision(revision)
|
|
{
|
|
console.assert(revision instanceof WI.SourceCodeRevision);
|
|
if (!(revision instanceof WI.SourceCodeRevision))
|
|
return;
|
|
|
|
console.assert(revision.sourceCode === this);
|
|
if (revision.sourceCode !== this)
|
|
return;
|
|
|
|
this._currentRevision = revision;
|
|
|
|
this.dispatchEventToListeners(WI.SourceCode.Event.ContentDidChange);
|
|
}
|
|
|
|
get editableRevision()
|
|
{
|
|
if (this._currentRevision === this._originalRevision)
|
|
this._currentRevision = this._originalRevision.copy();
|
|
return this._currentRevision;
|
|
}
|
|
|
|
get content()
|
|
{
|
|
return this._currentRevision.content;
|
|
}
|
|
|
|
get base64Encoded()
|
|
{
|
|
return this._currentRevision.base64Encoded;
|
|
}
|
|
|
|
get url()
|
|
{
|
|
return this._url;
|
|
}
|
|
|
|
get urlComponents()
|
|
{
|
|
if (!this._urlComponents)
|
|
this._urlComponents = parseURL(this._url);
|
|
return this._urlComponents;
|
|
}
|
|
|
|
get contentIdentifier()
|
|
{
|
|
// A contentIdentifier is roughly `url || sourceURL` for cases where
|
|
// the content is consistent between sessions and not ephemeral.
|
|
|
|
// Can be overridden by subclasses if better behavior is possible.
|
|
return this.url;
|
|
}
|
|
|
|
get isScript()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return false;
|
|
}
|
|
|
|
get supportsScriptBlackboxing()
|
|
{
|
|
if (!this.isScript)
|
|
return false;
|
|
if (!WI.DebuggerManager.supportsBlackboxingScripts())
|
|
return false;
|
|
let contentIdentifier = this.contentIdentifier;
|
|
return contentIdentifier && !isWebKitInjectedScript(contentIdentifier);
|
|
}
|
|
|
|
get localResourceOverride()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return null;
|
|
}
|
|
|
|
get sourceMaps()
|
|
{
|
|
return this._sourceMaps || [];
|
|
}
|
|
|
|
addSourceMap(sourceMap)
|
|
{
|
|
console.assert(sourceMap instanceof WI.SourceMap);
|
|
|
|
if (!this._sourceMaps)
|
|
this._sourceMaps = [];
|
|
|
|
this._sourceMaps.push(sourceMap);
|
|
|
|
this.dispatchEventToListeners(WI.SourceCode.Event.SourceMapAdded);
|
|
}
|
|
|
|
get formatterSourceMap()
|
|
{
|
|
return this._formatterSourceMap;
|
|
}
|
|
|
|
set formatterSourceMap(formatterSourceMap)
|
|
{
|
|
console.assert(this._formatterSourceMap === null || formatterSourceMap === null);
|
|
console.assert(formatterSourceMap === null || formatterSourceMap instanceof WI.FormatterSourceMap);
|
|
|
|
this._formatterSourceMap = formatterSourceMap;
|
|
|
|
this.dispatchEventToListeners(WI.SourceCode.Event.FormatterDidChange);
|
|
}
|
|
|
|
requestContent()
|
|
{
|
|
this._requestContentPromise = this._requestContentPromise || this.requestContentFromBackend().then(this._processContent.bind(this));
|
|
|
|
return this._requestContentPromise;
|
|
}
|
|
|
|
createSourceCodeLocation(lineNumber, columnNumber)
|
|
{
|
|
return new WI.SourceCodeLocation(this, lineNumber, columnNumber);
|
|
}
|
|
|
|
createLazySourceCodeLocation(lineNumber, columnNumber)
|
|
{
|
|
return new WI.LazySourceCodeLocation(this, lineNumber, columnNumber);
|
|
}
|
|
|
|
createSourceCodeTextRange(textRange)
|
|
{
|
|
return new WI.SourceCodeTextRange(this, textRange);
|
|
}
|
|
|
|
// Protected
|
|
|
|
revisionContentDidChange(revision)
|
|
{
|
|
if (this._ignoreRevisionContentDidChangeEvent)
|
|
return;
|
|
|
|
console.assert(revision === this._currentRevision);
|
|
if (revision !== this._currentRevision)
|
|
return;
|
|
|
|
this.handleCurrentRevisionContentChange();
|
|
|
|
this.dispatchEventToListeners(WI.SourceCode.Event.ContentDidChange);
|
|
}
|
|
|
|
handleCurrentRevisionContentChange()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
}
|
|
|
|
get revisionForRequestedContent()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return this._originalRevision;
|
|
}
|
|
|
|
markContentAsStale()
|
|
{
|
|
this._requestContentPromise = null;
|
|
this._contentReceived = false;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
// Implemented by subclasses.
|
|
console.error("Needs to be implemented by a subclass.");
|
|
return Promise.reject(new Error("Needs to be implemented by a subclass."));
|
|
}
|
|
|
|
get mimeType()
|
|
{
|
|
// Implemented by subclasses.
|
|
console.error("Needs to be implemented by a subclass.");
|
|
}
|
|
|
|
// Private
|
|
|
|
_processContent(parameters)
|
|
{
|
|
// Different backend APIs return one of `content, `body`, `text`, or `scriptSource`.
|
|
let rawContent = parameters.content || parameters.body || parameters.text || parameters.scriptSource;
|
|
let rawBase64Encoded = !!parameters.base64Encoded;
|
|
let content = rawContent;
|
|
let error = parameters.error;
|
|
let message = parameters.message;
|
|
|
|
if (parameters.base64Encoded)
|
|
content = content ? WI.BlobUtilities.decodeBase64ToBlob(content, this.mimeType) : "";
|
|
|
|
let revision = this.revisionForRequestedContent;
|
|
|
|
this._ignoreRevisionContentDidChangeEvent = true;
|
|
revision.updateRevisionContent(rawContent, {
|
|
base64Encoded: rawBase64Encoded,
|
|
mimeType: this.mimeType,
|
|
blobContent: content instanceof Blob ? content : null,
|
|
});
|
|
this._ignoreRevisionContentDidChangeEvent = false;
|
|
|
|
// FIXME: Returning the content in this promise is misleading. It may not be current content
|
|
// now, and it may become out-dated later on. We should drop content from this promise
|
|
// and require clients to ask for the current contents from the sourceCode in the result.
|
|
// That would also avoid confusion around `content` being a Blob and eliminate the work
|
|
// of creating the Blob if it is not used.
|
|
|
|
return Promise.resolve({
|
|
error,
|
|
message,
|
|
sourceCode: this,
|
|
content,
|
|
rawContent,
|
|
rawBase64Encoded,
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.SourceCode.Event = {
|
|
ContentDidChange: "source-code-content-did-change",
|
|
SourceMapAdded: "source-code-source-map-added",
|
|
FormatterDidChange: "source-code-formatter-did-change",
|
|
LoadingDidFinish: "source-code-loading-did-finish",
|
|
LoadingDidFail: "source-code-loading-did-fail"
|
|
};
|
|
|
|
/* Models/SourceCodeLocation.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceCodeLocation = class SourceCodeLocation extends WI.Object
|
|
{
|
|
constructor(sourceCode, lineNumber, columnNumber)
|
|
{
|
|
super();
|
|
|
|
console.assert(sourceCode === null || sourceCode instanceof WI.SourceCode);
|
|
console.assert(!(sourceCode instanceof WI.SourceMapResource));
|
|
console.assert(typeof lineNumber === "number" && !isNaN(lineNumber) && lineNumber >= 0);
|
|
console.assert(typeof columnNumber === "number" && !isNaN(columnNumber) && columnNumber >= 0);
|
|
|
|
this._sourceCode = sourceCode || null;
|
|
this._lineNumber = lineNumber;
|
|
this._columnNumber = columnNumber;
|
|
this._resolveFormattedLocation();
|
|
|
|
if (this._sourceCode) {
|
|
this._sourceCode.addEventListener(WI.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
|
|
this._sourceCode.addEventListener(WI.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
|
|
}
|
|
|
|
this._resetMappedLocation();
|
|
}
|
|
|
|
// Static
|
|
|
|
static get specialBreakpointLocation()
|
|
{
|
|
return new WI.SourceCodeLocation(null, Infinity, Infinity);
|
|
}
|
|
|
|
// Public
|
|
|
|
isEqual(other)
|
|
{
|
|
if (!other)
|
|
return false;
|
|
|
|
if (this.lineNumber !== other.lineNumber)
|
|
return false;
|
|
|
|
if (this.columnNumber !== other.columnNumber)
|
|
return false;
|
|
|
|
function resolveSourceCode(sourceCode) {
|
|
if (sourceCode instanceof WI.Script)
|
|
return sourceCode.resource;
|
|
return sourceCode;
|
|
}
|
|
let thisSourceCode = resolveSourceCode(this.sourceCode);
|
|
let otherSourceCode = resolveSourceCode(other.sourceCode);
|
|
if (thisSourceCode !== otherSourceCode)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
get sourceCode()
|
|
{
|
|
return this._sourceCode;
|
|
}
|
|
|
|
set sourceCode(sourceCode)
|
|
{
|
|
this.setSourceCode(sourceCode);
|
|
}
|
|
|
|
// Raw line and column in the original source code.
|
|
|
|
get lineNumber()
|
|
{
|
|
return this._lineNumber;
|
|
}
|
|
|
|
get columnNumber()
|
|
{
|
|
return this._columnNumber;
|
|
}
|
|
|
|
position()
|
|
{
|
|
return new WI.SourceCodePosition(this.lineNumber, this.columnNumber);
|
|
}
|
|
|
|
// Formatted line and column if the original source code is pretty printed.
|
|
// This is the same as the raw location if there is no formatter.
|
|
|
|
get formattedLineNumber()
|
|
{
|
|
return this._formattedLineNumber;
|
|
}
|
|
|
|
get formattedColumnNumber()
|
|
{
|
|
return this._formattedColumnNumber;
|
|
}
|
|
|
|
formattedPosition()
|
|
{
|
|
return new WI.SourceCodePosition(this.formattedLineNumber, this.formattedColumnNumber);
|
|
}
|
|
|
|
// Display line and column:
|
|
// - Mapped line and column if the original source code has a source map.
|
|
// - Otherwise this is the formatted / raw line and column.
|
|
|
|
get displaySourceCode()
|
|
{
|
|
this.resolveMappedLocation();
|
|
return this._mappedResource || this._sourceCode;
|
|
}
|
|
|
|
get displayLineNumber()
|
|
{
|
|
this.resolveMappedLocation();
|
|
return isNaN(this._mappedLineNumber) ? this._formattedLineNumber : this._mappedLineNumber;
|
|
}
|
|
|
|
get displayColumnNumber()
|
|
{
|
|
this.resolveMappedLocation();
|
|
return isNaN(this._mappedColumnNumber) ? this._formattedColumnNumber : this._mappedColumnNumber;
|
|
}
|
|
|
|
displayPosition()
|
|
{
|
|
return new WI.SourceCodePosition(this.displayLineNumber, this.displayColumnNumber);
|
|
}
|
|
|
|
// User presentable location strings: "file:lineNumber:columnNumber".
|
|
|
|
originalLocationString(columnStyle, nameStyle, prefix)
|
|
{
|
|
return this._locationString(this.sourceCode, this.lineNumber, this.columnNumber, columnStyle, nameStyle, prefix);
|
|
}
|
|
|
|
formattedLocationString(columnStyle, nameStyle, prefix)
|
|
{
|
|
return this._locationString(this.sourceCode, this.formattedLineNumber, this.formattedColumn, columnStyle, nameStyle, prefix);
|
|
}
|
|
|
|
displayLocationString(columnStyle, nameStyle, prefix)
|
|
{
|
|
return this._locationString(this.displaySourceCode, this.displayLineNumber, this.displayColumnNumber, columnStyle, nameStyle, prefix);
|
|
}
|
|
|
|
tooltipString()
|
|
{
|
|
if (!this.hasDifferentDisplayLocation())
|
|
return this.originalLocationString(WI.SourceCodeLocation.ColumnStyle.Shown, WI.SourceCodeLocation.NameStyle.Full);
|
|
|
|
var tooltip = WI.UIString("Located at %s").format(this.displayLocationString(WI.SourceCodeLocation.ColumnStyle.Shown, WI.SourceCodeLocation.NameStyle.Full));
|
|
tooltip += "\n" + WI.UIString("Originally %s").format(this.originalLocationString(WI.SourceCodeLocation.ColumnStyle.Shown, WI.SourceCodeLocation.NameStyle.Full));
|
|
return tooltip;
|
|
}
|
|
|
|
hasMappedLocation()
|
|
{
|
|
this.resolveMappedLocation();
|
|
return this._mappedResource !== null;
|
|
}
|
|
|
|
hasFormattedLocation()
|
|
{
|
|
return this._formattedLineNumber !== this._lineNumber || this._formattedColumnNumber !== this._columnNumber;
|
|
}
|
|
|
|
hasDifferentDisplayLocation()
|
|
{
|
|
return this.hasMappedLocation() || this.hasFormattedLocation();
|
|
}
|
|
|
|
update(sourceCode, lineNumber, columnNumber)
|
|
{
|
|
console.assert(sourceCode === this._sourceCode || (this._mappedResource && sourceCode === this._mappedResource));
|
|
console.assert(typeof lineNumber === "number" && !isNaN(lineNumber) && lineNumber >= 0);
|
|
console.assert(typeof columnNumber === "number" && !isNaN(columnNumber) && columnNumber >= 0);
|
|
|
|
if (sourceCode === this._sourceCode && lineNumber === this._lineNumber && columnNumber === this._columnNumber)
|
|
return;
|
|
if (this._mappedResource && sourceCode === this._mappedResource && lineNumber === this._mappedLineNumber && columnNumber === this._mappedColumnNumber)
|
|
return;
|
|
|
|
var newSourceCodeLocation = sourceCode.createSourceCodeLocation(lineNumber, columnNumber);
|
|
console.assert(newSourceCodeLocation.sourceCode === this._sourceCode);
|
|
|
|
this._makeChangeAndDispatchChangeEventIfNeeded(function() {
|
|
this._lineNumber = newSourceCodeLocation._lineNumber;
|
|
this._columnNumber = newSourceCodeLocation._columnNumber;
|
|
if (newSourceCodeLocation._mappedLocationIsResolved) {
|
|
this._mappedLocationIsResolved = true;
|
|
this._mappedResource = newSourceCodeLocation._mappedResource;
|
|
this._mappedLineNumber = newSourceCodeLocation._mappedLineNumber;
|
|
this._mappedColumnNumber = newSourceCodeLocation._mappedColumnNumber;
|
|
}
|
|
});
|
|
}
|
|
|
|
populateLiveDisplayLocationTooltip(element, prefix, suffix)
|
|
{
|
|
prefix = prefix || "";
|
|
suffix = suffix || "";
|
|
|
|
element.title = prefix + this.tooltipString() + suffix;
|
|
|
|
this.addEventListener(WI.SourceCodeLocation.Event.DisplayLocationChanged, function(event) {
|
|
if (this.sourceCode)
|
|
element.title = prefix + this.tooltipString() + suffix;
|
|
}, this);
|
|
}
|
|
|
|
populateLiveDisplayLocationString(element, propertyName, columnStyle, nameStyle, prefix)
|
|
{
|
|
var currentDisplay;
|
|
|
|
function updateDisplayString(showAlternativeLocation, forceUpdate)
|
|
{
|
|
if (!forceUpdate && currentDisplay === showAlternativeLocation)
|
|
return;
|
|
|
|
currentDisplay = showAlternativeLocation;
|
|
|
|
if (!showAlternativeLocation) {
|
|
element[propertyName] = this.displayLocationString(columnStyle, nameStyle, prefix);
|
|
element.classList.toggle(WI.SourceCodeLocation.DisplayLocationClassName, this.hasDifferentDisplayLocation());
|
|
} else if (this.hasDifferentDisplayLocation()) {
|
|
element[propertyName] = this.originalLocationString(columnStyle, nameStyle, prefix);
|
|
element.classList.remove(WI.SourceCodeLocation.DisplayLocationClassName);
|
|
}
|
|
}
|
|
|
|
function mouseOverOrMove(event)
|
|
{
|
|
updateDisplayString.call(this, event.metaKey && !event.altKey && !event.shiftKey);
|
|
}
|
|
|
|
updateDisplayString.call(this, false);
|
|
|
|
this.addEventListener(WI.SourceCodeLocation.Event.DisplayLocationChanged, function(event) {
|
|
if (this.sourceCode)
|
|
updateDisplayString.call(this, currentDisplay, true);
|
|
}, this);
|
|
|
|
var boundMouseOverOrMove = mouseOverOrMove.bind(this);
|
|
element.addEventListener("mouseover", boundMouseOverOrMove);
|
|
element.addEventListener("mousemove", boundMouseOverOrMove);
|
|
element.addEventListener("mouseout", (event) => { updateDisplayString.call(this, false); });
|
|
}
|
|
|
|
// Protected
|
|
|
|
setSourceCode(sourceCode)
|
|
{
|
|
console.assert((this._sourceCode === null && sourceCode instanceof WI.SourceCode) || (this._sourceCode instanceof WI.SourceCode && sourceCode === null));
|
|
|
|
if (sourceCode === this._sourceCode)
|
|
return;
|
|
|
|
this._makeChangeAndDispatchChangeEventIfNeeded(function() {
|
|
if (this._sourceCode) {
|
|
this._sourceCode.removeEventListener(WI.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
|
|
this._sourceCode.removeEventListener(WI.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
|
|
}
|
|
|
|
this._sourceCode = sourceCode;
|
|
|
|
if (this._sourceCode) {
|
|
this._sourceCode.addEventListener(WI.SourceCode.Event.SourceMapAdded, this._sourceCodeSourceMapAdded, this);
|
|
this._sourceCode.addEventListener(WI.SourceCode.Event.FormatterDidChange, this._sourceCodeFormatterDidChange, this);
|
|
}
|
|
});
|
|
}
|
|
|
|
resolveMappedLocation()
|
|
{
|
|
if (this._mappedLocationIsResolved)
|
|
return;
|
|
|
|
console.assert(this._mappedResource === null);
|
|
console.assert(isNaN(this._mappedLineNumber));
|
|
console.assert(isNaN(this._mappedColumnNumber));
|
|
|
|
this._mappedLocationIsResolved = true;
|
|
|
|
if (!this._sourceCode)
|
|
return;
|
|
|
|
var sourceMaps = this._sourceCode.sourceMaps;
|
|
if (!sourceMaps.length)
|
|
return;
|
|
|
|
for (var i = 0; i < sourceMaps.length; ++i) {
|
|
var sourceMap = sourceMaps[i];
|
|
var entry = sourceMap.findEntry(this._lineNumber, this._columnNumber);
|
|
if (!entry || entry.length === 2)
|
|
continue;
|
|
console.assert(entry.length === 5);
|
|
var url = entry[2];
|
|
var sourceMapResource = sourceMap.resourceForURL(url);
|
|
if (!sourceMapResource)
|
|
return;
|
|
this._mappedResource = sourceMapResource;
|
|
this._mappedLineNumber = entry[3];
|
|
this._mappedColumnNumber = entry[4];
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
_locationString(sourceCode, lineNumber, columnNumber, columnStyle, nameStyle, prefix)
|
|
{
|
|
console.assert(sourceCode);
|
|
if (!sourceCode)
|
|
return "";
|
|
|
|
columnStyle = columnStyle || WI.SourceCodeLocation.ColumnStyle.OnlyIfLarge;
|
|
nameStyle = nameStyle || WI.SourceCodeLocation.NameStyle.Short;
|
|
prefix = prefix || "";
|
|
|
|
let lineString = lineNumber + 1; // The user visible line number is 1-based.
|
|
if (columnStyle === WI.SourceCodeLocation.ColumnStyle.Shown && columnNumber > 0)
|
|
lineString += ":" + (columnNumber + 1); // The user visible column number is 1-based.
|
|
else if (columnStyle === WI.SourceCodeLocation.ColumnStyle.OnlyIfLarge && columnNumber > WI.SourceCodeLocation.LargeColumnNumber)
|
|
lineString += ":" + (columnNumber + 1); // The user visible column number is 1-based.
|
|
else if (columnStyle === WI.SourceCodeLocation.ColumnStyle.Hidden)
|
|
lineString = "";
|
|
|
|
switch (nameStyle) {
|
|
case WI.SourceCodeLocation.NameStyle.None:
|
|
return prefix + lineString;
|
|
|
|
case WI.SourceCodeLocation.NameStyle.Short:
|
|
case WI.SourceCodeLocation.NameStyle.Full:
|
|
var displayURL = sourceCode.displayURL;
|
|
var name = nameStyle === WI.SourceCodeLocation.NameStyle.Full && displayURL ? displayURL : sourceCode.displayName;
|
|
if (columnStyle === WI.SourceCodeLocation.ColumnStyle.Hidden)
|
|
return prefix + name;
|
|
var lineSuffix = displayURL ? ":" + lineString : WI.UIString(" (line %s)").format(lineString);
|
|
return prefix + name + lineSuffix;
|
|
|
|
default:
|
|
console.error("Unknown nameStyle: " + nameStyle);
|
|
return prefix + lineString;
|
|
}
|
|
}
|
|
|
|
_resetMappedLocation()
|
|
{
|
|
this._mappedLocationIsResolved = false;
|
|
this._mappedResource = null;
|
|
this._mappedLineNumber = NaN;
|
|
this._mappedColumnNumber = NaN;
|
|
}
|
|
|
|
_setMappedLocation(mappedResource, mappedLineNumber, mappedColumnNumber)
|
|
{
|
|
// Called by SourceMapResource when it creates a SourceCodeLocation and already knows the resolved location.
|
|
this._mappedLocationIsResolved = true;
|
|
this._mappedResource = mappedResource;
|
|
this._mappedLineNumber = mappedLineNumber;
|
|
this._mappedColumnNumber = mappedColumnNumber;
|
|
}
|
|
|
|
_resolveFormattedLocation()
|
|
{
|
|
if (this._sourceCode && this._sourceCode.formatterSourceMap) {
|
|
var formattedLocation = this._sourceCode.formatterSourceMap.originalToFormatted(this._lineNumber, this._columnNumber);
|
|
this._formattedLineNumber = formattedLocation.lineNumber;
|
|
this._formattedColumnNumber = formattedLocation.columnNumber;
|
|
} else {
|
|
this._formattedLineNumber = this._lineNumber;
|
|
this._formattedColumnNumber = this._columnNumber;
|
|
}
|
|
}
|
|
|
|
_makeChangeAndDispatchChangeEventIfNeeded(changeFunction)
|
|
{
|
|
var oldSourceCode = this._sourceCode;
|
|
var oldLineNumber = this._lineNumber;
|
|
var oldColumnNumber = this._columnNumber;
|
|
|
|
var oldFormattedLineNumber = this._formattedLineNumber;
|
|
var oldFormattedColumnNumber = this._formattedColumnNumber;
|
|
|
|
var oldDisplaySourceCode = this.displaySourceCode;
|
|
var oldDisplayLineNumber = this.displayLineNumber;
|
|
var oldDisplayColumnNumber = this.displayColumnNumber;
|
|
|
|
this._resetMappedLocation();
|
|
|
|
if (changeFunction)
|
|
changeFunction.call(this);
|
|
|
|
this.resolveMappedLocation();
|
|
this._resolveFormattedLocation();
|
|
|
|
// If the display source code is non-null then the addresses are not NaN and can be compared.
|
|
var displayLocationChanged = false;
|
|
var newDisplaySourceCode = this.displaySourceCode;
|
|
if (oldDisplaySourceCode !== newDisplaySourceCode)
|
|
displayLocationChanged = true;
|
|
else if (newDisplaySourceCode && (oldDisplayLineNumber !== this.displayLineNumber || oldDisplayColumnNumber !== this.displayColumnNumber))
|
|
displayLocationChanged = true;
|
|
|
|
var anyLocationChanged = false;
|
|
if (displayLocationChanged)
|
|
anyLocationChanged = true;
|
|
else if (oldSourceCode !== this._sourceCode)
|
|
anyLocationChanged = true;
|
|
else if (this._sourceCode && (oldLineNumber !== this._lineNumber || oldColumnNumber !== this._columnNumber))
|
|
anyLocationChanged = true;
|
|
else if (this._sourceCode && (oldFormattedLineNumber !== this._formattedLineNumber || oldFormattedColumnNumber !== this._formattedColumnNumber))
|
|
anyLocationChanged = true;
|
|
|
|
if (displayLocationChanged || anyLocationChanged) {
|
|
var oldData = {
|
|
oldSourceCode,
|
|
oldLineNumber,
|
|
oldColumnNumber,
|
|
oldFormattedLineNumber,
|
|
oldFormattedColumnNumber,
|
|
oldDisplaySourceCode,
|
|
oldDisplayLineNumber,
|
|
oldDisplayColumnNumber
|
|
};
|
|
if (displayLocationChanged)
|
|
this.dispatchEventToListeners(WI.SourceCodeLocation.Event.DisplayLocationChanged, oldData);
|
|
if (anyLocationChanged)
|
|
this.dispatchEventToListeners(WI.SourceCodeLocation.Event.LocationChanged, oldData);
|
|
}
|
|
}
|
|
|
|
_sourceCodeSourceMapAdded()
|
|
{
|
|
this._makeChangeAndDispatchChangeEventIfNeeded(null);
|
|
}
|
|
|
|
_sourceCodeFormatterDidChange()
|
|
{
|
|
this._makeChangeAndDispatchChangeEventIfNeeded(null);
|
|
}
|
|
};
|
|
|
|
WI.SourceCodeLocation.DisplayLocationClassName = "display-location";
|
|
|
|
WI.SourceCodeLocation.LargeColumnNumber = 80;
|
|
|
|
WI.SourceCodeLocation.NameStyle = {
|
|
None: "none", // File name not included.
|
|
Short: "short", // Only the file name.
|
|
Full: "full" // Full URL is used.
|
|
};
|
|
|
|
WI.SourceCodeLocation.ColumnStyle = {
|
|
Hidden: "hidden", // line and column numbers are not included.
|
|
OnlyIfLarge: "only-if-large", // column numbers greater than 80 are shown.
|
|
Shown: "shown" // non-zero column numbers are shown.
|
|
};
|
|
|
|
WI.SourceCodeLocation.Event = {
|
|
LocationChanged: "source-code-location-location-changed",
|
|
DisplayLocationChanged: "source-code-location-display-location-changed"
|
|
};
|
|
|
|
/* Models/SourceCodePosition.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceCodePosition = class SourceCodePosition
|
|
{
|
|
constructor(lineNumber, columnNumber)
|
|
{
|
|
this._lineNumber = lineNumber || 0;
|
|
this._columnNumber = columnNumber || 0;
|
|
}
|
|
|
|
// Public
|
|
|
|
get lineNumber() { return this._lineNumber; }
|
|
get columnNumber() { return this._columnNumber; }
|
|
|
|
offsetColumn(delta)
|
|
{
|
|
console.assert(this._columnNumber + delta >= 0);
|
|
return new WI.SourceCodePosition(this._lineNumber, this._columnNumber + delta);
|
|
}
|
|
|
|
equals(position)
|
|
{
|
|
return this._lineNumber === position.lineNumber && this._columnNumber === position.columnNumber;
|
|
}
|
|
|
|
isBefore(position)
|
|
{
|
|
if (this._lineNumber < position.lineNumber)
|
|
return true;
|
|
if (this._lineNumber === position.lineNumber && this._columnNumber < position.columnNumber)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
isAfter(position)
|
|
{
|
|
if (this._lineNumber > position.lineNumber)
|
|
return true;
|
|
if (this._lineNumber === position.lineNumber && this._columnNumber > position.columnNumber)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
isWithin(startPosition, endPosition)
|
|
{
|
|
console.assert(startPosition.isBefore(endPosition) || startPosition.equals(endPosition));
|
|
|
|
if (this.equals(startPosition) || this.equals(endPosition))
|
|
return true;
|
|
if (this.isAfter(startPosition) && this.isBefore(endPosition))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
toCodeMirror()
|
|
{
|
|
return {line: this._lineNumber, ch: this._columnNumber};
|
|
}
|
|
};
|
|
|
|
/* Models/Timeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Timeline = class Timeline extends WI.Object
|
|
{
|
|
constructor(type)
|
|
{
|
|
super();
|
|
|
|
this._type = type;
|
|
|
|
this.reset(true);
|
|
}
|
|
|
|
// Static
|
|
|
|
static create(type)
|
|
{
|
|
if (type === WI.TimelineRecord.Type.Network)
|
|
return new WI.NetworkTimeline(type);
|
|
|
|
if (type === WI.TimelineRecord.Type.CPU)
|
|
return new WI.CPUTimeline(type);
|
|
|
|
if (type === WI.TimelineRecord.Type.Memory)
|
|
return new WI.MemoryTimeline(type);
|
|
|
|
if (type === WI.TimelineRecord.Type.Media)
|
|
return new WI.MediaTimeline(type);
|
|
|
|
return new WI.Timeline(type);
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get startTime() { return this._startTime; }
|
|
get endTime() { return this._endTime; }
|
|
get records() { return this._records; }
|
|
|
|
reset(suppressEvents)
|
|
{
|
|
this._records = [];
|
|
this._startTime = NaN;
|
|
this._endTime = NaN;
|
|
|
|
if (!suppressEvents) {
|
|
this.dispatchEventToListeners(WI.Timeline.Event.TimesUpdated);
|
|
this.dispatchEventToListeners(WI.Timeline.Event.Reset);
|
|
}
|
|
}
|
|
|
|
addRecord(record, options = {})
|
|
{
|
|
if (record.updatesDynamically)
|
|
record.addEventListener(WI.TimelineRecord.Event.Updated, this._recordUpdated, this);
|
|
|
|
// Because records can be nested, it is possible that outer records with an early start time
|
|
// may be completed and added to the Timeline after inner records with a later start time
|
|
// were already added. In most cases this is a small drift, so make an effort to still keep
|
|
// the list sorted. Do it now, when inserting, so if the timeline is visible it has the
|
|
// best chance of being as accurate as possible during a recording.
|
|
this._tryInsertingRecordInSortedOrder(record);
|
|
|
|
this._updateTimesIfNeeded(record);
|
|
|
|
this.dispatchEventToListeners(WI.Timeline.Event.RecordAdded, {record});
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.Timeline.TimelineTypeCookieKey] = this._type;
|
|
}
|
|
|
|
refresh()
|
|
{
|
|
this.dispatchEventToListeners(WI.Timeline.Event.Refreshed);
|
|
}
|
|
|
|
closestRecordTo(timestamp)
|
|
{
|
|
let lowerIndex = this._records.lowerBound(timestamp, (time, record) => time - record.endTime);
|
|
|
|
let recordBefore = this._records[lowerIndex - 1];
|
|
let recordAfter = this._records[lowerIndex];
|
|
if (!recordBefore && !recordAfter)
|
|
return null;
|
|
if (!recordBefore && recordAfter)
|
|
return recordAfter;
|
|
if (!recordAfter && recordBefore)
|
|
return recordBefore;
|
|
|
|
let before = Math.abs(recordBefore.endTime - timestamp);
|
|
let after = Math.abs(recordAfter.startTime - timestamp);
|
|
return (before < after) ? recordBefore : recordAfter;
|
|
}
|
|
|
|
recordsInTimeRange(startTime, endTime, {includeRecordBeforeStart, includeRecordAfterEnd} = {})
|
|
{
|
|
let lowerIndex = this._records.lowerBound(startTime, (time, record) => time - record.endTime);
|
|
if (includeRecordBeforeStart && lowerIndex > 0) {
|
|
lowerIndex--;
|
|
|
|
// If the record right before is a child of the same type of record, then use the parent as the before index.
|
|
let recordBefore = this._records[lowerIndex];
|
|
if (recordBefore.parent && recordBefore.parent.type === recordBefore.type) {
|
|
lowerIndex--;
|
|
while (this._records[lowerIndex] !== recordBefore.parent)
|
|
lowerIndex--;
|
|
}
|
|
}
|
|
|
|
let upperIndex = this._records.upperBound(endTime, (time, record) => time - record.startTime);
|
|
if (includeRecordAfterEnd && upperIndex < this._records.length)
|
|
++upperIndex;
|
|
|
|
return this._records.slice(lowerIndex, upperIndex);
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateTimesIfNeeded(record)
|
|
{
|
|
let changed = false;
|
|
|
|
// Some records adjust their start time / end time to values that may be before
|
|
// or after the bounds the recording actually ran. Use the unadjusted times for
|
|
// the Timeline's bounds. Otherwise we may extend the timeline graphs to a time
|
|
// that was conceptually before / after the user started / stopping recording.
|
|
let recordStartTime = record.unadjustedStartTime;
|
|
let recordEndTime = record.unadjustedEndTime;
|
|
|
|
if (isNaN(this._startTime) || recordStartTime < this._startTime) {
|
|
this._startTime = recordStartTime;
|
|
changed = true;
|
|
}
|
|
|
|
if (isNaN(this._endTime) || this._endTime < recordEndTime) {
|
|
this._endTime = recordEndTime;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed)
|
|
this.dispatchEventToListeners(WI.Timeline.Event.TimesUpdated);
|
|
}
|
|
|
|
_recordUpdated(event)
|
|
{
|
|
this._updateTimesIfNeeded(event.target);
|
|
}
|
|
|
|
_tryInsertingRecordInSortedOrder(record)
|
|
{
|
|
// Fast case add to the end.
|
|
let lastValue = this._records.lastValue;
|
|
if (!lastValue || lastValue.startTime < record.startTime || record.updatesDynamically) {
|
|
this._records.push(record);
|
|
return;
|
|
}
|
|
|
|
// Slow case, try to insert in the last 20 records.
|
|
let start = this._records.length - 2;
|
|
let end = Math.max(this._records.length - 20, 0);
|
|
for (let i = start; i >= end; --i) {
|
|
if (this._records[i].startTime < record.startTime) {
|
|
this._records.insertAtIndex(record, i + 1);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Give up and add to the end.
|
|
this._records.push(record);
|
|
}
|
|
};
|
|
|
|
WI.Timeline.Event = {
|
|
Reset: "timeline-reset",
|
|
RecordAdded: "timeline-record-added",
|
|
TimesUpdated: "timeline-times-updated",
|
|
Refreshed: "timeline-refreshed",
|
|
};
|
|
|
|
WI.Timeline.TimelineTypeCookieKey = "timeline-type";
|
|
|
|
/* Models/TimelineRange.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TimelineRange = class TimelineRange
|
|
{
|
|
constructor(startValue, endValue)
|
|
{
|
|
this._startValue = startValue;
|
|
this._endValue = endValue;
|
|
}
|
|
|
|
get startValue() { return this._startValue; }
|
|
set startValue(x) { this._startValue = x; }
|
|
|
|
get endValue() { return this._endValue; }
|
|
set endValue(x) { this._endValue = x; }
|
|
};
|
|
|
|
/* Models/TimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TimelineRecord = class TimelineRecord extends WI.Object
|
|
{
|
|
constructor(type, startTime, endTime, stackTrace, sourceCodeLocation)
|
|
{
|
|
super();
|
|
|
|
console.assert(type);
|
|
|
|
if (type in WI.TimelineRecord.Type)
|
|
type = WI.TimelineRecord.Type[type];
|
|
|
|
this._type = type;
|
|
this._startTime = startTime || NaN;
|
|
this._endTime = endTime || NaN;
|
|
this._stackTrace = stackTrace || null;
|
|
this._sourceCodeLocation = sourceCodeLocation || null;
|
|
this._children = [];
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
switch (json.type) {
|
|
case WI.TimelineRecord.Type.Network:
|
|
return WI.ResourceTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.Layout:
|
|
return WI.LayoutTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.Script:
|
|
return WI.ScriptTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.RenderingFrame:
|
|
return WI.RenderingFrameTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.CPU:
|
|
return WI.CPUTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.Memory:
|
|
return WI.MemoryTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.HeapAllocations:
|
|
return WI.HeapAllocationsTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.Media:
|
|
return WI.MediaTimelineRecord.fromJSON(json);
|
|
case WI.TimelineRecord.Type.Screenshots:
|
|
return WI.ScreenshotsTimelineRecord.fromJSON(json);
|
|
default:
|
|
console.error("Unknown TimelineRecord.Type: " + json.type, json);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
// Public
|
|
|
|
get type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
get startTime()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return this._startTime;
|
|
}
|
|
|
|
get activeStartTime()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return this.startTime;
|
|
}
|
|
|
|
get unadjustedStartTime()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return this.startTime;
|
|
}
|
|
|
|
get endTime()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return this._endTime;
|
|
}
|
|
|
|
get unadjustedEndTime()
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return this.endTime;
|
|
}
|
|
|
|
get duration()
|
|
{
|
|
// Use the getters instead of the properties so this works for subclasses that override the getters.
|
|
return this.endTime - this.startTime;
|
|
}
|
|
|
|
get inactiveDuration()
|
|
{
|
|
// Use the getters instead of the properties so this works for subclasses that override the getters.
|
|
return this.activeStartTime - this.startTime;
|
|
}
|
|
|
|
get activeDuration()
|
|
{
|
|
// Use the getters instead of the properties so this works for subclasses that override the getters.
|
|
return this.endTime - this.activeStartTime;
|
|
}
|
|
|
|
get updatesDynamically()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return false;
|
|
}
|
|
|
|
get usesActiveStartTime()
|
|
{
|
|
// Implemented by subclasses if needed.
|
|
return false;
|
|
}
|
|
|
|
get stackTrace()
|
|
{
|
|
return this._stackTrace;
|
|
}
|
|
|
|
get initiatorCallFrame()
|
|
{
|
|
if (!this._stackTrace)
|
|
return null;
|
|
|
|
// Return the first non-native code call frame as the initiator.
|
|
for (let frame of this._stackTrace.callFrames) {
|
|
if (!frame.nativeCode)
|
|
return frame;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
get sourceCodeLocation()
|
|
{
|
|
return this._sourceCodeLocation;
|
|
}
|
|
|
|
get parent()
|
|
{
|
|
return this._parent;
|
|
}
|
|
|
|
set parent(x)
|
|
{
|
|
if (this._parent === x)
|
|
return;
|
|
|
|
this._parent = x;
|
|
}
|
|
|
|
get children()
|
|
{
|
|
return this._children;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.TimelineRecord.SourceCodeURLCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.sourceCode.url ? this._sourceCodeLocation.sourceCode.url.hash : null : null;
|
|
cookie[WI.TimelineRecord.SourceCodeLocationLineCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.lineNumber : null;
|
|
cookie[WI.TimelineRecord.SourceCodeLocationColumnCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.columnNumber : null;
|
|
cookie[WI.TimelineRecord.TypeCookieKey] = this._type || null;
|
|
}
|
|
};
|
|
|
|
WI.TimelineRecord.Event = {
|
|
Updated: "timeline-record-updated"
|
|
};
|
|
|
|
WI.TimelineRecord.Type = {
|
|
Network: "timeline-record-type-network",
|
|
Layout: "timeline-record-type-layout",
|
|
Script: "timeline-record-type-script",
|
|
RenderingFrame: "timeline-record-type-rendering-frame",
|
|
CPU: "timeline-record-type-cpu",
|
|
Memory: "timeline-record-type-memory",
|
|
HeapAllocations: "timeline-record-type-heap-allocations",
|
|
Media: "timeline-record-type-media",
|
|
Screenshots: "timeline-record-type-screenshots",
|
|
};
|
|
|
|
WI.TimelineRecord.TypeIdentifier = "timeline-record";
|
|
WI.TimelineRecord.SourceCodeURLCookieKey = "timeline-record-source-code-url";
|
|
WI.TimelineRecord.SourceCodeLocationLineCookieKey = "timeline-record-source-code-location-line";
|
|
WI.TimelineRecord.SourceCodeLocationColumnCookieKey = "timeline-record-source-code-location-column";
|
|
WI.TimelineRecord.TypeCookieKey = "timeline-record-type";
|
|
|
|
/* Models/Resource.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Resource = class Resource extends WI.SourceCode
|
|
{
|
|
constructor(url, {mimeType, type, loaderIdentifier, targetId, requestIdentifier, requestMethod, requestHeaders, requestData, requestSentTimestamp, requestSentWalltime, referrerPolicy, integrity, initiatorStackTrace, initiatorSourceCodeLocation, initiatorNode} = {})
|
|
{
|
|
console.assert(url);
|
|
console.assert(!initiatorStackTrace || initiatorStackTrace instanceof WI.StackTrace, initiatorStackTrace);
|
|
|
|
super(url);
|
|
|
|
if (type in WI.Resource.Type)
|
|
type = WI.Resource.Type[type];
|
|
else if (type === "Stylesheet") {
|
|
// COMPATIBILITY (iOS 13): Page.ResourceType.Stylesheet was renamed to Page.ResourceType.StyleSheet.
|
|
type = WI.Resource.Type.StyleSheet;
|
|
}
|
|
|
|
this._mimeType = mimeType;
|
|
this._mimeTypeComponents = null;
|
|
this._type = Resource.resolvedType(type, mimeType);
|
|
this._loaderIdentifier = loaderIdentifier || null;
|
|
this._requestIdentifier = requestIdentifier || null;
|
|
this._queryStringParameters = undefined;
|
|
this._requestFormParameters = undefined;
|
|
this._requestMethod = requestMethod || null;
|
|
this._requestData = requestData || null;
|
|
this._requestHeaders = requestHeaders || {};
|
|
this._responseHeaders = {};
|
|
this._requestCookies = null;
|
|
this._responseCookies = null;
|
|
this._serverTimingEntries = null;
|
|
this._parentFrame = null;
|
|
this._initiatorStackTrace = initiatorStackTrace || null;
|
|
this._initiatorSourceCodeLocation = initiatorSourceCodeLocation || null;
|
|
this._initiatorNode = initiatorNode || null;
|
|
this._initiatedResources = [];
|
|
this._requestSentTimestamp = requestSentTimestamp || NaN;
|
|
this._requestSentWalltime = requestSentWalltime || NaN;
|
|
this._responseReceivedTimestamp = NaN;
|
|
this._lastDataReceivedTimestamp = NaN;
|
|
this._finishedOrFailedTimestamp = NaN;
|
|
this._finishThenRequestContentPromise = null;
|
|
this._statusCode = NaN;
|
|
this._statusText = null;
|
|
this._cached = false;
|
|
this._canceled = false;
|
|
this._finished = false;
|
|
this._failed = false;
|
|
this._failureReasonText = null;
|
|
this._receivedNetworkLoadMetrics = false;
|
|
this._responseSource = WI.Resource.ResponseSource.Unknown;
|
|
this._security = null;
|
|
this._timingData = new WI.ResourceTimingData(this);
|
|
this._protocol = null;
|
|
this._priority = WI.Resource.NetworkPriority.Unknown;
|
|
this._remoteAddress = null;
|
|
this._connectionIdentifier = null;
|
|
this._isProxyConnection = false;
|
|
this._target = targetId ? WI.targetManager.targetForIdentifier(targetId) : WI.mainTarget;
|
|
this._redirects = [];
|
|
this._referrerPolicy = referrerPolicy ?? null;
|
|
this._integrity = integrity ?? null;
|
|
|
|
// Exact sizes if loaded over the network or cache.
|
|
this._requestHeadersTransferSize = NaN;
|
|
this._requestBodyTransferSize = NaN;
|
|
this._responseHeadersTransferSize = NaN;
|
|
this._responseBodyTransferSize = NaN;
|
|
this._responseBodySize = NaN;
|
|
this._cachedResponseBodySize = NaN;
|
|
|
|
// Estimated sizes (if backend does not provide metrics).
|
|
this._estimatedSize = NaN;
|
|
this._estimatedTransferSize = NaN;
|
|
this._estimatedResponseHeadersSize = NaN;
|
|
|
|
if (this._initiatorSourceCodeLocation && this._initiatorSourceCodeLocation.sourceCode instanceof WI.Resource)
|
|
this._initiatorSourceCodeLocation.sourceCode.addInitiatedResource(this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static resolvedType(type, mimeType)
|
|
{
|
|
if (type && type !== WI.Resource.Type.Other)
|
|
return type;
|
|
|
|
return Resource.typeFromMIMEType(mimeType);
|
|
}
|
|
|
|
static typeFromMIMEType(mimeType)
|
|
{
|
|
if (!mimeType)
|
|
return WI.Resource.Type.Other;
|
|
|
|
mimeType = parseMIMEType(mimeType).type;
|
|
|
|
if (mimeType in WI.Resource._mimeTypeMap)
|
|
return WI.Resource._mimeTypeMap[mimeType];
|
|
|
|
if (mimeType.startsWith("image/"))
|
|
return WI.Resource.Type.Image;
|
|
|
|
if (mimeType.startsWith("font/"))
|
|
return WI.Resource.Type.Font;
|
|
|
|
return WI.Resource.Type.Other;
|
|
}
|
|
|
|
static displayNameForType(type, plural)
|
|
{
|
|
switch (type) {
|
|
case WI.Resource.Type.Document:
|
|
if (plural)
|
|
return WI.UIString("Documents");
|
|
return WI.UIString("Document");
|
|
case WI.Resource.Type.StyleSheet:
|
|
if (plural)
|
|
return WI.UIString("Style Sheets");
|
|
return WI.UIString("Style Sheet");
|
|
case WI.Resource.Type.Image:
|
|
if (plural)
|
|
return WI.UIString("Images");
|
|
return WI.UIString("Image");
|
|
case WI.Resource.Type.Font:
|
|
if (plural)
|
|
return WI.UIString("Fonts");
|
|
return WI.UIString("Font");
|
|
case WI.Resource.Type.Script:
|
|
if (plural)
|
|
return WI.UIString("Scripts");
|
|
return WI.UIString("Script");
|
|
case WI.Resource.Type.XHR:
|
|
if (plural)
|
|
return WI.UIString("XHRs");
|
|
return WI.UIString("XHR");
|
|
case WI.Resource.Type.Fetch:
|
|
if (plural)
|
|
return WI.UIString("Fetches", "Resources loaded via 'fetch' method");
|
|
return WI.repeatedUIString.fetch();
|
|
case WI.Resource.Type.Ping:
|
|
if (plural)
|
|
return WI.UIString("Pings");
|
|
return WI.UIString("Ping");
|
|
case WI.Resource.Type.Beacon:
|
|
if (plural)
|
|
return WI.UIString("Beacons");
|
|
return WI.UIString("Beacon");
|
|
case WI.Resource.Type.WebSocket:
|
|
if (plural)
|
|
return WI.UIString("Sockets");
|
|
return WI.UIString("Socket");
|
|
case WI.Resource.Type.EventSource:
|
|
if (plural)
|
|
return WI.UIString("EventSources", "Display name for the type of network requests sent via EventSource(s) API (https://developer.mozilla.org/en-US/docs/Web/API/EventSource)");
|
|
return WI.UIString("EventSource", "Display name for the type of network requests sent via EventSource API (https://developer.mozilla.org/en-US/docs/Web/API/EventSource)");
|
|
case WI.Resource.Type.Other:
|
|
return WI.UIString("Other");
|
|
default:
|
|
console.error("Unknown resource type", type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static classNamesForResource(resource)
|
|
{
|
|
let classes = [];
|
|
|
|
let localResourceOverride = resource.localResourceOverride || WI.networkManager.localResourceOverridesForURL(resource.url).filter((localResourceOverride) => !localResourceOverride.disabled)[0];
|
|
let isOverride = !!resource.localResourceOverride;
|
|
let wasOverridden = resource.responseSource === WI.Resource.ResponseSource.InspectorOverride;
|
|
let shouldBeOverridden = resource.isLoading() && localResourceOverride;
|
|
let shouldBeBlocked = (resource.failed || isOverride) && localResourceOverride?.type === WI.LocalResourceOverride.InterceptType.Block;
|
|
if (isOverride || wasOverridden || shouldBeOverridden || shouldBeBlocked) {
|
|
classes.push("override");
|
|
|
|
if (shouldBeBlocked || localResourceOverride?.type === WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork)
|
|
classes.push("skip-network");
|
|
|
|
if (localResourceOverride?.localResource.mappedFilePath)
|
|
classes.push("mapped-file");
|
|
}
|
|
|
|
if (resource.type === WI.Resource.Type.Other) {
|
|
if (resource.requestedByteRange)
|
|
classes.push("resource-type-range");
|
|
} else
|
|
classes.push(resource.type);
|
|
|
|
return classes;
|
|
}
|
|
|
|
static displayNameForProtocol(protocol)
|
|
{
|
|
switch (protocol) {
|
|
case "h2":
|
|
return "HTTP/2";
|
|
case "http/1.0":
|
|
return "HTTP/1.0";
|
|
case "http/1.1":
|
|
return "HTTP/1.1";
|
|
case "spdy/2":
|
|
return "SPDY/2";
|
|
case "spdy/3":
|
|
return "SPDY/3";
|
|
case "spdy/3.1":
|
|
return "SPDY/3.1";
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static comparePriority(a, b)
|
|
{
|
|
console.assert(typeof a === "symbol");
|
|
console.assert(typeof b === "symbol");
|
|
|
|
const map = {
|
|
[WI.Resource.NetworkPriority.Unknown]: 0,
|
|
[WI.Resource.NetworkPriority.Low]: 1,
|
|
[WI.Resource.NetworkPriority.Medium]: 2,
|
|
[WI.Resource.NetworkPriority.High]: 3,
|
|
};
|
|
|
|
let aNum = map[a] || 0;
|
|
let bNum = map[b] || 0;
|
|
return aNum - bNum;
|
|
}
|
|
|
|
static displayNameForPriority(priority)
|
|
{
|
|
switch (priority) {
|
|
case WI.Resource.NetworkPriority.Low:
|
|
return WI.UIString("Low", "Low @ Network Priority", "Low network request priority");
|
|
case WI.Resource.NetworkPriority.Medium:
|
|
return WI.UIString("Medium", "Medium @ Network Priority", "Medium network request priority");
|
|
case WI.Resource.NetworkPriority.High:
|
|
return WI.UIString("High", "High @ Network Priority", "High network request priority");
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static responseSourceFromPayload(source)
|
|
{
|
|
if (!source)
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
|
|
switch (source) {
|
|
case InspectorBackend.Enum.Network.ResponseSource.Unknown:
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
case InspectorBackend.Enum.Network.ResponseSource.Network:
|
|
return WI.Resource.ResponseSource.Network;
|
|
case InspectorBackend.Enum.Network.ResponseSource.MemoryCache:
|
|
return WI.Resource.ResponseSource.MemoryCache;
|
|
case InspectorBackend.Enum.Network.ResponseSource.DiskCache:
|
|
return WI.Resource.ResponseSource.DiskCache;
|
|
case InspectorBackend.Enum.Network.ResponseSource.ServiceWorker:
|
|
return WI.Resource.ResponseSource.ServiceWorker;
|
|
case InspectorBackend.Enum.Network.ResponseSource.InspectorOverride:
|
|
return WI.Resource.ResponseSource.InspectorOverride;
|
|
default:
|
|
console.error("Unknown response source type", source);
|
|
return WI.Resource.ResponseSource.Unknown;
|
|
}
|
|
}
|
|
|
|
static networkPriorityFromPayload(priority)
|
|
{
|
|
switch (priority) {
|
|
case InspectorBackend.Enum.Network.MetricsPriority.Low:
|
|
return WI.Resource.NetworkPriority.Low;
|
|
case InspectorBackend.Enum.Network.MetricsPriority.Medium:
|
|
return WI.Resource.NetworkPriority.Medium;
|
|
case InspectorBackend.Enum.Network.MetricsPriority.High:
|
|
return WI.Resource.NetworkPriority.High;
|
|
default:
|
|
console.error("Unknown metrics priority", priority);
|
|
return WI.Resource.NetworkPriority.Unknown;
|
|
}
|
|
}
|
|
|
|
static connectionIdentifierFromPayload(connectionIdentifier)
|
|
{
|
|
// Map backend connection identifiers to an easier to read number.
|
|
if (!WI.Resource.connectionIdentifierMap) {
|
|
WI.Resource.connectionIdentifierMap = new Map;
|
|
WI.Resource.nextConnectionIdentifier = 1;
|
|
}
|
|
|
|
let id = WI.Resource.connectionIdentifierMap.get(connectionIdentifier);
|
|
if (id)
|
|
return id;
|
|
|
|
id = WI.Resource.nextConnectionIdentifier++;
|
|
WI.Resource.connectionIdentifierMap.set(connectionIdentifier, id);
|
|
return id;
|
|
}
|
|
|
|
// Public
|
|
|
|
get mimeType() { return this._mimeType; }
|
|
get target() { return this._target; }
|
|
get type() { return this._type; }
|
|
get loaderIdentifier() { return this._loaderIdentifier; }
|
|
get requestIdentifier() { return this._requestIdentifier; }
|
|
get requestMethod() { return this._requestMethod; }
|
|
get requestData() { return this._requestData; }
|
|
get initiatorStackTrace() { return this._initiatorStackTrace; }
|
|
get initiatorSourceCodeLocation() { return this._initiatorSourceCodeLocation; }
|
|
get initiatorNode() { return this._initiatorNode; }
|
|
get initiatedResources() { return this._initiatedResources; }
|
|
get statusCode() { return this._statusCode; }
|
|
get statusText() { return this._statusText; }
|
|
get responseSource() { return this._responseSource; }
|
|
get security() { return this._security; }
|
|
get timingData() { return this._timingData; }
|
|
get protocol() { return this._protocol; }
|
|
get priority() { return this._priority; }
|
|
get remoteAddress() { return this._remoteAddress; }
|
|
get connectionIdentifier() { return this._connectionIdentifier; }
|
|
get parentFrame() { return this._parentFrame; }
|
|
get finished() { return this._finished; }
|
|
get failed() { return this._failed; }
|
|
get canceled() { return this._canceled; }
|
|
get failureReasonText() { return this._failureReasonText; }
|
|
get requestHeaders() { return this._requestHeaders; }
|
|
get responseHeaders() { return this._responseHeaders; }
|
|
get requestSentTimestamp() { return this._requestSentTimestamp; }
|
|
get requestSentWalltime() { return this._requestSentWalltime; }
|
|
get responseReceivedTimestamp() { return this._responseReceivedTimestamp; }
|
|
get lastDataReceivedTimestamp() { return this._lastDataReceivedTimestamp; }
|
|
get finishedOrFailedTimestamp() { return this._finishedOrFailedTimestamp; }
|
|
get cached() { return this._cached; }
|
|
get requestHeadersTransferSize() { return this._requestHeadersTransferSize; }
|
|
get requestBodyTransferSize() { return this._requestBodyTransferSize; }
|
|
get responseHeadersTransferSize() { return this._responseHeadersTransferSize; }
|
|
get responseBodyTransferSize() { return this._responseBodyTransferSize; }
|
|
get cachedResponseBodySize() { return this._cachedResponseBodySize; }
|
|
get redirects() { return this._redirects; }
|
|
get referrerPolicy() { return this._referrerPolicy; }
|
|
get integrity() { return this._integrity; }
|
|
|
|
get loadedSecurely()
|
|
{
|
|
if (this.urlComponents.scheme !== "https" && this.urlComponents.scheme !== "wss" && this.urlComponents.scheme !== "sftp")
|
|
return false;
|
|
if (isNaN(this._timingData.secureConnectionStart) && !isNaN(this._timingData.connectionStart))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
get isScript()
|
|
{
|
|
return this._type === Resource.Type.Script;
|
|
}
|
|
|
|
get supportsScriptBlackboxing()
|
|
{
|
|
if (this.localResourceOverride)
|
|
return false;
|
|
if (!this.finished || this.failed)
|
|
return false;
|
|
return super.supportsScriptBlackboxing;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
return WI.displayNameForURL(this._url, this.urlComponents);
|
|
}
|
|
|
|
get displayURL()
|
|
{
|
|
const isMultiLine = true;
|
|
const dataURIMaxSize = 64;
|
|
return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize);
|
|
}
|
|
|
|
get displayRemoteAddress()
|
|
{
|
|
if (this._isProxyConnection)
|
|
return WI.UIString("%s (Proxy)", "%s (Proxy) @ Resource Remote Address", "Label for the IP address of a proxy server used to retrieve a network resource.").format(this._remoteAddress);
|
|
|
|
return this._remoteAddress;
|
|
}
|
|
|
|
get mimeTypeComponents()
|
|
{
|
|
if (!this._mimeTypeComponents)
|
|
this._mimeTypeComponents = parseMIMEType(this._mimeType);
|
|
return this._mimeTypeComponents;
|
|
}
|
|
|
|
get syntheticMIMEType()
|
|
{
|
|
// Resources are often transferred with a MIME-type that doesn't match the purpose the
|
|
// resource was loaded for, which is what WI.Resource.Type represents.
|
|
// This getter generates a MIME-type, if needed, that matches the resource type.
|
|
|
|
// If the type matches the Resource.Type of the MIME-type, then return the actual MIME-type.
|
|
if (this._type === WI.Resource.typeFromMIMEType(this._mimeType))
|
|
return this._mimeType;
|
|
|
|
// Return the default MIME-types for the Resource.Type, since the current MIME-type
|
|
// does not match what is expected for the Resource.Type.
|
|
switch (this._type) {
|
|
case WI.Resource.Type.StyleSheet:
|
|
return "text/css";
|
|
case WI.Resource.Type.Script:
|
|
return "text/javascript";
|
|
}
|
|
|
|
// Return the actual MIME-type since we don't have a better synthesized one to return.
|
|
return this._mimeType;
|
|
}
|
|
|
|
get hasMetadata()
|
|
{
|
|
// Some metadata is only collected when Web Inspector is open (e.g. resource timing data, HTTP method, request headers, etc.).
|
|
// Use `_requestIdentifier` as a general signal since it is always included when metadata is collected.
|
|
return !!this._requestIdentifier;
|
|
}
|
|
|
|
createObjectURL()
|
|
{
|
|
let revision = this.currentRevision;
|
|
let blobContent = revision.blobContent;
|
|
if (blobContent)
|
|
return URL.createObjectURL(blobContent)
|
|
|
|
// If content is not available, fallback to using original URL.
|
|
// The client may try to revoke it, but nothing will happen.
|
|
return this._url;
|
|
}
|
|
|
|
isMainResource()
|
|
{
|
|
return this._parentFrame ? this._parentFrame.mainResource === this : false;
|
|
}
|
|
|
|
addInitiatedResource(resource)
|
|
{
|
|
if (!(resource instanceof WI.Resource))
|
|
return;
|
|
|
|
this._initiatedResources.push(resource);
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.InitiatedResourcesDidChange);
|
|
}
|
|
|
|
get queryStringParameters()
|
|
{
|
|
if (this._queryStringParameters === undefined)
|
|
this._queryStringParameters = parseQueryString(this.urlComponents.queryString, true);
|
|
return this._queryStringParameters;
|
|
}
|
|
|
|
get requestFormParameters()
|
|
{
|
|
if (this._requestFormParameters === undefined)
|
|
this._requestFormParameters = this.hasRequestFormParameters() ? parseQueryString(this.requestData, true) : null;
|
|
return this._requestFormParameters;
|
|
}
|
|
|
|
get requestDataContentType()
|
|
{
|
|
return this._requestHeaders.valueForCaseInsensitiveKey("Content-Type") || null;
|
|
}
|
|
|
|
get requestCookies()
|
|
{
|
|
if (!this._requestCookies)
|
|
this._requestCookies = WI.Cookie.parseCookieRequestHeader(this._requestHeaders.valueForCaseInsensitiveKey("Cookie"));
|
|
|
|
return this._requestCookies;
|
|
}
|
|
|
|
get responseCookies()
|
|
{
|
|
if (!this._responseCookies) {
|
|
// FIXME: The backend sends multiple "Set-Cookie" headers in one "Set-Cookie" with multiple values
|
|
// separated by ", ". This doesn't allow us to safely distinguish between a ", " that separates
|
|
// multiple headers or one that may be valid part of a Cookie's value or attribute, such as the
|
|
// ", " in the the date format "Expires=Tue, 03-Oct-2017 04:39:21 GMT". To improve heuristics
|
|
// we do a negative lookahead for numbers, but we can still fail on cookie values containing ", ".
|
|
let rawCombinedHeader = this._responseHeaders.valueForCaseInsensitiveKey("Set-Cookie") || "";
|
|
let setCookieHeaders = rawCombinedHeader.split(/, (?![0-9])/);
|
|
let cookies = [];
|
|
for (let header of setCookieHeaders) {
|
|
let cookie = WI.Cookie.parseSetCookieResponseHeader(header);
|
|
if (cookie)
|
|
cookies.push(cookie);
|
|
}
|
|
this._responseCookies = cookies;
|
|
}
|
|
|
|
return this._responseCookies;
|
|
}
|
|
|
|
get requestSentDate()
|
|
{
|
|
return isNaN(this._requestSentWalltime) ? null : new Date(this._requestSentWalltime * 1000);
|
|
}
|
|
|
|
get lastRedirectReceivedTimestamp()
|
|
{
|
|
return this._redirects.length ? this._redirects.lastValue.timestamp : NaN;
|
|
}
|
|
|
|
get firstTimestamp()
|
|
{
|
|
return this.timingData.startTime || this.lastRedirectReceivedTimestamp || this.responseReceivedTimestamp || this.lastDataReceivedTimestamp || this.finishedOrFailedTimestamp;
|
|
}
|
|
|
|
get lastTimestamp()
|
|
{
|
|
return this.timingData.responseEnd || this.lastDataReceivedTimestamp || this.responseReceivedTimestamp || this.lastRedirectReceivedTimestamp || this.requestSentTimestamp;
|
|
}
|
|
|
|
get latency()
|
|
{
|
|
return this.timingData.responseStart - this.timingData.requestStart;
|
|
}
|
|
|
|
get receiveDuration()
|
|
{
|
|
return this.timingData.responseEnd - this.timingData.responseStart;
|
|
}
|
|
|
|
get totalDuration()
|
|
{
|
|
return this.timingData.responseEnd - this.timingData.startTime;
|
|
}
|
|
|
|
get size()
|
|
{
|
|
if (!isNaN(this._cachedResponseBodySize))
|
|
return this._cachedResponseBodySize;
|
|
|
|
if (!isNaN(this._responseBodySize) && this._responseBodySize !== 0)
|
|
return this._responseBodySize;
|
|
|
|
return this._estimatedSize;
|
|
}
|
|
|
|
get networkEncodedSize()
|
|
{
|
|
return this._responseBodyTransferSize;
|
|
}
|
|
|
|
get networkDecodedSize()
|
|
{
|
|
return this._responseBodySize;
|
|
}
|
|
|
|
get networkTotalTransferSize()
|
|
{
|
|
return this._responseHeadersTransferSize + this._responseBodyTransferSize;
|
|
}
|
|
|
|
get estimatedNetworkEncodedSize()
|
|
{
|
|
let exact = this.networkEncodedSize;
|
|
if (!isNaN(exact))
|
|
return exact;
|
|
|
|
if (this._cached)
|
|
return 0;
|
|
|
|
// FIXME: <https://webkit.org/b/158463> Network: Correctly report encoded data length (transfer size) from CFNetwork to NetworkResourceLoader
|
|
// macOS provides the decoded transfer size instead of the encoded size
|
|
// for estimatedTransferSize. So prefer the "Content-Length" property
|
|
// on mac if it is available.
|
|
if (WI.Platform.name === "mac") {
|
|
let contentLength = Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"));
|
|
if (!isNaN(contentLength))
|
|
return contentLength;
|
|
}
|
|
|
|
if (!isNaN(this._estimatedTransferSize))
|
|
return this._estimatedTransferSize;
|
|
|
|
// If we did not receive actual transfer size from network
|
|
// stack, we prefer using Content-Length over resourceSize as
|
|
// resourceSize may differ from actual transfer size if platform's
|
|
// network stack performed decoding (e.g. gzip decompression).
|
|
// The Content-Length, though, is expected to come from raw
|
|
// response headers and will reflect actual transfer length.
|
|
// This won't work for chunked content encoding, so fall back to
|
|
// resourceSize when we don't have Content-Length. This still won't
|
|
// work for chunks with non-trivial encodings. We need a way to
|
|
// get actual transfer size from the network stack.
|
|
|
|
return Number(this._responseHeaders.valueForCaseInsensitiveKey("Content-Length") || this._estimatedSize);
|
|
}
|
|
|
|
get estimatedTotalTransferSize()
|
|
{
|
|
let exact = this.networkTotalTransferSize;
|
|
if (!isNaN(exact))
|
|
return exact;
|
|
|
|
if (this.statusCode === 304) // Not modified
|
|
return this._estimatedResponseHeadersSize;
|
|
|
|
if (this._cached)
|
|
return 0;
|
|
|
|
return this._estimatedResponseHeadersSize + this.estimatedNetworkEncodedSize;
|
|
}
|
|
|
|
get compressed()
|
|
{
|
|
let contentEncoding = this._responseHeaders.valueForCaseInsensitiveKey("Content-Encoding");
|
|
return !!(contentEncoding && /\b(?:gzip|deflate|br)\b/.test(contentEncoding));
|
|
}
|
|
|
|
get requestedByteRange()
|
|
{
|
|
let range = this._requestHeaders.valueForCaseInsensitiveKey("Range");
|
|
if (!range)
|
|
return null;
|
|
|
|
let rangeValues = range.match(/bytes=(\d+)-(\d+)/);
|
|
if (!rangeValues)
|
|
return null;
|
|
|
|
let start = parseInt(rangeValues[1]);
|
|
if (isNaN(start))
|
|
return null;
|
|
|
|
let end = parseInt(rangeValues[2]);
|
|
if (isNaN(end))
|
|
return null;
|
|
|
|
return {start, end};
|
|
}
|
|
|
|
get scripts()
|
|
{
|
|
return this._scripts || [];
|
|
}
|
|
|
|
get serverTiming()
|
|
{
|
|
if (!this._serverTimingEntries)
|
|
this._serverTimingEntries = WI.ServerTimingEntry.parseHeaders(this._responseHeaders.valueForCaseInsensitiveKey("Server-Timing"));
|
|
return this._serverTimingEntries;
|
|
}
|
|
|
|
scriptForLocation(sourceCodeLocation)
|
|
{
|
|
console.assert(!(this instanceof WI.SourceMapResource));
|
|
console.assert(sourceCodeLocation.sourceCode === this, "SourceCodeLocation must be in this Resource");
|
|
if (sourceCodeLocation.sourceCode !== this)
|
|
return null;
|
|
|
|
var lineNumber = sourceCodeLocation.lineNumber;
|
|
var columnNumber = sourceCodeLocation.columnNumber;
|
|
for (var i = 0; i < this._scripts.length; ++i) {
|
|
var script = this._scripts[i];
|
|
if (script.range.startLine <= lineNumber && script.range.endLine >= lineNumber) {
|
|
if (script.range.startLine === lineNumber && columnNumber < script.range.startColumn)
|
|
continue;
|
|
if (script.range.endLine === lineNumber && columnNumber > script.range.endColumn)
|
|
continue;
|
|
return script;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
updateForRedirectResponse(request, response, elapsedTime, walltime)
|
|
{
|
|
console.assert(!this._finished);
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
let oldURL = this._url;
|
|
let oldHeaders = this._requestHeaders;
|
|
let oldMethod = this._requestMethod;
|
|
|
|
if (request.url)
|
|
this._url = request.url;
|
|
|
|
this._requestHeaders = request.headers || {};
|
|
this._requestCookies = null;
|
|
this._requestMethod = request.method || null;
|
|
this._redirects.push(new WI.Redirect(oldURL, oldMethod, oldHeaders, response.status, response.statusText, response.headers, elapsedTime));
|
|
this._referrerPolicy = request.referrerPolicy ?? null;
|
|
this._integrity = request.integrity ?? null;
|
|
|
|
if (oldURL !== request.url) {
|
|
// Delete the URL components so the URL is re-parsed the next time it is requested.
|
|
this._urlComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
hasResponse()
|
|
{
|
|
return !isNaN(this._statusCode) || this._finished || this._failed;
|
|
}
|
|
|
|
hasRequestFormParameters()
|
|
{
|
|
let requestDataContentType = this.requestDataContentType;
|
|
return requestDataContentType && requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i);
|
|
}
|
|
|
|
updateForResponse(url, mimeType, type, responseHeaders, statusCode, statusText, elapsedTime, timingData, source, security)
|
|
{
|
|
console.assert(!this._finished);
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
let oldURL = this._url;
|
|
let oldMIMEType = this._mimeType;
|
|
let oldType = this._type;
|
|
|
|
if (type in WI.Resource.Type)
|
|
type = WI.Resource.Type[type];
|
|
else if (type === "Stylesheet") {
|
|
// COMPATIBILITY (iOS 13): Page.ResourceType.Stylesheet was renamed to Page.ResourceType.StyleSheet.
|
|
type = WI.Resource.Type.StyleSheet;
|
|
}
|
|
|
|
if (url)
|
|
this._url = url;
|
|
|
|
this._mimeType = mimeType;
|
|
this._type = Resource.resolvedType(type, mimeType);
|
|
this._statusCode = statusCode;
|
|
this._statusText = statusText;
|
|
this._responseHeaders = responseHeaders || {};
|
|
this._responseCookies = null;
|
|
this._serverTimingEntries = null;
|
|
this._responseReceivedTimestamp = elapsedTime || NaN;
|
|
this._timingData = WI.ResourceTimingData.fromPayload(timingData, this);
|
|
|
|
if (source)
|
|
this._responseSource = WI.Resource.responseSourceFromPayload(source);
|
|
|
|
this._security = security || {};
|
|
|
|
const headerBaseSize = 12; // Length of "HTTP/1.1 ", " ", and "\r\n".
|
|
const headerPad = 4; // Length of ": " and "\r\n".
|
|
this._estimatedResponseHeadersSize = String(this._statusCode).length + this._statusText.length + headerBaseSize;
|
|
for (let name in this._responseHeaders)
|
|
this._estimatedResponseHeadersSize += name.length + this._responseHeaders[name].length + headerPad;
|
|
|
|
if (!this._cached) {
|
|
if (statusCode === 304 || (this._responseSource === WI.Resource.ResponseSource.MemoryCache || this._responseSource === WI.Resource.ResponseSource.DiskCache))
|
|
this.markAsCached();
|
|
}
|
|
|
|
if (oldURL !== url) {
|
|
// Delete the URL components so the URL is re-parsed the next time it is requested.
|
|
this._urlComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.URLDidChange, {oldURL});
|
|
}
|
|
|
|
if (oldMIMEType !== mimeType) {
|
|
// Delete the MIME-type components so the MIME-type is re-parsed the next time it is requested.
|
|
this._mimeTypeComponents = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
|
|
}
|
|
|
|
if (oldType !== type)
|
|
this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
|
|
|
|
console.assert(isNaN(this._estimatedSize));
|
|
console.assert(isNaN(this._estimatedTransferSize));
|
|
|
|
// The transferSize becomes 0 when status is 304 or Content-Length is available, so
|
|
// notify listeners of that change.
|
|
if (statusCode === 304 || this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.ResponseReceived);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
updateWithMetrics(metrics)
|
|
{
|
|
this._receivedNetworkLoadMetrics = true;
|
|
|
|
if (metrics.protocol)
|
|
this._protocol = metrics.protocol;
|
|
if (metrics.priority)
|
|
this._priority = WI.Resource.networkPriorityFromPayload(metrics.priority);
|
|
if (metrics.remoteAddress)
|
|
this._remoteAddress = metrics.remoteAddress;
|
|
if (metrics.connectionIdentifier)
|
|
this._connectionIdentifier = WI.Resource.connectionIdentifierFromPayload(metrics.connectionIdentifier);
|
|
if (metrics.requestHeaders) {
|
|
this._requestHeaders = metrics.requestHeaders;
|
|
this._requestCookies = null;
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestHeadersDidChange);
|
|
}
|
|
|
|
if ("requestHeaderBytesSent" in metrics) {
|
|
this._requestHeadersTransferSize = metrics.requestHeaderBytesSent;
|
|
this._requestBodyTransferSize = metrics.requestBodyBytesSent;
|
|
this._responseHeadersTransferSize = metrics.responseHeaderBytesReceived;
|
|
this._responseBodyTransferSize = metrics.responseBodyBytesReceived;
|
|
this._responseBodySize = metrics.responseBodyDecodedSize;
|
|
|
|
console.assert(this._requestHeadersTransferSize >= 0);
|
|
console.assert(this._requestBodyTransferSize >= 0);
|
|
console.assert(this._responseHeadersTransferSize >= 0);
|
|
console.assert(this._responseBodyTransferSize >= 0);
|
|
console.assert(this._responseBodySize >= 0);
|
|
|
|
// There may have been no size updates received during load if Content-Length was 0.
|
|
if (isNaN(this._estimatedSize))
|
|
this._estimatedSize = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize: this._estimatedSize});
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
if (metrics.securityConnection) {
|
|
if (!this._security)
|
|
this._security = {};
|
|
this._security.connection = metrics.securityConnection;
|
|
}
|
|
|
|
this._isProxyConnection = !!metrics.isProxyConnection;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.MetricsDidChange);
|
|
}
|
|
|
|
setCachedResponseBodySize(size)
|
|
{
|
|
console.assert(!isNaN(size), "Size should be a valid number.");
|
|
console.assert(isNaN(this._cachedResponseBodySize), "This should only be set once.");
|
|
console.assert(this._estimatedSize === size, "The legacy path was updated already and matches.");
|
|
|
|
this._cachedResponseBodySize = size;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
let specialContentPromise = WI.SourceCode.generateSpecialContentForURL(this._url);
|
|
if (specialContentPromise)
|
|
return specialContentPromise;
|
|
|
|
if (this._target.type === WI.TargetType.Worker) {
|
|
console.assert(this.isScript);
|
|
let scriptForTarget = this.scripts.find((script) => script.target === this._target);
|
|
console.assert(scriptForTarget);
|
|
if (scriptForTarget)
|
|
return scriptForTarget.requestContentFromBackend();
|
|
} else {
|
|
// If we have the requestIdentifier we can get the actual response for this specific resource.
|
|
// Otherwise the content will be cached resource data, which might not exist anymore.
|
|
if (this._requestIdentifier)
|
|
return this._target.NetworkAgent.getResponseBody(this._requestIdentifier);
|
|
|
|
// There is no request identifier or frame to request content from.
|
|
if (this._parentFrame)
|
|
return this._target.PageAgent.getResourceContent(this._parentFrame.id, this._url);
|
|
}
|
|
|
|
return Promise.reject(new Error("Content request failed."));
|
|
}
|
|
|
|
increaseSize(dataLength, elapsedTime)
|
|
{
|
|
console.assert(dataLength >= 0);
|
|
console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated size.");
|
|
|
|
if (isNaN(this._estimatedSize))
|
|
this._estimatedSize = 0;
|
|
|
|
let previousSize = this._estimatedSize;
|
|
|
|
this._estimatedSize += dataLength;
|
|
|
|
this._lastDataReceivedTimestamp = elapsedTime || NaN;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.SizeDidChange, {previousSize});
|
|
|
|
// The estimatedTransferSize is based off of size when status is not 304 or Content-Length is missing.
|
|
if (isNaN(this._estimatedTransferSize) && this._statusCode !== 304 && !this._responseHeaders.valueForCaseInsensitiveKey("Content-Length"))
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
increaseTransferSize(encodedDataLength)
|
|
{
|
|
console.assert(encodedDataLength >= 0);
|
|
console.assert(!this._receivedNetworkLoadMetrics, "If we received metrics we don't need to change the estimated transfer size.");
|
|
|
|
if (isNaN(this._estimatedTransferSize))
|
|
this._estimatedTransferSize = 0;
|
|
this._estimatedTransferSize += encodedDataLength;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
markAsCached()
|
|
{
|
|
this._cached = true;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.CacheStatusDidChange);
|
|
|
|
// The transferSize starts returning 0 when cached is true, unless status is 304.
|
|
if (this._statusCode !== 304)
|
|
this.dispatchEventToListeners(WI.Resource.Event.TransferSizeDidChange);
|
|
}
|
|
|
|
markAsFinished(elapsedTime)
|
|
{
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
|
|
this._finished = true;
|
|
this._finishedOrFailedTimestamp = elapsedTime || NaN;
|
|
this._timingData.markResponseEndTime(elapsedTime || NaN);
|
|
|
|
if (this._finishThenRequestContentPromise)
|
|
this._finishThenRequestContentPromise = null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFinish);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
markAsFailed(canceled, elapsedTime, errorText)
|
|
{
|
|
console.assert(!this._finished);
|
|
|
|
this._failed = true;
|
|
this._canceled = canceled;
|
|
this._finishedOrFailedTimestamp = elapsedTime || NaN;
|
|
|
|
if (!this._failureReasonText)
|
|
this._failureReasonText = errorText || null;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.LoadingDidFail);
|
|
this.dispatchEventToListeners(WI.Resource.Event.TimestampsDidChange);
|
|
}
|
|
|
|
revertMarkAsFinished()
|
|
{
|
|
console.assert(!this._failed);
|
|
console.assert(!this._canceled);
|
|
console.assert(this._finished);
|
|
|
|
this._finished = false;
|
|
this._finishedOrFailedTimestamp = NaN;
|
|
}
|
|
|
|
isLoading()
|
|
{
|
|
return !this._finished && !this._failed;
|
|
}
|
|
|
|
hadLoadingError()
|
|
{
|
|
return this._failed || this._canceled || this._statusCode >= 400;
|
|
}
|
|
|
|
getImageSize(callback)
|
|
{
|
|
// Throw an error in the case this resource is not an image.
|
|
if (this.type !== WI.Resource.Type.Image)
|
|
throw "Resource is not an image.";
|
|
|
|
// See if we've already computed and cached the image size,
|
|
// in which case we can provide them directly.
|
|
if (this._imageSize !== undefined) {
|
|
callback(this._imageSize);
|
|
return;
|
|
}
|
|
|
|
var objectURL = null;
|
|
|
|
// Event handler for the image "load" event.
|
|
function imageDidLoad() {
|
|
URL.revokeObjectURL(objectURL);
|
|
|
|
// Cache the image metrics.
|
|
this._imageSize = {
|
|
width: image.width,
|
|
height: image.height
|
|
};
|
|
|
|
callback(this._imageSize);
|
|
}
|
|
|
|
function requestContentFailure() {
|
|
this._imageSize = null;
|
|
callback(this._imageSize);
|
|
}
|
|
|
|
// Create an <img> element that we'll use to load the image resource
|
|
// so that we can query its intrinsic size.
|
|
var image = new Image;
|
|
image.addEventListener("load", imageDidLoad.bind(this), false);
|
|
|
|
// Set the image source using an object URL once we've obtained its data.
|
|
this.requestContent().then((content) => {
|
|
objectURL = image.src = content.sourceCode.createObjectURL();
|
|
if (!objectURL)
|
|
requestContentFailure.call(this);
|
|
}, requestContentFailure.bind(this));
|
|
}
|
|
|
|
requestContent()
|
|
{
|
|
if (this._finished)
|
|
return super.requestContent().catch(this._requestContentFailure.bind(this));
|
|
|
|
if (this._failed)
|
|
return this._requestContentFailure();
|
|
|
|
if (!this._finishThenRequestContentPromise) {
|
|
this._finishThenRequestContentPromise = new Promise((resolve, reject) => {
|
|
this.singleFireEventListener(WI.Resource.Event.LoadingDidFinish, resolve, this);
|
|
this.singleFireEventListener(WI.Resource.Event.LoadingDidFail, reject, this);
|
|
}).then(this.requestContent.bind(this));
|
|
}
|
|
|
|
return this._finishThenRequestContentPromise;
|
|
}
|
|
|
|
associateWithScript(script)
|
|
{
|
|
if (!this._scripts)
|
|
this._scripts = [];
|
|
|
|
this._scripts.push(script);
|
|
|
|
if (this._type === WI.Resource.Type.Other || this._type === WI.Resource.Type.XHR) {
|
|
let oldType = this._type;
|
|
this._type = WI.Resource.Type.Script;
|
|
this.dispatchEventToListeners(WI.Resource.Event.TypeDidChange, {oldType});
|
|
}
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.Resource.URLCookieKey] = this.url.hash;
|
|
cookie[WI.Resource.MainResourceCookieKey] = this.isMainResource();
|
|
}
|
|
|
|
async createLocalResourceOverride(type, {mimeType, base64Encoded, content} = {})
|
|
{
|
|
console.assert(!this.localResourceOverride);
|
|
console.assert(WI.NetworkManager.supportsOverridingResponses());
|
|
|
|
let resourceData = {
|
|
requestURL: this.url,
|
|
};
|
|
|
|
switch (type) {
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
resourceData.requestMethod = this.requestMethod ?? WI.HTTPUtilities.RequestMethod.GET;
|
|
resourceData.requestHeaders = Object.shallowCopy(this.requestHeaders);
|
|
resourceData.requestData = this.requestData ?? "";
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
resourceData.responseMIMEType = this.mimeType ?? WI.mimeTypeForFileExtension(WI.fileExtensionForFilename(this.urlComponents.lastPathComponent));
|
|
resourceData.responseStatusCode = this.statusCode;
|
|
resourceData.responseStatusText = this.statusText;
|
|
if (!resourceData.responseStatusCode) {
|
|
resourceData.responseStatusCode = 200;
|
|
resourceData.responseStatusText = null;
|
|
}
|
|
resourceData.responseStatusText ||= WI.HTTPUtilities.statusTextForStatusCode(resourceData.responseStatusCode);
|
|
|
|
if (base64Encoded === undefined || content === undefined) {
|
|
try {
|
|
let {rawContent, rawBase64Encoded} = await this.requestContent();
|
|
content ??= rawContent;
|
|
base64Encoded ??= rawBase64Encoded;
|
|
} catch {
|
|
content ??= "";
|
|
base64Encoded ??= !WI.shouldTreatMIMETypeAsText(resourceData.mimeType);
|
|
}
|
|
}
|
|
resourceData.responseContent = content;
|
|
resourceData.responseBase64Encoded = base64Encoded;
|
|
resourceData.responseHeaders = Object.shallowCopy(this.responseHeaders);
|
|
break;
|
|
}
|
|
|
|
return WI.LocalResourceOverride.create(this.url, type, resourceData);
|
|
}
|
|
|
|
updateLocalResourceOverrideRequestData(data)
|
|
{
|
|
console.assert(this.localResourceOverride);
|
|
|
|
if (data === this._requestData)
|
|
return;
|
|
|
|
this._requestData = data;
|
|
|
|
this.dispatchEventToListeners(WI.Resource.Event.RequestDataDidChange);
|
|
}
|
|
|
|
generateFetchCode()
|
|
{
|
|
let options = {};
|
|
|
|
if (this.requestData)
|
|
options.body = this.requestData;
|
|
|
|
options.cache = "default";
|
|
options.credentials = (this.requestCookies.length || this._requestHeaders.valueForCaseInsensitiveKey("Authorization")) ? "include" : "omit";
|
|
|
|
// https://fetch.spec.whatwg.org/#forbidden-header-name
|
|
const forbiddenHeaders = new Set([
|
|
"accept-charset",
|
|
"accept-encoding",
|
|
"access-control-request-headers",
|
|
"access-control-request-method",
|
|
"connection",
|
|
"content-length",
|
|
"cookie",
|
|
"cookie2",
|
|
"date",
|
|
"dnt",
|
|
"expect",
|
|
"host",
|
|
"keep-alive",
|
|
"origin",
|
|
"referer",
|
|
"te",
|
|
"trailer",
|
|
"transfer-encoding",
|
|
"upgrade",
|
|
"via",
|
|
]);
|
|
let headers = Object.entries(this.requestHeaders)
|
|
.filter((header) => {
|
|
let key = header[0].toLowerCase();
|
|
if (forbiddenHeaders.has(key))
|
|
return false;
|
|
if (key.startsWith("proxy-") || key.startsWith("sec-"))
|
|
return false;
|
|
return true;
|
|
})
|
|
.sort((a, b) => a[0].extendedLocaleCompare(b[0]))
|
|
.reduce((accumulator, current) => {
|
|
accumulator[current[0]] = current[1];
|
|
return accumulator;
|
|
}, {});
|
|
if (!isEmptyObject(headers))
|
|
options.headers = headers;
|
|
|
|
if (this._integrity)
|
|
options.integrity = this._integrity;
|
|
|
|
if (this.requestMethod)
|
|
options.method = this.requestMethod;
|
|
|
|
options.mode = "cors";
|
|
options.redirect = "follow";
|
|
|
|
let referrer = this.requestHeaders.valueForCaseInsensitiveKey("Referer");
|
|
if (referrer)
|
|
options.referrer = referrer;
|
|
|
|
if (this._referrerPolicy)
|
|
options.referrerPolicy = this._referrerPolicy;
|
|
|
|
return `fetch(${JSON.stringify(this.url)}, ${JSON.stringify(options, null, WI.indentString())})`;
|
|
}
|
|
|
|
generateCURLCommand()
|
|
{
|
|
function escapeStringPosix(str) {
|
|
function escapeCharacter(x) {
|
|
let code = x.charCodeAt(0);
|
|
let hex = code.toString(16);
|
|
if (code < 256)
|
|
return "\\x" + hex.padStart(2, "0");
|
|
return "\\u" + hex.padStart(4, "0");
|
|
}
|
|
|
|
if (/[^\x20-\x7E]|'/.test(str)) {
|
|
// Use ANSI-C quoting syntax.
|
|
return "$'" + str.replace(/\\/g, "\\\\")
|
|
.replace(/'/g, "\\'")
|
|
.replace(/\n/g, "\\n")
|
|
.replace(/\r/g, "\\r")
|
|
.replace(/!/g, "\\041")
|
|
.replace(/[^\x20-\x7E]/g, escapeCharacter) + "'";
|
|
}
|
|
|
|
// Use single quote syntax.
|
|
return `'${str}'`;
|
|
}
|
|
|
|
let command = ["curl " + escapeStringPosix(this.url).replace(/[[{}\]]/g, "\\$&")];
|
|
command.push("-X " + escapeStringPosix(this.requestMethod));
|
|
|
|
for (let key in this.requestHeaders)
|
|
command.push("-H " + escapeStringPosix(`${key}: ${this.requestHeaders[key]}`));
|
|
|
|
if (this.requestDataContentType && this.requestMethod !== "GET" && this.requestData) {
|
|
if (this.requestDataContentType.match(/^application\/x-www-form-urlencoded\s*(;.*)?$/i))
|
|
command.push("--data " + escapeStringPosix(this.requestData));
|
|
else
|
|
command.push("--data-binary " + escapeStringPosix(this.requestData));
|
|
}
|
|
|
|
return command.join(" \\\n");
|
|
}
|
|
|
|
stringifyHTTPRequest()
|
|
{
|
|
let lines = [];
|
|
|
|
let protocol = this.protocol || "";
|
|
if (protocol === "h2") {
|
|
// HTTP/2 Request pseudo headers:
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.3
|
|
lines.push(`:method: ${this.requestMethod}`);
|
|
lines.push(`:scheme: ${this.urlComponents.scheme}`);
|
|
lines.push(`:authority: ${WI.h2Authority(this.urlComponents)}`);
|
|
lines.push(`:path: ${WI.h2Path(this.urlComponents)}`);
|
|
} else {
|
|
// HTTP/1.1 request line:
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1
|
|
lines.push(`${this.requestMethod} ${this.urlComponents.path}${protocol ? " " + protocol.toUpperCase() : ""}`);
|
|
}
|
|
|
|
for (let key in this.requestHeaders)
|
|
lines.push(`${key}: ${this.requestHeaders[key]}`);
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
stringifyHTTPResponse()
|
|
{
|
|
let lines = [];
|
|
|
|
let protocol = this.protocol || "";
|
|
if (protocol === "h2") {
|
|
// HTTP/2 Response pseudo headers:
|
|
// https://tools.ietf.org/html/rfc7540#section-8.1.2.4
|
|
lines.push(`:status: ${this.statusCode}`);
|
|
} else {
|
|
// HTTP/1.1 response status line:
|
|
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec6.html#sec6.1
|
|
lines.push(`${protocol ? protocol.toUpperCase() + " " : ""}${this.statusCode} ${this.statusText}`);
|
|
}
|
|
|
|
for (let key in this.responseHeaders)
|
|
lines.push(`${key}: ${this.responseHeaders[key]}`);
|
|
|
|
return lines.join("\n") + "\n";
|
|
}
|
|
|
|
async showCertificate()
|
|
{
|
|
let errorString = WI.UIString("Unable to show certificate for \u201C%s\u201D").format(this.url);
|
|
|
|
try {
|
|
let {serializedCertificate} = await this._target.NetworkAgent.getSerializedCertificate(this._requestIdentifier);
|
|
if (InspectorFrontendHost.showCertificate(serializedCertificate))
|
|
return;
|
|
} catch (e) {
|
|
console.error(e);
|
|
throw errorString;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(this._target, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, errorString);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
|
|
throw errorString;
|
|
}
|
|
|
|
// Private
|
|
|
|
_requestContentFailure(error)
|
|
{
|
|
return Promise.resolve({
|
|
error: WI.UIString("An error occurred trying to load the resource."),
|
|
reason: error?.message || this._failureReasonText,
|
|
sourceCode: this,
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.Resource.TypeIdentifier = "resource";
|
|
WI.Resource.URLCookieKey = "resource-url";
|
|
WI.Resource.MainResourceCookieKey = "resource-is-main-resource";
|
|
|
|
WI.Resource.Event = {
|
|
URLDidChange: "resource-url-did-change",
|
|
MIMETypeDidChange: "resource-mime-type-did-change",
|
|
TypeDidChange: "resource-type-did-change",
|
|
RequestHeadersDidChange: "resource-request-headers-did-change",
|
|
RequestDataDidChange: "resource-request-data-did-change",
|
|
ResponseReceived: "resource-response-received",
|
|
LoadingDidFinish: "resource-loading-did-finish",
|
|
LoadingDidFail: "resource-loading-did-fail",
|
|
TimestampsDidChange: "resource-timestamps-did-change",
|
|
SizeDidChange: "resource-size-did-change",
|
|
TransferSizeDidChange: "resource-transfer-size-did-change",
|
|
CacheStatusDidChange: "resource-cached-did-change",
|
|
MetricsDidChange: "resource-metrics-did-change",
|
|
InitiatedResourcesDidChange: "resource-initiated-resources-did-change",
|
|
};
|
|
|
|
// Keep these in sync with the "ResourceType" enum defined by the "Page" domain.
|
|
WI.Resource.Type = {
|
|
Document: "resource-type-document",
|
|
StyleSheet: "resource-type-style-sheet",
|
|
Image: "resource-type-image",
|
|
Font: "resource-type-font",
|
|
Script: "resource-type-script",
|
|
XHR: "resource-type-xhr",
|
|
Fetch: "resource-type-fetch",
|
|
Ping: "resource-type-ping",
|
|
Beacon: "resource-type-beacon",
|
|
WebSocket: "resource-type-websocket",
|
|
EventSource: "resource-type-eventsource",
|
|
Other: "resource-type-other",
|
|
};
|
|
|
|
WI.Resource.ResponseSource = {
|
|
Unknown: Symbol("unknown"),
|
|
Network: Symbol("network"),
|
|
MemoryCache: Symbol("memory-cache"),
|
|
DiskCache: Symbol("disk-cache"),
|
|
ServiceWorker: Symbol("service-worker"),
|
|
InspectorOverride: Symbol("inspector-override"),
|
|
};
|
|
|
|
WI.Resource.NetworkPriority = {
|
|
Unknown: Symbol("unknown"),
|
|
Low: Symbol("low"),
|
|
Medium: Symbol("medium"),
|
|
High: Symbol("high"),
|
|
};
|
|
|
|
WI.Resource.GroupingMode = {
|
|
Path: "group-resource-by-path",
|
|
Type: "group-resource-by-type",
|
|
};
|
|
WI.settings.resourceGroupingMode = new WI.Setting("resource-grouping-mode", WI.Resource.GroupingMode.Type);
|
|
|
|
// This MIME Type map is private, use WI.Resource.typeFromMIMEType().
|
|
WI.Resource._mimeTypeMap = {
|
|
"text/html": WI.Resource.Type.Document,
|
|
"text/xml": WI.Resource.Type.Document,
|
|
"application/xhtml+xml": WI.Resource.Type.Document,
|
|
|
|
"text/plain": WI.Resource.Type.Other,
|
|
|
|
"text/css": WI.Resource.Type.StyleSheet,
|
|
"text/xsl": WI.Resource.Type.StyleSheet,
|
|
"text/x-less": WI.Resource.Type.StyleSheet,
|
|
"text/x-sass": WI.Resource.Type.StyleSheet,
|
|
"text/x-scss": WI.Resource.Type.StyleSheet,
|
|
|
|
"application/pdf": WI.Resource.Type.Image,
|
|
"image/svg+xml": WI.Resource.Type.Image,
|
|
|
|
"application/x-font-type1": WI.Resource.Type.Font,
|
|
"application/x-font-ttf": WI.Resource.Type.Font,
|
|
"application/x-font-woff": WI.Resource.Type.Font,
|
|
"application/x-truetype-font": WI.Resource.Type.Font,
|
|
|
|
"text/javascript": WI.Resource.Type.Script,
|
|
"text/ecmascript": WI.Resource.Type.Script,
|
|
"application/javascript": WI.Resource.Type.Script,
|
|
"application/ecmascript": WI.Resource.Type.Script,
|
|
"application/x-javascript": WI.Resource.Type.Script,
|
|
"application/json": WI.Resource.Type.Script,
|
|
"application/x-json": WI.Resource.Type.Script,
|
|
"text/x-javascript": WI.Resource.Type.Script,
|
|
"text/x-json": WI.Resource.Type.Script,
|
|
"text/javascript1.1": WI.Resource.Type.Script,
|
|
"text/javascript1.2": WI.Resource.Type.Script,
|
|
"text/javascript1.3": WI.Resource.Type.Script,
|
|
"text/jscript": WI.Resource.Type.Script,
|
|
"text/livescript": WI.Resource.Type.Script,
|
|
"text/x-livescript": WI.Resource.Type.Script,
|
|
"text/typescript": WI.Resource.Type.Script,
|
|
"text/typescript-jsx": WI.Resource.Type.Script,
|
|
"text/jsx": WI.Resource.Type.Script,
|
|
"text/x-clojure": WI.Resource.Type.Script,
|
|
"text/x-coffeescript": WI.Resource.Type.Script,
|
|
};
|
|
|
|
/* Models/Script.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Script = class Script extends WI.SourceCode
|
|
{
|
|
constructor(target, id, range, url, sourceType, injected, sourceURL, sourceMapURL)
|
|
{
|
|
super(url);
|
|
|
|
console.assert(target instanceof WI.Target || this instanceof WI.LocalScript);
|
|
console.assert(range instanceof WI.TextRange);
|
|
|
|
this._target = target;
|
|
this._id = id || null;
|
|
this._range = range || null;
|
|
this._sourceType = sourceType || WI.Script.SourceType.Program;
|
|
this._sourceURL = sourceURL || null;
|
|
this._sourceMappingURL = sourceMapURL || null;
|
|
this._injected = injected || false;
|
|
this._dynamicallyAddedScriptElement = false;
|
|
this._scriptSyntaxTree = null;
|
|
|
|
this._resource = this._resolveResource();
|
|
|
|
// If this Script was a dynamically added <script> to a Document,
|
|
// do not associate with the Document resource, instead associate
|
|
// with the frame as a dynamic script.
|
|
if (this._resource && this._resource.type === WI.Resource.Type.Document && !this._range.startLine && !this._range.startColumn) {
|
|
console.assert(this._resource.isMainResource());
|
|
let documentResource = this._resource;
|
|
this._resource = null;
|
|
this._dynamicallyAddedScriptElement = true;
|
|
documentResource.parentFrame.addExtraScript(this);
|
|
this._dynamicallyAddedScriptElementNumber = documentResource.parentFrame.extraScriptCollection.size;
|
|
} else if (this._resource)
|
|
this._resource.associateWithScript(this);
|
|
|
|
if (isWebInspectorConsoleEvaluationScript(this._sourceURL)) {
|
|
// Assign a unique number to the script object so it will stay the same.
|
|
this._uniqueDisplayNameNumber = this._nextUniqueConsoleDisplayNameNumber();
|
|
}
|
|
|
|
if (this._sourceMappingURL)
|
|
WI.networkManager.downloadSourceMap(this._sourceMappingURL, this._url, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static resetUniqueDisplayNameNumbers(target)
|
|
{
|
|
if (WI.Script._uniqueDisplayNameNumbersForRootTargetMap)
|
|
WI.Script._uniqueDisplayNameNumbersForRootTargetMap.delete(target);
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get id() { return this._id; }
|
|
get range() { return this._range; }
|
|
get sourceType() { return this._sourceType; }
|
|
get sourceURL() { return this._sourceURL; }
|
|
get sourceMappingURL() { return this._sourceMappingURL; }
|
|
get injected() { return this._injected; }
|
|
|
|
get contentIdentifier()
|
|
{
|
|
if (this._url)
|
|
return this._url;
|
|
|
|
if (!this._sourceURL)
|
|
return null;
|
|
|
|
// Since reused content identifiers can cause breakpoints
|
|
// to show up in completely unrelated files, sourceURLs should
|
|
// be unique where possible. The checks below exclude cases
|
|
// where sourceURLs are intentionally reused and we would never
|
|
// expect a breakpoint to be persisted across sessions.
|
|
if (isWebInspectorConsoleEvaluationScript(this._sourceURL))
|
|
return null;
|
|
|
|
if (isWebInspectorInternalScript(this._sourceURL))
|
|
return null;
|
|
|
|
return this._sourceURL;
|
|
}
|
|
|
|
get mimeType()
|
|
{
|
|
return this._resource ? this._resource.mimeType : "text/javascript";
|
|
}
|
|
|
|
get isScript()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (isWebInspectorBootstrapScript(this._sourceURL || this._url)) {
|
|
console.assert(WI.NetworkManager.supportsBootstrapScript());
|
|
return WI.UIString("Inspector Bootstrap Script");
|
|
}
|
|
|
|
if (this._url && !this._dynamicallyAddedScriptElement)
|
|
return WI.displayNameForURL(this._url, this.urlComponents);
|
|
|
|
if (isWebInspectorConsoleEvaluationScript(this._sourceURL)) {
|
|
console.assert(this._uniqueDisplayNameNumber);
|
|
return WI.UIString("Console Evaluation %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
if (this._sourceURL) {
|
|
if (!this._sourceURLComponents)
|
|
this._sourceURLComponents = parseURL(this._sourceURL);
|
|
return WI.displayNameForURL(this._sourceURL, this._sourceURLComponents);
|
|
}
|
|
|
|
if (this._dynamicallyAddedScriptElement)
|
|
return WI.UIString("Script Element %d").format(this._dynamicallyAddedScriptElementNumber);
|
|
|
|
// Assign a unique number to the script object so it will stay the same.
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = this._nextUniqueDisplayNameNumber();
|
|
|
|
return WI.UIString("Anonymous Script %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
get displayURL()
|
|
{
|
|
if (isWebInspectorBootstrapScript(this._sourceURL || this._url)) {
|
|
console.assert(WI.NetworkManager.supportsBootstrapScript());
|
|
return WI.UIString("Inspector Bootstrap Script");
|
|
}
|
|
|
|
const isMultiLine = true;
|
|
const dataURIMaxSize = 64;
|
|
if (this._url)
|
|
return WI.truncateURL(this._url, isMultiLine, dataURIMaxSize);
|
|
if (this._sourceURL)
|
|
return WI.truncateURL(this._sourceURL, isMultiLine, dataURIMaxSize);
|
|
return null;
|
|
}
|
|
|
|
get dynamicallyAddedScriptElement()
|
|
{
|
|
return this._dynamicallyAddedScriptElement;
|
|
}
|
|
|
|
get anonymous()
|
|
{
|
|
return !this._resource && !this._url && !this._sourceURL;
|
|
}
|
|
|
|
get resource()
|
|
{
|
|
return this._resource;
|
|
}
|
|
|
|
get scriptSyntaxTree()
|
|
{
|
|
return this._scriptSyntaxTree;
|
|
}
|
|
|
|
isMainResource()
|
|
{
|
|
return this._target && this._target.mainResource === this;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
let specialContentPromise = WI.SourceCode.generateSpecialContentForURL(this._url);
|
|
if (specialContentPromise)
|
|
return specialContentPromise;
|
|
|
|
if (!this._id) {
|
|
// There is no identifier to request content with. Return false to cause the
|
|
// pending callbacks to get null content.
|
|
return Promise.reject(new Error("There is no identifier to request content with."));
|
|
}
|
|
|
|
return this._target.DebuggerAgent.getScriptSource(this._id);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.Script.URLCookieKey] = this.url;
|
|
cookie[WI.Script.DisplayNameCookieKey] = this.displayName;
|
|
}
|
|
|
|
requestScriptSyntaxTree(callback)
|
|
{
|
|
if (this._scriptSyntaxTree) {
|
|
setTimeout(() => { callback(this._scriptSyntaxTree); }, 0);
|
|
return;
|
|
}
|
|
|
|
var makeSyntaxTreeAndCallCallback = (content) => {
|
|
this._makeSyntaxTree(content);
|
|
callback(this._scriptSyntaxTree);
|
|
};
|
|
|
|
var content = this.content;
|
|
if (!content && this._resource && this._resource.type === WI.Resource.Type.Script && this._resource.finished)
|
|
content = this._resource.content;
|
|
if (content) {
|
|
setTimeout(makeSyntaxTreeAndCallCallback, 0, content);
|
|
return;
|
|
}
|
|
|
|
this.requestContent().then(function(parameters) {
|
|
makeSyntaxTreeAndCallCallback(parameters.sourceCode.content);
|
|
}).catch(function(error) {
|
|
makeSyntaxTreeAndCallCallback(null);
|
|
});
|
|
}
|
|
|
|
async breakpointLocations(startPosition, endPosition)
|
|
{
|
|
console.assert(startPosition instanceof WI.SourceCodePosition, startPosition);
|
|
console.assert(endPosition instanceof WI.SourceCodePosition, endPosition);
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): Debugger.getBreakpointLocations did not exist yet.
|
|
if (!this._target.hasCommand("Debugger.getBreakpointLocations"))
|
|
return [];
|
|
|
|
let {locations} = await this._target.DebuggerAgent.getBreakpointLocations.invoke({
|
|
start: {
|
|
scriptId: this._id,
|
|
lineNumber: startPosition.lineNumber,
|
|
columnNumber: startPosition.columnNumber,
|
|
},
|
|
end: {
|
|
scriptId: this._id,
|
|
lineNumber: endPosition.lineNumber,
|
|
columnNumber: endPosition.columnNumber,
|
|
},
|
|
});
|
|
return locations.map((location) => {
|
|
console.assert(location.scriptId === this._id, location);
|
|
let sourceCode = this._resource || this;
|
|
return sourceCode.createLazySourceCodeLocation(location.lineNumber, location.columnNumber);
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
_nextUniqueDisplayNameNumber()
|
|
{
|
|
let numbers = this._uniqueDisplayNameNumbersForRootTarget();
|
|
return ++numbers.lastUniqueDisplayNameNumber;
|
|
}
|
|
|
|
_nextUniqueConsoleDisplayNameNumber()
|
|
{
|
|
let numbers = this._uniqueDisplayNameNumbersForRootTarget();
|
|
return ++numbers.lastUniqueConsoleDisplayNameNumber;
|
|
}
|
|
|
|
_uniqueDisplayNameNumbersForRootTarget()
|
|
{
|
|
if (!WI.Script._uniqueDisplayNameNumbersForRootTargetMap)
|
|
WI.Script._uniqueDisplayNameNumbersForRootTargetMap = new WeakMap();
|
|
|
|
console.assert(this._target);
|
|
let key = this._target.rootTarget;
|
|
let numbers = WI.Script._uniqueDisplayNameNumbersForRootTargetMap.get(key);
|
|
if (!numbers) {
|
|
numbers = {
|
|
lastUniqueDisplayNameNumber: 0,
|
|
lastUniqueConsoleDisplayNameNumber: 0
|
|
};
|
|
WI.Script._uniqueDisplayNameNumbersForRootTargetMap.set(key, numbers);
|
|
}
|
|
return numbers;
|
|
}
|
|
|
|
_resolveResource()
|
|
{
|
|
// FIXME: We should be able to associate a Script with a Resource through identifiers,
|
|
// we shouldn't need to lookup by URL, which is not safe with frames, where there might
|
|
// be multiple resources with the same URL.
|
|
// <rdar://problem/13373951> Scripts should be able to associate directly with a Resource
|
|
|
|
// No URL, no resource.
|
|
if (!this._url)
|
|
return null;
|
|
|
|
let resolver = WI.networkManager;
|
|
if (this._target && this._target !== WI.mainTarget)
|
|
resolver = this._target.resourceCollection;
|
|
|
|
function isScriptResource(item) {
|
|
return item.type === WI.Resource.Type.Document || item.type === WI.Resource.Type.Script;
|
|
}
|
|
|
|
try {
|
|
// Try with the Script's full URL.
|
|
let resource = resolver.resourcesForURL(this._url).find(isScriptResource);
|
|
if (resource)
|
|
return resource;
|
|
|
|
// Try with the Script's full decoded URL.
|
|
let decodedURL = decodeURI(this._url);
|
|
if (decodedURL !== this._url) {
|
|
resource = resolver.resourcesForURL(decodedURL).find(isScriptResource);
|
|
if (resource)
|
|
return resource;
|
|
}
|
|
|
|
// Next try removing any fragment in the original URL.
|
|
let urlWithoutFragment = removeURLFragment(this._url);
|
|
if (urlWithoutFragment !== this._url) {
|
|
resource = resolver.resourcesForURL(urlWithoutFragment).find(isScriptResource);
|
|
if (resource)
|
|
return resource;
|
|
}
|
|
|
|
// Finally try removing any fragment in the decoded URL.
|
|
let decodedURLWithoutFragment = removeURLFragment(decodedURL);
|
|
if (decodedURLWithoutFragment !== decodedURL) {
|
|
resource = resolver.resourcesForURL(decodedURLWithoutFragment).find(isScriptResource);
|
|
if (resource)
|
|
return resource;
|
|
}
|
|
} catch { }
|
|
|
|
if (!this.isMainResource()) {
|
|
for (let frame of WI.networkManager.frames) {
|
|
if (frame.mainResource.type === WI.Resource.Type.Document && frame.mainResource.url.startsWith(this._url))
|
|
return frame.mainResource;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_makeSyntaxTree(sourceText)
|
|
{
|
|
if (this._scriptSyntaxTree || !sourceText)
|
|
return;
|
|
|
|
this._scriptSyntaxTree = new WI.ScriptSyntaxTree(sourceText, this);
|
|
}
|
|
};
|
|
|
|
WI.Script.SourceType = {
|
|
Program: "script-source-type-program",
|
|
Module: "script-source-type-module",
|
|
};
|
|
|
|
WI.Script.TypeIdentifier = "script";
|
|
WI.Script.URLCookieKey = "script-url";
|
|
WI.Script.DisplayNameCookieKey = "script-display-name";
|
|
|
|
/* Models/LocalScript.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LocalScript = class LocalScript extends WI.Script
|
|
{
|
|
constructor(target, url, sourceURL, sourceType, source, {injected, editable} = {})
|
|
{
|
|
const id = null;
|
|
super(target, id, WI.TextRange.fromText(source), url, sourceType, injected, sourceURL);
|
|
|
|
this._editable = !!editable;
|
|
|
|
// Finalize WI.SourceCode.
|
|
const base64Encoded = false;
|
|
const mimeType = "text/javascript";
|
|
this._originalRevision = new WI.SourceCodeRevision(this, source, base64Encoded, mimeType);
|
|
this._currentRevision = this._originalRevision;
|
|
}
|
|
|
|
// Public
|
|
|
|
get editable() { return this._editable; }
|
|
|
|
get supportsScriptBlackboxing()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
return Promise.resolve({
|
|
scriptSource: this._originalRevision.content,
|
|
});
|
|
}
|
|
|
|
// Protected
|
|
|
|
handleCurrentRevisionContentChange()
|
|
{
|
|
super.handleCurrentRevisionContentChange();
|
|
|
|
this._range = WI.TextRange.fromText(this._currentRevision.content);
|
|
}
|
|
};
|
|
|
|
/* Models/Animation.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Animation = class Animation extends WI.Object
|
|
{
|
|
constructor(animationId, {name, cssAnimationName, cssTransitionProperty, effect, stackTrace} = {})
|
|
{
|
|
super();
|
|
|
|
console.assert(animationId);
|
|
console.assert((!cssAnimationName && !cssTransitionProperty) || !!cssAnimationName !== !!cssTransitionProperty);
|
|
console.assert(!stackTrace || stackTrace instanceof WI.StackTrace, stackTrace);
|
|
|
|
this._animationId = animationId;
|
|
|
|
this._name = name || null;
|
|
this._cssAnimationName = cssAnimationName || null;
|
|
this._cssTransitionProperty = cssTransitionProperty || null;
|
|
this._updateEffect(effect);
|
|
this._stackTrace = stackTrace || null;
|
|
|
|
this._effectTarget = undefined;
|
|
this._requestEffectTargetCallbacks = null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `backtrace` was renamed to `stackTrace`.
|
|
if (payload.backtrace)
|
|
payload.stackTrace = {callFrames: payload.backtrace};
|
|
|
|
return new WI.Animation(payload.animationId, {
|
|
name: payload.name,
|
|
cssAnimationName: payload.cssAnimationName,
|
|
cssTransitionProperty: payload.cssTransitionProperty,
|
|
effect: payload.effect,
|
|
stackTrace: WI.StackTrace.fromPayload(WI.assumingMainTarget(), payload.stackTrace),
|
|
});
|
|
}
|
|
|
|
static displayNameForAnimationType(animationType, plural)
|
|
{
|
|
switch (animationType) {
|
|
case WI.Animation.Type.WebAnimation:
|
|
return plural ? WI.UIString("Web Animations") : WI.UIString("Web Animation");
|
|
case WI.Animation.Type.CSSAnimation:
|
|
return plural ? WI.UIString("CSS Animations") : WI.UIString("CSS Animation");
|
|
case WI.Animation.Type.CSSTransition:
|
|
return plural ? WI.UIString("CSS Transitions") : WI.UIString("CSS Transition");
|
|
}
|
|
|
|
console.assert(false, "Unknown animation type", animationType);
|
|
return null;
|
|
}
|
|
|
|
static displayNameForPlaybackDirection(playbackDirection)
|
|
{
|
|
switch (playbackDirection) {
|
|
case WI.Animation.PlaybackDirection.Normal:
|
|
return WI.UIString("Normal", "Web Animation Playback Direction Normal", "Indicates that the playback direction of this web animation is normal (e.g. forwards)");
|
|
case WI.Animation.PlaybackDirection.Reverse:
|
|
return WI.UIString("Reverse", "Web Animation Playback Direction Reverse", "Indicates that the playback direction of this web animation is reversed (e.g. backwards)");
|
|
case WI.Animation.PlaybackDirection.Alternate:
|
|
return WI.UIString("Alternate", "Web Animation Playback Direction Alternate", "Indicates that the playback direction of this web animation alternates between normal and reversed on each iteration");
|
|
case WI.Animation.PlaybackDirection.AlternateReverse:
|
|
return WI.UIString("Alternate Reverse", "Web Animation Playback Direction Alternate Reverse", "Indicates that the playback direction of this web animation alternates between reversed and normal on each iteration");
|
|
}
|
|
|
|
console.assert(false, "Unknown playback direction", playbackDirection);
|
|
return null;
|
|
}
|
|
|
|
static displayNameForFillMode(fillMode)
|
|
{
|
|
switch (fillMode) {
|
|
case WI.Animation.FillMode.None:
|
|
return WI.UIString("None", "Web Animation Fill Mode None", "Indicates that this web animation does not apply any styles before it begins and after it ends");
|
|
case WI.Animation.FillMode.Forwards:
|
|
return WI.UIString("Forwards", "Web Animation Fill Mode Forwards", "Indicates that this web animation also applies styles after it ends");
|
|
case WI.Animation.FillMode.Backwards:
|
|
return WI.UIString("Backwards", "Web Animation Fill Mode Backwards", "Indicates that this web animation also applies styles before it begins");
|
|
case WI.Animation.FillMode.Both:
|
|
return WI.UIString("Both", "Web Animation Fill Mode Both", "Indicates that this web animation also applies styles before it begins and after it ends");
|
|
case WI.Animation.FillMode.Auto:
|
|
return WI.UIString("Auto", "Web Animation Fill Mode Auto", "Indicates that this web animation either does not apply any styles before it begins and after it ends or that it applies to both, depending on it's configuration");
|
|
}
|
|
|
|
console.assert(false, "Unknown fill mode", fillMode);
|
|
return null;
|
|
}
|
|
|
|
static resetUniqueDisplayNameNumbers()
|
|
{
|
|
WI.Animation._nextUniqueDisplayNameNumber = 1;
|
|
}
|
|
|
|
// Public
|
|
|
|
get animationId() { return this._animationId; }
|
|
get name() { return this._name; }
|
|
get cssAnimationName() { return this._cssAnimationName; }
|
|
get cssTransitionProperty() { return this._cssTransitionProperty; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
|
|
get animationType()
|
|
{
|
|
if (this._cssAnimationName)
|
|
return WI.Animation.Type.CSSAnimation;
|
|
if (this._cssTransitionProperty)
|
|
return WI.Animation.Type.CSSTransition;
|
|
return WI.Animation.Type.WebAnimation;
|
|
}
|
|
|
|
get startDelay()
|
|
{
|
|
return "startDelay" in this._effect ? this._effect.startDelay : NaN;
|
|
}
|
|
|
|
get endDelay()
|
|
{
|
|
return "endDelay" in this._effect ? this._effect.endDelay : NaN;
|
|
}
|
|
|
|
get iterationCount()
|
|
{
|
|
return "iterationCount" in this._effect ? this._effect.iterationCount : NaN;
|
|
}
|
|
|
|
get iterationStart()
|
|
{
|
|
return "iterationStart" in this._effect ? this._effect.iterationStart : NaN;
|
|
}
|
|
|
|
get iterationDuration()
|
|
{
|
|
return "iterationDuration" in this._effect ? this._effect.iterationDuration : NaN;
|
|
}
|
|
|
|
get timingFunction()
|
|
{
|
|
return "timingFunction" in this._effect ? this._effect.timingFunction : null;
|
|
}
|
|
|
|
get playbackDirection()
|
|
{
|
|
return "playbackDirection" in this._effect ? this._effect.playbackDirection : null;
|
|
}
|
|
|
|
get fillMode()
|
|
{
|
|
return "fillMode" in this._effect ? this._effect.fillMode : null;
|
|
}
|
|
|
|
get keyframes()
|
|
{
|
|
return "keyframes" in this._effect ? this._effect.keyframes : [];
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (this._name)
|
|
return this._name;
|
|
|
|
if (this._cssAnimationName)
|
|
return this._cssAnimationName;
|
|
|
|
if (this._cssTransitionProperty)
|
|
return this._cssTransitionProperty;
|
|
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = WI.Animation._nextUniqueDisplayNameNumber++;
|
|
return WI.UIString("Animation %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
requestEffectTarget(callback)
|
|
{
|
|
if (this._effectTarget !== undefined) {
|
|
callback(this._effectTarget);
|
|
return;
|
|
}
|
|
|
|
if (this._requestEffectTargetCallbacks) {
|
|
this._requestEffectTargetCallbacks.push(callback);
|
|
return;
|
|
}
|
|
|
|
this._requestEffectTargetCallbacks = [callback];
|
|
|
|
WI.domManager.ensureDocument();
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.AnimationAgent.requestEffectTarget(this._animationId, (error, effectTarget) => {
|
|
// COMPATIBILITY (macOS 12.3, iOS 15.4): nodeId was renamed to effectTarget and changed from DOM.NodeId to DOM.Styleable.
|
|
if (!isNaN(effectTarget))
|
|
effectTarget = {nodeId: effectTarget};
|
|
|
|
this._effectTarget = !error ? WI.DOMStyleable.fromPayload(effectTarget) : null;
|
|
|
|
for (let requestEffectTargetCallback of this._requestEffectTargetCallbacks)
|
|
requestEffectTargetCallback(this._effectTarget);
|
|
|
|
this._requestEffectTargetCallbacks = null;
|
|
});
|
|
}
|
|
|
|
// AnimationManager
|
|
|
|
nameChanged(name)
|
|
{
|
|
this._name = name || null;
|
|
|
|
this.dispatchEventToListeners(WI.Animation.Event.NameChanged);
|
|
}
|
|
|
|
effectChanged(effect)
|
|
{
|
|
this._updateEffect(effect);
|
|
}
|
|
|
|
targetChanged()
|
|
{
|
|
this._effectTarget = undefined;
|
|
|
|
this.dispatchEventToListeners(WI.Animation.Event.TargetChanged);
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateEffect(effect)
|
|
{
|
|
this._effect = effect || {};
|
|
|
|
if ("iterationCount" in this._effect) {
|
|
if (this._effect.iterationCount === -1)
|
|
this._effect.iterationCount = Infinity;
|
|
else if (this._effect.iterationCount === null) {
|
|
// COMPATIBILITY (iOS 14): an iteration count of `Infinity` was not properly handled.
|
|
this._effect.iterationCount = Infinity;
|
|
}
|
|
}
|
|
|
|
if ("timingFunction" in this._effect) {
|
|
let timingFunction = this._effect.timingFunction;
|
|
this._effect.timingFunction = WI.CubicBezierTimingFunction.fromString(timingFunction) || WI.LinearTimingFunction.fromString(timingFunction) || WI.StepsTimingFunction.fromString(timingFunction) || WI.SpringTimingFunction.fromString(timingFunction);
|
|
console.assert(this._effect.timingFunction, timingFunction);
|
|
}
|
|
|
|
if ("keyframes" in this._effect) {
|
|
for (let keyframe of this._effect.keyframes) {
|
|
if (keyframe.easing) {
|
|
let easing = keyframe.easing;
|
|
keyframe.easing = WI.CubicBezierTimingFunction.fromString(easing) || WI.LinearTimingFunction.fromString(easing) || WI.StepsTimingFunction.fromString(easing) || WI.SpringTimingFunction.fromString(easing);
|
|
console.assert(keyframe.easing, easing);
|
|
}
|
|
|
|
if (keyframe.style)
|
|
keyframe.style = keyframe.style.replaceAll(/;\s+/g, ";\n");
|
|
}
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.Animation.Event.EffectChanged);
|
|
}
|
|
};
|
|
|
|
WI.Animation._nextUniqueDisplayNameNumber = 1;
|
|
|
|
WI.Animation.Type = {
|
|
WebAnimation: "web-animation",
|
|
CSSAnimation: "css-animation",
|
|
CSSTransition: "css-transition",
|
|
};
|
|
|
|
WI.Animation.PlaybackDirection = {
|
|
Normal: "normal",
|
|
Reverse: "reverse",
|
|
Alternate: "alternate",
|
|
AlternateReverse: "alternate-reverse",
|
|
};
|
|
|
|
WI.Animation.FillMode = {
|
|
None: "none",
|
|
Forwards: "forwards",
|
|
Backwards: "backwards",
|
|
Both: "both",
|
|
Auto: "auto",
|
|
};
|
|
|
|
WI.Animation.Event = {
|
|
NameChanged: "animation-name-changed",
|
|
EffectChanged: "animation-effect-changed",
|
|
TargetChanged: "animation-target-changed",
|
|
};
|
|
|
|
/* Models/AnimationCollection.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AnimationCollection = class AnimationCollection extends WI.Collection
|
|
{
|
|
constructor(animationType)
|
|
{
|
|
console.assert(!animationType || Object.values(WI.Animation.Type).includes(animationType));
|
|
|
|
super();
|
|
|
|
this._animationType = animationType || null;
|
|
|
|
if (!this._animationType)
|
|
this._animationCollectionForTypeMap = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get animationType() { return this._animationType; }
|
|
|
|
get displayName()
|
|
{
|
|
if (this._animationType) {
|
|
const plural = true;
|
|
return WI.Animation.displayNameForType(this._animationType, plural);
|
|
}
|
|
|
|
return WI.UIString("Web Animations");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
if (!(object instanceof WI.Animation))
|
|
return false;
|
|
|
|
return !this._animationType || object.animationType === this._animationType;
|
|
}
|
|
|
|
animationCollectionForType(animationType)
|
|
{
|
|
console.assert(Object.values(WI.Animation.Type).includes(animationType));
|
|
|
|
if (this._animationType) {
|
|
console.assert(animationType === this._animationType);
|
|
return this;
|
|
}
|
|
|
|
if (!this._animationCollectionForTypeMap)
|
|
this._animationCollectionForTypeMap = new Map;
|
|
|
|
let animationCollectionForType = this._animationCollectionForTypeMap.get(animationType);
|
|
if (!animationCollectionForType) {
|
|
animationCollectionForType = new WI.AnimationCollection(animationType);
|
|
this._animationCollectionForTypeMap.set(animationType, animationCollectionForType);
|
|
}
|
|
return animationCollectionForType;
|
|
}
|
|
|
|
// Protected
|
|
|
|
itemAdded(item)
|
|
{
|
|
super.itemAdded(item);
|
|
|
|
if (!this._animationType) {
|
|
let animationCollectionForType = this.animationCollectionForType(item.animationType);
|
|
animationCollectionForType.add(item);
|
|
}
|
|
}
|
|
|
|
itemRemoved(item)
|
|
{
|
|
if (!this._animationType) {
|
|
let animationCollectionForType = this.animationCollectionForType(item.animationType);
|
|
animationCollectionForType.remove(item);
|
|
}
|
|
|
|
super.itemRemoved(item);
|
|
}
|
|
|
|
itemsCleared(items)
|
|
{
|
|
if (this._animationCollectionForTypeMap) {
|
|
for (let animationCollectionForType of this._animationCollectionForTypeMap.values())
|
|
animationCollectionForType.clear();
|
|
}
|
|
|
|
super.itemsCleared(items);
|
|
}
|
|
};
|
|
|
|
/* Models/BoxShadow.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BoxShadow = class BoxShadow
|
|
{
|
|
constructor(offsetX, offsetY, blurRadius, spreadRadius, inset, color)
|
|
{
|
|
console.assert(!offsetX || (typeof offsetX === "object" && !isNaN(offsetX.value) && offsetX.unit), offsetX);
|
|
console.assert(!offsetY || (typeof offsetY === "object" && !isNaN(offsetY.value) && offsetY.unit), offsetY);
|
|
console.assert(!blurRadius || (typeof blurRadius === "object" && blurRadius.value >= 0 && blurRadius.unit), blurRadius);
|
|
console.assert(!spreadRadius || (typeof spreadRadius === "object" && !isNaN(spreadRadius.value) && spreadRadius.unit), spreadRadius);
|
|
console.assert(!inset || typeof inset === "boolean", inset);
|
|
console.assert(!color || color instanceof WI.Color, color);
|
|
|
|
this._offsetX = offsetX || null;
|
|
this._offsetY = offsetY || null;
|
|
this._blurRadius = blurRadius || null;
|
|
this._spreadRadius = spreadRadius || null;
|
|
this._inset = !!inset;
|
|
this._color = color || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromString(cssString)
|
|
{
|
|
if (cssString === "none")
|
|
return new WI.BoxShadow;
|
|
|
|
let offsetX = null;
|
|
let offsetY = null;
|
|
let blurRadius = null;
|
|
let spreadRadius = null;
|
|
let inset = false;
|
|
let color = null;
|
|
|
|
let startIndex = 0;
|
|
let openParentheses = 0;
|
|
let numberComponentCount = 0;
|
|
for (let i = 0; i <= cssString.length; ++i) {
|
|
if (cssString[i] === "(") {
|
|
++openParentheses;
|
|
continue;
|
|
}
|
|
|
|
if (cssString[i] === ")") {
|
|
--openParentheses;
|
|
continue;
|
|
}
|
|
|
|
if ((cssString[i] !== " " || openParentheses) && i !== cssString.length)
|
|
continue;
|
|
|
|
let component = cssString.substring(startIndex, i + 1).trim();
|
|
|
|
startIndex = i + 1;
|
|
|
|
if (!component.length)
|
|
continue;
|
|
|
|
if (component === "inset") {
|
|
if (inset)
|
|
return null;
|
|
inset = true;
|
|
continue;
|
|
}
|
|
|
|
let possibleColor = WI.Color.fromString(component);
|
|
if (possibleColor) {
|
|
if (color)
|
|
return null;
|
|
color = possibleColor;
|
|
continue;
|
|
}
|
|
|
|
let numberComponent = WI.BoxShadow.parseNumberComponent(component);
|
|
if (!numberComponent)
|
|
return null;
|
|
|
|
switch (++numberComponentCount) {
|
|
case 1:
|
|
offsetX = numberComponent;
|
|
break;
|
|
|
|
case 2:
|
|
offsetY = numberComponent;
|
|
break;
|
|
|
|
case 3:
|
|
blurRadius = numberComponent;
|
|
if (blurRadius.value < 0)
|
|
return null;
|
|
break;
|
|
|
|
case 4:
|
|
spreadRadius = numberComponent;
|
|
break;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
if (!offsetX || !offsetY)
|
|
return null;
|
|
|
|
return new WI.BoxShadow(offsetX, offsetY, blurRadius, spreadRadius, inset, color);
|
|
}
|
|
|
|
static parseNumberComponent(string)
|
|
{
|
|
let value = parseFloat(string);
|
|
if (isNaN(value))
|
|
return null;
|
|
|
|
let unit = string.replace(value, "");
|
|
if (!unit) {
|
|
if (value)
|
|
return null;
|
|
unit = "px";
|
|
} else if (!WI.CSSCompletions.lengthUnits.has(unit))
|
|
return null;
|
|
|
|
return {unit, value};
|
|
}
|
|
|
|
// Public
|
|
|
|
get offsetX() { return this._offsetX; }
|
|
get offsetY() { return this._offsetY; }
|
|
get blurRadius() { return this._blurRadius; }
|
|
get spreadRadius() { return this._spreadRadius; }
|
|
get inset() { return this._inset; }
|
|
get color() { return this._color; }
|
|
|
|
copy()
|
|
{
|
|
return new WI.BoxShadow(this._offsetX, this._offsetY, this._blurRadius, this._spreadRadius, this._inset, this._color);
|
|
}
|
|
|
|
toString()
|
|
{
|
|
if (!this._offsetX || !this._offsetY)
|
|
return "none";
|
|
|
|
function stringifyNumberComponent({value, unit}) {
|
|
return value + (value ? unit : "");
|
|
}
|
|
|
|
let values = [
|
|
stringifyNumberComponent(this._offsetX),
|
|
stringifyNumberComponent(this._offsetY),
|
|
];
|
|
|
|
if (this._blurRadius)
|
|
values.push(stringifyNumberComponent(this._blurRadius));
|
|
|
|
if (this._spreadRadius)
|
|
values.push(stringifyNumberComponent(this._spreadRadius));
|
|
|
|
if (this._color)
|
|
values.push(this._color.toString());
|
|
|
|
if (this._inset)
|
|
values.push("inset");
|
|
|
|
return values.join(" ");
|
|
}
|
|
};
|
|
|
|
/* Models/CPUInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CPUInstrument = class CPUInstrument extends WI.Instrument
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
console.assert(WI.CPUInstrument.supported());
|
|
}
|
|
|
|
// Static
|
|
|
|
static supported()
|
|
{
|
|
// COMPATIBILITY (iOS 12): CPUProfiler domain did not exist.
|
|
return InspectorBackend.hasDomain("CPUProfiler");
|
|
}
|
|
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.CPU;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.CPUProfilerAgent.startTracking();
|
|
}
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.CPUProfilerAgent.stopTracking();
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Models/CPUTimeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CPUTimeline = class CPUTimeline extends WI.Timeline
|
|
{
|
|
// Public
|
|
|
|
addRecord(record, options = {})
|
|
{
|
|
let lastRecord = this.records.lastValue;
|
|
if (lastRecord) {
|
|
let startTime = lastRecord.endTime;
|
|
if (options.discontinuity)
|
|
startTime = options.discontinuity.endTime;
|
|
record.adjustStartTime(startTime);
|
|
}
|
|
|
|
super.addRecord(record, options);
|
|
}
|
|
};
|
|
|
|
/* Models/CPUTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CPUTimelineRecord = class CPUTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor({timestamp, usage, threads})
|
|
{
|
|
super(WI.TimelineRecord.Type.CPU, timestamp - CPUTimelineRecord.samplingRatePerSecond, timestamp);
|
|
|
|
console.assert(typeof timestamp === "number");
|
|
console.assert(typeof usage === "number");
|
|
console.assert(usage >= 0);
|
|
console.assert(threads === undefined || Array.isArray(threads));
|
|
|
|
this._timestamp = timestamp;
|
|
this._usage = usage;
|
|
this._threads = threads || [];
|
|
|
|
this._mainThreadUsage = 0;
|
|
this._webkitThreadUsage = 0;
|
|
this._workerThreadUsage = 0;
|
|
this._unknownThreadUsage = 0;
|
|
this._workersData = null;
|
|
|
|
for (let thread of this._threads) {
|
|
if (thread.type === InspectorBackend.Enum.CPUProfiler.ThreadInfoType.Main) {
|
|
console.assert(!this._mainThreadUsage, "There should only be one main thread.");
|
|
this._mainThreadUsage += thread.usage;
|
|
continue;
|
|
}
|
|
|
|
if (thread.type === InspectorBackend.Enum.CPUProfiler.ThreadInfoType.WebKit) {
|
|
if (thread.targetId) {
|
|
if (!this._workersData)
|
|
this._workersData = [];
|
|
this._workersData.push(thread);
|
|
this._workerThreadUsage += thread.usage;
|
|
continue;
|
|
}
|
|
|
|
this._webkitThreadUsage += thread.usage;
|
|
continue;
|
|
}
|
|
|
|
this._unknownThreadUsage += thread.usage;
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static get samplingRatePerSecond()
|
|
{
|
|
// 500ms. This matches the ResourceUsageThread sampling frequency in the backend.
|
|
return 0.5;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
return new WI.CPUTimelineRecord(json);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
type: this.type,
|
|
timestamp: this._timestamp,
|
|
usage: this._usage,
|
|
threads: this._threads,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get timestamp() { return this._timestamp; }
|
|
get usage() { return this._usage; }
|
|
|
|
get unadjustedStartTime() { return this._timestamp; }
|
|
|
|
get mainThreadUsage() { return this._mainThreadUsage; }
|
|
get webkitThreadUsage() { return this._webkitThreadUsage; }
|
|
get workerThreadUsage() { return this._workerThreadUsage; }
|
|
get unknownThreadUsage() { return this._unknownThreadUsage; }
|
|
get workersData() { return this._workersData; }
|
|
|
|
adjustStartTime(startTime)
|
|
{
|
|
console.assert(startTime < this._endTime);
|
|
this._startTime = startTime;
|
|
}
|
|
};
|
|
|
|
/* Models/CSSCompletions.js */
|
|
|
|
/*
|
|
* Copyright (C) 2010 Nikita Vasilyev. All rights reserved.
|
|
* Copyright (C) 2010 Joseph Pecoraro. All rights reserved.
|
|
* Copyright (C) 2010 Google Inc. All rights reserved.
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSCompletions = class CSSCompletions
|
|
{
|
|
constructor(values, {acceptEmptyPrefix} = {})
|
|
{
|
|
console.assert(Array.isArray(values), values);
|
|
console.assert(!values.length || typeof values[0] === "string", "Expected an array of string values or an empty array", values);
|
|
|
|
this._values = values.slice();
|
|
this._values.sort();
|
|
this._acceptEmptyPrefix = !!acceptEmptyPrefix;
|
|
this._queryController = null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static completeUnbalancedValue(value)
|
|
{
|
|
const State = {
|
|
Data: 0,
|
|
SingleQuoteString: 1,
|
|
DoubleQuoteString: 2,
|
|
Comment: 3
|
|
};
|
|
|
|
let state = State.Data;
|
|
let unclosedParenthesisCount = 0;
|
|
let trailingBackslash = false;
|
|
let length = value.length;
|
|
|
|
for (let i = 0; i < length; ++i) {
|
|
switch (value[i]) {
|
|
case "'":
|
|
if (state === State.Data)
|
|
state = State.SingleQuoteString;
|
|
else if (state === State.SingleQuoteString)
|
|
state = State.Data;
|
|
break;
|
|
|
|
case "\"":
|
|
if (state === State.Data)
|
|
state = State.DoubleQuoteString;
|
|
else if (state === State.DoubleQuoteString)
|
|
state = State.Data;
|
|
break;
|
|
|
|
case "(":
|
|
if (state === State.Data)
|
|
++unclosedParenthesisCount;
|
|
break;
|
|
|
|
case ")":
|
|
if (state === State.Data && unclosedParenthesisCount)
|
|
--unclosedParenthesisCount;
|
|
break;
|
|
|
|
case "/":
|
|
if (state === State.Data) {
|
|
if (value[i + 1] === "*")
|
|
state = State.Comment;
|
|
}
|
|
break;
|
|
|
|
case "\\":
|
|
if (i === length - 1)
|
|
trailingBackslash = true;
|
|
else
|
|
++i; // Skip next character.
|
|
break;
|
|
|
|
case "*":
|
|
if (state === State.Comment) {
|
|
if (value[i + 1] === "/")
|
|
state = State.Data;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
let suffix = "";
|
|
|
|
if (trailingBackslash)
|
|
suffix += "\\";
|
|
|
|
switch (state) {
|
|
case State.SingleQuoteString:
|
|
suffix += "'";
|
|
break;
|
|
case State.DoubleQuoteString:
|
|
suffix += "\"";
|
|
break;
|
|
case State.Comment:
|
|
suffix += "*/";
|
|
break;
|
|
}
|
|
|
|
suffix += ")".repeat(unclosedParenthesisCount);
|
|
|
|
return suffix;
|
|
}
|
|
|
|
static getCompletionText(completion)
|
|
{
|
|
console.assert(typeof completion === "string" || completion instanceof WI.QueryResult, completion);
|
|
|
|
if (typeof completion === "string")
|
|
return completion;
|
|
|
|
if (completion instanceof WI.QueryResult)
|
|
return completion.value;
|
|
|
|
return "";
|
|
}
|
|
|
|
// Public
|
|
|
|
get values()
|
|
{
|
|
return this._values;
|
|
}
|
|
|
|
executeQuery(query)
|
|
{
|
|
if (!query)
|
|
return this._acceptEmptyPrefix ? this._values.slice() : [];
|
|
|
|
this._queryController ||= new WI.CSSQueryController(this._values);
|
|
|
|
return this._queryController.executeQuery(query);
|
|
}
|
|
|
|
startsWith(prefix)
|
|
{
|
|
if (!prefix)
|
|
return this._acceptEmptyPrefix ? this._values.slice() : [];
|
|
|
|
let firstIndex = this._firstIndexOfPrefix(prefix);
|
|
if (firstIndex === -1)
|
|
return [];
|
|
|
|
let results = [];
|
|
while (firstIndex < this._values.length && this._values[firstIndex].startsWith(prefix))
|
|
results.push(this._values[firstIndex++]);
|
|
return results;
|
|
}
|
|
|
|
// Protected
|
|
|
|
replaceValues(values)
|
|
{
|
|
console.assert(Array.isArray(values), values);
|
|
console.assert(typeof values[0] === "string", "Expect an array of string values", values);
|
|
|
|
this._values = values;
|
|
this._values.sort();
|
|
|
|
this._queryController?.reset();
|
|
this._queryController?.addValues(values);
|
|
}
|
|
|
|
// Private
|
|
|
|
_firstIndexOfPrefix(prefix)
|
|
{
|
|
if (!this._values.length)
|
|
return -1;
|
|
if (!prefix)
|
|
return this._acceptEmptyPrefix ? 0 : -1;
|
|
|
|
var maxIndex = this._values.length - 1;
|
|
var minIndex = 0;
|
|
var foundIndex;
|
|
|
|
do {
|
|
var middleIndex = (maxIndex + minIndex) >> 1;
|
|
if (this._values[middleIndex].startsWith(prefix)) {
|
|
foundIndex = middleIndex;
|
|
break;
|
|
}
|
|
if (this._values[middleIndex] < prefix)
|
|
minIndex = middleIndex + 1;
|
|
else
|
|
maxIndex = middleIndex - 1;
|
|
} while (minIndex <= maxIndex);
|
|
|
|
if (foundIndex === undefined)
|
|
return -1;
|
|
|
|
while (foundIndex && this._values[foundIndex - 1].startsWith(prefix))
|
|
foundIndex--;
|
|
|
|
return foundIndex;
|
|
}
|
|
};
|
|
|
|
WI.CSSCompletions.lengthUnits = new Set([
|
|
"ch",
|
|
"cm",
|
|
"dvb",
|
|
"dvh",
|
|
"dvi",
|
|
"dvmax",
|
|
"dvmin",
|
|
"dvw",
|
|
"em",
|
|
"ex",
|
|
"in",
|
|
"lvb",
|
|
"lvh",
|
|
"lvi",
|
|
"lvmax",
|
|
"lvmin",
|
|
"lvw",
|
|
"mm",
|
|
"pc",
|
|
"pt",
|
|
"px",
|
|
"q",
|
|
"rem",
|
|
"svb",
|
|
"svh",
|
|
"svi",
|
|
"svmax",
|
|
"svmin",
|
|
"svw",
|
|
"vb",
|
|
"vh",
|
|
"vi",
|
|
"vmax",
|
|
"vmin",
|
|
"vw",
|
|
]);
|
|
|
|
/* Models/CSSGrouping.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019-2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSGrouping = class CSSGrouping extends WI.Object
|
|
{
|
|
constructor(nodeStyles, type, {ownerStyleSheet, id, text, sourceCodeLocation} = {})
|
|
{
|
|
super();
|
|
|
|
console.assert(nodeStyles);
|
|
console.assert(Object.values(CSSGrouping.Type).includes(type));
|
|
console.assert(!text || (typeof text === "string" && text.length));
|
|
console.assert(!sourceCodeLocation || sourceCodeLocation instanceof WI.SourceCodeLocation);
|
|
|
|
this._nodeStyles = nodeStyles;
|
|
this._type = type;
|
|
|
|
this._ownerStyleSheet = ownerStyleSheet || null;
|
|
this._id = id || null;
|
|
this._text = text || null;
|
|
this._sourceCodeLocation = sourceCodeLocation || null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get ownerStyleSheet() { return this._ownerStyleSheet; }
|
|
get id() { return this._id; }
|
|
get type() { return this._type; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
|
|
get editable()
|
|
{
|
|
return !!this._id && !!this.ownerStyleSheet;
|
|
}
|
|
|
|
get text()
|
|
{
|
|
return this._text;
|
|
}
|
|
|
|
async setText(newText)
|
|
{
|
|
console.assert(this.editable);
|
|
if (!this.editable)
|
|
throw "Cannot set text on non-editable CSSGrouping.";
|
|
|
|
newText ||= "";
|
|
|
|
this._nodeStyles.ignoreNextContentDidChangeForStyleSheet = this._ownerStyleSheet;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
let {grouping: groupingPayload} = await target.CSSAgent.setGroupingHeaderText(this._id, newText);
|
|
|
|
target.DOMAgent.markUndoableState();
|
|
await this._nodeStyles.refresh();
|
|
|
|
console.assert(groupingPayload.type == this._type);
|
|
|
|
console.assert(groupingPayload.ruleId);
|
|
console.assert(groupingPayload.text);
|
|
|
|
this._id = groupingPayload.ruleId;
|
|
this._text = groupingPayload.text;
|
|
|
|
let location = {};
|
|
if (groupingPayload.sourceRange) {
|
|
location.line = groupingPayload.sourceRange.startLine;
|
|
location.column = groupingPayload.sourceRange.startColumn;
|
|
location.documentNode = this._nodeStyles.node.ownerDocument;
|
|
}
|
|
|
|
let groupingSourceCodeLocation = WI.DOMNodeStyles.createSourceCodeLocation(groupingPayload.sourceURL, location);
|
|
this._sourceCodeLocation = WI.cssManager.styleSheetForIdentifier(this._id.styleSheetId).offsetSourceCodeLocation(groupingSourceCodeLocation);
|
|
|
|
this.dispatchEventToListeners(WI.CSSGrouping.Event.TextChanged);
|
|
}
|
|
|
|
get isMedia()
|
|
{
|
|
return this._type === WI.CSSGrouping.Type.MediaRule
|
|
|| this._type === WI.CSSGrouping.Type.MediaImportRule
|
|
|| this._type === WI.CSSGrouping.Type.MediaLinkNode
|
|
|| this._type === WI.CSSGrouping.Type.MediaStyleNode;
|
|
}
|
|
|
|
get isSupports()
|
|
{
|
|
return this._type === WI.CSSGrouping.Type.SupportsRule;
|
|
}
|
|
|
|
get isLayer()
|
|
{
|
|
return this._type === WI.CSSGrouping.Type.LayerRule
|
|
|| this._type === WI.CSSGrouping.Type.LayerImportRule;
|
|
}
|
|
|
|
get isContainer()
|
|
{
|
|
return this._type === WI.CSSGrouping.Type.ContainerRule;
|
|
}
|
|
|
|
get isStyle()
|
|
{
|
|
return this._type === WI.CSSGrouping.Type.StyleRule;
|
|
}
|
|
|
|
get prefix()
|
|
{
|
|
if (this.isSupports)
|
|
return "@supports";
|
|
|
|
if (this.isLayer)
|
|
return "@layer";
|
|
|
|
if (this.isContainer)
|
|
return "@container";
|
|
|
|
if (this.isStyle)
|
|
return null;
|
|
|
|
console.assert(this.isMedia);
|
|
return "@media";
|
|
}
|
|
};
|
|
|
|
WI.CSSGrouping.Type = {
|
|
MediaRule: "media-rule",
|
|
MediaImportRule: "media-import-rule",
|
|
MediaLinkNode: "media-link-node",
|
|
MediaStyleNode: "media-style-node",
|
|
SupportsRule: "supports-rule",
|
|
LayerRule: "layer-rule",
|
|
LayerImportRule: "layer-import-rule",
|
|
ContainerRule: "container-rule",
|
|
StyleRule: "style-rule",
|
|
};
|
|
|
|
WI.CSSGrouping.Event = {
|
|
TextChanged: "css-grouping-event-grouping-text-changed",
|
|
};
|
|
|
|
/* Models/CSSKeywordCompletions.js */
|
|
|
|
/*
|
|
* Copyright (C) 2011 Google Inc. All rights reserved.
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSKeywordCompletions = {};
|
|
|
|
WI.CSSKeywordCompletions.forPartialPropertyName = function(text, {caretPosition, allowEmptyPrefix} = {})
|
|
{
|
|
allowEmptyPrefix ??= false;
|
|
|
|
// FIXME: <webkit.org/b/227157> Styles: Support completions mid-token.
|
|
if (caretPosition !== text.length)
|
|
return {prefix: "", completions: []};
|
|
|
|
if (!text.length && allowEmptyPrefix)
|
|
return {prefix: text, completions: WI.cssManager.propertyNameCompletions.values};
|
|
|
|
return {prefix: text, completions: WI.cssManager.propertyNameCompletions.executeQuery(text)};
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.forPartialPropertyValue = function(text, propertyName, {caretPosition, additionalFunctionValueCompletionsProvider} = {})
|
|
{
|
|
caretPosition ??= text.length;
|
|
|
|
console.assert(caretPosition >= 0 && caretPosition <= text.length, text, caretPosition);
|
|
if (caretPosition < 0 || caretPosition > text.length)
|
|
return {prefix: "", completions: []};
|
|
|
|
if (!text.length)
|
|
return {prefix: "", completions: WI.CSSKeywordCompletions.forProperty(propertyName).values};
|
|
|
|
let tokens = WI.tokenizeCSSValue(text);
|
|
|
|
// Find the token that the cursor is either in or at the end of.
|
|
let indexOfTokenAtCaret = -1;
|
|
let passedCharacters = 0;
|
|
for (let i = 0; i < tokens.length; ++i) {
|
|
passedCharacters += tokens[i].value.length;
|
|
if (passedCharacters >= caretPosition) {
|
|
indexOfTokenAtCaret = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let tokenAtCaret = tokens[indexOfTokenAtCaret];
|
|
console.assert(tokenAtCaret, text, caretPosition);
|
|
if (!tokenAtCaret)
|
|
return {prefix: "", completions: []};
|
|
|
|
if (tokenAtCaret.type && /\b(comment|string)\b/.test(tokenAtCaret.type))
|
|
return {prefix: "", completions: []};
|
|
|
|
let currentTokenValue = tokenAtCaret.value.trim();
|
|
let caretIsInMiddleOfToken = caretPosition !== passedCharacters;
|
|
|
|
// FIXME: <webkit.org/b/227157 Styles: Support completions mid-token.
|
|
// If the cursor was in middle of a token or the next token starts with a valid character for a value, we are effectively mid-token.
|
|
let tokenAfterCaret = tokens[indexOfTokenAtCaret + 1];
|
|
if ((caretIsInMiddleOfToken && currentTokenValue.length) || (!caretIsInMiddleOfToken && tokenAfterCaret && /[a-zA-Z0-9-]/.test(tokenAfterCaret.value[0])))
|
|
return {prefix: "", completions: []};
|
|
|
|
// If the current token value is a comma or open parenthesis, treat it as if we are at the start of a new token.
|
|
if (currentTokenValue === "(" || currentTokenValue === ",")
|
|
currentTokenValue = "";
|
|
|
|
// It's not valid CSS to append completions immediately after a closing parenthesis.
|
|
let tokenBeforeCaret = tokens[indexOfTokenAtCaret - 1];
|
|
if (currentTokenValue === ")" || tokenBeforeCaret?.value === ")")
|
|
return {prefix: "", completions: []};
|
|
|
|
// The CodeMirror CSS-mode tokenizer splits a string like `-name` into two tokens: `-` and `name`.
|
|
if (currentTokenValue.length && tokenBeforeCaret?.value === "-") {
|
|
currentTokenValue = tokenBeforeCaret.value + currentTokenValue;
|
|
}
|
|
|
|
let functionName = null;
|
|
let preceedingFunctionDepth = 0;
|
|
for (let i = indexOfTokenAtCaret; i >= 0; --i) {
|
|
let value = tokens[i].value;
|
|
|
|
// There may be one or more complete functions between the cursor and the current scope's functions name.
|
|
if (value === ")")
|
|
++preceedingFunctionDepth;
|
|
else if (value === "(") {
|
|
if (preceedingFunctionDepth)
|
|
--preceedingFunctionDepth;
|
|
else {
|
|
functionName = tokens[i - 1]?.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let valueCompletions;
|
|
if (functionName)
|
|
valueCompletions = WI.CSSKeywordCompletions.forFunction(functionName, {additionalFunctionValueCompletionsProvider});
|
|
else
|
|
valueCompletions = WI.CSSKeywordCompletions.forProperty(propertyName);
|
|
|
|
return {prefix: currentTokenValue, completions: valueCompletions.executeQuery(currentTokenValue)};
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.forProperty = function(propertyName)
|
|
{
|
|
let acceptedKeywords = ["initial", "unset", "revert", "revert-layer", "var()", "env()"];
|
|
|
|
function addKeywordsForName(name) {
|
|
let isNotPrefixed = name.charAt(0) !== "-";
|
|
|
|
if (name in WI.CSSKeywordCompletions._propertyKeywordMap)
|
|
acceptedKeywords.pushAll(WI.CSSKeywordCompletions._propertyKeywordMap[name]);
|
|
else if (isNotPrefixed && ("-webkit-" + name) in WI.CSSKeywordCompletions._propertyKeywordMap)
|
|
acceptedKeywords.pushAll(WI.CSSKeywordCompletions._propertyKeywordMap["-webkit-" + name]);
|
|
|
|
if (WI.CSSKeywordCompletions.isColorAwareProperty(name))
|
|
acceptedKeywords.pushAll(WI.CSSKeywordCompletions._colors);
|
|
|
|
// Only suggest "inherit" on inheritable properties even though it is valid on all properties.
|
|
if (WI.CSSKeywordCompletions.InheritedProperties.has(name))
|
|
acceptedKeywords.push("inherit");
|
|
else if (isNotPrefixed && WI.CSSKeywordCompletions.InheritedProperties.has("-webkit-" + name))
|
|
acceptedKeywords.push("inherit");
|
|
}
|
|
|
|
addKeywordsForName(propertyName);
|
|
|
|
let unaliasedName = WI.CSSKeywordCompletions.PropertyNameForAlias.get(propertyName);
|
|
if (unaliasedName)
|
|
addKeywordsForName(unaliasedName);
|
|
|
|
let longhandNames = WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.get(propertyName);
|
|
if (longhandNames) {
|
|
for (let longhandName of longhandNames)
|
|
addKeywordsForName(longhandName);
|
|
}
|
|
|
|
if (acceptedKeywords.includes(WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder) && WI.cssManager.propertyNameCompletions) {
|
|
acceptedKeywords.remove(WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder);
|
|
acceptedKeywords.pushAll(WI.cssManager.propertyNameCompletions.values);
|
|
}
|
|
|
|
return new WI.CSSCompletions(Array.from(new Set(acceptedKeywords)), {acceptEmptyPrefix: true});
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.isColorAwareProperty = function(name)
|
|
{
|
|
if (WI.CSSKeywordCompletions._colorAwareProperties.has(name))
|
|
return true;
|
|
|
|
let isNotPrefixed = name.charAt(0) !== "-";
|
|
if (isNotPrefixed && WI.CSSKeywordCompletions._colorAwareProperties.has("-webkit-" + name))
|
|
return true;
|
|
|
|
if (name.endsWith("color"))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.isTimingFunctionAwareProperty = function(name)
|
|
{
|
|
if (WI.CSSKeywordCompletions._timingFunctionAwareProperties.has(name))
|
|
return true;
|
|
|
|
let isNotPrefixed = name.charAt(0) !== "-";
|
|
if (isNotPrefixed && WI.CSSKeywordCompletions._timingFunctionAwareProperties.has("-webkit-" + name))
|
|
return true;
|
|
|
|
return false;
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.forFunction = function(functionName, {additionalFunctionValueCompletionsProvider} = {})
|
|
{
|
|
let suggestions = ["var()"];
|
|
|
|
if (functionName === "var")
|
|
suggestions = [];
|
|
else if (functionName === "calc" || functionName === "min" || functionName === "max")
|
|
suggestions.push("calc()", "min()", "max()");
|
|
else if (functionName === "env")
|
|
suggestions.push("safe-area-inset-top", "safe-area-inset-right", "safe-area-inset-bottom", "safe-area-inset-left");
|
|
else if (functionName === "image-set")
|
|
suggestions.push("url()");
|
|
else if (functionName === "repeat")
|
|
suggestions.push("auto", "auto-fill", "auto-fit", "min-content", "max-content");
|
|
else if (functionName === "steps")
|
|
suggestions.push("jump-none", "jump-start", "jump-end", "jump-both", "start", "end");
|
|
else if (functionName.endsWith("gradient")) {
|
|
suggestions.push("to", "left", "right", "top", "bottom");
|
|
suggestions.pushAll(WI.CSSKeywordCompletions._colors);
|
|
}
|
|
|
|
if (additionalFunctionValueCompletionsProvider)
|
|
suggestions.pushAll(additionalFunctionValueCompletionsProvider(functionName));
|
|
|
|
return new WI.CSSCompletions(suggestions, {acceptEmptyPrefix: true});
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.addCustomCompletions = function(properties)
|
|
{
|
|
for (var property of properties) {
|
|
if (property.aliases) {
|
|
for (let alias of property.aliases)
|
|
WI.CSSKeywordCompletions.PropertyNameForAlias.set(alias, property.name);
|
|
}
|
|
|
|
if (property.values)
|
|
WI.CSSKeywordCompletions.addPropertyCompletionValues(property.name, property.values);
|
|
|
|
if (property.inherited)
|
|
WI.CSSKeywordCompletions.InheritedProperties.add(property.name);
|
|
|
|
if (property.longhands) {
|
|
WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.set(property.name, property.longhands);
|
|
|
|
for (let longhand of property.longhands) {
|
|
let shorthands = WI.CSSKeywordCompletions.ShorthandNamesForLongHandProperty.getOrInitialize(longhand, []);
|
|
shorthands.push(property.name);
|
|
}
|
|
}
|
|
|
|
}
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.addPropertyCompletionValues = function(propertyName, newValues)
|
|
{
|
|
var existingValues = WI.CSSKeywordCompletions._propertyKeywordMap[propertyName];
|
|
if (!existingValues) {
|
|
WI.CSSKeywordCompletions._propertyKeywordMap[propertyName] = newValues;
|
|
return;
|
|
}
|
|
|
|
var union = new Set;
|
|
for (var value of existingValues)
|
|
union.add(value);
|
|
for (var value of newValues)
|
|
union.add(value);
|
|
|
|
WI.CSSKeywordCompletions._propertyKeywordMap[propertyName] = [...union.values()];
|
|
};
|
|
|
|
WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder = "__all-properties__";
|
|
|
|
// Populated by CSS.getSupportedCSSProperties.
|
|
WI.CSSKeywordCompletions.PropertyNameForAlias = new Map;
|
|
WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty = new Map;
|
|
WI.CSSKeywordCompletions.ShorthandNamesForLongHandProperty = new Map;
|
|
|
|
WI.CSSKeywordCompletions.InheritedProperties = new Set([
|
|
// Compatibility (iOS 12): `inherited` didn't exist on `CSSPropertyInfo`
|
|
"-apple-color-filter",
|
|
"-webkit-aspect-ratio",
|
|
"-webkit-border-horizontal-spacing",
|
|
"-webkit-border-vertical-spacing",
|
|
"-webkit-box-direction",
|
|
"-webkit-cursor-visibility",
|
|
"-webkit-font-kerning",
|
|
"-webkit-font-smoothing",
|
|
"-webkit-hyphenate-character",
|
|
"-webkit-hyphenate-limit-after",
|
|
"-webkit-hyphenate-limit-before",
|
|
"-webkit-hyphenate-limit-lines",
|
|
"-webkit-hyphens",
|
|
"-webkit-line-align",
|
|
"-webkit-line-break",
|
|
"-webkit-line-box-contain",
|
|
"-webkit-line-grid",
|
|
"-webkit-line-snap",
|
|
"-webkit-locale",
|
|
"-webkit-nbsp-mode",
|
|
"-webkit-overflow-scrolling",
|
|
"-webkit-rtl-ordering",
|
|
"-webkit-ruby-position",
|
|
"-webkit-text-combine",
|
|
"-webkit-text-decoration-skip",
|
|
"-webkit-text-decorations-in-effect",
|
|
"-webkit-text-emphasis",
|
|
"-webkit-text-emphasis-color",
|
|
"-webkit-text-emphasis-position",
|
|
"-webkit-text-emphasis-style",
|
|
"-webkit-text-fill-color",
|
|
"-webkit-text-orientation",
|
|
"-webkit-text-security",
|
|
"-webkit-text-size-adjust",
|
|
"-webkit-text-stroke",
|
|
"-webkit-text-stroke-color",
|
|
"-webkit-text-stroke-width",
|
|
"-webkit-text-underline-position",
|
|
"-webkit-text-zoom",
|
|
"-webkit-touch-callout",
|
|
"-webkit-user-modify",
|
|
"-webkit-user-select",
|
|
"border-collapse",
|
|
"border-spacing",
|
|
"caption-side",
|
|
"caret-color",
|
|
"clip-rule",
|
|
"color",
|
|
"color-interpolation",
|
|
"color-interpolation-filters",
|
|
"color-rendering",
|
|
"cursor",
|
|
"direction",
|
|
"empty-cells",
|
|
"fill",
|
|
"fill-opacity",
|
|
"fill-rule",
|
|
"font",
|
|
"font-family",
|
|
"font-feature-settings",
|
|
"font-optical-sizing",
|
|
"font-size",
|
|
"font-stretch",
|
|
"font-style",
|
|
"font-synthesis",
|
|
"font-synthesis-weight",
|
|
"font-synthesis-style",
|
|
"font-synthesis-small-caps",
|
|
"font-variant",
|
|
"font-variant-alternates",
|
|
"font-variant-caps",
|
|
"font-variant-east-asian",
|
|
"font-variant-ligatures",
|
|
"font-variant-numeric",
|
|
"font-variant-position",
|
|
"font-variation-settings",
|
|
"font-weight",
|
|
"glyph-orientation-horizontal",
|
|
"glyph-orientation-vertical",
|
|
"hanging-punctuation",
|
|
"image-orientation",
|
|
"image-rendering",
|
|
"image-resolution",
|
|
"kerning",
|
|
"letter-spacing",
|
|
"line-break",
|
|
"line-height",
|
|
"list-style",
|
|
"list-style-image",
|
|
"list-style-position",
|
|
"list-style-type",
|
|
"marker",
|
|
"marker-end",
|
|
"marker-mid",
|
|
"marker-start",
|
|
"orphans",
|
|
"pointer-events",
|
|
"print-color-adjust",
|
|
"quotes",
|
|
"resize",
|
|
"shape-rendering",
|
|
"speak-as",
|
|
"stroke",
|
|
"stroke-color",
|
|
"stroke-dasharray",
|
|
"stroke-dashoffset",
|
|
"stroke-linecap",
|
|
"stroke-linejoin",
|
|
"stroke-miterlimit",
|
|
"stroke-opacity",
|
|
"stroke-width",
|
|
"tab-size",
|
|
"text-align",
|
|
"text-align-last",
|
|
"text-anchor",
|
|
"text-indent",
|
|
"text-justify",
|
|
"text-rendering",
|
|
"text-shadow",
|
|
"text-transform",
|
|
"visibility",
|
|
"white-space",
|
|
"widows",
|
|
"word-break",
|
|
"word-spacing",
|
|
"word-wrap",
|
|
"writing-mode",
|
|
]);
|
|
|
|
WI.CSSKeywordCompletions._colors = [
|
|
"aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon", "navy", "olive", "orange", "purple", "red",
|
|
"silver", "teal", "white", "yellow", "transparent", "currentcolor", "grey", "aliceblue", "antiquewhite",
|
|
"aquamarine", "azure", "beige", "bisque", "blanchedalmond", "blueviolet", "brown", "burlywood", "cadetblue",
|
|
"chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan",
|
|
"darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange",
|
|
"darkorchid", "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkslategrey",
|
|
"darkturquoise", "darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
|
|
"floralwhite", "forestgreen", "gainsboro", "ghostwhite", "gold", "goldenrod", "greenyellow", "honeydew", "hotpink",
|
|
"indianred", "indigo", "ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue",
|
|
"lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey", "lightpink",
|
|
"lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey", "lightsteelblue", "lightyellow",
|
|
"limegreen", "linen", "magenta", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen",
|
|
"mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue", "mintcream",
|
|
"mistyrose", "moccasin", "navajowhite", "oldlace", "olivedrab", "orangered", "orchid", "palegoldenrod", "palegreen",
|
|
"paleturquoise", "palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "rebeccapurple", "rosybrown",
|
|
"royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell", "sienna", "skyblue", "slateblue",
|
|
"slategray", "slategrey", "snow", "springgreen", "steelblue", "tan", "thistle", "tomato", "turquoise", "violet",
|
|
"wheat", "whitesmoke", "yellowgreen", "rgb()", "rgba()", "hsl()", "hsla()", "color()", "hwb()", "lch()", "lab()",
|
|
"color-mix()", "color-contrast()", "light-dark()",
|
|
];
|
|
|
|
WI.CSSKeywordCompletions._colorAwareProperties = new Set([
|
|
"background",
|
|
"background-color",
|
|
"background-image",
|
|
"border",
|
|
"border-color",
|
|
"border-bottom",
|
|
"border-bottom-color",
|
|
"border-left",
|
|
"border-left-color",
|
|
"border-right",
|
|
"border-right-color",
|
|
"border-top",
|
|
"border-top-color",
|
|
"box-shadow", "-webkit-box-shadow",
|
|
"color",
|
|
"column-rule", "-webkit-column-rule",
|
|
"column-rule-color", "-webkit-column-rule-color",
|
|
"fill",
|
|
"outline",
|
|
"outline-color",
|
|
"stroke",
|
|
"text-decoration-color", "-webkit-text-decoration-color",
|
|
"text-emphasis", "-webkit-text-emphasis",
|
|
"text-emphasis-color", "-webkit-text-emphasis-color",
|
|
"text-line-through",
|
|
"text-line-through-color",
|
|
"text-overline",
|
|
"text-overline-color",
|
|
"text-shadow",
|
|
"text-underline",
|
|
"text-underline-color",
|
|
"-webkit-text-fill-color",
|
|
"-webkit-text-stroke",
|
|
"-webkit-text-stroke-color",
|
|
|
|
// iOS Properties
|
|
"-webkit-tap-highlight-color",
|
|
]);
|
|
|
|
WI.CSSKeywordCompletions._timingFunctionAwareProperties = new Set([
|
|
"animation", "-webkit-animation",
|
|
"animation-timing-function", "-webkit-animation-timing-function",
|
|
"transition", "-webkit-transition",
|
|
"transition-timing-function", "-webkit-transition-timing-function",
|
|
]);
|
|
|
|
WI.CSSKeywordCompletions._propertyKeywordMap = {
|
|
"content": [
|
|
"list-item", "close-quote", "no-close-quote", "no-open-quote", "open-quote", "attr()", "counter()", "counters()", "url()", "linear-gradient()", "radial-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()", "-webkit-canvas()", "cross-fade()", "image-set()"
|
|
],
|
|
"list-style-image": [
|
|
"none", "url()", "linear-gradient()", "radial-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()", "-webkit-canvas()", "cross-fade()", "image-set()"
|
|
],
|
|
"baseline-shift": [
|
|
"baseline", "sub", "super"
|
|
],
|
|
"block-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()",
|
|
],
|
|
"border-block-end-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"border-block-start-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"border-bottom-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"border-inline-end-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"border-inline-start-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"font-stretch": [
|
|
"normal", "wider", "narrower", "ultra-condensed", "extra-condensed", "condensed", "semi-condensed",
|
|
"semi-expanded", "expanded", "extra-expanded", "ultra-expanded"
|
|
],
|
|
"border-left-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"border-top-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"outline-color": [
|
|
"invert", "-webkit-focus-ring-color"
|
|
],
|
|
"cursor": [
|
|
"auto", "default", "none", "context-menu", "help", "pointer", "progress", "wait", "cell", "crosshair", "text", "vertical-text",
|
|
"alias", "copy", "move", "no-drop", "not-allowed", "grab", "grabbing",
|
|
"e-resize", "n-resize", "ne-resize", "nw-resize", "s-resize", "se-resize", "sw-resize", "w-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize",
|
|
"col-resize", "row-resize", "all-scroll", "zoom-in", "zoom-out",
|
|
"-webkit-grab", "-webkit-grabbing", "-webkit-zoom-in", "-webkit-zoom-out",
|
|
"url()", "image-set()"
|
|
],
|
|
"border-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"size": [
|
|
"a3", "a4", "a5", "b4", "b5", "landscape", "ledger", "legal", "letter", "portrait"
|
|
],
|
|
"background": [
|
|
"none", "url()", "linear-gradient()", "radial-gradient()", "conic-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()", "repeating-conic-gradient()", "-webkit-canvas()", "cross-fade()", "image-set()",
|
|
"repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round",
|
|
"scroll", "fixed", "local",
|
|
"auto", "contain", "cover",
|
|
"top", "right", "left", "bottom", "center",
|
|
"border-box", "padding-box", "content-box"
|
|
],
|
|
"background-image": [
|
|
"none", "url()", "linear-gradient()", "radial-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()", "-webkit-canvas()", "cross-fade()", "image-set()"
|
|
],
|
|
"background-size": [
|
|
"auto", "contain", "cover"
|
|
],
|
|
"background-attachment": [
|
|
"scroll", "fixed", "local"
|
|
],
|
|
"background-repeat": [
|
|
"repeat", "repeat-x", "repeat-y", "no-repeat", "space", "round"
|
|
],
|
|
"background-blend-mode": [
|
|
"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity"
|
|
],
|
|
"background-position": [
|
|
"top", "right", "left", "bottom", "center"
|
|
],
|
|
"background-origin": [
|
|
"border-box", "padding-box", "content-box"
|
|
],
|
|
"background-clip": [
|
|
"border-box", "padding-box", "content-box"
|
|
],
|
|
"enable-background": [
|
|
"accumulate", "new"
|
|
],
|
|
"font-palette": [
|
|
"none", "normal", "light", "dark"
|
|
],
|
|
"hanging-punctuation": [
|
|
"none", "first", "last", "allow-end", "force-end"
|
|
],
|
|
"inline-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()",
|
|
],
|
|
"overflow": [
|
|
"hidden", "auto", "visible", "scroll", "marquee", "-webkit-paged-x", "-webkit-paged-y"
|
|
],
|
|
"-webkit-box-reflect": [
|
|
"none", "left", "right", "above", "below"
|
|
],
|
|
"margin-block": [
|
|
"auto",
|
|
],
|
|
"margin-block-end": [
|
|
"auto",
|
|
],
|
|
"margin-block-start": [
|
|
"auto",
|
|
],
|
|
"margin-bottom": [
|
|
"auto"
|
|
],
|
|
"margin-inline": [
|
|
"auto",
|
|
],
|
|
"margin-inline-end": [
|
|
"auto",
|
|
],
|
|
"margin-inline-start": [
|
|
"auto",
|
|
],
|
|
"font-weight": [
|
|
"normal", "bold", "bolder", "lighter", "100", "200", "300", "400", "500", "600", "700", "800", "900"
|
|
],
|
|
"font-synthesis": [
|
|
"none", "weight", "style", "small-caps"
|
|
],
|
|
"font-synthesis-weight": [
|
|
"none", "auto"
|
|
],
|
|
"font-synthesis-style": [
|
|
"none", "auto"
|
|
],
|
|
"font-synthesis-small-caps": [
|
|
"none", "auto"
|
|
],
|
|
"font-style": [
|
|
"italic", "oblique", "normal"
|
|
],
|
|
"outline": [
|
|
"none", "hidden", "inset", "groove", "ridge", "outset", "dotted", "dashed", "solid", "double"
|
|
],
|
|
"font": [
|
|
"caption", "icon", "menu", "message-box", "small-caption", "-webkit-mini-control", "-webkit-small-control",
|
|
"-webkit-control", "status-bar", "italic", "oblique", "small-caps", "normal", "bold", "bolder", "lighter",
|
|
"100", "200", "300", "400", "500", "600", "700", "800", "900", "xx-small", "x-small", "small", "medium",
|
|
"large", "x-large", "xx-large", "xxx-large", "smaller", "larger", "serif", "sans-serif", "cursive",
|
|
"fantasy", "monospace", "-webkit-body", "-webkit-pictograph", "-apple-system",
|
|
"-apple-system-headline", "-apple-system-body", "-apple-system-subheadline", "-apple-system-footnote",
|
|
"-apple-system-caption1", "-apple-system-caption2", "-apple-system-short-headline", "-apple-system-short-body",
|
|
"-apple-system-short-subheadline", "-apple-system-short-footnote", "-apple-system-short-caption1",
|
|
"-apple-system-tall-body", "-apple-system-title0", "-apple-system-title1", "-apple-system-title2", "-apple-system-title3", "-apple-system-title4", "system-ui"
|
|
],
|
|
"outline-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"box-shadow": [
|
|
"none"
|
|
],
|
|
"text-shadow": [
|
|
"none"
|
|
],
|
|
"-webkit-box-shadow": [
|
|
"none"
|
|
],
|
|
"border-right-width": [
|
|
"medium", "thick", "thin"
|
|
],
|
|
"line-height": [
|
|
"normal"
|
|
],
|
|
"counter-increment": [
|
|
"none"
|
|
],
|
|
"counter-reset": [
|
|
"none"
|
|
],
|
|
"page-break-after": [
|
|
"left", "right", "auto", "always", "avoid"
|
|
],
|
|
"page-break-before": [
|
|
"left", "right", "auto", "always", "avoid"
|
|
],
|
|
"page-break-inside": [
|
|
"auto", "avoid"
|
|
],
|
|
"-webkit-column-break-after": [
|
|
"left", "right", "auto", "always", "avoid"
|
|
],
|
|
"-webkit-column-break-before": [
|
|
"left", "right", "auto", "always", "avoid"
|
|
],
|
|
"-webkit-column-break-inside": [
|
|
"auto", "avoid"
|
|
],
|
|
"border-image": [
|
|
"repeat", "stretch", "url()", "linear-gradient()", "radial-gradient()", "repeating-linear-gradient()", "repeating-radial-gradient()", "-webkit-canvas()", "cross-fade()", "image-set()"
|
|
],
|
|
"border-image-repeat": [
|
|
"repeat", "stretch", "space", "round"
|
|
],
|
|
"-webkit-mask-box-image-repeat": [
|
|
"repeat", "stretch", "space", "round"
|
|
],
|
|
"font-family": [
|
|
"serif", "sans-serif", "cursive", "fantasy", "monospace", "-webkit-body", "-webkit-pictograph",
|
|
"-apple-system", "-apple-system-headline", "-apple-system-body",
|
|
"-apple-system-subheadline", "-apple-system-footnote", "-apple-system-caption1", "-apple-system-caption2",
|
|
"-apple-system-short-headline", "-apple-system-short-body", "-apple-system-short-subheadline",
|
|
"-apple-system-short-footnote", "-apple-system-short-caption1", "-apple-system-tall-body",
|
|
"-apple-system-title0", "-apple-system-title1", "-apple-system-title2", "-apple-system-title3", "-apple-system-title4", "system-ui"
|
|
],
|
|
"margin-left": [
|
|
"auto"
|
|
],
|
|
"margin-top": [
|
|
"auto"
|
|
],
|
|
"zoom": [
|
|
"normal", "document", "reset"
|
|
],
|
|
"z-index": [
|
|
"auto"
|
|
],
|
|
"width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"max-width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()"
|
|
],
|
|
"min-width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"max-block-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()",
|
|
],
|
|
"max-height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()"
|
|
],
|
|
"max-inline-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()",
|
|
],
|
|
"min-block-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()",
|
|
],
|
|
"min-height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"min-inline-size": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()",
|
|
],
|
|
"letter-spacing": [
|
|
"normal", "calc()"
|
|
],
|
|
"word-spacing": [
|
|
"normal", "calc()"
|
|
],
|
|
"border": [
|
|
"none", "hidden", "inset", "groove", "ridge", "outset", "dotted", "dashed", "solid", "double"
|
|
],
|
|
"font-size": [
|
|
"xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large", "smaller", "larger"
|
|
],
|
|
"font-variant": [
|
|
"small-caps", "normal"
|
|
],
|
|
"font-variant-numeric": [
|
|
"normal", "ordinal", "slashed-zero", "lining-nums", "oldstyle-nums", "proportional-nums", "tabular-nums",
|
|
"diagonal-fractions", "stacked-fractions"
|
|
],
|
|
"vertical-align": [
|
|
"baseline", "middle", "sub", "super", "text-top", "text-bottom", "top", "bottom", "-webkit-baseline-middle"
|
|
],
|
|
"text-indent": [
|
|
"each-line", "hanging"
|
|
],
|
|
"clip": [
|
|
"auto", "rect()"
|
|
],
|
|
"clip-path": [
|
|
"none", "url()", "circle()", "ellipse()", "inset()", "polygon()", "margin-box", "border-box", "padding-box", "content-box"
|
|
],
|
|
"shape-outside": [
|
|
"none", "url()", "circle()", "ellipse()", "inset()", "polygon()", "margin-box", "border-box", "padding-box", "content-box"
|
|
],
|
|
"orphans": [
|
|
"auto"
|
|
],
|
|
"widows": [
|
|
"auto"
|
|
],
|
|
"margin": [
|
|
"auto"
|
|
],
|
|
"page": [
|
|
"auto"
|
|
],
|
|
"perspective": [
|
|
"none"
|
|
],
|
|
"perspective-origin": [
|
|
"none", "left", "right", "bottom", "top", "center"
|
|
],
|
|
"margin-right": [
|
|
"auto"
|
|
],
|
|
"-webkit-text-emphasis": [
|
|
"circle", "filled", "open", "dot", "double-circle", "triangle", "sesame"
|
|
],
|
|
"-webkit-text-emphasis-style": [
|
|
"circle", "filled", "open", "dot", "double-circle", "triangle", "sesame"
|
|
],
|
|
"-webkit-text-emphasis-position": [
|
|
"over", "under", "left", "right"
|
|
],
|
|
"transform": [
|
|
"none",
|
|
"scale()", "scaleX()", "scaleY()", "scale3d()", "rotate()", "rotateX()", "rotateY()", "rotateZ()", "rotate3d()", "skew()", "skewX()", "skewY()",
|
|
"translate()", "translateX()", "translateY()", "translateZ()", "translate3d()", "matrix()", "matrix3d()", "perspective()"
|
|
],
|
|
"text-decoration": [
|
|
"none", "underline", "overline", "line-through", "blink"
|
|
],
|
|
"-webkit-text-decorations-in-effect": [
|
|
"none", "underline", "overline", "line-through", "blink"
|
|
],
|
|
"-webkit-text-decoration-line": [
|
|
"none", "underline", "overline", "line-through", "blink"
|
|
],
|
|
"-webkit-text-decoration-skip": [
|
|
"auto", "none", "objects", "ink"
|
|
],
|
|
"-webkit-text-underline-position": [
|
|
"auto", "alphabetic", "under"
|
|
],
|
|
"transition": [
|
|
"none", "ease", "linear", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end", "steps()", "cubic-bezier()", "linear()", "spring()", "all", WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder
|
|
],
|
|
"transition-timing-function": [
|
|
"ease", "linear", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end", "steps()", "cubic-bezier()", "linear()", "spring()"
|
|
],
|
|
"transition-property": [
|
|
"all", "none", WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder
|
|
],
|
|
"animation-direction": [
|
|
"normal", "alternate", "reverse", "alternate-reverse"
|
|
],
|
|
"animation-fill-mode": [
|
|
"none", "forwards", "backwards", "both"
|
|
],
|
|
"animation-iteration-count": [
|
|
"infinite"
|
|
],
|
|
"animation-play-state": [
|
|
"paused", "running"
|
|
],
|
|
"animation-timeline": [
|
|
"auto", "none", "scroll()", "view()"
|
|
],
|
|
"animation-timing-function": [
|
|
"ease", "linear", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end", "steps()", "cubic-bezier()", "linear()", "spring()"
|
|
],
|
|
"align-content": [
|
|
"auto",
|
|
"baseline", "last-baseline",
|
|
"space-between", "space-around", "space-evenly", "stretch",
|
|
"center", "start", "end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"justify-content": [
|
|
"auto",
|
|
"baseline", "last-baseline", "space-between", "space-around", "space-evenly", "stretch",
|
|
"center", "start", "end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"align-items": [
|
|
"auto", "stretch",
|
|
"baseline", "last-baseline",
|
|
"center", "start", "end", "self-start", "self-end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"align-self": [
|
|
"auto", "stretch",
|
|
"baseline", "last-baseline",
|
|
"center", "start", "end", "self-start", "self-end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"justify-items": [
|
|
"auto", "stretch",
|
|
"baseline", "last-baseline",
|
|
"center", "start", "end", "self-start", "self-end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"justify-self": [
|
|
"auto", "stretch",
|
|
"baseline", "last-baseline",
|
|
"center", "start", "end", "self-start", "self-end", "flex-start", "flex-end", "left", "right",
|
|
"true", "safe"
|
|
],
|
|
"flex-flow": [
|
|
"row", "row-reverse", "column", "column-reverse",
|
|
"nowrap", "wrap", "wrap-reverse"
|
|
],
|
|
"flex": [
|
|
"none"
|
|
],
|
|
"flex-basis": [
|
|
"auto"
|
|
],
|
|
"grid": [
|
|
"none"
|
|
],
|
|
"grid-area": [
|
|
"auto"
|
|
],
|
|
"grid-auto-columns": [
|
|
"auto", "max-content", "-webkit-max-content", "min-content", "-webkit-min-content", "minmax()",
|
|
],
|
|
"grid-auto-flow": [
|
|
"row", "column", "dense"
|
|
],
|
|
"grid-auto-rows": [
|
|
"auto", "max-content", "-webkit-max-content", "min-content", "-webkit-min-content", "minmax()",
|
|
],
|
|
"grid-column": [
|
|
"auto"
|
|
],
|
|
"grid-column-start": [
|
|
"auto"
|
|
],
|
|
"grid-column-end": [
|
|
"auto"
|
|
],
|
|
"grid-row": [
|
|
"auto"
|
|
],
|
|
"grid-row-start": [
|
|
"auto"
|
|
],
|
|
"grid-row-end": [
|
|
"auto"
|
|
],
|
|
"grid-template": [
|
|
"none"
|
|
],
|
|
"grid-template-areas": [
|
|
"none"
|
|
],
|
|
"grid-template-columns": [
|
|
"none", "auto", "max-content", "-webkit-max-content", "min-content", "-webkit-min-content", "minmax()", "repeat()"
|
|
],
|
|
"grid-template-rows": [
|
|
"none", "auto", "max-content", "-webkit-max-content", "min-content", "-webkit-min-content", "minmax()", "repeat()"
|
|
],
|
|
"scroll-snap-align": [
|
|
"none", "start", "center", "end"
|
|
],
|
|
"scroll-snap-type": [
|
|
"none", "mandatory", "proximity", "x", "y", "inline", "block", "both"
|
|
],
|
|
"-webkit-mask-composite": [
|
|
"clear", "copy", "source-over", "source-in", "source-out", "source-atop", "destination-over", "destination-in", "destination-out", "destination-atop", "xor", "plus-darker", "plus-lighter"
|
|
],
|
|
"-webkit-text-stroke-width": [
|
|
"medium", "thick", "thin", "calc()"
|
|
],
|
|
"-webkit-aspect-ratio": [
|
|
"auto", "from-dimensions", "from-intrinsic", "/"
|
|
],
|
|
"filter": [
|
|
"none", "grayscale()", "sepia()", "saturate()", "hue-rotate()", "invert()", "opacity()", "brightness()", "contrast()", "blur()", "drop-shadow()", "custom()"
|
|
],
|
|
"-webkit-backdrop-filter": [
|
|
"none", "grayscale()", "sepia()", "saturate()", "hue-rotate()", "invert()", "opacity()", "brightness()", "contrast()", "blur()", "drop-shadow()", "custom()"
|
|
],
|
|
"-webkit-hyphenate-character": [
|
|
"none"
|
|
],
|
|
"-webkit-hyphenate-limit-after": [
|
|
"auto"
|
|
],
|
|
"-webkit-hyphenate-limit-before": [
|
|
"auto"
|
|
],
|
|
"-webkit-hyphenate-limit-lines": [
|
|
"no-limit"
|
|
],
|
|
"-webkit-line-grid": [
|
|
"none"
|
|
],
|
|
"-webkit-locale": [
|
|
"auto"
|
|
],
|
|
"-webkit-line-box-contain": [
|
|
"block", "inline", "font", "glyphs", "replaced", "inline-box", "none"
|
|
],
|
|
"font-feature-settings": [
|
|
"normal"
|
|
],
|
|
|
|
// iOS Properties
|
|
"-webkit-text-size-adjust": [
|
|
"none", "auto"
|
|
],
|
|
|
|
// Compatibility (iOS 12): `inherited` didn't exist on `CSSPropertyInfo`
|
|
"-apple-pay-button-style": [
|
|
"black", "white", "white-outline",
|
|
],
|
|
"-apple-pay-button-type": [
|
|
"plain", "buy", "set-up", "donate", "check-out", "book", "subscribe",
|
|
],
|
|
"-webkit-alt": [
|
|
"attr()",
|
|
],
|
|
"-webkit-animation-direction": [
|
|
"normal", "alternate", "reverse", "alternate-reverse",
|
|
],
|
|
"-webkit-animation-fill-mode": [
|
|
"none", "forwards", "backwards", "both",
|
|
],
|
|
"-webkit-animation-iteration-count": [
|
|
"infinite",
|
|
],
|
|
"-webkit-animation-play-state": [
|
|
"paused", "running",
|
|
],
|
|
"-webkit-animation-timing-function": [
|
|
"ease", "linear", "ease-in", "ease-out", "ease-in-out", "step-start", "step-end", "steps()", "cubic-bezier()", "linear()", "spring()",
|
|
],
|
|
"-webkit-appearance": [
|
|
"none", "checkbox", "radio", "push-button", "square-button", "button", "button-bevel", "default-button", "inner-spin-button", "listbox", "listitem", "media-controls-background", "media-controls-dark-bar-background", "media-controls-fullscreen-background", "media-controls-light-bar-background", "media-current-time-display", "media-enter-fullscreen-button", "media-exit-fullscreen-button", "media-fullscreen-volume-slider", "media-fullscreen-volume-slider-thumb", "media-mute-button", "media-overlay-play-button", "media-play-button", "media-return-to-realtime-button", "media-rewind-button", "media-seek-back-button", "media-seek-forward-button", "media-slider", "media-sliderthumb", "media-time-remaining-display", "media-toggle-closed-captions-button", "media-volume-slider", "media-volume-slider-container", "media-volume-slider-mute-button", "media-volume-sliderthumb", "menulist", "menulist-button", "menulist-text", "menulist-textfield", "meter", "progress-bar", "progress-bar-value", "slider-horizontal", "slider-vertical", "sliderthumb-horizontal", "sliderthumb-vertical", "caret", "searchfield", "searchfield-decoration", "searchfield-results-decoration", "searchfield-results-button", "searchfield-cancel-button", "snapshotted-plugin-overlay", "textfield", "relevancy-level-indicator", "continuous-capacity-level-indicator", "discrete-capacity-level-indicator", "rating-level-indicator", "-apple-pay-button", "textarea", "attachment", "borderless-attachment", "caps-lock-indicator",
|
|
],
|
|
"-webkit-backface-visibility": [
|
|
"hidden", "visible",
|
|
],
|
|
"-webkit-border-after-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"-webkit-border-before-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"-webkit-border-end-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"-webkit-border-start-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"-webkit-box-align": [
|
|
"baseline", "center", "stretch", "start", "end",
|
|
],
|
|
"-webkit-box-decoration-break": [
|
|
"clone", "slice",
|
|
],
|
|
"-webkit-box-direction": [
|
|
"normal", "reverse",
|
|
],
|
|
"-webkit-box-lines": [
|
|
"single", "multiple",
|
|
],
|
|
"-webkit-box-orient": [
|
|
"horizontal", "vertical", "inline-axis", "block-axis",
|
|
],
|
|
"-webkit-box-pack": [
|
|
"center", "justify", "start", "end",
|
|
],
|
|
"-webkit-column-axis": [
|
|
"auto", "horizontal", "vertical",
|
|
],
|
|
"-webkit-column-count": [
|
|
"auto", "calc()",
|
|
],
|
|
"-webkit-column-fill": [
|
|
"auto", "balance",
|
|
],
|
|
"-webkit-column-gap": [
|
|
"normal", "calc()",
|
|
],
|
|
"-webkit-column-progression": [
|
|
"normal", "reverse",
|
|
],
|
|
"-webkit-column-rule-width": [
|
|
"medium", "thick", "thin", "calc()",
|
|
],
|
|
"-webkit-column-span": [
|
|
"all", "none", "calc()",
|
|
],
|
|
"-webkit-column-width": [
|
|
"auto", "calc()",
|
|
],
|
|
"-webkit-cursor-visibility": [
|
|
"auto", "auto-hide",
|
|
],
|
|
"-webkit-font-kerning": [
|
|
"none", "normal", "auto",
|
|
],
|
|
"-webkit-font-smoothing": [
|
|
"none", "auto", "antialiased", "subpixel-antialiased",
|
|
],
|
|
"-webkit-hyphens": [
|
|
"none", "auto", "manual",
|
|
],
|
|
"-webkit-line-align": [
|
|
"none", "edges",
|
|
],
|
|
"-webkit-line-break": [
|
|
"auto", "loose", "normal", "strict", "after-white-space",
|
|
],
|
|
"-webkit-line-snap": [
|
|
"none", "baseline", "contain",
|
|
],
|
|
"-webkit-logical-height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"-webkit-logical-width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"-webkit-max-logical-height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()"
|
|
],
|
|
"-webkit-max-logical-width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "none", "calc()"
|
|
],
|
|
"-webkit-min-logical-height": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"-webkit-min-logical-width": [
|
|
"auto", "intrinsic", "min-intrinsic", "min-content", "-webkit-min-content", "max-content", "-webkit-max-content", "-webkit-fill-available", "fit-content", "-webkit-fit-content", "calc()"
|
|
],
|
|
"-webkit-nbsp-mode": [
|
|
"normal", "space",
|
|
],
|
|
"-webkit-overflow-scrolling": [
|
|
"auto", "touch",
|
|
],
|
|
"print-color-adjust": [
|
|
"economy", "exact",
|
|
],
|
|
"-webkit-rtl-ordering": [
|
|
"logical", "visual",
|
|
],
|
|
"-webkit-ruby-position": [
|
|
"after", "before", "inter-character",
|
|
],
|
|
"-webkit-text-combine": [
|
|
"none", "horizontal",
|
|
],
|
|
"-webkit-text-decoration-style": [
|
|
"dotted", "dashed", "solid", "double", "wavy",
|
|
],
|
|
"-webkit-text-orientation": [
|
|
"sideways", "sideways-right", "upright", "mixed",
|
|
],
|
|
"-webkit-text-security": [
|
|
"none", "disc", "circle", "square",
|
|
],
|
|
"-webkit-text-zoom": [
|
|
"normal", "reset",
|
|
],
|
|
"-webkit-transform-style": [
|
|
"flat", "preserve-3d",
|
|
],
|
|
"-webkit-user-drag": [
|
|
"none", "auto", "element",
|
|
],
|
|
"-webkit-user-modify": [
|
|
"read-only", "read-write", "read-write-plaintext-only",
|
|
],
|
|
"-webkit-user-select": [
|
|
"none", "all", "auto", "text",
|
|
],
|
|
"-webkit-writing-mode": [
|
|
"lr", "rl", "tb", "lr-tb", "rl-tb", "tb-rl", "horizontal-tb", "vertical-rl", "vertical-lr", "horizontal-bt",
|
|
],
|
|
"alignment-baseline": [
|
|
"baseline", "middle", "auto", "alphabetic", "before-edge", "after-edge", "central", "text-before-edge", "text-after-edge", "ideographic", "hanging", "mathematical",
|
|
],
|
|
"border-block-end-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-block-start-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-bottom-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-collapse": [
|
|
"collapse", "separate",
|
|
],
|
|
"border-inline-end-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-inline-start-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-left-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-right-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"border-top-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"box-sizing": [
|
|
"border-box", "content-box",
|
|
],
|
|
"break-after": [
|
|
"left", "right", "auto", "avoid", "column", "avoid-column", "avoid-page", "page", "recto", "verso",
|
|
],
|
|
"break-before": [
|
|
"left", "right", "auto", "avoid", "column", "avoid-column", "avoid-page", "page", "recto", "verso",
|
|
],
|
|
"break-inside": [
|
|
"auto", "avoid", "avoid-column", "avoid-page",
|
|
],
|
|
"buffered-rendering": [
|
|
"auto", "static", "dynamic",
|
|
],
|
|
"caption-side": [
|
|
"top", "bottom", "left", "right",
|
|
],
|
|
"clear": [
|
|
"none", "left", "right", "both",
|
|
],
|
|
"clip-rule": [
|
|
"nonzero", "evenodd",
|
|
],
|
|
"color-interpolation": [
|
|
"auto", "sRGB", "linearRGB",
|
|
],
|
|
"color-interpolation-filters": [
|
|
"auto", "sRGB", "linearRGB",
|
|
],
|
|
"color-rendering": [
|
|
"auto", "optimizeSpeed", "optimizeQuality",
|
|
],
|
|
"column-fill": [
|
|
"auto", "balance",
|
|
],
|
|
"column-rule-style": [
|
|
"none", "hidden", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double",
|
|
],
|
|
"direction": [
|
|
"ltr", "rtl",
|
|
],
|
|
"display": [
|
|
"none", "inline", "block", "list-item", "compact", "inline-block", "table", "inline-table", "table-row-group", "table-header-group", "table-footer-group", "table-row", "table-column-group", "table-column", "table-cell", "table-caption", "-webkit-box", "-webkit-inline-box", "flex", "-webkit-flex", "inline-flex", "-webkit-inline-flex", "contents", "grid", "inline-grid",
|
|
],
|
|
"dominant-baseline": [
|
|
"middle", "auto", "alphabetic", "central", "text-before-edge", "text-after-edge", "ideographic", "hanging", "mathematical", "use-script", "no-change", "reset-size",
|
|
],
|
|
"empty-cells": [
|
|
"hide", "show",
|
|
],
|
|
"fill-rule": [
|
|
"nonzero", "evenodd",
|
|
],
|
|
"flex-direction": [
|
|
"row", "row-reverse", "column", "column-reverse",
|
|
],
|
|
"flex-wrap": [
|
|
"nowrap", "wrap-reverse", "wrap",
|
|
],
|
|
"float": [
|
|
"none", "left", "right",
|
|
],
|
|
"font-optical-sizing": [
|
|
"none", "auto",
|
|
],
|
|
"font-variant-alternates": [
|
|
"historical-forms", "normal",
|
|
],
|
|
"font-variant-caps": [
|
|
"small-caps", "all-small-caps", "petite-caps", "all-petite-caps", "unicase", "titling-caps", "normal",
|
|
],
|
|
"font-variant-position": [
|
|
"normal", "sub", "super",
|
|
],
|
|
"image-rendering": [
|
|
"auto", "optimizeSpeed", "optimizeQuality", "crisp-edges", "pixelated", "-webkit-crisp-edges", "-webkit-optimize-contrast",
|
|
],
|
|
"image-resolution": [
|
|
"from-image", "snap",
|
|
],
|
|
"isolation": [
|
|
"auto", "isolate",
|
|
],
|
|
"line-break": [
|
|
"normal", "auto", "loose", "strict", "after-white-space",
|
|
],
|
|
"list-style-position": [
|
|
"outside", "inside",
|
|
],
|
|
"list-style-type": [
|
|
"none", "disc", "circle", "square", "decimal", "decimal-leading-zero", "arabic-indic", "binary", "bengali", "cambodian", "khmer", "devanagari", "gujarati", "gurmukhi", "kannada", "lower-hexadecimal", "lao", "malayalam", "mongolian", "myanmar", "octal", "oriya", "persian", "urdu", "telugu", "tibetan", "thai", "upper-hexadecimal", "lower-roman", "upper-roman", "lower-greek", "lower-alpha", "lower-latin", "upper-alpha", "upper-latin", "afar", "ethiopic-halehame-aa-et", "ethiopic-halehame-aa-er", "amharic", "ethiopic-halehame-am-et", "amharic-abegede", "ethiopic-abegede-am-et", "cjk-earthly-branch", "cjk-heavenly-stem", "ethiopic", "ethiopic-halehame-gez", "ethiopic-abegede", "ethiopic-abegede-gez", "hangul-consonant", "hangul", "lower-norwegian", "oromo", "ethiopic-halehame-om-et", "sidama", "ethiopic-halehame-sid-et", "somali", "ethiopic-halehame-so-et", "tigre", "ethiopic-halehame-tig", "tigrinya-er", "ethiopic-halehame-ti-er", "tigrinya-er-abegede", "ethiopic-abegede-ti-er", "tigrinya-et", "ethiopic-halehame-ti-et", "tigrinya-et-abegede", "ethiopic-abegede-ti-et", "upper-greek", "upper-norwegian", "asterisks", "footnotes", "hebrew", "armenian", "lower-armenian", "upper-armenian", "georgian", "cjk-ideographic", "hiragana", "katakana", "hiragana-iroha", "katakana-iroha",
|
|
],
|
|
"mask-type": [
|
|
"alpha", "luminance",
|
|
],
|
|
"max-zoom": [
|
|
"auto",
|
|
],
|
|
"min-zoom": [
|
|
"auto",
|
|
],
|
|
"mix-blend-mode": [
|
|
"normal", "plus-darker", "plus-lighter", "overlay", "multiply", "screen", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity",
|
|
],
|
|
"object-fit": [
|
|
"none", "contain", "cover", "fill", "scale-down",
|
|
],
|
|
"orientation": [
|
|
"auto", "portait", "landscape",
|
|
],
|
|
"outline-style": [
|
|
"none", "inset", "groove", "outset", "ridge", "dotted", "dashed", "solid", "double", "auto",
|
|
],
|
|
"overflow-wrap": [
|
|
"normal", "break-word",
|
|
],
|
|
"overflow-x": [
|
|
"hidden", "auto", "visible", "scroll",
|
|
],
|
|
"overflow-y": [
|
|
"hidden", "auto", "visible", "scroll", "-webkit-paged-x", "-webkit-paged-y",
|
|
],
|
|
"pointer-events": [
|
|
"none", "all", "auto", "visible", "visiblePainted", "visibleFill", "visibleStroke", "painted", "fill", "stroke",
|
|
],
|
|
"position": [
|
|
"absolute", "fixed", "relative", "static", "sticky", "-webkit-sticky",
|
|
],
|
|
"resize": [
|
|
"none", "auto", "both", "horizontal", "vertical",
|
|
],
|
|
"shape-rendering": [
|
|
"auto", "optimizeSpeed", "geometricPrecision", "crispedges",
|
|
],
|
|
"stroke-linecap": [
|
|
"square", "round", "butt",
|
|
],
|
|
"stroke-linejoin": [
|
|
"round", "miter", "bevel",
|
|
],
|
|
"table-layout": [
|
|
"auto", "fixed",
|
|
],
|
|
"text-align": [
|
|
"-webkit-auto", "left", "right", "center", "justify", "match-parent", "-webkit-left", "-webkit-right", "-webkit-center", "-webkit-match-parent", "start", "end",
|
|
],
|
|
"text-align-last": [
|
|
"auto", "start", "end", "left", "right", "center", "justify", "match-parent",
|
|
],
|
|
"text-anchor": [
|
|
"middle", "start", "end",
|
|
],
|
|
"text-justify": [
|
|
"auto", "none", "inter-word", "inter-character", "distribute",
|
|
],
|
|
"text-line-through": [
|
|
"none", "dotted", "dashed", "solid", "double", "dot-dash", "dot-dot-dash", "wave", "continuous", "skip-white-space",
|
|
],
|
|
"text-line-through-mode": [
|
|
"continuous", "skip-white-space",
|
|
],
|
|
"text-line-through-style": [
|
|
"none", "dotted", "dashed", "solid", "double", "dot-dash", "dot-dot-dash", "wave",
|
|
],
|
|
"text-line-through-width": [
|
|
"normal", "medium", "auto", "thick", "thin",
|
|
],
|
|
"text-overflow": [
|
|
"clip", "ellipsis",
|
|
],
|
|
"text-overline-mode": [
|
|
"continuous", "skip-white-space",
|
|
],
|
|
"text-overline-style": [
|
|
"none", "dotted", "dashed", "solid", "double", "dot-dash", "dot-dot-dash", "wave",
|
|
],
|
|
"text-overline-width": [
|
|
"normal", "medium", "auto", "thick", "thin", "calc()",
|
|
],
|
|
"text-rendering": [
|
|
"auto", "optimizeSpeed", "optimizeLegibility", "geometricPrecision",
|
|
],
|
|
"text-transform": [
|
|
"none", "capitalize", "uppercase", "lowercase",
|
|
],
|
|
"text-underline": [
|
|
"none", "dotted", "dashed", "solid", "double", "dot-dash", "dot-dot-dash", "wave",
|
|
],
|
|
"text-underline-mode": [
|
|
"continuous", "skip-white-space",
|
|
],
|
|
"text-underline-style": [
|
|
"none", "dotted", "dashed", "solid", "double", "dot-dash", "dot-dot-dash", "wave",
|
|
],
|
|
"text-underline-width": [
|
|
"normal", "medium", "auto", "thick", "thin", "calc()",
|
|
],
|
|
"transform-style": [
|
|
"flat", "preserve-3d",
|
|
],
|
|
"unicode-bidi": [
|
|
"normal", "bidi-override", "embed", "isolate-override", "plaintext", "-webkit-isolate", "-webkit-isolate-override", "-webkit-plaintext", "isolate",
|
|
],
|
|
"user-zoom": [
|
|
"zoom", "fixed",
|
|
],
|
|
"vector-effect": [
|
|
"none",
|
|
],
|
|
"visibility": [
|
|
"hidden", "visible", "collapse",
|
|
],
|
|
"white-space": [
|
|
"normal", "nowrap", "pre", "pre-line", "pre-wrap",
|
|
],
|
|
"word-break": [
|
|
"normal", "break-all", "keep-all", "break-word",
|
|
],
|
|
"word-wrap": [
|
|
"normal", "break-word",
|
|
],
|
|
"writing-mode": [
|
|
"lr", "rl", "tb", "lr-tb", "rl-tb", "tb-rl", "horizontal-tb", "vertical-rl", "vertical-lr", "horizontal-bt",
|
|
],
|
|
};
|
|
|
|
/* Models/CSSProperty.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSProperty = class CSSProperty extends WI.Object
|
|
{
|
|
constructor(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange)
|
|
{
|
|
WI.CSSProperty._initializePropertyNameCounts();
|
|
|
|
super();
|
|
|
|
this._ownerStyle = null;
|
|
this._index = index;
|
|
this._overridingProperty = null;
|
|
this._initialState = null;
|
|
this._modified = false;
|
|
this._isUpdatingText = false;
|
|
this._isNewProperty = false;
|
|
|
|
this.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, true);
|
|
}
|
|
|
|
// Static
|
|
|
|
static isVariable(name)
|
|
{
|
|
return name.startsWith("--") && name.length > 2;
|
|
}
|
|
|
|
static isInheritedPropertyName(name)
|
|
{
|
|
console.assert(typeof name === "string");
|
|
if (WI.CSSKeywordCompletions.InheritedProperties.has(name))
|
|
return true;
|
|
return WI.CSSProperty.isVariable(name);
|
|
}
|
|
|
|
// FIXME: <https://webkit.org/b/226647> This naively collects variable-like names used in values. It should be hardened.
|
|
static findVariableNames(string)
|
|
{
|
|
const prefix = "var(--";
|
|
let prefixCursor = 0;
|
|
let cursor = 0;
|
|
let nameStartIndex = 0;
|
|
let names = [];
|
|
|
|
function isTerminatingChar(char) {
|
|
return char === ")" || char === "," || char === " " || char === "\n" || char === "\t";
|
|
}
|
|
|
|
while (cursor < string.length) {
|
|
if (nameStartIndex && isTerminatingChar(string.charAt(cursor))) {
|
|
names.push("--" + string.substring(nameStartIndex, cursor));
|
|
nameStartIndex = 0;
|
|
}
|
|
|
|
if (prefixCursor === prefix.length) {
|
|
prefixCursor = 0;
|
|
nameStartIndex = cursor;
|
|
}
|
|
|
|
if (string.charAt(cursor) === prefix.charAt(prefixCursor))
|
|
prefixCursor++;
|
|
|
|
cursor++;
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
static sortPreferringNonPrefixed(a, b)
|
|
{
|
|
let aIsPrefixed = a[0] === "-" && a[1] !== "-";
|
|
let bIsPrefixed = b[0] === "-" && b[1] !== "-";
|
|
if (!aIsPrefixed && bIsPrefixed)
|
|
return -1;
|
|
if (aIsPrefixed && !bIsPrefixed)
|
|
return 1;
|
|
|
|
return a.extendedLocaleCompare(b);
|
|
}
|
|
|
|
static sortByPropertyNameUsageCount(propertyNameA, propertyNameB)
|
|
{
|
|
let countA = WI.CSSProperty._cachedNameCounts[propertyNameA];
|
|
let countB = WI.CSSProperty._cachedNameCounts[propertyNameB];
|
|
|
|
const minimumCount = 100;
|
|
let validA = countA >= minimumCount;
|
|
let validB = countB >= minimumCount;
|
|
|
|
if (validA && !validB)
|
|
return -1;
|
|
if (!validA && validB)
|
|
return 1;
|
|
|
|
if (validA && validB) {
|
|
if (countA !== countB)
|
|
return countB - countA;
|
|
|
|
let canonicalPropertyNameA = WI.cssManager.canonicalNameForPropertyName(propertyNameA);
|
|
let canonicalPropertyNameB = WI.cssManager.canonicalNameForPropertyName(propertyNameB);
|
|
if (canonicalPropertyNameA !== propertyNameA || canonicalPropertyNameB !== propertyNameB)
|
|
return WI.CSSProperty.sortByPropertyNameUsageCount(canonicalPropertyNameA, canonicalPropertyNameB);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static indexOfCompletionForMostUsedPropertyName(completions)
|
|
{
|
|
let highestRankCompletions = completions;
|
|
if (highestRankCompletions.every((completion) => completion instanceof WI.QueryResult)) {
|
|
let highestRankValue = -1;
|
|
for (let completion of completions) {
|
|
if (completion.rank > highestRankValue) {
|
|
highestRankValue = completion.rank;
|
|
highestRankCompletions = [];
|
|
}
|
|
|
|
if (completion.rank === highestRankValue)
|
|
highestRankCompletions.push(completion);
|
|
}
|
|
}
|
|
let mostUsedHighestRankCompletion = highestRankCompletions.min((a, b) => WI.CSSProperty.sortByPropertyNameUsageCount(WI.CSSCompletions.getCompletionText(a), WI.CSSCompletions.getCompletionText(b)));
|
|
return completions.indexOf(mostUsedHighestRankCompletion);
|
|
}
|
|
|
|
static _initializePropertyNameCounts()
|
|
{
|
|
if (WI.CSSProperty._cachedNameCounts)
|
|
return;
|
|
|
|
WI.CSSProperty._cachedNameCounts = {};
|
|
|
|
WI.CSSProperty._storedNameCountsQueue = new Promise((resolve, reject) => {
|
|
WI.objectStores.cssPropertyNameCounts.getAllKeys().then((propertyNames) => {
|
|
Promise.allSettled(propertyNames.map(async (propertyName) => {
|
|
let storedCount = await WI.objectStores.cssPropertyNameCounts.get(propertyName);
|
|
|
|
WI.CSSProperty._cachedNameCounts[propertyName] = (WI.CSSProperty._cachedNameCounts[propertyName] || 0) + storedCount;
|
|
}))
|
|
.then(resolve, reject);
|
|
});
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get ownerStyle()
|
|
{
|
|
return this._ownerStyle;
|
|
}
|
|
|
|
set ownerStyle(ownerStyle)
|
|
{
|
|
this._ownerStyle = ownerStyle || null;
|
|
}
|
|
|
|
get index()
|
|
{
|
|
return this._index;
|
|
}
|
|
|
|
set index(index)
|
|
{
|
|
this._index = index;
|
|
}
|
|
|
|
update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange, dontFireEvents)
|
|
{
|
|
// Locked CSSProperty can still be updated from the back-end when the text matches.
|
|
// We need to do this to keep attributes such as valid and overridden up to date.
|
|
if (this._ownerStyle && this._ownerStyle.locked && text !== this._text)
|
|
return;
|
|
|
|
text = text || "";
|
|
name = name || "";
|
|
value = value || "";
|
|
priority = priority || "";
|
|
enabled = enabled || false;
|
|
overridden = overridden || false;
|
|
implicit = implicit || false;
|
|
anonymous = anonymous || false;
|
|
valid = valid || false;
|
|
|
|
var changed = false;
|
|
|
|
if (!dontFireEvents) {
|
|
changed = this._name !== name || this._rawValue !== value || this._priority !== priority ||
|
|
this._enabled !== enabled || this._implicit !== implicit || this._anonymous !== anonymous || this._valid !== valid;
|
|
}
|
|
|
|
// Use the setter for overridden if we want to fire events since the
|
|
// OverriddenStatusChanged event coalesces changes before it fires.
|
|
if (!dontFireEvents)
|
|
this.overridden = overridden;
|
|
else
|
|
this._overridden = overridden;
|
|
|
|
if (!overridden)
|
|
this._overridingProperty = null;
|
|
|
|
this._text = text;
|
|
this._rawValue = value;
|
|
this._value = undefined;
|
|
this._priority = priority;
|
|
this._enabled = enabled;
|
|
this._implicit = implicit;
|
|
this._anonymous = anonymous;
|
|
this._inherited = WI.CSSProperty.isInheritedPropertyName(name);
|
|
this._valid = valid;
|
|
this._isVariable = WI.CSSProperty.isVariable(name);
|
|
this._styleSheetTextRange = styleSheetTextRange || null;
|
|
|
|
this._rawValueNewlineIndent = "";
|
|
if (this._rawValue) {
|
|
let match = this._rawValue.match(/^[^\n]+\n(\s*)/);
|
|
if (match)
|
|
this._rawValueNewlineIndent = match[1];
|
|
}
|
|
this._rawValue = this._rawValue.replace(/\n\s+/g, "\n");
|
|
|
|
this._isShorthand = undefined;
|
|
this._shorthandPropertyNames = undefined;
|
|
|
|
this._updateName(name);
|
|
this._relatedShorthandProperty = null;
|
|
this._relatedLonghandProperties = [];
|
|
|
|
// Clear computed properties.
|
|
delete this._styleDeclarationTextRange;
|
|
delete this._canonicalName;
|
|
delete this._hasOtherVendorNameOrKeyword;
|
|
|
|
if (changed)
|
|
this.dispatchEventToListeners(WI.CSSProperty.Event.Changed);
|
|
}
|
|
|
|
remove()
|
|
{
|
|
this._markModified();
|
|
|
|
// Setting name or value to an empty string removes the entire CSSProperty.
|
|
this._updateName("");
|
|
const forceRemove = true;
|
|
this._updateStyleText(forceRemove);
|
|
}
|
|
|
|
commentOut(disabled)
|
|
{
|
|
console.assert(this.editable);
|
|
if (this._enabled === !disabled)
|
|
return;
|
|
|
|
this._markModified();
|
|
this._enabled = !disabled;
|
|
|
|
if (disabled)
|
|
this.text = "/* " + this._text + " */";
|
|
else
|
|
this.text = this._text.slice(2, -2).trim();
|
|
}
|
|
|
|
get text()
|
|
{
|
|
return this._text;
|
|
}
|
|
|
|
set text(newText)
|
|
{
|
|
newText = newText.trim();
|
|
if (this._text === newText)
|
|
return;
|
|
|
|
this._markModified();
|
|
this._text = newText;
|
|
this._isUpdatingText = true;
|
|
this._updateOwnerStyleText();
|
|
this._isUpdatingText = false;
|
|
}
|
|
|
|
get formattedText()
|
|
{
|
|
if (this._isUpdatingText)
|
|
return this._text;
|
|
|
|
if (!this._name)
|
|
return "";
|
|
|
|
let text = `${this._name}: ${this._rawValue};`;
|
|
if (!this._enabled)
|
|
text = "/* " + text + " */";
|
|
return text;
|
|
}
|
|
|
|
get modified()
|
|
{
|
|
return this._modified;
|
|
}
|
|
|
|
set modified(value)
|
|
{
|
|
if (this._modified === value)
|
|
return;
|
|
|
|
this._modified = value;
|
|
this.dispatchEventToListeners(WI.CSSProperty.Event.ModifiedChanged);
|
|
}
|
|
|
|
get name()
|
|
{
|
|
return this._name;
|
|
}
|
|
|
|
set name(name)
|
|
{
|
|
if (name === this._name)
|
|
return;
|
|
|
|
if (!name) {
|
|
// Deleting property name causes CSSProperty to be detached from CSSStyleDeclaration.
|
|
console.assert(!isNaN(this._index), this);
|
|
this._indexBeforeDetached = this._index;
|
|
} else if (!isNaN(this._indexBeforeDetached) && isNaN(this._index)) {
|
|
// Reattach CSSProperty.
|
|
console.assert(!this._ownerStyle.properties.includes(this), this);
|
|
this._ownerStyle.insertProperty(this, this._indexBeforeDetached);
|
|
this._indexBeforeDetached = NaN;
|
|
}
|
|
|
|
this._markModified();
|
|
this._updateName(name);
|
|
this._updateStyleText();
|
|
}
|
|
|
|
get canonicalName()
|
|
{
|
|
if (this._canonicalName)
|
|
return this._canonicalName;
|
|
|
|
this._canonicalName = WI.cssManager.canonicalNameForPropertyName(this.name);
|
|
|
|
return this._canonicalName;
|
|
}
|
|
|
|
// FIXME: Remove current value getter and rename rawValue to value once the old styles sidebar is removed.
|
|
get value()
|
|
{
|
|
if (!this._value)
|
|
this._value = this._rawValue.replace(/\s*!important\s*$/, "");
|
|
|
|
return this._value;
|
|
}
|
|
|
|
get rawValue()
|
|
{
|
|
return this._rawValue;
|
|
}
|
|
|
|
set rawValue(value)
|
|
{
|
|
if (value === this._rawValue)
|
|
return;
|
|
|
|
this._markModified();
|
|
|
|
let suffix = WI.CSSCompletions.completeUnbalancedValue(value);
|
|
if (suffix)
|
|
value += suffix;
|
|
|
|
this._rawValue = value;
|
|
this._value = undefined;
|
|
this._updateStyleText();
|
|
}
|
|
|
|
get important()
|
|
{
|
|
return this.priority === "important";
|
|
}
|
|
|
|
get priority() { return this._priority; }
|
|
|
|
get attached()
|
|
{
|
|
return this._enabled && this._ownerStyle && (!isNaN(this._index) || this._ownerStyle.type === WI.CSSStyleDeclaration.Type.Computed);
|
|
}
|
|
|
|
// Only commented out properties are disabled.
|
|
get enabled() { return this._enabled; }
|
|
|
|
get overridden() { return this._overridden; }
|
|
set overridden(overridden)
|
|
{
|
|
overridden = overridden || false;
|
|
|
|
if (this._overridden === overridden)
|
|
return;
|
|
|
|
if (!overridden)
|
|
this._overridingProperty = null;
|
|
|
|
var previousOverridden = this._overridden;
|
|
|
|
this._overridden = overridden;
|
|
|
|
if (this._overriddenStatusChangedTimeout)
|
|
return;
|
|
|
|
function delayed()
|
|
{
|
|
delete this._overriddenStatusChangedTimeout;
|
|
|
|
if (this._overridden === previousOverridden)
|
|
return;
|
|
|
|
this.dispatchEventToListeners(WI.CSSProperty.Event.OverriddenStatusChanged);
|
|
}
|
|
|
|
this._overriddenStatusChangedTimeout = setTimeout(delayed.bind(this), 0);
|
|
}
|
|
|
|
get overridingProperty()
|
|
{
|
|
console.assert(this._overridden);
|
|
return this._overridingProperty;
|
|
}
|
|
|
|
set overridingProperty(effectiveProperty)
|
|
{
|
|
if (!WI.settings.experimentalEnableStylesJumpToEffective.value)
|
|
return;
|
|
|
|
console.assert(this !== effectiveProperty, `Property "${this.formattedText}" can't override itself.`, this);
|
|
this._overridingProperty = effectiveProperty || null;
|
|
}
|
|
|
|
get implicit() { return this._implicit; }
|
|
set implicit(implicit) { this._implicit = implicit; }
|
|
|
|
get anonymous() { return this._anonymous; }
|
|
get inherited() { return this._inherited; }
|
|
get valid() { return this._valid; }
|
|
get isVariable() { return this._isVariable; }
|
|
get styleSheetTextRange() { return this._styleSheetTextRange; }
|
|
|
|
get initialState()
|
|
{
|
|
return this._initialState;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
return !!(this._styleSheetTextRange && this._ownerStyle && this._ownerStyle.styleSheetTextRange);
|
|
}
|
|
|
|
get styleDeclarationTextRange()
|
|
{
|
|
if ("_styleDeclarationTextRange" in this)
|
|
return this._styleDeclarationTextRange;
|
|
|
|
if (!this._ownerStyle || !this._styleSheetTextRange)
|
|
return null;
|
|
|
|
var styleTextRange = this._ownerStyle.styleSheetTextRange;
|
|
if (!styleTextRange)
|
|
return null;
|
|
|
|
var startLine = this._styleSheetTextRange.startLine - styleTextRange.startLine;
|
|
var endLine = this._styleSheetTextRange.endLine - styleTextRange.startLine;
|
|
|
|
var startColumn = this._styleSheetTextRange.startColumn;
|
|
if (!startLine)
|
|
startColumn -= styleTextRange.startColumn;
|
|
|
|
var endColumn = this._styleSheetTextRange.endColumn;
|
|
if (!endLine)
|
|
endColumn -= styleTextRange.startColumn;
|
|
|
|
this._styleDeclarationTextRange = new WI.TextRange(startLine, startColumn, endLine, endColumn);
|
|
|
|
return this._styleDeclarationTextRange;
|
|
}
|
|
|
|
get relatedShorthandProperty() { return this._relatedShorthandProperty; }
|
|
set relatedShorthandProperty(property)
|
|
{
|
|
this._relatedShorthandProperty = property || null;
|
|
}
|
|
|
|
get relatedLonghandProperties() { return this._relatedLonghandProperties; }
|
|
|
|
addRelatedLonghandProperty(property)
|
|
{
|
|
this._relatedLonghandProperties.push(property);
|
|
}
|
|
|
|
clearRelatedLonghandProperties(property)
|
|
{
|
|
this._relatedLonghandProperties = [];
|
|
}
|
|
|
|
get isShorthand()
|
|
{
|
|
if (this._isShorthand === undefined) {
|
|
this._isShorthand = WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.has(this._name);
|
|
if (this._isShorthand) {
|
|
let longhands = WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.get(this._name);
|
|
if (longhands && longhands.length === 1)
|
|
this._isShorthand = false;
|
|
}
|
|
}
|
|
return this._isShorthand;
|
|
}
|
|
|
|
get shorthandPropertyNames()
|
|
{
|
|
if (!this._shorthandPropertyNames) {
|
|
this._shorthandPropertyNames = WI.CSSKeywordCompletions.ShorthandNamesForLongHandProperty.get(this._name) || [];
|
|
this._shorthandPropertyNames.remove("all");
|
|
}
|
|
return this._shorthandPropertyNames;
|
|
}
|
|
|
|
get isNewProperty() { return this._isNewProperty; }
|
|
set isNewProperty(value) { this._isNewProperty = value; }
|
|
|
|
hasOtherVendorNameOrKeyword()
|
|
{
|
|
if ("_hasOtherVendorNameOrKeyword" in this)
|
|
return this._hasOtherVendorNameOrKeyword;
|
|
|
|
this._hasOtherVendorNameOrKeyword = WI.cssManager.propertyNameHasOtherVendorPrefix(this.name) || WI.cssManager.propertyValueHasOtherVendorKeyword(this.value);
|
|
|
|
return this._hasOtherVendorNameOrKeyword;
|
|
}
|
|
|
|
equals(property)
|
|
{
|
|
if (property === this)
|
|
return true;
|
|
|
|
if (!property)
|
|
return false;
|
|
|
|
return this._name === property.name && this._rawValue === property.rawValue && this._enabled === property.enabled;
|
|
}
|
|
|
|
clone()
|
|
{
|
|
let cssProperty = new WI.CSSProperty(
|
|
this._index,
|
|
this._text,
|
|
this._name,
|
|
this._rawValue,
|
|
this._priority,
|
|
this._enabled,
|
|
this._overridden,
|
|
this._implicit,
|
|
this._anonymous,
|
|
this._valid,
|
|
this._styleSheetTextRange);
|
|
|
|
cssProperty.ownerStyle = this._ownerStyle;
|
|
|
|
return cssProperty;
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateName(name)
|
|
{
|
|
if (name === this._name)
|
|
return;
|
|
|
|
let changeCount = (propertyName, delta) => {
|
|
if (this._implicit || this._anonymous || !this._enabled)
|
|
return;
|
|
|
|
if (!propertyName || WI.CSSProperty.isVariable(propertyName))
|
|
return;
|
|
|
|
let cachedCount = WI.CSSProperty._cachedNameCounts[propertyName];
|
|
|
|
// Allow property counts to be updated if the property name has already been counted before.
|
|
// This can happen when inspecting a device that has different CSS properties enabled.
|
|
if (isNaN(cachedCount) && !WI.cssManager.propertyNameCompletions.isValidPropertyName(propertyName))
|
|
return;
|
|
|
|
console.assert(delta > 0 || cachedCount >= delta, cachedCount, delta);
|
|
WI.CSSProperty._cachedNameCounts[propertyName] = Math.max(0, (cachedCount || 0) + delta);
|
|
|
|
WI.CSSProperty._storedNameCountsQueue = WI.CSSProperty._storedNameCountsQueue.finally(async () => {
|
|
let storedCount = await WI.objectStores.cssPropertyNameCounts.get(propertyName);
|
|
|
|
console.assert(delta > 0 || storedCount >= delta, storedCount, delta);
|
|
await WI.objectStores.cssPropertyNameCounts.put(Math.max(0, (storedCount || 0) + delta), propertyName);
|
|
});
|
|
|
|
if (propertyName !== this.canonicalName)
|
|
changeCount(this.canonicalName, delta);
|
|
};
|
|
|
|
changeCount(this._name, -1);
|
|
this._name = name;
|
|
changeCount(this._name, 1);
|
|
}
|
|
|
|
_updateStyleText(forceRemove = false)
|
|
{
|
|
let text = "";
|
|
|
|
if (this._name && this._rawValue) {
|
|
let value = this._rawValue.replace(/\n/g, "\n" + this._rawValueNewlineIndent);
|
|
text = this._name + ": " + value + ";";
|
|
}
|
|
|
|
this._text = text;
|
|
|
|
if (forceRemove)
|
|
this._ownerStyle.removeProperty(this);
|
|
|
|
this._updateOwnerStyleText();
|
|
}
|
|
|
|
_updateOwnerStyleText()
|
|
{
|
|
console.assert(this._ownerStyle);
|
|
if (!this._ownerStyle)
|
|
return;
|
|
|
|
this._ownerStyle.text = this._ownerStyle.generateFormattedText({multiline: this._ownerStyle.type === WI.CSSStyleDeclaration.Type.Rule});
|
|
this._ownerStyle.updatePropertiesModifiedState();
|
|
}
|
|
|
|
_markModified()
|
|
{
|
|
if (this._ownerStyle)
|
|
this._ownerStyle.markModified();
|
|
}
|
|
};
|
|
|
|
WI.CSSProperty._cachedNameCounts = null;
|
|
WI.CSSProperty._storedNameCountsQueue = null;
|
|
|
|
WI.CSSProperty.Event = {
|
|
Changed: "css-property-changed",
|
|
ModifiedChanged: "css-property-modified-changed",
|
|
OverriddenStatusChanged: "css-property-overridden-status-changed"
|
|
};
|
|
|
|
/* Models/CSSPropertyNameCompletions.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSPropertyNameCompletions = class CSSPropertyNameCompletions extends WI.CSSCompletions
|
|
{
|
|
constructor(properties, options = {})
|
|
{
|
|
console.assert(Array.isArray(properties), properties);
|
|
console.assert(properties[0].name, "Expected an array of objects with `name` key", properties);
|
|
|
|
let values = [];
|
|
for (let property of properties) {
|
|
console.assert(property.name);
|
|
|
|
values.push(property.name);
|
|
if (Array.isArray(property.aliases))
|
|
values.pushAll(property.aliases);
|
|
}
|
|
|
|
super(values, options);
|
|
|
|
this._cachedSortedPropertyNames = this.values.slice();
|
|
this._needsVariablesFromInspectedNode = true;
|
|
this._inspectedDOMNodeStyles = null;
|
|
|
|
WI.domManager.addEventListener(WI.DOMManager.Event.InspectedNodeChanged, this._handleInspectedNodeChanged, this);
|
|
}
|
|
|
|
// Public
|
|
|
|
isValidPropertyName(name)
|
|
{
|
|
return this.values.includes(name);
|
|
}
|
|
|
|
executeQuery(query)
|
|
{
|
|
this._updateValuesWithLatestCSSVariablesIfNeeded();
|
|
|
|
return super.executeQuery(query);
|
|
}
|
|
|
|
startsWith(prefix)
|
|
{
|
|
this._updateValuesWithLatestCSSVariablesIfNeeded();
|
|
|
|
return super.startsWith(prefix);
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateValuesWithLatestCSSVariablesIfNeeded()
|
|
{
|
|
if (!this._needsVariablesFromInspectedNode)
|
|
return;
|
|
|
|
// An inspected node is not guaranteed to exist when unit testing.
|
|
if (!WI.domManager.inspectedNode) {
|
|
this._needsVariablesFromInspectedNode = false;
|
|
return;
|
|
}
|
|
|
|
let values = Array.from(WI.cssManager.stylesForNode(WI.domManager.inspectedNode).allCSSVariables);
|
|
values.pushAll(this._cachedSortedPropertyNames);
|
|
this.replaceValues(values);
|
|
|
|
this._needsVariablesFromInspectedNode = false;
|
|
}
|
|
|
|
_handleInspectedNodeChanged(event)
|
|
{
|
|
this._needsVariablesFromInspectedNode = true;
|
|
|
|
this._inspectedDOMNodeStyles?.removeEventListener(WI.DOMNodeStyles.Event.NeedsRefresh, this._handleNodesStylesNeedsRefresh, this);
|
|
this._inspectedDOMNodeStyles = WI.cssManager.stylesForNode(WI.domManager.inspectedNode);
|
|
this._inspectedDOMNodeStyles.addEventListener(WI.DOMNodeStyles.Event.NeedsRefresh, this._handleNodesStylesNeedsRefresh, this);
|
|
}
|
|
|
|
_handleNodesStylesNeedsRefresh(event)
|
|
{
|
|
this._needsVariablesFromInspectedNode = true;
|
|
}
|
|
};
|
|
|
|
/* Models/CSSRule.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSRule = class CSSRule extends WI.Object
|
|
{
|
|
constructor(nodeStyles, ownerStyleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings, isImplicitlyNested)
|
|
{
|
|
super();
|
|
|
|
console.assert(nodeStyles);
|
|
this._nodeStyles = nodeStyles;
|
|
|
|
this._ownerStyleSheet = ownerStyleSheet || null;
|
|
this._id = id || null;
|
|
this._type = type || null;
|
|
this._initialState = null;
|
|
this._isImplicitlyNested = isImplicitlyNested || false;
|
|
|
|
this.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings, true);
|
|
}
|
|
|
|
// Public
|
|
|
|
get ownerStyleSheet() { return this._ownerStyleSheet; }
|
|
get id() { return this._id; }
|
|
get type() { return this._type; }
|
|
get initialState() { return this._initialState; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
get selectors() { return this._selectors; }
|
|
get matchedSelectorIndices() { return this._matchedSelectorIndices; }
|
|
get style() { return this._style; }
|
|
get groupings() { return this._groupings; }
|
|
get isImplicitlyNested() { return this._isImplicitlyNested; }
|
|
|
|
get editable()
|
|
{
|
|
return !!this._id && this._type !== WI.CSSStyleSheet.Type.UserAgent;
|
|
}
|
|
|
|
get selectorText()
|
|
{
|
|
return this._selectorText;
|
|
}
|
|
|
|
setSelectorText(selectorText)
|
|
{
|
|
console.assert(this.editable);
|
|
if (!this.editable)
|
|
return Promise.reject();
|
|
|
|
return this._nodeStyles.changeRuleSelector(this, selectorText).then(this._selectorResolved.bind(this));
|
|
}
|
|
|
|
update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings)
|
|
{
|
|
sourceCodeLocation = sourceCodeLocation || null;
|
|
selectorText = selectorText || "";
|
|
selectors = selectors || [];
|
|
matchedSelectorIndices = matchedSelectorIndices || [];
|
|
style = style || null;
|
|
groupings = groupings || [];
|
|
|
|
if (this._style)
|
|
this._style.ownerRule = null;
|
|
|
|
this._sourceCodeLocation = sourceCodeLocation;
|
|
this._selectorText = selectorText;
|
|
this._selectors = selectors;
|
|
this._matchedSelectorIndices = matchedSelectorIndices;
|
|
this._style = style;
|
|
this._groupings = groupings;
|
|
|
|
if (this._style)
|
|
this._style.ownerRule = this;
|
|
}
|
|
|
|
isEqualTo(rule)
|
|
{
|
|
if (!rule)
|
|
return false;
|
|
|
|
return Object.shallowEqual(this._id, rule.id);
|
|
}
|
|
|
|
// Protected
|
|
|
|
get nodeStyles()
|
|
{
|
|
return this._nodeStyles;
|
|
}
|
|
|
|
// Private
|
|
|
|
// This method only needs to be called for CSS rules that don't match the selected DOM node.
|
|
// CSS rules that match the selected DOM node get updated by WI.DOMNodeStyles.prototype._parseRulePayload.
|
|
_selectorResolved(rulePayload)
|
|
{
|
|
if (!rulePayload)
|
|
return;
|
|
|
|
let selectorText = rulePayload.selectorList.text;
|
|
if (selectorText === this._selectorText)
|
|
return;
|
|
|
|
let selectors = WI.DOMNodeStyles.parseSelectorListPayload(rulePayload.selectorList);
|
|
|
|
let sourceCodeLocation = null;
|
|
let sourceRange = rulePayload.selectorList.range;
|
|
if (sourceRange) {
|
|
sourceCodeLocation = WI.DOMNodeStyles.createSourceCodeLocation(rulePayload.sourceURL, {
|
|
line: sourceRange.startLine,
|
|
column: sourceRange.startColumn,
|
|
documentNode: this._nodeStyles.node.ownerDocument,
|
|
});
|
|
}
|
|
|
|
if (this._ownerStyleSheet) {
|
|
if (!sourceCodeLocation && sourceRange)
|
|
sourceCodeLocation = this._ownerStyleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
|
|
sourceCodeLocation = this._ownerStyleSheet.offsetSourceCodeLocation(sourceCodeLocation);
|
|
}
|
|
|
|
this.update(sourceCodeLocation, selectorText, selectors, [], this._style, this._groupings);
|
|
}
|
|
};
|
|
|
|
/* Models/CSSSelector.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSSelector = class CSSSelector
|
|
{
|
|
constructor(text, specificity, dynamic)
|
|
{
|
|
console.assert(text);
|
|
|
|
this._text = text;
|
|
this._specificity = specificity || null;
|
|
this._dynamic = dynamic || false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get text() { return this._text; }
|
|
get specificity() { return this._specificity; }
|
|
get dynamic() { return this._dynamic; }
|
|
|
|
isPseudoSelector()
|
|
{
|
|
return Object.values(WI.CSSManager.PseudoSelectorNames).some((pseudoId) => (new RegExp("(?:\\b|^):{1,2}(?:-webkit-)?" + pseudoId + "(?:\\b|$)")).test(this._text));
|
|
}
|
|
};
|
|
|
|
/* Models/CSSStyleDeclaration.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSStyleDeclaration = class CSSStyleDeclaration extends WI.Object
|
|
{
|
|
constructor(nodeStyles, ownerStyleSheet, id, type, node, inherited, text, properties, styleSheetTextRange)
|
|
{
|
|
super();
|
|
|
|
console.assert(nodeStyles);
|
|
this._nodeStyles = nodeStyles;
|
|
|
|
this._ownerRule = null;
|
|
|
|
this._ownerStyleSheet = ownerStyleSheet || null;
|
|
this._id = id || null;
|
|
this._type = type || null;
|
|
this._node = node || null;
|
|
this._inherited = inherited || false;
|
|
|
|
this._initialState = null;
|
|
this._updatesInProgressCount = 0;
|
|
this._pendingPropertiesChanged = false;
|
|
this._locked = false;
|
|
this._pendingProperties = [];
|
|
this._propertyNameMap = {};
|
|
|
|
this._properties = [];
|
|
this._enabledProperties = null;
|
|
this._visibleProperties = null;
|
|
this._variablesForType = new Map;
|
|
|
|
this.update(text, properties, styleSheetTextRange, {dontFireEvents: true});
|
|
}
|
|
|
|
// Public
|
|
|
|
get initialState() { return this._initialState; }
|
|
|
|
get id()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
get stringId()
|
|
{
|
|
if (this._id)
|
|
return this._id.styleSheetId + "/" + this._id.ordinal;
|
|
else
|
|
return "";
|
|
}
|
|
|
|
get ownerStyleSheet()
|
|
{
|
|
return this._ownerStyleSheet;
|
|
}
|
|
|
|
get type()
|
|
{
|
|
return this._type;
|
|
}
|
|
|
|
get inherited()
|
|
{
|
|
return this._inherited;
|
|
}
|
|
|
|
get node()
|
|
{
|
|
return this._node;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
if (!this._id)
|
|
return false;
|
|
|
|
if (this._type === WI.CSSStyleDeclaration.Type.Rule)
|
|
return this._ownerRule && this._ownerRule.editable;
|
|
|
|
if (this._type === WI.CSSStyleDeclaration.Type.Inline)
|
|
return !this._node.isInUserAgentShadowTree() || WI.DOMManager.supportsEditingUserAgentShadowTrees();
|
|
|
|
return false;
|
|
}
|
|
|
|
get selectorEditable()
|
|
{
|
|
return this._ownerRule && this._ownerRule.editable && !this._ownerRule.isImplicitlyNested && InspectorBackend.hasCommand("CSS.setRuleSelector");
|
|
}
|
|
|
|
get locked() { return this._locked; }
|
|
set locked(value) { this._locked = value; }
|
|
|
|
variablesForType(type)
|
|
{
|
|
console.assert(Object.values(WI.CSSStyleDeclaration.VariablesGroupType).includes(type), type);
|
|
|
|
let variables = this._variablesForType.get(type);
|
|
if (variables)
|
|
return variables;
|
|
|
|
// Will iterate in order through type checkers for each CSS variable to identify its type.
|
|
// The catch-all "other" must always be last.
|
|
const typeCheckFunctions = [
|
|
{
|
|
type: WI.CSSStyleDeclaration.VariablesGroupType.Colors,
|
|
checker: (property) => WI.Color.fromString(property.value),
|
|
},
|
|
{
|
|
type: WI.CSSStyleDeclaration.VariablesGroupType.Dimensions,
|
|
checker: (property) => /^-?\d+(\.\d+)?\D+$/.test(property.value),
|
|
},
|
|
{
|
|
type: WI.CSSStyleDeclaration.VariablesGroupType.Numbers,
|
|
checker: (property) => /^-?\d+(\.\d+)?$/.test(property.value),
|
|
},
|
|
{
|
|
type: WI.CSSStyleDeclaration.VariablesGroupType.Other,
|
|
checker: (property) => true,
|
|
},
|
|
];
|
|
|
|
// Ensure all types have a list. Empty lists are a signal to views to skip rendering.
|
|
for (let {type} of typeCheckFunctions)
|
|
this._variablesForType.set(type, []);
|
|
|
|
for (let property of this._properties) {
|
|
if (!property.isVariable)
|
|
continue;
|
|
|
|
for (let {type, checker} of typeCheckFunctions) {
|
|
if (checker(property)) {
|
|
this._variablesForType.get(type).push(property);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
return this._variablesForType.get(type);
|
|
}
|
|
|
|
update(text, properties, styleSheetTextRange, options = {})
|
|
{
|
|
let dontFireEvents = options.dontFireEvents || false;
|
|
|
|
// When two consequent setText calls happen (A and B), only update when the last call (B) is finished.
|
|
// Front-end: A B
|
|
// Back-end: A B
|
|
// _updatesInProgressCount: 0 1 2 1 0
|
|
// ^
|
|
// update only happens here
|
|
if (this._updatesInProgressCount > 0 && !options.forceUpdate) {
|
|
if (WI.settings.debugEnableStyleEditingDebugMode.value && text !== this._text)
|
|
console.warn("Style modified while editing:", text);
|
|
|
|
return;
|
|
}
|
|
|
|
// Allow updates from the backend when text matches because `properties` may contain warnings that need to be shown.
|
|
if (this._locked && !options.forceUpdate && text !== this._text)
|
|
return;
|
|
|
|
text = text || "";
|
|
properties = properties || [];
|
|
|
|
let oldProperties = this._properties || [];
|
|
let oldText = this._text;
|
|
|
|
this._text = text;
|
|
this._properties = properties;
|
|
|
|
this._styleSheetTextRange = styleSheetTextRange;
|
|
this._propertyNameMap = {};
|
|
this._variablesForType.clear();
|
|
|
|
this._enabledProperties = null;
|
|
this._visibleProperties = null;
|
|
|
|
let editable = this.editable;
|
|
|
|
for (let property of this._properties) {
|
|
property.ownerStyle = this;
|
|
|
|
// Store the property in a map if we aren't editable. This
|
|
// allows for quick lookup for computed style. Editable
|
|
// styles don't use the map since they need to account for
|
|
// overridden properties.
|
|
if (!editable)
|
|
this._propertyNameMap[property.name] = property;
|
|
else {
|
|
// Remove from pendingProperties (if it was pending).
|
|
this._pendingProperties.remove(property);
|
|
}
|
|
}
|
|
|
|
for (let oldProperty of oldProperties) {
|
|
if (this._properties.includes(oldProperty))
|
|
continue;
|
|
|
|
// Clear the index, since it is no longer valid.
|
|
oldProperty.index = NaN;
|
|
|
|
// Keep around old properties in pending in case they
|
|
// are needed again during editing.
|
|
if (editable)
|
|
this._pendingProperties.push(oldProperty);
|
|
}
|
|
|
|
if (dontFireEvents)
|
|
return;
|
|
|
|
// Don't fire the event if text hasn't changed. However, it should still fire for Computed style declarations
|
|
// because it never has text.
|
|
if (oldText === this._text && !this._pendingPropertiesChanged && this._type !== WI.CSSStyleDeclaration.Type.Computed)
|
|
return;
|
|
|
|
this._pendingPropertiesChanged = false;
|
|
|
|
function delayed()
|
|
{
|
|
this.dispatchEventToListeners(WI.CSSStyleDeclaration.Event.PropertiesChanged);
|
|
}
|
|
|
|
// Delay firing the PropertiesChanged event so DOMNodeStyles has a chance to mark overridden and associated properties.
|
|
setTimeout(delayed.bind(this), 0);
|
|
}
|
|
|
|
get ownerRule()
|
|
{
|
|
return this._ownerRule;
|
|
}
|
|
|
|
set ownerRule(rule)
|
|
{
|
|
this._ownerRule = rule || null;
|
|
}
|
|
|
|
get text()
|
|
{
|
|
return this._text;
|
|
}
|
|
|
|
set text(text)
|
|
{
|
|
if (this._text === text)
|
|
return;
|
|
|
|
let trimmedText = text.trim();
|
|
if (this._text === trimmedText)
|
|
return;
|
|
|
|
if (!trimmedText.length || this._type === WI.CSSStyleDeclaration.Type.Inline)
|
|
text = trimmedText;
|
|
|
|
this._text = text;
|
|
++this._updatesInProgressCount;
|
|
|
|
let timeoutId = setTimeout(() => {
|
|
console.error("Timed out when setting style text:", text);
|
|
styleTextDidChange();
|
|
}, 2000);
|
|
|
|
let styleTextDidChange = () => {
|
|
if (!timeoutId)
|
|
return;
|
|
|
|
clearTimeout(timeoutId);
|
|
timeoutId = null;
|
|
this._updatesInProgressCount = Math.max(0, this._updatesInProgressCount - 1);
|
|
this._pendingPropertiesChanged = true;
|
|
};
|
|
|
|
this._nodeStyles.changeStyleText(this, text, styleTextDidChange);
|
|
}
|
|
|
|
get enabledProperties()
|
|
{
|
|
if (!this._enabledProperties)
|
|
this._enabledProperties = this._properties.filter((property) => property.enabled);
|
|
|
|
return this._enabledProperties;
|
|
}
|
|
|
|
get properties()
|
|
{
|
|
return this._properties;
|
|
}
|
|
|
|
set properties(properties)
|
|
{
|
|
if (properties === this._properties)
|
|
return;
|
|
|
|
this._properties = properties;
|
|
this._enabledProperties = null;
|
|
this._visibleProperties = null;
|
|
}
|
|
|
|
get visibleProperties()
|
|
{
|
|
if (!this._visibleProperties)
|
|
this._visibleProperties = this._properties.filter((property) => !!property.styleDeclarationTextRange);
|
|
|
|
return this._visibleProperties;
|
|
}
|
|
|
|
get pendingProperties()
|
|
{
|
|
return this._pendingProperties;
|
|
}
|
|
|
|
get styleSheetTextRange()
|
|
{
|
|
return this._styleSheetTextRange;
|
|
}
|
|
|
|
get groupings()
|
|
{
|
|
if (this._ownerRule)
|
|
return this._ownerRule.groupings;
|
|
return [];
|
|
}
|
|
|
|
get selectorText()
|
|
{
|
|
if (this._ownerRule)
|
|
return this._ownerRule.selectorText;
|
|
return this._node.appropriateSelectorFor(true);
|
|
}
|
|
|
|
propertyForName(name)
|
|
{
|
|
console.assert(name);
|
|
if (!name)
|
|
return null;
|
|
|
|
if (!this.editable)
|
|
return this._propertyNameMap[name] || null;
|
|
|
|
// Editable styles don't use the map since they need to
|
|
// account for overridden properties.
|
|
|
|
let bestMatchProperty = null;
|
|
for (let property of this.enabledProperties) {
|
|
if (property.canonicalName !== name && property.name !== name)
|
|
continue;
|
|
if (bestMatchProperty && !bestMatchProperty.overridden && property.overridden)
|
|
continue;
|
|
bestMatchProperty = property;
|
|
}
|
|
|
|
return bestMatchProperty;
|
|
}
|
|
|
|
resolveVariableValue(text)
|
|
{
|
|
const invalid = Symbol("invalid");
|
|
|
|
let checkTokens = (tokens) => {
|
|
let startIndex = NaN;
|
|
let openParenthesis = 0;
|
|
for (let i = 0; i < tokens.length; i++) {
|
|
let token = tokens[i];
|
|
if (token.value === "var" && token.type && token.type.includes("atom")) {
|
|
if (isNaN(startIndex)) {
|
|
startIndex = i;
|
|
openParenthesis = 0;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isNaN(startIndex))
|
|
continue;
|
|
|
|
if (token.value === "(") {
|
|
++openParenthesis;
|
|
continue;
|
|
}
|
|
|
|
if (token.value === ")") {
|
|
--openParenthesis;
|
|
if (openParenthesis > 0)
|
|
continue;
|
|
|
|
let variableTokens = tokens.slice(startIndex, i + 1);
|
|
startIndex = NaN;
|
|
|
|
let variableNameIndex = variableTokens.findIndex((token) => WI.CSSProperty.isVariable(token.value) && /\bvariable-2\b/.test(token.type));
|
|
if (variableNameIndex === -1)
|
|
continue;
|
|
|
|
let variableProperty = this.propertyForName(variableTokens[variableNameIndex].value);
|
|
if (variableProperty)
|
|
return variableProperty.value.trim();
|
|
|
|
let fallbackStartIndex = variableTokens.findIndex((value, j) => j > variableNameIndex + 1 && /\bm-css\b/.test(value.type));
|
|
if (fallbackStartIndex === -1)
|
|
return invalid;
|
|
|
|
let fallbackTokens = variableTokens.slice(fallbackStartIndex, i);
|
|
return checkTokens(fallbackTokens) || fallbackTokens.reduce((accumulator, token) => accumulator + token.value, "").trim();
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
let resolved = checkTokens(WI.tokenizeCSSValue(text));
|
|
return resolved === invalid ? null : resolved;
|
|
}
|
|
|
|
newBlankProperty(propertyIndex)
|
|
{
|
|
let text, name, value, priority, overridden, implicit, anonymous;
|
|
let enabled = true;
|
|
let valid = false;
|
|
let styleSheetTextRange = this._rangeAfterPropertyAtIndex(propertyIndex - 1);
|
|
|
|
this.markModified();
|
|
let property = new WI.CSSProperty(propertyIndex, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
|
|
property.isNewProperty = true;
|
|
this.insertProperty(property, propertyIndex);
|
|
this.update(this._text, this._properties, this._styleSheetTextRange, {dontFireEvents: true, forceUpdate: true});
|
|
|
|
return property;
|
|
}
|
|
|
|
markModified()
|
|
{
|
|
if (!this._initialState) {
|
|
let visibleProperties = this.visibleProperties.map((property) => {
|
|
return property.clone();
|
|
});
|
|
|
|
this._initialState = new WI.CSSStyleDeclaration(
|
|
this._nodeStyles,
|
|
this._ownerStyleSheet,
|
|
this._id,
|
|
this._type,
|
|
this._node,
|
|
this._inherited,
|
|
this._text,
|
|
visibleProperties,
|
|
this._styleSheetTextRange);
|
|
}
|
|
|
|
WI.cssManager.addModifiedStyle(this);
|
|
}
|
|
|
|
insertProperty(cssProperty, propertyIndex)
|
|
{
|
|
this._properties.insertAtIndex(cssProperty, propertyIndex);
|
|
for (let index = propertyIndex + 1; index < this._properties.length; index++)
|
|
this._properties[index].index = index;
|
|
|
|
// Invalidate cached properties.
|
|
this._enabledProperties = null;
|
|
this._visibleProperties = null;
|
|
}
|
|
|
|
removeProperty(cssProperty)
|
|
{
|
|
// cssProperty.index could be set to NaN by WI.CSSStyleDeclaration.prototype.update.
|
|
let realIndex = this._properties.indexOf(cssProperty);
|
|
if (realIndex === -1)
|
|
return;
|
|
|
|
this._properties.splice(realIndex, 1);
|
|
|
|
// Invalidate cached properties.
|
|
this._enabledProperties = null;
|
|
this._visibleProperties = null;
|
|
}
|
|
|
|
updatePropertiesModifiedState()
|
|
{
|
|
if (!this._initialState)
|
|
return;
|
|
|
|
if (this._type === WI.CSSStyleDeclaration.Type.Computed)
|
|
return;
|
|
|
|
let initialCSSProperties = this._initialState.visibleProperties;
|
|
let cssProperties = this.visibleProperties;
|
|
|
|
let hasModified = false;
|
|
|
|
function onEach(cssProperty, action) {
|
|
if (action !== 0)
|
|
hasModified = true;
|
|
|
|
cssProperty.modified = action === 1;
|
|
}
|
|
|
|
function comparator(a, b) {
|
|
return a.equals(b);
|
|
}
|
|
|
|
Array.diffArrays(initialCSSProperties, cssProperties, onEach, comparator);
|
|
|
|
if (!hasModified)
|
|
WI.cssManager.removeModifiedStyle(this);
|
|
}
|
|
|
|
generateFormattedText(options = {})
|
|
{
|
|
let indentString = WI.indentString();
|
|
let styleText = "";
|
|
let groupings = this.groupings.filter((grouping) => !grouping.isMedia || grouping.text !== "all");
|
|
let groupingsCount = groupings.length;
|
|
|
|
if (options.includeGroupingsAndSelectors) {
|
|
for (let i = groupingsCount - 1; i >= 0; --i) {
|
|
if (options.multiline)
|
|
styleText += indentString.repeat(groupingsCount - i - 1);
|
|
|
|
let prefix = groupings[i].prefix;
|
|
if (prefix)
|
|
styleText += prefix;
|
|
|
|
if (groupings[i].text)
|
|
styleText += " " + groupings[i].text;
|
|
styleText += " {";
|
|
|
|
if (options.multiline)
|
|
styleText += "\n";
|
|
}
|
|
|
|
if (options.multiline)
|
|
styleText += indentString.repeat(groupingsCount);
|
|
|
|
styleText += this.selectorText + " {";
|
|
}
|
|
|
|
let properties = this._styleSheetTextRange ? this.visibleProperties : this._properties;
|
|
if (properties.length) {
|
|
if (options.multiline) {
|
|
let propertyIndent = indentString.repeat(groupingsCount + 1);
|
|
for (let property of properties)
|
|
styleText += "\n" + propertyIndent + property.formattedText;
|
|
|
|
styleText += "\n";
|
|
if (!options.includeGroupingsAndSelectors) {
|
|
// Indent the closing "}" for nested rules.
|
|
styleText += indentString.repeat(groupingsCount);
|
|
}
|
|
} else
|
|
styleText += properties.map((property) => property.formattedText).join(" ");
|
|
}
|
|
|
|
if (options.includeGroupingsAndSelectors) {
|
|
for (let i = groupingsCount; i > 0; --i) {
|
|
if (options.multiline)
|
|
styleText += indentString.repeat(i);
|
|
|
|
styleText += "}";
|
|
|
|
if (options.multiline)
|
|
styleText += "\n";
|
|
}
|
|
|
|
styleText += "}";
|
|
}
|
|
|
|
return styleText;
|
|
}
|
|
|
|
// Protected
|
|
|
|
get nodeStyles()
|
|
{
|
|
return this._nodeStyles;
|
|
}
|
|
|
|
// Private
|
|
|
|
_rangeAfterPropertyAtIndex(index)
|
|
{
|
|
if (index < 0)
|
|
return this._styleSheetTextRange.collapseToStart();
|
|
|
|
if (index >= this.visibleProperties.length)
|
|
return this._styleSheetTextRange.collapseToEnd();
|
|
|
|
let property = this.visibleProperties[index];
|
|
return property.styleSheetTextRange.collapseToEnd();
|
|
}
|
|
};
|
|
|
|
WI.CSSStyleDeclaration.Event = {
|
|
PropertiesChanged: "css-style-declaration-properties-changed",
|
|
};
|
|
|
|
WI.CSSStyleDeclaration.Type = {
|
|
Rule: "css-style-declaration-type-rule",
|
|
Inline: "css-style-declaration-type-inline",
|
|
Attribute: "css-style-declaration-type-attribute",
|
|
Computed: "css-style-declaration-type-computed"
|
|
};
|
|
|
|
WI.CSSStyleDeclaration.VariablesGroupType = {
|
|
Ungrouped: "ungrouped",
|
|
Colors: "colors",
|
|
Dimensions: "dimensions",
|
|
Numbers: "numbers",
|
|
Other: "other",
|
|
};
|
|
|
|
/* Models/CSSStyleSheet.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSStyleSheet = class CSSStyleSheet extends WI.SourceCode
|
|
{
|
|
constructor(id)
|
|
{
|
|
super();
|
|
|
|
console.assert(id);
|
|
|
|
this._id = id || null;
|
|
this._parentFrame = null;
|
|
this._origin = null;
|
|
this._startLineNumber = 0;
|
|
this._startColumnNumber = 0;
|
|
|
|
this._inlineStyleAttribute = false;
|
|
this._inlineStyleTag = false;
|
|
|
|
this._hasInfo = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static resetUniqueDisplayNameNumbers()
|
|
{
|
|
WI.CSSStyleSheet._nextUniqueDisplayNameNumber = 1;
|
|
}
|
|
|
|
// Public
|
|
|
|
get id()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
get parentFrame()
|
|
{
|
|
return this._parentFrame;
|
|
}
|
|
|
|
get origin()
|
|
{
|
|
return this._origin;
|
|
}
|
|
|
|
get injected()
|
|
{
|
|
return WI.browserManager.isExtensionScheme(this.urlComponents.scheme);
|
|
}
|
|
|
|
get anonymous()
|
|
{
|
|
return !this.isInspectorStyleSheet() && !this._url;
|
|
}
|
|
|
|
get mimeType()
|
|
{
|
|
return "text/css";
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (this.isInspectorStyleSheet())
|
|
return WI.UIString("Inspector Style Sheet");
|
|
|
|
if (this._url)
|
|
return WI.displayNameForURL(this._url, this.urlComponents);
|
|
|
|
// Assign a unique number to the StyleSheet object so it will stay the same.
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = this.constructor._nextUniqueDisplayNameNumber++;
|
|
|
|
return WI.UIString("Anonymous Style Sheet %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
get startLineNumber()
|
|
{
|
|
return this._startLineNumber;
|
|
}
|
|
|
|
get startColumnNumber()
|
|
{
|
|
return this._startColumnNumber;
|
|
}
|
|
|
|
hasInfo()
|
|
{
|
|
return this._hasInfo;
|
|
}
|
|
|
|
isInspectorStyleSheet()
|
|
{
|
|
return this._origin === WI.CSSStyleSheet.Type.Inspector;
|
|
}
|
|
|
|
isInlineStyleTag()
|
|
{
|
|
return this._inlineStyleTag;
|
|
}
|
|
|
|
isInlineStyleAttributeStyleSheet()
|
|
{
|
|
return this._inlineStyleAttribute;
|
|
}
|
|
|
|
markAsInlineStyleAttributeStyleSheet()
|
|
{
|
|
this._inlineStyleAttribute = true;
|
|
}
|
|
|
|
offsetSourceCodeLocation(sourceCodeLocation)
|
|
{
|
|
if (!sourceCodeLocation)
|
|
return null;
|
|
|
|
if (!this._hasInfo)
|
|
return sourceCodeLocation;
|
|
|
|
let sourceCode = sourceCodeLocation.sourceCode;
|
|
let lineNumber = this._startLineNumber + sourceCodeLocation.lineNumber;
|
|
let columnNumber = this._startColumnNumber + sourceCodeLocation.columnNumber;
|
|
return sourceCode.createSourceCodeLocation(lineNumber, columnNumber);
|
|
}
|
|
|
|
// Protected
|
|
|
|
updateInfo(url, parentFrame, origin, inlineStyle, startLineNumber, startColumnNumber)
|
|
{
|
|
this._hasInfo = true;
|
|
|
|
this._url = url || null;
|
|
this._urlComponents = undefined;
|
|
|
|
this._parentFrame = parentFrame || null;
|
|
this._origin = origin;
|
|
|
|
this._inlineStyleTag = inlineStyle;
|
|
this._startLineNumber = startLineNumber;
|
|
this._startColumnNumber = startColumnNumber;
|
|
}
|
|
|
|
get revisionForRequestedContent()
|
|
{
|
|
return this.currentRevision;
|
|
}
|
|
|
|
handleCurrentRevisionContentChange()
|
|
{
|
|
if (!this._id)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
function contentDidChange(error)
|
|
{
|
|
if (error)
|
|
return;
|
|
|
|
if (target.hasCommand("DOM.markUndoableState"))
|
|
target.DOMAgent.markUndoableState();
|
|
|
|
this.dispatchEventToListeners(WI.CSSStyleSheet.Event.ContentDidChange);
|
|
}
|
|
|
|
this._ignoreNextContentDidChangeNotification = true;
|
|
|
|
target.CSSAgent.setStyleSheetText(this._id, this.currentRevision.content, contentDidChange.bind(this));
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
let specialContentPromise = WI.SourceCode.generateSpecialContentForURL(this._url);
|
|
if (specialContentPromise)
|
|
return specialContentPromise;
|
|
|
|
if (!this._id) {
|
|
// There is no identifier to request content with. Reject the promise to cause the
|
|
// pending callbacks to get null content.
|
|
return Promise.reject(new Error("There is no identifier to request content with."));
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.CSSAgent.getStyleSheetText(this._id);
|
|
}
|
|
|
|
noteContentDidChange()
|
|
{
|
|
if (this._ignoreNextContentDidChangeNotification) {
|
|
this._ignoreNextContentDidChangeNotification = false;
|
|
return false;
|
|
}
|
|
|
|
this.markContentAsStale();
|
|
this.dispatchEventToListeners(WI.CSSStyleSheet.Event.ContentDidChange);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
WI.CSSStyleSheet._nextUniqueDisplayNameNumber = 1;
|
|
|
|
WI.CSSStyleSheet.Event = {
|
|
ContentDidChange: "css-style-sheet-content-did-change"
|
|
};
|
|
|
|
WI.CSSStyleSheet.Type = {
|
|
Author: "css-style-sheet-type-author",
|
|
User: "css-style-sheet-type-user",
|
|
UserAgent: "css-style-sheet-type-user-agent",
|
|
Inspector: "css-style-sheet-type-inspector"
|
|
};
|
|
|
|
/* Models/CallFrame.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CallFrame = class CallFrame
|
|
{
|
|
constructor(target, {id, sourceCodeLocation, functionName, thisObject, scopeChain, nativeCode, programCode, isTailDeleted, blackboxed} = {})
|
|
{
|
|
console.assert(target instanceof WI.Target, target);
|
|
console.assert(!sourceCodeLocation || sourceCodeLocation instanceof WI.SourceCodeLocation, sourceCodeLocation);
|
|
console.assert(!thisObject || thisObject instanceof WI.RemoteObject, thisObject);
|
|
console.assert(!scopeChain || scopeChain.every((item) => item instanceof WI.ScopeChainNode), scopeChain);
|
|
|
|
this._isConsoleEvaluation = sourceCodeLocation && isWebInspectorConsoleEvaluationScript(sourceCodeLocation.sourceCode.sourceURL);
|
|
if (this._isConsoleEvaluation) {
|
|
functionName = WI.UIString("Console Evaluation");
|
|
programCode = true;
|
|
}
|
|
|
|
this._target = target;
|
|
this._id = id || null;
|
|
this._sourceCodeLocation = sourceCodeLocation || null;
|
|
this._functionName = functionName || "";
|
|
this._thisObject = thisObject || null;
|
|
this._scopeChain = scopeChain || [];
|
|
this._nativeCode = nativeCode || false;
|
|
this._programCode = programCode || false;
|
|
this._isTailDeleted = isTailDeleted || false;
|
|
this._blackboxed = blackboxed || false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get id() { return this._id; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
get functionName() { return this._functionName; }
|
|
get nativeCode() { return this._nativeCode; }
|
|
get programCode() { return this._programCode; }
|
|
get thisObject() { return this._thisObject; }
|
|
get scopeChain() { return this._scopeChain; }
|
|
get isTailDeleted() { return this._isTailDeleted; }
|
|
get blackboxed() { return this._blackboxed; }
|
|
get isConsoleEvaluation() { return this._isConsoleEvaluation; }
|
|
|
|
get displayName()
|
|
{
|
|
return this._functionName || WI.UIString("(anonymous function)");
|
|
}
|
|
|
|
isEqual(other)
|
|
{
|
|
if (!other)
|
|
return false;
|
|
|
|
if (this._sourceCodeLocation && other._sourceCodeLocation)
|
|
return this._sourceCodeLocation.isEqual(other._sourceCodeLocation);
|
|
|
|
return false;
|
|
}
|
|
|
|
saveIdentityToCookie()
|
|
{
|
|
// Do nothing. The call frame is torn down when the inspector closes, and
|
|
// we shouldn't restore call frame content views across debugger pauses.
|
|
}
|
|
|
|
collectScopeChainVariableNames(callback)
|
|
{
|
|
let result = ["this", "__proto__"];
|
|
|
|
var pendingRequests = this._scopeChain.length;
|
|
|
|
function propertiesCollected(properties)
|
|
{
|
|
for (var i = 0; properties && i < properties.length; ++i)
|
|
result.push(properties[i].name);
|
|
|
|
if (--pendingRequests)
|
|
return;
|
|
|
|
callback(result);
|
|
}
|
|
|
|
for (var i = 0; i < this._scopeChain.length; ++i)
|
|
this._scopeChain[i].objects[0].getPropertyDescriptors(propertiesCollected);
|
|
}
|
|
|
|
mergedScopeChain()
|
|
{
|
|
let mergedScopes = [];
|
|
|
|
// Scopes list goes from top/local (1) to bottom/global (5)
|
|
// [scope1, scope2, scope3, scope4, scope5]
|
|
let scopes = this._scopeChain.slice();
|
|
|
|
// Merge similiar scopes. Some function call frames may have multiple
|
|
// top level closure scopes (one for `var`s one for `let`s) that can be
|
|
// combined to a single scope of variables. Go in reverse order so we
|
|
// merge the first two closure scopes with the same name. Also mark
|
|
// the first time we see a new name, so we know the base for the name.
|
|
// [scope1&2, scope3, scope4, scope5]
|
|
// foo bar GLE global
|
|
let lastMarkedHash = null;
|
|
function markAsBaseIfNeeded(scope) {
|
|
if (!scope.hash)
|
|
return false;
|
|
if (scope.type !== WI.ScopeChainNode.Type.Closure)
|
|
return false;
|
|
if (scope.hash === lastMarkedHash)
|
|
return false;
|
|
lastMarkedHash = scope.hash;
|
|
scope.__baseClosureScope = true;
|
|
return true;
|
|
}
|
|
|
|
function shouldMergeClosureScopes(youngScope, oldScope, lastMerge) {
|
|
if (!youngScope || !oldScope)
|
|
return false;
|
|
|
|
// Don't merge unknown locations.
|
|
if (!youngScope.hash || !oldScope.hash)
|
|
return false;
|
|
|
|
// Only merge closure scopes.
|
|
if (youngScope.type !== WI.ScopeChainNode.Type.Closure)
|
|
return false;
|
|
if (oldScope.type !== WI.ScopeChainNode.Type.Closure)
|
|
return false;
|
|
|
|
// Don't merge if they are not the same.
|
|
if (youngScope.hash !== oldScope.hash)
|
|
return false;
|
|
|
|
// Don't merge if there was already a merge.
|
|
if (lastMerge && youngScope.hash === lastMerge.hash)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
let lastScope = null;
|
|
let lastMerge = null;
|
|
for (let i = scopes.length - 1; i >= 0; --i) {
|
|
let scope = scopes[i];
|
|
markAsBaseIfNeeded(scope);
|
|
if (shouldMergeClosureScopes(scope, lastScope, lastMerge)) {
|
|
console.assert(lastScope.__baseClosureScope);
|
|
let type = WI.ScopeChainNode.Type.Closure;
|
|
let objects = lastScope.objects.concat(scope.objects);
|
|
let merged = new WI.ScopeChainNode(type, objects, scope.name, scope.location);
|
|
merged.__baseClosureScope = true;
|
|
console.assert(objects.length === 2);
|
|
|
|
mergedScopes.pop(); // Remove the last.
|
|
mergedScopes.push(merged); // Add the merged scope.
|
|
|
|
lastMerge = merged;
|
|
lastScope = null;
|
|
} else {
|
|
mergedScopes.push(scope);
|
|
|
|
lastMerge = null;
|
|
lastScope = scope;
|
|
}
|
|
}
|
|
|
|
mergedScopes = mergedScopes.reverse();
|
|
|
|
// Mark the first Closure as Local if the name matches this call frame.
|
|
for (let scope of mergedScopes) {
|
|
if (scope.type === WI.ScopeChainNode.Type.Closure) {
|
|
if (scope.name === this._functionName)
|
|
scope.convertToLocalScope();
|
|
break;
|
|
}
|
|
}
|
|
|
|
return mergedScopes;
|
|
}
|
|
|
|
// Static
|
|
|
|
static functionNameFromPayload(payload)
|
|
{
|
|
let functionName = payload.functionName;
|
|
if (functionName === "global code")
|
|
return WI.UIString("Global Code");
|
|
if (functionName === "eval code")
|
|
return WI.UIString("Eval Code");
|
|
if (functionName === "module code")
|
|
return WI.UIString("Module Code");
|
|
return functionName;
|
|
}
|
|
|
|
static programCodeFromPayload(payload)
|
|
{
|
|
return payload.functionName.endsWith(" code");
|
|
}
|
|
|
|
static fromDebuggerPayload(target, payload, scopeChain, sourceCodeLocation)
|
|
{
|
|
return new WI.CallFrame(target, {
|
|
id: payload.callFrameId,
|
|
sourceCodeLocation,
|
|
functionName: WI.CallFrame.functionNameFromPayload(payload),
|
|
thisObject: WI.RemoteObject.fromPayload(payload.this, target),
|
|
scopeChain,
|
|
programCode: WI.CallFrame.programCodeFromPayload(payload),
|
|
isTailDeleted: payload.isTailDeleted,
|
|
blackboxed: sourceCodeLocation && !!WI.debuggerManager.blackboxDataForSourceCode(sourceCodeLocation.sourceCode),
|
|
});
|
|
}
|
|
|
|
static fromPayload(target, payload)
|
|
{
|
|
console.assert(payload);
|
|
|
|
let {url, scriptId} = payload;
|
|
let nativeCode = false;
|
|
let sourceCodeLocation = null;
|
|
|
|
if (url === "[native code]") {
|
|
nativeCode = true;
|
|
url = null;
|
|
} else if (url || scriptId) {
|
|
let sourceCode = null;
|
|
if (scriptId) {
|
|
sourceCode = WI.debuggerManager.scriptForIdentifier(scriptId, target);
|
|
if (sourceCode && sourceCode.resource)
|
|
sourceCode = sourceCode.resource;
|
|
}
|
|
if (!sourceCode)
|
|
sourceCode = WI.networkManager.resourcesForURL(url).firstValue;
|
|
if (!sourceCode)
|
|
sourceCode = WI.debuggerManager.scriptsForURL(url, target)[0];
|
|
|
|
if (sourceCode) {
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
let lineNumber = payload.lineNumber - 1;
|
|
sourceCodeLocation = sourceCode.createLazySourceCodeLocation(lineNumber, payload.columnNumber);
|
|
} else {
|
|
// Treat this as native code if we were unable to find a source.
|
|
console.assert(!url, "We should have detected source code for something with a url");
|
|
nativeCode = true;
|
|
url = null;
|
|
}
|
|
}
|
|
|
|
return new WI.CallFrame(target, {
|
|
sourceCodeLocation,
|
|
functionName: WI.CallFrame.functionNameFromPayload(payload),
|
|
nativeCode,
|
|
programCode: WI.CallFrame.programCodeFromPayload(payload),
|
|
blackboxed: sourceCodeLocation && !!WI.debuggerManager.blackboxDataForSourceCode(sourceCodeLocation.sourceCode),
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Models/CallingContextTree.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CallingContextTree = class CallingContextTree
|
|
{
|
|
constructor(type)
|
|
{
|
|
this._type = type || WI.CallingContextTree.Type.TopDown;
|
|
|
|
this.reset();
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get totalNumberOfSamples() { return this._totalNumberOfSamples; }
|
|
|
|
reset()
|
|
{
|
|
this._root = new WI.CallingContextTreeNode(-1, -1, -1, "<root>", null);
|
|
this._totalNumberOfSamples = 0;
|
|
}
|
|
|
|
totalDurationInTimeRange(startTime, endTime)
|
|
{
|
|
return this._root.filteredTimestampsAndDuration(startTime, endTime).duration;
|
|
}
|
|
|
|
updateTreeWithStackTrace({timestamp, stackFrames}, duration)
|
|
{
|
|
this._totalNumberOfSamples++;
|
|
|
|
let node = this._root;
|
|
node.addTimestampAndExpressionLocation(timestamp, duration, null);
|
|
|
|
switch (this._type) {
|
|
case WI.CallingContextTree.Type.TopDown:
|
|
for (let i = stackFrames.length; i--; ) {
|
|
let stackFrame = stackFrames[i];
|
|
node = node.findOrMakeChild(stackFrame);
|
|
node.addTimestampAndExpressionLocation(timestamp, duration, stackFrame.expressionLocation || null, i === 0);
|
|
}
|
|
break;
|
|
case WI.CallingContextTree.Type.BottomUp:
|
|
for (let i = 0; i < stackFrames.length; ++i) {
|
|
let stackFrame = stackFrames[i];
|
|
node = node.findOrMakeChild(stackFrame);
|
|
node.addTimestampAndExpressionLocation(timestamp, duration, stackFrame.expressionLocation || null, i === 0);
|
|
}
|
|
break;
|
|
case WI.CallingContextTree.Type.TopFunctionsTopDown:
|
|
for (let i = stackFrames.length; i--; ) {
|
|
node = this._root;
|
|
for (let j = i + 1; j--; ) {
|
|
let stackFrame = stackFrames[j];
|
|
node = node.findOrMakeChild(stackFrame);
|
|
node.addTimestampAndExpressionLocation(timestamp, duration, stackFrame.expressionLocation || null, j === 0);
|
|
}
|
|
}
|
|
break;
|
|
case WI.CallingContextTree.Type.TopFunctionsBottomUp:
|
|
for (let i = 0; i < stackFrames.length; i++) {
|
|
node = this._root;
|
|
for (let j = i; j < stackFrames.length; j++) {
|
|
let stackFrame = stackFrames[j];
|
|
node = node.findOrMakeChild(stackFrame);
|
|
node.addTimestampAndExpressionLocation(timestamp, duration, stackFrame.expressionLocation || null, j === 0);
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
console.assert(false, "This should not be reached.");
|
|
break;
|
|
}
|
|
}
|
|
|
|
toCPUProfilePayload(startTime, endTime)
|
|
{
|
|
let cpuProfile = {};
|
|
let roots = [];
|
|
let numSamplesInTimeRange = this._root.filteredTimestampsAndDuration(startTime, endTime).timestamps.length;
|
|
|
|
this._root.forEachChild((child) => {
|
|
if (child.hasStackTraceInTimeRange(startTime, endTime))
|
|
roots.push(child.toCPUProfileNode(numSamplesInTimeRange, startTime, endTime));
|
|
});
|
|
|
|
cpuProfile.rootNodes = roots;
|
|
return cpuProfile;
|
|
}
|
|
|
|
forEachChild(callback)
|
|
{
|
|
this._root.forEachChild(callback);
|
|
}
|
|
|
|
forEachNode(callback)
|
|
{
|
|
this._root.forEachNode(callback);
|
|
}
|
|
|
|
// Testing.
|
|
|
|
static __test_makeTreeFromProtocolMessageObject(messageObject)
|
|
{
|
|
let tree = new WI.CallingContextTree;
|
|
let stackTraces = messageObject.params.samples.stackTraces;
|
|
for (let i = 0; i < stackTraces.length; i++)
|
|
tree.updateTreeWithStackTrace(stackTraces[i]);
|
|
return tree;
|
|
}
|
|
|
|
__test_matchesStackTrace(stackTrace)
|
|
{
|
|
// StackTrace should have top frame first in the array and bottom frame last.
|
|
// We don't look for a match that traces down the tree from the root; instead,
|
|
// we match by looking at all the leafs, and matching while walking up the tree
|
|
// towards the root. If we successfully make the walk, we've got a match that
|
|
// suffices for a particular test. A successful match doesn't mean we actually
|
|
// walk all the way up to the root; it just means we didn't fail while walking
|
|
// in the direction of the root.
|
|
let leaves = this.__test_buildLeafLinkedLists();
|
|
|
|
outer:
|
|
for (let node of leaves) {
|
|
for (let stackNode of stackTrace) {
|
|
for (let propertyName of Object.getOwnPropertyNames(stackNode)) {
|
|
if (stackNode[propertyName] !== node[propertyName])
|
|
continue outer;
|
|
}
|
|
node = node.parent;
|
|
}
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
__test_buildLeafLinkedLists()
|
|
{
|
|
let result = [];
|
|
let parent = null;
|
|
this._root.__test_buildLeafLinkedLists(parent, result);
|
|
return result;
|
|
}
|
|
};
|
|
|
|
WI.CallingContextTree.Type = {
|
|
TopDown: Symbol("TopDown"),
|
|
BottomUp: Symbol("BottomUp"),
|
|
TopFunctionsTopDown: Symbol("TopFunctionsTopDown"),
|
|
TopFunctionsBottomUp: Symbol("TopFunctionsBottomUp"),
|
|
};
|
|
|
|
/* Models/CallingContextTreeNode.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CallingContextTreeNode = class CallingContextTreeNode
|
|
{
|
|
constructor(sourceID, line, column, name, url, hash)
|
|
{
|
|
this._children = {};
|
|
this._sourceID = sourceID;
|
|
this._line = line;
|
|
this._column = column;
|
|
this._name = name;
|
|
this._url = url;
|
|
this._uid = WI.CallingContextTreeNode.__uid++;
|
|
|
|
this._timestamps = [];
|
|
this._durations = [];
|
|
this._leafTimestamps = [];
|
|
this._leafDurations = [];
|
|
this._expressionLocations = {}; // Keys are "line:column" strings. Values are arrays of timestamps in sorted order.
|
|
|
|
this._hash = hash || WI.CallingContextTreeNode._hash(this);
|
|
}
|
|
|
|
// Static and Private
|
|
|
|
static _hash(stackFrame)
|
|
{
|
|
return stackFrame.name + ":" + stackFrame.sourceID + ":" + stackFrame.line + ":" + stackFrame.column;
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceID() { return this._sourceID; }
|
|
get line() { return this._line; }
|
|
get column() { return this._column; }
|
|
get name() { return this._name; }
|
|
get uid() { return this._uid; }
|
|
get url() { return this._url; }
|
|
get hash() { return this._hash; }
|
|
|
|
hasChildrenInTimeRange(startTime, endTime)
|
|
{
|
|
for (let propertyName of Object.getOwnPropertyNames(this._children)) {
|
|
let child = this._children[propertyName];
|
|
if (child.hasStackTraceInTimeRange(startTime, endTime))
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
hasStackTraceInTimeRange(startTime, endTime)
|
|
{
|
|
console.assert(startTime <= endTime);
|
|
if (startTime > endTime)
|
|
return false;
|
|
|
|
let timestamps = this._timestamps;
|
|
let length = timestamps.length;
|
|
if (!length)
|
|
return false;
|
|
|
|
let index = timestamps.lowerBound(startTime);
|
|
if (index === length)
|
|
return false;
|
|
console.assert(startTime <= timestamps[index]);
|
|
|
|
let hasTimestampInRange = timestamps[index] <= endTime;
|
|
return hasTimestampInRange;
|
|
}
|
|
|
|
filteredTimestampsAndDuration(startTime, endTime)
|
|
{
|
|
let lowerIndex = this._timestamps.lowerBound(startTime);
|
|
let upperIndex = this._timestamps.upperBound(endTime);
|
|
|
|
let totalDuration = 0;
|
|
for (let i = lowerIndex; i < upperIndex; ++i)
|
|
totalDuration += this._durations[i];
|
|
|
|
return {
|
|
timestamps: this._timestamps.slice(lowerIndex, upperIndex),
|
|
duration: totalDuration,
|
|
};
|
|
}
|
|
|
|
filteredLeafTimestampsAndDuration(startTime, endTime)
|
|
{
|
|
let lowerIndex = this._leafTimestamps.lowerBound(startTime);
|
|
let upperIndex = this._leafTimestamps.upperBound(endTime);
|
|
|
|
let totalDuration = 0;
|
|
for (let i = lowerIndex; i < upperIndex; ++i)
|
|
totalDuration += this._leafDurations[i];
|
|
|
|
return {
|
|
leafTimestamps: this._leafTimestamps.slice(lowerIndex, upperIndex),
|
|
leafDuration: totalDuration,
|
|
};
|
|
}
|
|
|
|
hasChildren()
|
|
{
|
|
return !isEmptyObject(this._children);
|
|
}
|
|
|
|
findOrMakeChild(stackFrame)
|
|
{
|
|
let hash = WI.CallingContextTreeNode._hash(stackFrame);
|
|
let node = this._children[hash];
|
|
if (node)
|
|
return node;
|
|
node = new WI.CallingContextTreeNode(stackFrame.sourceID, stackFrame.line, stackFrame.column, stackFrame.name, stackFrame.url, hash);
|
|
this._children[hash] = node;
|
|
return node;
|
|
}
|
|
|
|
addTimestampAndExpressionLocation(timestamp, duration, expressionLocation, leaf)
|
|
{
|
|
console.assert(!this._timestamps.length || this._timestamps.lastValue <= timestamp, "Expected timestamps to be added in sorted, increasing, order.");
|
|
this._timestamps.push(timestamp);
|
|
this._durations.push(duration);
|
|
|
|
if (leaf) {
|
|
this._leafTimestamps.push(timestamp);
|
|
this._leafDurations.push(duration);
|
|
}
|
|
|
|
if (!expressionLocation)
|
|
return;
|
|
|
|
let {line, column} = expressionLocation;
|
|
let hashCons = line + ":" + column;
|
|
let timestamps = this._expressionLocations[hashCons];
|
|
if (!timestamps) {
|
|
timestamps = [];
|
|
this._expressionLocations[hashCons] = timestamps;
|
|
}
|
|
console.assert(!timestamps.length || timestamps.lastValue <= timestamp, "Expected timestamps to be added in sorted, increasing, order.");
|
|
timestamps.push(timestamp);
|
|
}
|
|
|
|
forEachChild(callback)
|
|
{
|
|
for (let propertyName of Object.getOwnPropertyNames(this._children))
|
|
callback(this._children[propertyName]);
|
|
}
|
|
|
|
forEachNode(callback)
|
|
{
|
|
callback(this);
|
|
this.forEachChild(function(child) {
|
|
child.forEachNode(callback);
|
|
});
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
return this._hash === other.hash;
|
|
}
|
|
|
|
toCPUProfileNode(numSamples, startTime, endTime)
|
|
{
|
|
let children = [];
|
|
this.forEachChild((child) => {
|
|
if (child.hasStackTraceInTimeRange(startTime, endTime))
|
|
children.push(child.toCPUProfileNode(numSamples, startTime, endTime));
|
|
});
|
|
let cpuProfileNode = {
|
|
id: this._uid,
|
|
functionName: this._name,
|
|
url: this._url,
|
|
lineNumber: this._line,
|
|
columnNumber: this._column,
|
|
children: children
|
|
};
|
|
|
|
let timestamps = [];
|
|
let frameStartTime = Number.MAX_VALUE;
|
|
let frameEndTime = Number.MIN_VALUE;
|
|
for (let i = 0; i < this._timestamps.length; i++) {
|
|
let timestamp = this._timestamps[i];
|
|
if (startTime <= timestamp && timestamp <= endTime) {
|
|
timestamps.push(timestamp);
|
|
frameStartTime = Math.min(frameStartTime, timestamp);
|
|
frameEndTime = Math.max(frameEndTime, timestamp);
|
|
}
|
|
}
|
|
|
|
cpuProfileNode.callInfo = {
|
|
callCount: timestamps.length, // Totally not callCount, but oh well, this makes life easier because of field names.
|
|
startTime: frameStartTime,
|
|
endTime: frameEndTime,
|
|
totalTime: (timestamps.length / numSamples) * (endTime - startTime)
|
|
};
|
|
|
|
return cpuProfileNode;
|
|
}
|
|
|
|
// Testing.
|
|
|
|
__test_buildLeafLinkedLists(parent, result)
|
|
{
|
|
let linkedListNode = {
|
|
name: this._name,
|
|
url: this._url,
|
|
parent: parent
|
|
};
|
|
if (this.hasChildren()) {
|
|
this.forEachChild((child) => {
|
|
child.__test_buildLeafLinkedLists(linkedListNode, result);
|
|
});
|
|
} else {
|
|
// We're a leaf.
|
|
result.push(linkedListNode);
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.CallingContextTreeNode.__uid = 0;
|
|
|
|
/* Models/Canvas.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Canvas = class Canvas extends WI.Object
|
|
{
|
|
constructor(target, identifier, contextType, size, {domNode, cssCanvasName, contextAttributes, memoryCost, stackTrace} = {})
|
|
{
|
|
super();
|
|
|
|
console.assert(target instanceof WI.Target, target);
|
|
console.assert(identifier);
|
|
console.assert(contextType);
|
|
console.assert(!size || size instanceof WI.Size, size);
|
|
console.assert(!stackTrace || stackTrace instanceof WI.StackTrace, stackTrace);
|
|
|
|
this._target = target;
|
|
this._identifier = identifier;
|
|
this._contextType = contextType;
|
|
this._size = size || null;
|
|
this._domNode = domNode || null;
|
|
this._cssCanvasName = cssCanvasName || "";
|
|
this._contextAttributes = contextAttributes || {};
|
|
this._extensions = new Set;
|
|
this._memoryCost = memoryCost || NaN;
|
|
this._stackTrace = stackTrace || null;
|
|
|
|
this._clientNodes = null;
|
|
this._shaderProgramCollection = new WI.ShaderProgramCollection;
|
|
this._recordingCollection = new WI.RecordingCollection;
|
|
|
|
this._nextShaderProgramDisplayNumber = null;
|
|
|
|
this._requestNodePromise = null;
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.Inactive;
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
// COMPATIBILITY (macOS 14.2, iOS 17.2): `Canvas.canvasSizeChanged` did not exist yet.
|
|
if (!InspectorBackend.hasEvent("Canvas.canvasSizeChanged")) {
|
|
console.assert(!size);
|
|
|
|
this.requestNode().then((node) => {
|
|
if (node) {
|
|
node.addEventListener(WI.DOMNode.Event.AttributeModified, this._calculateSize, this);
|
|
node.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._calculateSize, this);
|
|
}
|
|
});
|
|
this._calculateSize();
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(target, payload)
|
|
{
|
|
let contextType = null;
|
|
switch (payload.contextType) {
|
|
case InspectorBackend.Enum.Canvas.ContextType.Canvas2D:
|
|
contextType = WI.Canvas.ContextType.Canvas2D;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.OffscreenCanvas2D:
|
|
contextType = WI.Canvas.ContextType.OffscreenCanvas2D;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.BitmapRenderer:
|
|
contextType = WI.Canvas.ContextType.BitmapRenderer;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.OffscreenBitmapRenderer:
|
|
contextType = WI.Canvas.ContextType.OffscreenBitmapRenderer;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGL:
|
|
contextType = WI.Canvas.ContextType.WebGL;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.OffscreenWebGL:
|
|
contextType = WI.Canvas.ContextType.OffscreenWebGL;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGL2:
|
|
contextType = WI.Canvas.ContextType.WebGL2;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.OffscreenWebGL2:
|
|
contextType = WI.Canvas.ContextType.OffscreenWebGL2;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebGPU:
|
|
contextType = WI.Canvas.ContextType.WebGPU;
|
|
break;
|
|
case InspectorBackend.Enum.Canvas.ContextType.WebMetal:
|
|
contextType = WI.Canvas.ContextType.WebMetal;
|
|
break;
|
|
default:
|
|
console.error("Invalid canvas context type", payload.contextType);
|
|
}
|
|
|
|
// COMPATIBILITY (macOS 14.2, iOS 17.2): `width` and `height` did not exist yet.
|
|
let size = ("width" in payload && "height" in payload) ? new WI.Size(payload.width, payload.height) : null;
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `backtrace` was renamed to `stackTrace`.
|
|
if (payload.backtrace)
|
|
payload.stackTrace = {callFrames: payload.backtrace};
|
|
|
|
return new WI.Canvas(target, payload.canvasId, contextType, size, {
|
|
height: payload.height,
|
|
domNode: payload.nodeId ? WI.domManager.nodeForId(payload.nodeId) : null,
|
|
cssCanvasName: payload.cssCanvasName,
|
|
contextAttributes: payload.contextAttributes,
|
|
memoryCost: payload.memoryCost,
|
|
stackTrace: WI.StackTrace.fromPayload(target, payload.stackTrace),
|
|
});
|
|
}
|
|
|
|
static displayNameForContextType(contextType)
|
|
{
|
|
switch (contextType) {
|
|
case WI.Canvas.ContextType.Canvas2D:
|
|
return WI.UIString("2D", "2D @ Canvas Context Type", "2D is a type of rendering context associated with a <canvas> element.");
|
|
case WI.Canvas.ContextType.OffscreenCanvas2D:
|
|
return WI.UIString("Offscreen2D", "2D @ Offscreen Canvas Context Type", "2D is a type of rendering context associated with a OffscreenCanvas.");
|
|
case WI.Canvas.ContextType.BitmapRenderer:
|
|
return WI.UIString("Bitmap Renderer", "Bitmap Renderer @ Canvas Context Type", "Bitmap Renderer is a type of rendering context associated with a <canvas> element.");
|
|
case WI.Canvas.ContextType.OffscreenBitmapRenderer:
|
|
return WI.UIString("Bitmap Renderer (Offscreen)", "Bitmap Renderer @ Offscreen Canvas Context Type", "Bitmap Renderer is a type of rendering context associated with a OffscreenCanvas.");
|
|
case WI.Canvas.ContextType.WebGL:
|
|
return WI.UIString("WebGL", "WebGL @ Canvas Context Type", "WebGL is a type of rendering context associated with a <canvas> element.");
|
|
case WI.Canvas.ContextType.OffscreenWebGL:
|
|
return WI.UIString("WebGL (Offscreen)", "WebGL @ Offscreen Canvas Context Type", "WebGL is a type of rendering context associated with a OffscreenCanvas.");
|
|
case WI.Canvas.ContextType.WebGL2:
|
|
return WI.UIString("WebGL2", "WebGL2 @ Canvas Context Type", "WebGL2 is a type of rendering context associated with a <canvas> element.");
|
|
case WI.Canvas.ContextType.OffscreenWebGL2:
|
|
return WI.UIString("WebGL2 (Offscreen)", "WebGL2 @ Offscreen Canvas Context Type", "WebGL2 is a type of rendering context associated with a OffscreenCanvas.");
|
|
case WI.Canvas.ContextType.WebGPU:
|
|
return WI.UIString("WebGPU", "WebGPU @ Canvas Context Type", "WebGPU is a type of rendering context associated with a <canvas> element.");
|
|
case WI.Canvas.ContextType.WebMetal:
|
|
return WI.UIString("WebMetal", "WebMetal @ Canvas Context Type", "WebMetal is a type of rendering context associated with a <canvas> element.");
|
|
}
|
|
|
|
console.assert(false, "Unknown canvas context type", contextType);
|
|
return null;
|
|
}
|
|
|
|
static displayNameForColorSpace(colorSpace)
|
|
{
|
|
switch(colorSpace) {
|
|
case WI.Canvas.ColorSpace.SRGB:
|
|
return WI.UIString("sRGB", "sRGB @ Color Space", "Label for a canvas that uses the sRGB color space.");
|
|
case WI.Canvas.ColorSpace.DisplayP3:
|
|
return WI.UIString("Display P3", "Display P3 @ Color Space", "Label for a canvas that uses the Display P3 color space.");
|
|
}
|
|
|
|
console.assert(false, "Unknown canvas color space", colorSpace);
|
|
return null;
|
|
}
|
|
|
|
static resetUniqueDisplayNameNumbers()
|
|
{
|
|
Canvas._nextContextUniqueDisplayNameNumber = 1;
|
|
Canvas._nextDeviceUniqueDisplayNameNumber = 1;
|
|
}
|
|
|
|
static supportsRequestContentForContextType(contextType)
|
|
{
|
|
switch (contextType) {
|
|
case Canvas.ContextType.WebGPU:
|
|
case Canvas.ContextType.WebMetal:
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get identifier() { return this._identifier; }
|
|
get contextType() { return this._contextType; }
|
|
get size() { return this._size; }
|
|
get memoryCost() { return this._memoryCost; }
|
|
get cssCanvasName() { return this._cssCanvasName; }
|
|
get contextAttributes() { return this._contextAttributes; }
|
|
get extensions() { return this._extensions; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
get shaderProgramCollection() { return this._shaderProgramCollection; }
|
|
get recordingCollection() { return this._recordingCollection; }
|
|
get recordingFrameCount() { return this._recordingFrames.length; }
|
|
get recordingBufferUsed() { return this._recordingBufferUsed; }
|
|
|
|
get supportsRecording()
|
|
{
|
|
switch (this._contextType) {
|
|
case WI.Canvas.ContextType.Canvas2D:
|
|
case WI.Canvas.ContextType.OffscreenCanvas2D:
|
|
case WI.Canvas.ContextType.BitmapRenderer:
|
|
case WI.Canvas.ContextType.OffscreenBitmapRenderer:
|
|
case WI.Canvas.ContextType.WebGL:
|
|
case WI.Canvas.ContextType.OffscreenWebGL:
|
|
case WI.Canvas.ContextType.WebGL2:
|
|
case WI.Canvas.ContextType.OffscreenWebGL2:
|
|
return true;
|
|
|
|
case WI.Canvas.ContextType.WebGPU:
|
|
case WI.Canvas.ContextType.WebMetal:
|
|
return false;
|
|
}
|
|
|
|
console.assert(false, "not reached");
|
|
return false;
|
|
}
|
|
|
|
get recordingActive()
|
|
{
|
|
return this._recordingState !== WI.Canvas.RecordingState.Inactive;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (this._cssCanvasName)
|
|
return WI.UIString("CSS canvas \u201C%s\u201D").format(this._cssCanvasName);
|
|
|
|
if (this._domNode) {
|
|
let idSelector = this._domNode.escapedIdSelector;
|
|
if (idSelector)
|
|
return WI.UIString("Canvas %s").format(idSelector);
|
|
}
|
|
|
|
if (this._contextType === Canvas.ContextType.WebGPU) {
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = Canvas._nextDeviceUniqueDisplayNameNumber++;
|
|
return WI.UIString("Device %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
if (!this._uniqueDisplayNameNumber)
|
|
this._uniqueDisplayNameNumber = Canvas._nextContextUniqueDisplayNameNumber++;
|
|
return WI.UIString("Canvas %d").format(this._uniqueDisplayNameNumber);
|
|
}
|
|
|
|
get is2D()
|
|
{
|
|
return this._contextType === Canvas.ContextType.Canvas2D || this._contextType === Canvas.ContextType.OffscreenCanvas2D;
|
|
}
|
|
|
|
get isBitmapRender()
|
|
{
|
|
return this._contextType === Canvas.ContextType.BitmapRenderer || this._contextType === Canvas.ContextType.OffscreenBitmapRenderer;
|
|
}
|
|
|
|
get isWebGL()
|
|
{
|
|
return this._contextType === Canvas.ContextType.WebGL || this._contextType === Canvas.ContextType.OffscreenWebGL;
|
|
}
|
|
|
|
get isWebGL2()
|
|
{
|
|
return this._contextType === Canvas.ContextType.WebGL2 || this._contextType === Canvas.ContextType.OffscreenWebGL2;
|
|
}
|
|
|
|
requestNode()
|
|
{
|
|
if (!this._requestNodePromise) {
|
|
this._requestNodePromise = new Promise((resolve, reject) => {
|
|
WI.domManager.ensureDocument();
|
|
|
|
if (!this._target.hasCommand("Canvas.requestNode")) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
this._target.CanvasAgent.requestNode(this._identifier, (error, nodeId) => {
|
|
if (error) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
this._domNode = WI.domManager.nodeForId(nodeId);
|
|
if (!this._domNode) {
|
|
resolve(null);
|
|
return;
|
|
}
|
|
|
|
resolve(this._domNode);
|
|
});
|
|
});
|
|
}
|
|
return this._requestNodePromise;
|
|
}
|
|
|
|
requestContent()
|
|
{
|
|
if (!Canvas.supportsRequestContentForContextType(this._contextType))
|
|
return Promise.resolve(null);
|
|
|
|
return this._target.CanvasAgent.requestContent(this._identifier).then((result) => result.content).catch((error) => console.error(error));
|
|
}
|
|
|
|
requestClientNodes(callback)
|
|
{
|
|
if (this._clientNodes) {
|
|
callback(this._clientNodes);
|
|
return;
|
|
}
|
|
|
|
WI.domManager.ensureDocument();
|
|
|
|
let wrappedCallback = (error, clientNodeIds) => {
|
|
if (error) {
|
|
callback([]);
|
|
return;
|
|
}
|
|
|
|
clientNodeIds = Array.isArray(clientNodeIds) ? clientNodeIds : [];
|
|
this._clientNodes = clientNodeIds.map((clientNodeId) => WI.domManager.nodeForId(clientNodeId));
|
|
callback(this._clientNodes);
|
|
};
|
|
|
|
if (this._target.hasCommand("Canvas.requestClientNodes")) {
|
|
this._target.CanvasAgent.requestClientNodes(this._identifier, wrappedCallback);
|
|
return;
|
|
}
|
|
|
|
// COMPATIBILITY (iOS 13): Canvas.requestCSSCanvasClientNodes was renamed to Canvas.requestClientNodes.
|
|
if (this._target.hasCommand("Canvas.requestCSSCanvasClientNodes")) {
|
|
this._target.CanvasAgent.requestCSSCanvasClientNodes(this._identifier, wrappedCallback);
|
|
return;
|
|
}
|
|
|
|
wrappedCallback(null, []);
|
|
}
|
|
|
|
startRecording(singleFrame)
|
|
{
|
|
let handleStartRecording = (error) => {
|
|
if (error) {
|
|
console.error(error);
|
|
return;
|
|
}
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
|
|
|
|
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
|
|
if (this._target.hasEvent("Canvas.recordingStarted"))
|
|
return;
|
|
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
|
|
};
|
|
|
|
// COMPATIBILITY (iOS 12.1): `frameCount` did not exist yet.
|
|
if (this._target.hasCommand("Canvas.startRecording", "singleFrame")) {
|
|
this._target.CanvasAgent.startRecording(this._identifier, singleFrame, handleStartRecording);
|
|
return;
|
|
}
|
|
|
|
if (singleFrame) {
|
|
const frameCount = 1;
|
|
this._target.CanvasAgent.startRecording(this._identifier, frameCount, handleStartRecording);
|
|
} else
|
|
this._target.CanvasAgent.startRecording(this._identifier, handleStartRecording);
|
|
}
|
|
|
|
stopRecording()
|
|
{
|
|
this._target.CanvasAgent.stopRecording(this._identifier);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
if (this._cssCanvasName)
|
|
cookie[WI.Canvas.CSSCanvasNameCookieKey] = this._cssCanvasName;
|
|
else if (this._domNode)
|
|
cookie[WI.Canvas.NodePathCookieKey] = this._domNode.path;
|
|
|
|
}
|
|
|
|
sizeChanged(size)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
// COMPATIBILITY (macOS 14.2, iOS 17.2): `width` and `height` did not exist yet.
|
|
if (this._size?.equals(size))
|
|
return;
|
|
|
|
this._size = size;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.SizeChanged);
|
|
}
|
|
|
|
memoryChanged(memoryCost)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
if (memoryCost === this._memoryCost)
|
|
return;
|
|
|
|
this._memoryCost = memoryCost;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.MemoryChanged);
|
|
}
|
|
|
|
enableExtension(extension)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._extensions.add(extension);
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.ExtensionEnabled, {extension});
|
|
}
|
|
|
|
clientNodesChanged()
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._clientNodes = null;
|
|
|
|
this.dispatchEventToListeners(Canvas.Event.ClientNodesChanged);
|
|
}
|
|
|
|
recordingStarted(initiator)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
if (initiator === InspectorBackend.Enum.Recording.Initiator.Console)
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveConsole;
|
|
else if (initiator === InspectorBackend.Enum.Recording.Initiator.AutoCapture)
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveAutoCapture;
|
|
else {
|
|
console.assert(initiator === InspectorBackend.Enum.Recording.Initiator.Frontend);
|
|
this._recordingState = WI.Canvas.RecordingState.ActiveFrontend;
|
|
}
|
|
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStarted);
|
|
}
|
|
|
|
recordingProgress(framesPayload, bufferUsed)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
this._recordingFrames.pushAll(framesPayload.map(WI.RecordingFrame.fromPayload));
|
|
|
|
this._recordingBufferUsed = bufferUsed;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingProgress);
|
|
}
|
|
|
|
recordingFinished(recordingPayload)
|
|
{
|
|
// Called from WI.CanvasManager.
|
|
|
|
let initiatedByUser = this._recordingState === WI.Canvas.RecordingState.ActiveFrontend;
|
|
|
|
// COMPATIBILITY (iOS 12.1): Canvas.recordingStarted did not exist yet
|
|
if (!initiatedByUser && !InspectorBackend.hasEvent("Canvas.recordingStarted"))
|
|
initiatedByUser = !!this.recordingActive;
|
|
|
|
let recording = recordingPayload ? WI.Recording.fromPayload(recordingPayload, this._recordingFrames) : null;
|
|
if (recording) {
|
|
recording.source = this;
|
|
recording.createDisplayName(recordingPayload.name);
|
|
|
|
this._recordingCollection.add(recording);
|
|
}
|
|
|
|
this._recordingState = WI.Canvas.RecordingState.Inactive;
|
|
this._recordingFrames = [];
|
|
this._recordingBufferUsed = 0;
|
|
|
|
this.dispatchEventToListeners(WI.Canvas.Event.RecordingStopped, {recording, initiatedByUser});
|
|
}
|
|
|
|
nextShaderProgramDisplayNumberForProgramType(programType)
|
|
{
|
|
// Called from WI.ShaderProgram.
|
|
|
|
if (!this._nextShaderProgramDisplayNumber)
|
|
this._nextShaderProgramDisplayNumber = {};
|
|
|
|
this._nextShaderProgramDisplayNumber[programType] = (this._nextShaderProgramDisplayNumber[programType] || 0) + 1;
|
|
return this._nextShaderProgramDisplayNumber[programType];
|
|
}
|
|
|
|
// Private
|
|
|
|
async _calculateSize()
|
|
{
|
|
let remoteObject = await WI.RemoteObject.resolveCanvasContext(this);
|
|
if (!remoteObject)
|
|
return;
|
|
|
|
function inspectedPage_context_getCanvasSize() {
|
|
return {
|
|
width: this.canvas.width,
|
|
height: this.canvas.height,
|
|
};
|
|
}
|
|
let size = await remoteObject.callFunctionJSON(inspectedPage_context_getCanvasSize);
|
|
remoteObject.release();
|
|
|
|
this.sizeChanged(WI.Size.fromJSON(size));
|
|
}
|
|
};
|
|
|
|
WI.Canvas._nextContextUniqueDisplayNameNumber = 1;
|
|
WI.Canvas._nextDeviceUniqueDisplayNameNumber = 1;
|
|
|
|
WI.Canvas.FrameURLCookieKey = "canvas-frame-url";
|
|
WI.Canvas.CSSCanvasNameCookieKey = "canvas-css-canvas-name";
|
|
|
|
WI.Canvas.ContextType = {
|
|
Canvas2D: "canvas-2d",
|
|
OffscreenCanvas2D: "offscreen-canvas-2d",
|
|
BitmapRenderer: "bitmaprenderer",
|
|
OffscreenBitmapRenderer: "offscreen-bitmaprenderer",
|
|
WebGL: "webgl",
|
|
OffscreenWebGL: "offscreen-webgl",
|
|
WebGL2: "webgl2",
|
|
OffscreenWebGL2: "offscreen-webgl2",
|
|
WebGPU: "webgpu",
|
|
WebMetal: "webmetal",
|
|
};
|
|
|
|
WI.Canvas.ColorSpace = {
|
|
SRGB: "srgb",
|
|
DisplayP3: "display-p3",
|
|
};
|
|
|
|
WI.Canvas.RecordingState = {
|
|
Inactive: "canvas-recording-state-inactive",
|
|
ActiveFrontend: "canvas-recording-state-active-frontend",
|
|
ActiveConsole: "canvas-recording-state-active-console",
|
|
ActiveAutoCapture: "canvas-recording-state-active-auto-capture",
|
|
};
|
|
|
|
WI.Canvas.Event = {
|
|
SizeChanged: "canvas-size-changed",
|
|
MemoryChanged: "canvas-memory-changed",
|
|
ExtensionEnabled: "canvas-extension-enabled",
|
|
ClientNodesChanged: "canvas-client-nodes-changed",
|
|
RecordingStarted: "canvas-recording-started",
|
|
RecordingProgress: "canvas-recording-progress",
|
|
RecordingStopped: "canvas-recording-stopped",
|
|
};
|
|
|
|
/* Models/CollectionEntry.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CollectionEntry = class CollectionEntry
|
|
{
|
|
constructor(key, value)
|
|
{
|
|
console.assert(value instanceof WI.RemoteObject);
|
|
console.assert(!key || key instanceof WI.RemoteObject);
|
|
|
|
this._key = key;
|
|
this._value = value;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Runtime.CollectionEntry.
|
|
static fromPayload(payload, target)
|
|
{
|
|
if (payload.key)
|
|
payload.key = WI.RemoteObject.fromPayload(payload.key, target);
|
|
if (payload.value)
|
|
payload.value = WI.RemoteObject.fromPayload(payload.value, target);
|
|
|
|
return new WI.CollectionEntry(payload.key, payload.value);
|
|
}
|
|
|
|
// Public
|
|
|
|
get key() { return this._key; }
|
|
get value() { return this._value; }
|
|
};
|
|
|
|
/* Models/CollectionEntryPreview.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CollectionEntryPreview = class CollectionEntryPreview
|
|
{
|
|
constructor(keyPreview, valuePreview)
|
|
{
|
|
console.assert(valuePreview instanceof WI.ObjectPreview);
|
|
console.assert(!keyPreview || keyPreview instanceof WI.ObjectPreview);
|
|
|
|
this._key = keyPreview;
|
|
this._value = valuePreview;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Runtime.EntryPreview.
|
|
static fromPayload(payload)
|
|
{
|
|
if (payload.key)
|
|
payload.key = WI.ObjectPreview.fromPayload(payload.key);
|
|
if (payload.value)
|
|
payload.value = WI.ObjectPreview.fromPayload(payload.value);
|
|
|
|
return new WI.CollectionEntryPreview(payload.key, payload.value);
|
|
}
|
|
|
|
// Public
|
|
|
|
get keyPreview() { return this._key; }
|
|
get valuePreview() { return this._value; }
|
|
};
|
|
|
|
/* Models/CollectionTypes.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2017 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FrameCollection = class FrameCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Frames");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.Frame;
|
|
}
|
|
};
|
|
|
|
WI.ScriptCollection = class ScriptCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Scripts");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.Script;
|
|
}
|
|
};
|
|
|
|
WI.CSSStyleSheetCollection = class CSSStyleSheetCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Style Sheets");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.CSSStyleSheet;
|
|
}
|
|
};
|
|
|
|
|
|
WI.CanvasCollection = class CanvasCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Canvases");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.Canvas;
|
|
}
|
|
};
|
|
|
|
WI.ShaderProgramCollection = class ShaderProgramCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Shader Programs");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.ShaderProgram;
|
|
}
|
|
};
|
|
|
|
WI.RecordingCollection = class RecordingCollection extends WI.Collection
|
|
{
|
|
// Public
|
|
|
|
get displayName()
|
|
{
|
|
return WI.UIString("Recordings");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
return object instanceof WI.Recording;
|
|
}
|
|
};
|
|
|
|
/* Models/Color.js */
|
|
|
|
/*
|
|
* Copyright (C) 2009-2022 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Color = class Color
|
|
{
|
|
constructor(format, components, gamut)
|
|
{
|
|
this.format = format;
|
|
|
|
console.assert(gamut === undefined || Object.values(WI.Color.Gamut).includes(gamut));
|
|
this._gamut = gamut || WI.Color.Gamut.SRGB;
|
|
|
|
console.assert(components.length === 3 || components.length === 4, components);
|
|
this.alpha = components.length === 4 ? components[3] : 1;
|
|
|
|
this._rgb = null;
|
|
this._normalizedRGB = null;
|
|
this._hsl = null;
|
|
|
|
if (format === WI.Color.Format.HSL || format === WI.Color.Format.HSLA)
|
|
this._hsl = components.slice(0, 3);
|
|
else if (format === WI.Color.Format.ColorFunction)
|
|
this._normalizedRGB = components.slice(0, 3);
|
|
else
|
|
this._rgb = components.slice(0, 3);
|
|
|
|
this.valid = !components.some(isNaN);
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromString(colorString)
|
|
{
|
|
const matchRegExp = /^(?:#(?<hex>[0-9a-f]{3,8})|rgba?\((?<rgb>[^)]+)\)|(?<keyword>\w+)|color\((?<color>[^)]+)\)|hsla?\((?<hsl>[^)]+)\))$/i;
|
|
let match = colorString.match(matchRegExp);
|
|
if (!match)
|
|
return null;
|
|
|
|
if (match.groups.hex) {
|
|
let hex = match.groups.hex.toUpperCase();
|
|
switch (hex.length) {
|
|
case 3:
|
|
return new WI.Color(WI.Color.Format.ShortHEX, [
|
|
parseInt(hex.charAt(0) + hex.charAt(0), 16),
|
|
parseInt(hex.charAt(1) + hex.charAt(1), 16),
|
|
parseInt(hex.charAt(2) + hex.charAt(2), 16),
|
|
1
|
|
]);
|
|
|
|
case 6:
|
|
return new WI.Color(WI.Color.Format.HEX, [
|
|
parseInt(hex.substring(0, 2), 16),
|
|
parseInt(hex.substring(2, 4), 16),
|
|
parseInt(hex.substring(4, 6), 16),
|
|
1
|
|
]);
|
|
|
|
case 4:
|
|
return new WI.Color(WI.Color.Format.ShortHEXAlpha, [
|
|
parseInt(hex.charAt(0) + hex.charAt(0), 16),
|
|
parseInt(hex.charAt(1) + hex.charAt(1), 16),
|
|
parseInt(hex.charAt(2) + hex.charAt(2), 16),
|
|
parseInt(hex.charAt(3) + hex.charAt(3), 16) / 255
|
|
]);
|
|
|
|
case 8:
|
|
return new WI.Color(WI.Color.Format.HEXAlpha, [
|
|
parseInt(hex.substring(0, 2), 16),
|
|
parseInt(hex.substring(2, 4), 16),
|
|
parseInt(hex.substring(4, 6), 16),
|
|
parseInt(hex.substring(6, 8), 16) / 255
|
|
]);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (match.groups.keyword) {
|
|
let keyword = match.groups.keyword.toLowerCase();
|
|
if (!WI.Color.Keywords.hasOwnProperty(keyword))
|
|
return null;
|
|
let color = new WI.Color(WI.Color.Format.Keyword, WI.Color.Keywords[keyword].slice());
|
|
color.keyword = keyword;
|
|
color.original = colorString;
|
|
return color;
|
|
}
|
|
|
|
function splitFunctionString(string) {
|
|
return string.trim().replace(/(\s*(,|\/)\s*|\s+)/g, "|").split("|");
|
|
}
|
|
|
|
function parseFunctionAlpha(alpha) {
|
|
let value = parseFloat(alpha);
|
|
if (alpha.includes("%"))
|
|
value /= 100;
|
|
return Number.constrain(value, 0, 1);
|
|
}
|
|
|
|
if (match.groups.rgb) {
|
|
let rgb = splitFunctionString(match.groups.rgb);
|
|
if (rgb.length !== 3 && rgb.length !== 4)
|
|
return null;
|
|
|
|
function parseFunctionComponent(component) {
|
|
let value = parseFloat(component);
|
|
if (component.includes("%"))
|
|
value = value * 255 / 100;
|
|
return Number.constrain(value, 0, 255);
|
|
}
|
|
|
|
let alpha = 1;
|
|
if (rgb.length === 4)
|
|
alpha = parseFunctionAlpha(rgb[3]);
|
|
|
|
return new WI.Color(rgb.length === 4 ? WI.Color.Format.RGBA : WI.Color.Format.RGB, [
|
|
parseFunctionComponent(rgb[0]),
|
|
parseFunctionComponent(rgb[1]),
|
|
parseFunctionComponent(rgb[2]),
|
|
alpha,
|
|
]);
|
|
}
|
|
|
|
if (match.groups.hsl) {
|
|
let hsl = splitFunctionString(match.groups.hsl);
|
|
if (hsl.length !== 3 && hsl.length !== 4)
|
|
return null;
|
|
|
|
let alpha = 1;
|
|
if (hsl.length === 4)
|
|
alpha = parseFunctionAlpha(hsl[3]);
|
|
|
|
function parseHueComponent(hue) {
|
|
let value = parseFloat(hue);
|
|
if (/(\b|\d)rad\b/.test(hue))
|
|
value = value * 180 / Math.PI;
|
|
else if (/(\b|\d)grad\b/.test(hue))
|
|
value = value * 360 / 400;
|
|
else if (/(\b|\d)turn\b/.test(hue))
|
|
value = value * 360;
|
|
return Number.constrain(value, 0, 360);
|
|
}
|
|
|
|
function parsePercentageComponent(component) {
|
|
let value = parseFloat(component);
|
|
return Number.constrain(value, 0, 100);
|
|
}
|
|
|
|
return new WI.Color(hsl.length === 4 ? WI.Color.Format.HSLA : WI.Color.Format.HSL, [
|
|
parseHueComponent(hsl[0]),
|
|
parsePercentageComponent(hsl[1]),
|
|
parsePercentageComponent(hsl[2]),
|
|
alpha,
|
|
]);
|
|
}
|
|
|
|
if (match.groups.color) {
|
|
let colorString = match.groups.color.trim();
|
|
let components = splitFunctionString(colorString);
|
|
if (components.length !== 4 && components.length !== 5)
|
|
return null;
|
|
|
|
let gamut = components[0].toLowerCase();
|
|
if (!Object.values(WI.Color.Gamut).includes(gamut))
|
|
return null;
|
|
|
|
let alpha = 1;
|
|
if (components.length === 5)
|
|
alpha = parseFunctionAlpha(components[4]);
|
|
|
|
function parseFunctionComponent(component) {
|
|
let value = parseFloat(component);
|
|
return Number.constrain(value, 0, 1);
|
|
}
|
|
|
|
return new WI.Color(WI.Color.Format.ColorFunction, [
|
|
parseFunctionComponent(components[1]),
|
|
parseFunctionComponent(components[2]),
|
|
parseFunctionComponent(components[3]),
|
|
alpha,
|
|
], gamut);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static fromStringBestMatchingSuggestedFormatAndGamut(colorString, {suggestedFormat, suggestedGamut, forceSuggestedFormatAndGamut} = {})
|
|
{
|
|
let newColor = WI.Color.fromString(colorString);
|
|
|
|
if (forceSuggestedFormatAndGamut) {
|
|
newColor.format = suggestedFormat;
|
|
newColor.gamut = suggestedGamut;
|
|
return newColor;
|
|
}
|
|
|
|
// Match the suggested gamut if we can do so losslessly.
|
|
if (suggestedGamut === WI.Color.Gamut.DisplayP3 && newColor.gamut !== WI.Color.Gamut.DisplayP3)
|
|
newColor.gamut = WI.Color.Gamut.DisplayP3;
|
|
else if (suggestedGamut !== WI.Color.Gamut.DisplayP3 && newColor.gamut === WI.Color.Gamut.DisplayP3 && !newColor.isOutsideSRGB())
|
|
newColor.gamut = WI.Color.Gamut.SRGB;
|
|
|
|
// Non-sRGB gamuts can only be expressed in the Color Function format.
|
|
if (newColor.gamut !== WI.Color.Gamut.SRGB)
|
|
return newColor;
|
|
|
|
// Match as closely as possible the suggested format, and progressively adjust the format (e.g. ShortHEX -> HEX
|
|
// -> HEXAlpha) if an exact match would be lossy.
|
|
switch (suggestedFormat) {
|
|
case WI.Color.Format.Original:
|
|
console.assert(false, "No color should have a format of 'Original'.");
|
|
break;
|
|
|
|
case WI.Color.Format.Keyword:
|
|
// Use the format of the color string as-provided.
|
|
break;
|
|
|
|
case WI.Color.Format.HEX:
|
|
newColor.format = newColor.simple ? WI.Color.Format.HEX : WI.Color.Format.HEXAlpha;
|
|
break;
|
|
|
|
case WI.Color.Format.ShortHEX:
|
|
if (newColor.canBeSerializedAsShortHEX())
|
|
newColor.format = newColor.simple ? WI.Color.Format.ShortHEX : WI.Color.Format.ShortHEXAlpha;
|
|
else
|
|
newColor.format = newColor.simple ? WI.Color.Format.HEX : WI.Color.Format.HEXAlpha;
|
|
break;
|
|
|
|
case WI.Color.Format.ShortHEXAlpha:
|
|
newColor.format = newColor.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEXAlpha : WI.Color.Format.HEXAlpha;
|
|
break;
|
|
|
|
case WI.Color.Format.RGB:
|
|
newColor.format = newColor.simple ? WI.Color.Format.RGB : WI.Color.Format.RGBA;
|
|
break;
|
|
|
|
case WI.Color.Format.HSL:
|
|
newColor.format = newColor.simple ? WI.Color.Format.HSL : WI.Color.Format.HSLA;
|
|
break;
|
|
|
|
case WI.Color.Format.HEXAlpha:
|
|
case WI.Color.Format.RGBA:
|
|
case WI.Color.Format.HSLA:
|
|
case WI.Color.Format.ColorFunction:
|
|
newColor.format = suggestedFormat;
|
|
break;
|
|
|
|
default:
|
|
console.assert(false, "Should not be reached.", suggestedFormat);
|
|
break;
|
|
}
|
|
|
|
return newColor;
|
|
}
|
|
|
|
static rgb2hsl(r, g, b)
|
|
{
|
|
r = WI.Color._eightBitChannel(r) / 255;
|
|
g = WI.Color._eightBitChannel(g) / 255;
|
|
b = WI.Color._eightBitChannel(b) / 255;
|
|
|
|
let min = Math.min(r, g, b);
|
|
let max = Math.max(r, g, b);
|
|
let delta = max - min;
|
|
|
|
let h = 0;
|
|
let s = 0;
|
|
let l = (max + min) / 2;
|
|
|
|
if (delta === 0)
|
|
h = 0;
|
|
else if (max === r)
|
|
h = (60 * ((g - b) / delta)) % 360;
|
|
else if (max === g)
|
|
h = 60 * ((b - r) / delta) + 120;
|
|
else if (max === b)
|
|
h = 60 * ((r - g) / delta) + 240;
|
|
|
|
if (h < 0)
|
|
h += 360;
|
|
|
|
// Saturation
|
|
if (delta === 0)
|
|
s = 0;
|
|
else
|
|
s = delta / (1 - Math.abs((2 * l) - 1));
|
|
|
|
return [
|
|
h,
|
|
s * 100,
|
|
l * 100,
|
|
];
|
|
}
|
|
|
|
static hsl2rgb(h, s, l)
|
|
{
|
|
h = Number.constrain(h, 0, 360) % 360;
|
|
s = Number.constrain(s, 0, 100) / 100;
|
|
l = Number.constrain(l, 0, 100) / 100;
|
|
|
|
let c = (1 - Math.abs((2 * l) - 1)) * s;
|
|
let x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
|
let m = l - (c / 2);
|
|
|
|
let r = 0;
|
|
let g = 0;
|
|
let b = 0;
|
|
|
|
if (h < 60) {
|
|
r = c;
|
|
g = x;
|
|
} else if (h < 120) {
|
|
r = x;
|
|
g = c;
|
|
} else if (h < 180) {
|
|
g = c;
|
|
b = x;
|
|
} else if (h < 240) {
|
|
g = x;
|
|
b = c;
|
|
} else if (h < 300) {
|
|
r = x;
|
|
b = c;
|
|
} else if (h < 360) {
|
|
r = c;
|
|
b = x;
|
|
}
|
|
|
|
return [
|
|
(r + m) * 255,
|
|
(g + m) * 255,
|
|
(b + m) * 255,
|
|
];
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_HSL
|
|
static hsv2hsl(h, s, v)
|
|
{
|
|
h = Number.constrain(h, 0, 360);
|
|
s = Number.constrain(s, 0, 100) / 100;
|
|
v = Number.constrain(v, 0, 100) / 100;
|
|
|
|
let l = v - v * s / 2;
|
|
let saturation;
|
|
if (l === 0 || l === 1)
|
|
saturation = 0;
|
|
else
|
|
saturation = (v - l) / Math.min(l, 1 - l);
|
|
|
|
return [h, saturation * 100, l * 100];
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
|
|
static rgb2hsv(r, g, b)
|
|
{
|
|
r = Number.constrain(r, 0, 1);
|
|
g = Number.constrain(g, 0, 1);
|
|
b = Number.constrain(b, 0, 1);
|
|
|
|
let max = Math.max(r, g, b);
|
|
let min = Math.min(r, g, b);
|
|
let h = 0;
|
|
let delta = max - min;
|
|
let s = max === 0 ? 0 : delta / max;
|
|
let v = max;
|
|
|
|
if (max === min)
|
|
h = 0; // Grayscale.
|
|
else {
|
|
switch (max) {
|
|
case r:
|
|
h = ((g - b) / delta) + ((g < b) ? 6 : 0);
|
|
break;
|
|
case g:
|
|
h = ((b - r) / delta) + 2;
|
|
break;
|
|
case b:
|
|
h = ((r - g) / delta) + 4;
|
|
break;
|
|
}
|
|
h /= 6;
|
|
}
|
|
|
|
return [h * 360, s * 100, v * 100];
|
|
}
|
|
|
|
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB_alternative
|
|
static hsv2rgb(h, s, v)
|
|
{
|
|
h = Number.constrain(h, 0, 360);
|
|
s = Number.constrain(s, 0, 100) / 100;
|
|
v = Number.constrain(v, 0, 100) / 100;
|
|
|
|
function fraction(n) {
|
|
let k = (n + (h / 60)) % 6;
|
|
return v - (v * s * Math.max(Math.min(k, 4 - k, 1), 0));
|
|
}
|
|
return [fraction(5), fraction(3), fraction(1)];
|
|
}
|
|
|
|
// https://www.w3.org/TR/css-color-4/#color-conversion-code
|
|
static displayP3toSRGB(r, g, b)
|
|
{
|
|
r = Number.constrain(r, 0, 1);
|
|
g = Number.constrain(g, 0, 1);
|
|
b = Number.constrain(b, 0, 1);
|
|
|
|
let linearP3 = WI.Color._toLinearLight([r, g, b]);
|
|
|
|
// Convert an array of linear-light display-p3 values to CIE XYZ
|
|
// using D65 (no chromatic adaptation).
|
|
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
|
const rgbToXYZMatrix = [
|
|
[0.4865709486482162, 0.26566769316909306, 0.1982172852343625],
|
|
[0.2289745640697488, 0.6917385218365064, 0.079286914093745],
|
|
[0.0000000000000000, 0.04511338185890264, 1.043944368900976],
|
|
];
|
|
let xyz = Math.multiplyMatrixByVector(rgbToXYZMatrix, linearP3);
|
|
|
|
// Convert XYZ to linear-light sRGB.
|
|
const xyzToLinearSRGBMatrix = [
|
|
[ 3.2404542, -1.5371385, -0.4985314],
|
|
[-0.9692660, 1.8760108, 0.0415560],
|
|
[ 0.0556434, -0.2040259, 1.0572252],
|
|
];
|
|
let linearSRGB = Math.multiplyMatrixByVector(xyzToLinearSRGBMatrix, xyz);
|
|
|
|
let srgb = WI.Color._gammaCorrect(linearSRGB);
|
|
return srgb.map((x) => x.maxDecimals(4));
|
|
}
|
|
|
|
// https://www.w3.org/TR/css-color-4/#color-conversion-code
|
|
static srgbToDisplayP3(r, g, b)
|
|
{
|
|
r = Number.constrain(r, 0, 1);
|
|
g = Number.constrain(g, 0, 1);
|
|
b = Number.constrain(b, 0, 1);
|
|
|
|
let linearSRGB = WI.Color._toLinearLight([r, g, b]);
|
|
|
|
// Convert an array of linear-light sRGB values to CIE XYZ
|
|
// using sRGB's own white, D65 (no chromatic adaptation)
|
|
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html
|
|
const linearSRGBtoXYZMatrix = [
|
|
[0.4124564, 0.3575761, 0.1804375],
|
|
[0.2126729, 0.7151522, 0.0721750],
|
|
[0.0193339, 0.1191920, 0.9503041],
|
|
];
|
|
let xyz = Math.multiplyMatrixByVector(linearSRGBtoXYZMatrix, linearSRGB);
|
|
|
|
const xyzToLinearP3Matrix = [
|
|
[ 2.493496911941425, -0.9313836179191239, -0.40271078445071684],
|
|
[-0.8294889695615747, 1.7626640603183463, 0.023624685841943577],
|
|
[ 0.03584583024378447, -0.07617238926804182, 0.9568845240076872],
|
|
];
|
|
let linearP3 = Math.multiplyMatrixByVector(xyzToLinearP3Matrix, xyz);
|
|
|
|
let p3 = WI.Color._gammaCorrect(linearP3);
|
|
return p3.map((x) => x.maxDecimals(4));
|
|
}
|
|
|
|
// Convert gamma-corrected sRGB or Display-P3 to linear light form.
|
|
// https://www.w3.org/TR/css-color-4/#color-conversion-code
|
|
static _toLinearLight(rgb)
|
|
{
|
|
return rgb.map(function(value) {
|
|
if (value < 0.04045)
|
|
return value / 12.92;
|
|
|
|
return Math.pow((value + 0.055) / 1.055, 2.4);
|
|
});
|
|
}
|
|
|
|
// Convert linear-light sRGB or Display-P3 to gamma corrected form.
|
|
// Inverse of `toLinearLight`.
|
|
// https://www.w3.org/TR/css-color-4/#color-conversion-code
|
|
static _gammaCorrect(rgb)
|
|
{
|
|
return rgb.map(function(value) {
|
|
if (value > 0.0031308)
|
|
return 1.055 * Math.pow(value, 1 / 2.4) - 0.055;
|
|
|
|
return 12.92 * value;
|
|
});
|
|
}
|
|
|
|
static cmyk2rgb(c, m, y, k)
|
|
{
|
|
c = Number.constrain(c, 0, 1);
|
|
m = Number.constrain(m, 0, 1);
|
|
y = Number.constrain(y, 0, 1);
|
|
k = Number.constrain(k, 0, 1);
|
|
return [
|
|
255 * (1 - c) * (1 - k),
|
|
255 * (1 - m) * (1 - k),
|
|
255 * (1 - y) * (1 - k),
|
|
];
|
|
}
|
|
|
|
static normalized2rgb(r, g, b)
|
|
{
|
|
return [
|
|
WI.Color._eightBitChannel(r * 255),
|
|
WI.Color._eightBitChannel(g * 255),
|
|
WI.Color._eightBitChannel(b * 255)
|
|
];
|
|
}
|
|
|
|
static _eightBitChannel(value)
|
|
{
|
|
return Number.constrain(Math.round(value), 0, 255);
|
|
}
|
|
|
|
// Public
|
|
|
|
nextFormat(format)
|
|
{
|
|
format = format || this.format;
|
|
|
|
switch (format) {
|
|
case WI.Color.Format.Original:
|
|
case WI.Color.Format.HEX:
|
|
case WI.Color.Format.HEXAlpha:
|
|
return this.simple ? WI.Color.Format.RGB : WI.Color.Format.RGBA;
|
|
|
|
case WI.Color.Format.RGB:
|
|
case WI.Color.Format.RGBA:
|
|
return WI.Color.Format.ColorFunction;
|
|
|
|
case WI.Color.Format.ColorFunction:
|
|
if (this.simple)
|
|
return WI.Color.Format.HSL;
|
|
return WI.Color.Format.HSLA;
|
|
|
|
case WI.Color.Format.HSL:
|
|
case WI.Color.Format.HSLA:
|
|
if (this.isKeyword())
|
|
return WI.Color.Format.Keyword;
|
|
if (this.simple)
|
|
return this.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEX : WI.Color.Format.HEX;
|
|
return this.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEXAlpha : WI.Color.Format.HEXAlpha;
|
|
|
|
case WI.Color.Format.ShortHEX:
|
|
return WI.Color.Format.HEX;
|
|
|
|
case WI.Color.Format.ShortHEXAlpha:
|
|
return WI.Color.Format.HEXAlpha;
|
|
|
|
case WI.Color.Format.Keyword:
|
|
if (this.simple)
|
|
return this.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEX : WI.Color.Format.HEX;
|
|
return this.canBeSerializedAsShortHEX() ? WI.Color.Format.ShortHEXAlpha : WI.Color.Format.HEXAlpha;
|
|
|
|
default:
|
|
console.error("Unknown color format.");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
get simple()
|
|
{
|
|
return this.alpha === 1;
|
|
}
|
|
|
|
get rgb()
|
|
{
|
|
if (!this._rgb) {
|
|
if (this._hsl)
|
|
this._rgb = WI.Color.hsl2rgb(...this._hsl);
|
|
else if (this._normalizedRGB)
|
|
this._rgb = this._normalizedRGB.map((component) => WI.Color._eightBitChannel(component * 255));
|
|
}
|
|
return this._rgb;
|
|
}
|
|
|
|
get hsl()
|
|
{
|
|
if (!this._hsl)
|
|
this._hsl = WI.Color.rgb2hsl(...this.rgb);
|
|
return this._hsl;
|
|
}
|
|
|
|
get normalizedRGB()
|
|
{
|
|
if (!this._normalizedRGB)
|
|
this._normalizedRGB = this.rgb.map((component) => component / 255);
|
|
return this._normalizedRGB;
|
|
}
|
|
|
|
get rgba()
|
|
{
|
|
return [...this.rgb, this.alpha];
|
|
}
|
|
|
|
get hsla()
|
|
{
|
|
return [...this.hsl, this.alpha];
|
|
}
|
|
|
|
get normalizedRGBA()
|
|
{
|
|
return [...this.normalizedRGB, this.alpha];
|
|
}
|
|
|
|
get gamut()
|
|
{
|
|
return this._gamut;
|
|
}
|
|
|
|
set gamut(gamut)
|
|
{
|
|
console.assert(gamut !== this._gamut);
|
|
|
|
if (this._gamut === WI.Color.Gamut.DisplayP3 && gamut === WI.Color.Gamut.SRGB) {
|
|
this._normalizedRGB = WI.Color.displayP3toSRGB(...this.normalizedRGB).map((x) => Number.constrain(x, 0, 1));
|
|
this._hsl = null;
|
|
this._rgb = null;
|
|
} else if (this._gamut === WI.Color.Gamut.SRGB && gamut === WI.Color.Gamut.DisplayP3) {
|
|
this._normalizedRGB = WI.Color.srgbToDisplayP3(...this.normalizedRGB);
|
|
this._hsl = null;
|
|
this._rgb = null;
|
|
|
|
// Display-P3 is only available with the color function syntax.
|
|
this.format = WI.Color.Format.ColorFunction;
|
|
}
|
|
|
|
this._gamut = gamut;
|
|
}
|
|
|
|
copy()
|
|
{
|
|
switch (this.format) {
|
|
case WI.Color.Format.RGB:
|
|
case WI.Color.Format.HEX:
|
|
case WI.Color.Format.ShortHEX:
|
|
case WI.Color.Format.HEXAlpha:
|
|
case WI.Color.Format.ShortHEXAlpha:
|
|
case WI.Color.Format.Keyword:
|
|
case WI.Color.Format.RGBA:
|
|
return new WI.Color(this.format, this.rgba, this._gamut);
|
|
case WI.Color.Format.HSL:
|
|
case WI.Color.Format.HSLA:
|
|
return new WI.Color(this.format, this.hsla, this._gamut);
|
|
case WI.Color.Format.ColorFunction:
|
|
return new WI.Color(this.format, this.normalizedRGBA, this._gamut);
|
|
}
|
|
|
|
console.error("Invalid color format: " + this.format);
|
|
}
|
|
|
|
toString(format)
|
|
{
|
|
if (!format)
|
|
format = this.format;
|
|
|
|
switch (format) {
|
|
case WI.Color.Format.Original:
|
|
return this._toOriginalString();
|
|
case WI.Color.Format.RGB:
|
|
return this._toRGBString();
|
|
case WI.Color.Format.RGBA:
|
|
return this._toRGBAString();
|
|
case WI.Color.Format.ColorFunction:
|
|
return this._toFunctionString();
|
|
case WI.Color.Format.HSL:
|
|
return this._toHSLString();
|
|
case WI.Color.Format.HSLA:
|
|
return this._toHSLAString();
|
|
case WI.Color.Format.HEX:
|
|
return this._toHEXString();
|
|
case WI.Color.Format.ShortHEX:
|
|
return this._toShortHEXString();
|
|
case WI.Color.Format.HEXAlpha:
|
|
return this._toHEXAlphaString();
|
|
case WI.Color.Format.ShortHEXAlpha:
|
|
return this._toShortHEXAlphaString();
|
|
case WI.Color.Format.Keyword:
|
|
return this._toKeywordString();
|
|
}
|
|
|
|
console.error("Invalid color format: " + format);
|
|
return "";
|
|
}
|
|
|
|
toProtocol()
|
|
{
|
|
let [r, g, b, a] = this.rgba;
|
|
return {r, g, b, a};
|
|
}
|
|
|
|
isKeyword()
|
|
{
|
|
if (this.keyword)
|
|
return true;
|
|
|
|
if (this._gamut !== WI.Color.Gamut.SRGB)
|
|
return false;
|
|
|
|
if (!this.simple)
|
|
return Array.shallowEqual(this.rgba, [0, 0, 0, 0]);
|
|
|
|
return Object.keys(WI.Color.Keywords).some(key => Array.shallowEqual(WI.Color.Keywords[key], this.rgb));
|
|
}
|
|
|
|
isOutsideSRGB()
|
|
{
|
|
if (this._gamut !== WI.Color.Gamut.DisplayP3)
|
|
return false;
|
|
|
|
let rgb = WI.Color.displayP3toSRGB(...this.normalizedRGB);
|
|
|
|
// displayP3toSRGB(1, 1, 1) produces [0.9999, 1, 1.0001], which aren't pure white color values.
|
|
// However, `color(sRGB 0.9999 1 1.0001)` looks exactly the same as color `color(sRGB 1 1 1)`
|
|
// because sRGB is only 8bit per channel. The values get rounded. For example,
|
|
// `rgb(255, 254.51, 255)` looks exactly the same as `rgb(255, 255, 255)`.
|
|
//
|
|
// Consider a color to be within sRGB even if it's actually outside of sRGB by less than half a bit.
|
|
const epsilon = (1 / 255) / 2;
|
|
return rgb.some((x) => x <= -epsilon || x >= 1 + epsilon);
|
|
}
|
|
|
|
canBeSerializedAsShortHEX()
|
|
{
|
|
let rgb = this.rgb;
|
|
|
|
let r = this._componentToHexValue(rgb[0]);
|
|
if (r[0] !== r[1])
|
|
return false;
|
|
|
|
let g = this._componentToHexValue(rgb[1]);
|
|
if (g[0] !== g[1])
|
|
return false;
|
|
|
|
let b = this._componentToHexValue(rgb[2]);
|
|
if (b[0] !== b[1])
|
|
return false;
|
|
|
|
if (!this.simple) {
|
|
let a = this._componentToHexValue(Math.round(this.alpha * 255));
|
|
if (a[0] !== a[1])
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Private
|
|
|
|
_toOriginalString()
|
|
{
|
|
return this.original || this._toKeywordString();
|
|
}
|
|
|
|
_toKeywordString()
|
|
{
|
|
if (this.keyword)
|
|
return this.keyword;
|
|
|
|
let rgba = this.rgba;
|
|
if (!this.simple) {
|
|
if (Array.shallowEqual(rgba, [0, 0, 0, 0]))
|
|
return "transparent";
|
|
return this._toRGBAString();
|
|
}
|
|
|
|
let keywords = WI.Color.Keywords;
|
|
for (let keyword in keywords) {
|
|
if (!keywords.hasOwnProperty(keyword))
|
|
continue;
|
|
|
|
let keywordRGB = keywords[keyword];
|
|
if (keywordRGB[0] === rgba[0] && keywordRGB[1] === rgba[1] && keywordRGB[2] === rgba[2])
|
|
return keyword;
|
|
}
|
|
|
|
return this._toRGBString();
|
|
}
|
|
|
|
_toShortHEXString()
|
|
{
|
|
if (!this.simple)
|
|
return this._toRGBAString();
|
|
|
|
let [r, g, b] = this.rgb.map(this._componentToHexValue);
|
|
if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1])
|
|
return "#" + r[0] + g[0] + b[0];
|
|
return "#" + r + g + b;
|
|
}
|
|
|
|
_toHEXString()
|
|
{
|
|
if (!this.simple)
|
|
return this._toRGBAString();
|
|
|
|
let [r, g, b] = this.rgb.map(this._componentToHexValue);
|
|
return "#" + r + g + b;
|
|
}
|
|
|
|
_toShortHEXAlphaString()
|
|
{
|
|
let [r, g, b] = this.rgb.map(this._componentToHexValue);
|
|
let a = this._componentToHexValue(Math.round(this.alpha * 255));
|
|
if (r[0] === r[1] && g[0] === g[1] && b[0] === b[1] && a[0] === a[1])
|
|
return "#" + r[0] + g[0] + b[0] + a[0];
|
|
return "#" + r + g + b + a;
|
|
}
|
|
|
|
_toHEXAlphaString()
|
|
{
|
|
let [r, g, b] = this.rgb.map(this._componentToHexValue);
|
|
let a = this._componentToHexValue(Math.round(this.alpha * 255));
|
|
return "#" + r + g + b + a;
|
|
}
|
|
|
|
_toRGBString()
|
|
{
|
|
if (!this.simple)
|
|
return this._toRGBAString();
|
|
|
|
let [r, g, b] = this.rgb.map(WI.Color._eightBitChannel);
|
|
return `rgb(${r}, ${g}, ${b})`;
|
|
}
|
|
|
|
_toRGBAString()
|
|
{
|
|
let [r, g, b] = this.rgb.map(WI.Color._eightBitChannel);
|
|
return `rgba(${r}, ${g}, ${b}, ${this.alpha})`;
|
|
}
|
|
|
|
_toFunctionString()
|
|
{
|
|
let [r, g, b] = this.normalizedRGB.map((x) => x.maxDecimals(4));
|
|
if (this.alpha === 1)
|
|
return `color(${this._gamut} ${r} ${g} ${b})`;
|
|
return `color(${this._gamut} ${r} ${g} ${b} / ${this.alpha})`;
|
|
}
|
|
|
|
_toHSLString()
|
|
{
|
|
if (!this.simple)
|
|
return this._toHSLAString();
|
|
|
|
let [h, s, l] = this.hsl.map((x) => x.maxDecimals(2));
|
|
return `hsl(${h}, ${s}%, ${l}%)`;
|
|
}
|
|
|
|
_toHSLAString()
|
|
{
|
|
let [h, s, l] = this.hsl.map((x) => x.maxDecimals(2));
|
|
return `hsla(${h}, ${s}%, ${l}%, ${this.alpha})`;
|
|
}
|
|
|
|
_componentToHexValue(value)
|
|
{
|
|
let hex = WI.Color._eightBitChannel(value).toString(16);
|
|
if (hex.length === 1)
|
|
hex = "0" + hex;
|
|
return hex;
|
|
}
|
|
};
|
|
|
|
WI.Color.Format = {
|
|
Original: "color-format-original",
|
|
Keyword: "color-format-keyword",
|
|
HEX: "color-format-hex",
|
|
ShortHEX: "color-format-short-hex",
|
|
HEXAlpha: "color-format-hex-alpha",
|
|
ShortHEXAlpha: "color-format-short-hex-alpha",
|
|
RGB: "color-format-rgb",
|
|
RGBA: "color-format-rgba",
|
|
HSL: "color-format-hsl",
|
|
HSLA: "color-format-hsla",
|
|
ColorFunction: "color-format-color-function",
|
|
};
|
|
|
|
WI.Color.Gamut = {
|
|
SRGB: "srgb",
|
|
DisplayP3: "display-p3",
|
|
};
|
|
|
|
WI.Color.FunctionNames = new Set([
|
|
"rgb",
|
|
"rgba",
|
|
"hsl",
|
|
"hsla",
|
|
"color",
|
|
"color-mix",
|
|
]);
|
|
|
|
WI.Color.Keywords = {
|
|
"aliceblue": [240, 248, 255, 1],
|
|
"antiquewhite": [250, 235, 215, 1],
|
|
"aqua": [0, 255, 255, 1],
|
|
"aquamarine": [127, 255, 212, 1],
|
|
"azure": [240, 255, 255, 1],
|
|
"beige": [245, 245, 220, 1],
|
|
"bisque": [255, 228, 196, 1],
|
|
"black": [0, 0, 0, 1],
|
|
"blanchedalmond": [255, 235, 205, 1],
|
|
"blue": [0, 0, 255, 1],
|
|
"blueviolet": [138, 43, 226, 1],
|
|
"brown": [165, 42, 42, 1],
|
|
"burlywood": [222, 184, 135, 1],
|
|
"cadetblue": [95, 158, 160, 1],
|
|
"chartreuse": [127, 255, 0, 1],
|
|
"chocolate": [210, 105, 30, 1],
|
|
"coral": [255, 127, 80, 1],
|
|
"cornflowerblue": [100, 149, 237, 1],
|
|
"cornsilk": [255, 248, 220, 1],
|
|
"crimson": [237, 164, 61, 1],
|
|
"cyan": [0, 255, 255, 1],
|
|
"darkblue": [0, 0, 139, 1],
|
|
"darkcyan": [0, 139, 139, 1],
|
|
"darkgoldenrod": [184, 134, 11, 1],
|
|
"darkgray": [169, 169, 169, 1],
|
|
"darkgreen": [0, 100, 0, 1],
|
|
"darkgrey": [169, 169, 169, 1],
|
|
"darkkhaki": [189, 183, 107, 1],
|
|
"darkmagenta": [139, 0, 139, 1],
|
|
"darkolivegreen": [85, 107, 47, 1],
|
|
"darkorange": [255, 140, 0, 1],
|
|
"darkorchid": [153, 50, 204, 1],
|
|
"darkred": [139, 0, 0, 1],
|
|
"darksalmon": [233, 150, 122, 1],
|
|
"darkseagreen": [143, 188, 143, 1],
|
|
"darkslateblue": [72, 61, 139, 1],
|
|
"darkslategray": [47, 79, 79, 1],
|
|
"darkslategrey": [47, 79, 79, 1],
|
|
"darkturquoise": [0, 206, 209, 1],
|
|
"darkviolet": [148, 0, 211, 1],
|
|
"deeppink": [255, 20, 147, 1],
|
|
"deepskyblue": [0, 191, 255, 1],
|
|
"dimgray": [105, 105, 105, 1],
|
|
"dimgrey": [105, 105, 105, 1],
|
|
"dodgerblue": [30, 144, 255, 1],
|
|
"firebrick": [178, 34, 34, 1],
|
|
"floralwhite": [255, 250, 240, 1],
|
|
"forestgreen": [34, 139, 34, 1],
|
|
"fuchsia": [255, 0, 255, 1],
|
|
"gainsboro": [220, 220, 220, 1],
|
|
"ghostwhite": [248, 248, 255, 1],
|
|
"gold": [255, 215, 0, 1],
|
|
"goldenrod": [218, 165, 32, 1],
|
|
"gray": [128, 128, 128, 1],
|
|
"green": [0, 128, 0, 1],
|
|
"greenyellow": [173, 255, 47, 1],
|
|
"grey": [128, 128, 128, 1],
|
|
"honeydew": [240, 255, 240, 1],
|
|
"hotpink": [255, 105, 180, 1],
|
|
"indianred": [205, 92, 92, 1],
|
|
"indigo": [75, 0, 130, 1],
|
|
"ivory": [255, 255, 240, 1],
|
|
"khaki": [240, 230, 140, 1],
|
|
"lavender": [230, 230, 250, 1],
|
|
"lavenderblush": [255, 240, 245, 1],
|
|
"lawngreen": [124, 252, 0, 1],
|
|
"lemonchiffon": [255, 250, 205, 1],
|
|
"lightblue": [173, 216, 230, 1],
|
|
"lightcoral": [240, 128, 128, 1],
|
|
"lightcyan": [224, 255, 255, 1],
|
|
"lightgoldenrodyellow": [250, 250, 210, 1],
|
|
"lightgray": [211, 211, 211, 1],
|
|
"lightgreen": [144, 238, 144, 1],
|
|
"lightgrey": [211, 211, 211, 1],
|
|
"lightpink": [255, 182, 193, 1],
|
|
"lightsalmon": [255, 160, 122, 1],
|
|
"lightseagreen": [32, 178, 170, 1],
|
|
"lightskyblue": [135, 206, 250, 1],
|
|
"lightslategray": [119, 136, 153, 1],
|
|
"lightslategrey": [119, 136, 153, 1],
|
|
"lightsteelblue": [176, 196, 222, 1],
|
|
"lightyellow": [255, 255, 224, 1],
|
|
"lime": [0, 255, 0, 1],
|
|
"limegreen": [50, 205, 50, 1],
|
|
"linen": [250, 240, 230, 1],
|
|
"magenta": [255, 0, 255, 1],
|
|
"maroon": [128, 0, 0, 1],
|
|
"mediumaquamarine": [102, 205, 170, 1],
|
|
"mediumblue": [0, 0, 205, 1],
|
|
"mediumorchid": [186, 85, 211, 1],
|
|
"mediumpurple": [147, 112, 219, 1],
|
|
"mediumseagreen": [60, 179, 113, 1],
|
|
"mediumslateblue": [123, 104, 238, 1],
|
|
"mediumspringgreen": [0, 250, 154, 1],
|
|
"mediumturquoise": [72, 209, 204, 1],
|
|
"mediumvioletred": [199, 21, 133, 1],
|
|
"midnightblue": [25, 25, 112, 1],
|
|
"mintcream": [245, 255, 250, 1],
|
|
"mistyrose": [255, 228, 225, 1],
|
|
"moccasin": [255, 228, 181, 1],
|
|
"navajowhite": [255, 222, 173, 1],
|
|
"navy": [0, 0, 128, 1],
|
|
"oldlace": [253, 245, 230, 1],
|
|
"olive": [128, 128, 0, 1],
|
|
"olivedrab": [107, 142, 35, 1],
|
|
"orange": [255, 165, 0, 1],
|
|
"orangered": [255, 69, 0, 1],
|
|
"orchid": [218, 112, 214, 1],
|
|
"palegoldenrod": [238, 232, 170, 1],
|
|
"palegreen": [152, 251, 152, 1],
|
|
"paleturquoise": [175, 238, 238, 1],
|
|
"palevioletred": [219, 112, 147, 1],
|
|
"papayawhip": [255, 239, 213, 1],
|
|
"peachpuff": [255, 218, 185, 1],
|
|
"peru": [205, 133, 63, 1],
|
|
"pink": [255, 192, 203, 1],
|
|
"plum": [221, 160, 221, 1],
|
|
"powderblue": [176, 224, 230, 1],
|
|
"purple": [128, 0, 128, 1],
|
|
"rebeccapurple": [102, 51, 153, 1],
|
|
"red": [255, 0, 0, 1],
|
|
"rosybrown": [188, 143, 143, 1],
|
|
"royalblue": [65, 105, 225, 1],
|
|
"saddlebrown": [139, 69, 19, 1],
|
|
"salmon": [250, 128, 114, 1],
|
|
"sandybrown": [244, 164, 96, 1],
|
|
"seagreen": [46, 139, 87, 1],
|
|
"seashell": [255, 245, 238, 1],
|
|
"sienna": [160, 82, 45, 1],
|
|
"silver": [192, 192, 192, 1],
|
|
"skyblue": [135, 206, 235, 1],
|
|
"slateblue": [106, 90, 205, 1],
|
|
"slategray": [112, 128, 144, 1],
|
|
"slategrey": [112, 128, 144, 1],
|
|
"snow": [255, 250, 250, 1],
|
|
"springgreen": [0, 255, 127, 1],
|
|
"steelblue": [70, 130, 180, 1],
|
|
"tan": [210, 180, 140, 1],
|
|
"teal": [0, 128, 128, 1],
|
|
"thistle": [216, 191, 216, 1],
|
|
"tomato": [255, 99, 71, 1],
|
|
"transparent": [0, 0, 0, 0],
|
|
"turquoise": [64, 224, 208, 1],
|
|
"violet": [238, 130, 238, 1],
|
|
"wheat": [245, 222, 179, 1],
|
|
"white": [255, 255, 255, 1],
|
|
"whitesmoke": [245, 245, 245, 1],
|
|
"yellow": [255, 255, 0, 1],
|
|
"yellowgreen": [154, 205, 50, 1],
|
|
};
|
|
|
|
/* Models/ConsoleCommandResultMessage.js */
|
|
|
|
/*
|
|
* Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ConsoleCommandResultMessage = class ConsoleCommandResult extends WI.ConsoleMessage
|
|
{
|
|
constructor(target, result, wasThrown, savedResultIndex, { shouldRevealConsole } ={})
|
|
{
|
|
let source = WI.ConsoleMessage.MessageSource.JS;
|
|
let level = wasThrown ? WI.ConsoleMessage.MessageLevel.Error : WI.ConsoleMessage.MessageLevel.Log;
|
|
let type = WI.ConsoleMessage.MessageType.Result;
|
|
|
|
super(target, source, level, "", type, undefined, undefined, undefined, 0, [result], undefined, undefined);
|
|
|
|
this._savedResultIndex = savedResultIndex;
|
|
this._shouldRevealConsole = !!shouldRevealConsole;
|
|
|
|
if (this._savedResultIndex && this._savedResultIndex > WI.ConsoleCommandResultMessage.maximumSavedResultIndex)
|
|
WI.ConsoleCommandResultMessage.maximumSavedResultIndex = this._savedResultIndex;
|
|
}
|
|
|
|
// Static
|
|
|
|
static clearMaximumSavedResultIndex()
|
|
{
|
|
WI.ConsoleCommandResultMessage.maximumSavedResultIndex = 0;
|
|
}
|
|
|
|
// Public
|
|
|
|
get savedResultIndex()
|
|
{
|
|
return this._savedResultIndex;
|
|
}
|
|
|
|
get shouldRevealConsole()
|
|
{
|
|
return this._shouldRevealConsole;
|
|
}
|
|
};
|
|
|
|
WI.ConsoleCommandResultMessage.maximumSavedResultIndex = 0;
|
|
|
|
/* Models/ConsoleSnippet.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ConsoleSnippet = class ConsoleSnippet extends WI.LocalScript
|
|
{
|
|
constructor(title, source)
|
|
{
|
|
console.assert(typeof title === "string" && title.trim().length, title);
|
|
|
|
const target = null;
|
|
const sourceURL = null;
|
|
super(target, title, sourceURL, WI.Script.SourceType.Program, source, {editable: true});
|
|
}
|
|
|
|
// Static
|
|
|
|
static createDefaultWithTitle(title)
|
|
{
|
|
const source = `
|
|
/*
|
|
* ${WI.UIString("Console Snippets are an easy way to save and evaluate JavaScript in the Console.")}
|
|
*
|
|
* ${WI.UIString("As such, the contents will be run as though it was typed into the Console.")}
|
|
* ${WI.UIString("This means all of the Console Command Line API is available <https://webkit.org/web-inspector/console-command-line-api/>.")}
|
|
*
|
|
* ${WI.UIString("Modifications are saved automatically and will apply the next time the Console Snippet is run.")}
|
|
* ${WI.UIString("The contents will be preserved across Web Inspector sessions.")}
|
|
*
|
|
* ${WI.UIString("More information is available at <https://webkit.org/web-inspector/console-snippets/>.")}
|
|
*/
|
|
|
|
"Hello World!"
|
|
`.trimStart();
|
|
return new WI.ConsoleSnippet(title, source);
|
|
}
|
|
|
|
// Public
|
|
|
|
get title()
|
|
{
|
|
return this.url;
|
|
}
|
|
|
|
run()
|
|
{
|
|
WI.runtimeManager.evaluateInInspectedWindow(this.content, {
|
|
objectGroup: WI.RuntimeManager.ConsoleObjectGroup,
|
|
includeCommandLineAPI: true,
|
|
doNotPauseOnExceptionsAndMuteConsole: true,
|
|
generatePreview: true,
|
|
saveResult: true,
|
|
}, (result, wasThrown) => {
|
|
WI.consoleLogViewController.appendImmediateExecutionWithResult(this.title, result, {
|
|
addSpecialUserLogClass: true,
|
|
shouldRevealConsole: true,
|
|
handleClick: (event) => {
|
|
if (!WI.consoleManager.snippets.has(this))
|
|
return;
|
|
|
|
const cookie = null;
|
|
WI.showRepresentedObject(this, cookie, {
|
|
ignoreNetworkTab: true,
|
|
ignoreSearchTab: true,
|
|
});
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.ConsoleSnippet(json.title, json.source);
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = {
|
|
title: this.title,
|
|
source: this.content,
|
|
};
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.breakpoints.keyPath] = this.title;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
/* Models/Cookie.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Cookie = class Cookie
|
|
{
|
|
constructor(type, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite} = {})
|
|
{
|
|
console.assert(Object.values(WI.Cookie.Type).includes(type));
|
|
console.assert(typeof name === "string");
|
|
console.assert(typeof value === "string");
|
|
console.assert(!header || typeof header === "string");
|
|
console.assert(!expires || expires instanceof Date);
|
|
console.assert(!session || typeof session === "boolean");
|
|
console.assert(!maxAge || typeof maxAge === "number");
|
|
console.assert(!path || typeof path === "string");
|
|
console.assert(!domain || typeof domain === "string");
|
|
console.assert(!secure || typeof secure === "boolean");
|
|
console.assert(!httpOnly || typeof httpOnly === "boolean");
|
|
console.assert(!sameSite || Object.values(WI.Cookie.SameSiteType).includes(sameSite));
|
|
|
|
this._type = type;
|
|
this._name = name;
|
|
this._value = value;
|
|
this._size = this._name.length + this._value.length;
|
|
|
|
if (this._type === WI.Cookie.Type.Response) {
|
|
this._header = header || "";
|
|
this._expires = (!session && expires) || null;
|
|
this._session = session || false;
|
|
this._maxAge = maxAge || null;
|
|
this._path = path || null;
|
|
this._domain = domain || null;
|
|
this._secure = secure || false;
|
|
this._httpOnly = httpOnly || false;
|
|
this._sameSite = sameSite || WI.Cookie.SameSiteType.None;
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
let {name, value, ...options} = payload;
|
|
options.expires = options.expires ? new Date(options.expires.maxDecimals(-3)) : null;
|
|
|
|
return new WI.Cookie(WI.Cookie.Type.Response, name, value, options);
|
|
}
|
|
|
|
// RFC 6265 defines the HTTP Cookie and Set-Cookie header fields:
|
|
// https://www.ietf.org/rfc/rfc6265.txt
|
|
|
|
static parseCookieRequestHeader(header)
|
|
{
|
|
if (!header)
|
|
return [];
|
|
|
|
header = header.trim();
|
|
if (!header)
|
|
return [];
|
|
|
|
let cookies = [];
|
|
|
|
// Cookie: <name> = <value> ( ";" SP <name> = <value> )*?
|
|
// NOTE: Just name/value pairs.
|
|
|
|
let pairs = header.split(/; /);
|
|
for (let pair of pairs) {
|
|
let match = pair.match(/^(?<name>[^\s=]+)[ \t]*=[ \t]*(?<value>.*)$/);
|
|
if (!match) {
|
|
WI.reportInternalError("Failed to parse Cookie pair", {header, pair});
|
|
continue;
|
|
}
|
|
|
|
let {name, value} = match.groups;
|
|
cookies.push(new WI.Cookie(WI.Cookie.Type.Request, name, value));
|
|
}
|
|
|
|
return cookies;
|
|
}
|
|
|
|
static displayNameForSameSiteType(sameSiteType)
|
|
{
|
|
switch (sameSiteType) {
|
|
case WI.Cookie.SameSiteType.None:
|
|
return WI.unlocalizedString("None");
|
|
case WI.Cookie.SameSiteType.Lax:
|
|
return WI.unlocalizedString("Lax");
|
|
case WI.Cookie.SameSiteType.Strict:
|
|
return WI.unlocalizedString("Strict");
|
|
default:
|
|
console.error("Invalid SameSite type", sameSiteType);
|
|
return sameSiteType;
|
|
}
|
|
}
|
|
|
|
// <https://httpwg.org/http-extensions/rfc6265bis.html#the-samesite-attribute-1>
|
|
static parseSameSiteAttributeValue(attributeValue)
|
|
{
|
|
if (!attributeValue)
|
|
return WI.Cookie.SameSiteType.None;
|
|
|
|
switch (attributeValue.toLowerCase()) {
|
|
case "lax":
|
|
return WI.Cookie.SameSiteType.Lax;
|
|
case "strict":
|
|
return WI.Cookie.SameSiteType.Strict;
|
|
}
|
|
|
|
return WI.Cookie.SameSiteType.None;
|
|
}
|
|
|
|
static parseSetCookieResponseHeader(header)
|
|
{
|
|
if (!header)
|
|
return null;
|
|
|
|
// Set-Cookie: <name> = <value> ( ";" SP <attr-maybe-pair> )*?
|
|
// NOTE: Some attributes can have pairs (e.g. "Path=/"), some are only a
|
|
// single word (e.g. "Secure").
|
|
|
|
// Parse name/value.
|
|
let nameValueMatch = header.match(/^(?<name>[^\s=]+)[ \t]*=[ \t]*(?<value>[^;]*)/);
|
|
if (!nameValueMatch) {
|
|
WI.reportInternalError("Failed to parse Set-Cookie header", {header});
|
|
return null;
|
|
}
|
|
|
|
let {name, value} = nameValueMatch.groups;
|
|
let expires = null;
|
|
let session = false;
|
|
let maxAge = null;
|
|
let path = null;
|
|
let domain = null;
|
|
let secure = false;
|
|
let httpOnly = false;
|
|
let sameSite = WI.Cookie.SameSiteType.None;
|
|
|
|
// Parse Attributes
|
|
let remaining = header.substr(nameValueMatch[0].length);
|
|
let attributes = remaining.split(/; ?/);
|
|
for (let attribute of attributes) {
|
|
if (!attribute)
|
|
continue;
|
|
|
|
let match = attribute.match(/^(?<name>[^\s=]+)(?:=(?<value>.*))?$/);
|
|
if (!match) {
|
|
console.error("Failed to parse Set-Cookie attribute:", attribute);
|
|
continue;
|
|
}
|
|
|
|
let attributeName = match.groups.name;
|
|
let attributeValue = match.groups.value;
|
|
switch (attributeName.toLowerCase()) {
|
|
case "expires":
|
|
console.assert(attributeValue);
|
|
expires = new Date(attributeValue);
|
|
if (isNaN(expires.getTime())) {
|
|
console.warn("Invalid Expires date:", attributeValue);
|
|
expires = null;
|
|
}
|
|
break;
|
|
case "max-age":
|
|
console.assert(attributeValue);
|
|
maxAge = parseInt(attributeValue, 10);
|
|
if (isNaN(maxAge) || !/^\d+$/.test(attributeValue)) {
|
|
console.warn("Invalid MaxAge value:", attributeValue);
|
|
maxAge = null;
|
|
}
|
|
break;
|
|
case "path":
|
|
console.assert(attributeValue);
|
|
path = attributeValue;
|
|
break;
|
|
case "domain":
|
|
console.assert(attributeValue);
|
|
domain = attributeValue;
|
|
break;
|
|
case "secure":
|
|
console.assert(!attributeValue);
|
|
secure = true;
|
|
break;
|
|
case "httponly":
|
|
console.assert(!attributeValue);
|
|
httpOnly = true;
|
|
break;
|
|
case "samesite":
|
|
sameSite = WI.Cookie.parseSameSiteAttributeValue(attributeValue);
|
|
break;
|
|
default:
|
|
console.warn("Unknown Cookie attribute:", attribute);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!expires)
|
|
session = true;
|
|
|
|
return new WI.Cookie(WI.Cookie.Type.Response, name, value, {header, expires, session, maxAge, path, domain, secure, httpOnly, sameSite});
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get name() { return this._name; }
|
|
get value() { return this._value; }
|
|
get header() { return this._header; }
|
|
get expires() { return this._expires; }
|
|
get session() { return this._session; }
|
|
get maxAge() { return this._maxAge; }
|
|
get path() { return this._path; }
|
|
get domain() { return this._domain; }
|
|
get secure() { return this._secure; }
|
|
get httpOnly() { return this._httpOnly; }
|
|
get sameSite() { return this._sameSite; }
|
|
get size() { return this._size; }
|
|
|
|
get url()
|
|
{
|
|
let url = this._secure ? "https://" : "http://";
|
|
url += this._domain || "";
|
|
url += this._path || "";
|
|
return url;
|
|
}
|
|
|
|
expirationDate(requestSentDate)
|
|
{
|
|
if (this._session)
|
|
return null;
|
|
|
|
if (this._maxAge) {
|
|
let startDate = requestSentDate || new Date;
|
|
return new Date(startDate.getTime() + (this._maxAge * 1000));
|
|
}
|
|
|
|
return this._expires;
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
return this._type === other.type
|
|
&& this._name === other.name
|
|
&& this._value === other.value
|
|
&& this._header === other.header
|
|
&& this._expires?.getTime() === other.expires?.getTime()
|
|
&& this._session === other.session
|
|
&& this._maxAge === other.maxAge
|
|
&& this._path === other.path
|
|
&& this._domain === other.domain
|
|
&& this._secure === other.secure
|
|
&& this._httpOnly === other.httpOnly
|
|
&& this._sameSite === other.sameSite;
|
|
}
|
|
|
|
toProtocol()
|
|
{
|
|
if (typeof this._name !== "string")
|
|
return null;
|
|
|
|
if (typeof this._value !== "string")
|
|
return null;
|
|
|
|
if (typeof this._domain !== "string")
|
|
return null;
|
|
|
|
if (typeof this._path !== "string")
|
|
return null;
|
|
|
|
if (!this._session && !this._expires)
|
|
return null;
|
|
|
|
if (!Object.values(WI.Cookie.SameSiteType).includes(this._sameSite))
|
|
return null;
|
|
|
|
let json = {
|
|
name: this._name,
|
|
value: this._value,
|
|
domain: this._domain,
|
|
path: this._path,
|
|
expires: this._expires?.getTime(),
|
|
session: this._session,
|
|
httpOnly: !!this._httpOnly,
|
|
secure: !!this._secure,
|
|
sameSite: this._sameSite,
|
|
};
|
|
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.Cookie.Type = {
|
|
Request: "request",
|
|
Response: "response",
|
|
};
|
|
|
|
// Keep these in sync with the "CookieSameSitePolicy" enum defined by the "Page" domain.
|
|
WI.Cookie.SameSiteType = {
|
|
None: "None",
|
|
Lax: "Lax",
|
|
Strict: "Strict",
|
|
};
|
|
|
|
/* Models/CookieStorageObject.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CookieStorageObject = class CookieStorageObject
|
|
{
|
|
constructor(host)
|
|
{
|
|
this._host = host;
|
|
}
|
|
|
|
// Static
|
|
|
|
static cookieMatchesResourceURL(cookie, resourceURL)
|
|
{
|
|
var parsedURL = parseURL(resourceURL);
|
|
if (!parsedURL || !WI.CookieStorageObject.cookieDomainMatchesResourceDomain(cookie.domain, parsedURL.host))
|
|
return false;
|
|
|
|
return parsedURL.path.startsWith(cookie.path)
|
|
&& (!cookie.port || parsedURL.port === cookie.port)
|
|
&& (!cookie.secure || parsedURL.scheme === "https");
|
|
}
|
|
|
|
static cookieDomainMatchesResourceDomain(cookieDomain, resourceDomain)
|
|
{
|
|
if (cookieDomain.charAt(0) !== ".")
|
|
return resourceDomain === cookieDomain;
|
|
return !!resourceDomain.match(new RegExp("^(?:[^\\.]+\\.)*" + cookieDomain.substring(1).escapeForRegExp() + "$"), "i");
|
|
}
|
|
|
|
// Public
|
|
|
|
get host()
|
|
{
|
|
return this._host;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
// FIXME <https://webkit.org/b/151413>: This class should actually store cookie data for this host.
|
|
cookie[WI.CookieStorageObject.CookieHostCookieKey] = this.host;
|
|
}
|
|
};
|
|
|
|
WI.CookieStorageObject.TypeIdentifier = "cookie-storage";
|
|
WI.CookieStorageObject.CookieHostCookieKey = "cookie-storage-host";
|
|
|
|
/* Models/CubicBezierTimingFunction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CubicBezierTimingFunction = class CubicBezierTimingFunction
|
|
{
|
|
constructor(x1, y1, x2, y2)
|
|
{
|
|
this._inPoint = new WI.Point(x1, y1);
|
|
this._outPoint = new WI.Point(x2, y2);
|
|
|
|
// Calculate the polynomial coefficients, implicit first and last control points are (0,0) and (1,1).
|
|
this._curveInfo = {
|
|
x: {c: 3.0 * x1},
|
|
y: {c: 3.0 * y1}
|
|
};
|
|
|
|
this._curveInfo.x.b = 3.0 * (x2 - x1) - this._curveInfo.x.c;
|
|
this._curveInfo.x.a = 1.0 - this._curveInfo.x.c - this._curveInfo.x.b;
|
|
|
|
this._curveInfo.y.b = 3.0 * (y2 - y1) - this._curveInfo.y.c;
|
|
this._curveInfo.y.a = 1.0 - this._curveInfo.y.c - this._curveInfo.y.b;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromCoordinates(coordinates)
|
|
{
|
|
if (!coordinates || coordinates.length < 4)
|
|
return null;
|
|
|
|
coordinates = coordinates.map(Number);
|
|
if (coordinates.includes(NaN))
|
|
return null;
|
|
|
|
return new WI.CubicBezierTimingFunction(coordinates[0], coordinates[1], coordinates[2], coordinates[3]);
|
|
}
|
|
|
|
static fromString(text)
|
|
{
|
|
if (!text || !text.length)
|
|
return null;
|
|
|
|
var trimmedText = text.toLowerCase().replace(/\s/g, "");
|
|
if (!trimmedText.length)
|
|
return null;
|
|
|
|
if (Object.keys(WI.CubicBezierTimingFunction.keywordValues).includes(trimmedText))
|
|
return WI.CubicBezierTimingFunction.fromCoordinates(WI.CubicBezierTimingFunction.keywordValues[trimmedText]);
|
|
|
|
var matches = trimmedText.match(/^cubic-bezier\(([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+)\)$/);
|
|
if (!matches)
|
|
return null;
|
|
|
|
matches.splice(0, 1);
|
|
return WI.CubicBezierTimingFunction.fromCoordinates(matches);
|
|
}
|
|
|
|
// Public
|
|
|
|
get inPoint()
|
|
{
|
|
return this._inPoint;
|
|
}
|
|
|
|
get outPoint()
|
|
{
|
|
return this._outPoint;
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.CubicBezierTimingFunction(this._inPoint.x, this._inPoint.y, this._outPoint.x, this._outPoint.y);
|
|
}
|
|
|
|
toString()
|
|
{
|
|
var values = [this._inPoint.x, this._inPoint.y, this._outPoint.x, this._outPoint.y];
|
|
for (var key in WI.CubicBezierTimingFunction.keywordValues) {
|
|
if (Array.shallowEqual(WI.CubicBezierTimingFunction.keywordValues[key], values))
|
|
return key;
|
|
}
|
|
|
|
return "cubic-bezier(" + values.join(", ") + ")";
|
|
}
|
|
|
|
solve(x, epsilon)
|
|
{
|
|
return this._sampleCurveY(this._solveCurveX(x, epsilon));
|
|
}
|
|
|
|
// Private
|
|
|
|
_sampleCurveX(t)
|
|
{
|
|
// `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
|
|
return ((this._curveInfo.x.a * t + this._curveInfo.x.b) * t + this._curveInfo.x.c) * t;
|
|
}
|
|
|
|
_sampleCurveY(t)
|
|
{
|
|
return ((this._curveInfo.y.a * t + this._curveInfo.y.b) * t + this._curveInfo.y.c) * t;
|
|
}
|
|
|
|
_sampleCurveDerivativeX(t)
|
|
{
|
|
return (3.0 * this._curveInfo.x.a * t + 2.0 * this._curveInfo.x.b) * t + this._curveInfo.x.c;
|
|
}
|
|
|
|
// Given an x value, find a parametric value it came from.
|
|
_solveCurveX(x, epsilon)
|
|
{
|
|
var t0, t1, t2, x2, d2, i;
|
|
|
|
// First try a few iterations of Newton's method -- normally very fast.
|
|
for (t2 = x, i = 0; i < 8; i++) {
|
|
x2 = this._sampleCurveX(t2) - x;
|
|
if (Math.abs(x2) < epsilon)
|
|
return t2;
|
|
d2 = this._sampleCurveDerivativeX(t2);
|
|
if (Math.abs(d2) < 1e-6)
|
|
break;
|
|
t2 = t2 - x2 / d2;
|
|
}
|
|
|
|
// Fall back to the bisection method for reliability.
|
|
t0 = 0.0;
|
|
t1 = 1.0;
|
|
t2 = x;
|
|
|
|
if (t2 < t0)
|
|
return t0;
|
|
if (t2 > t1)
|
|
return t1;
|
|
|
|
while (t0 < t1) {
|
|
x2 = this._sampleCurveX(t2);
|
|
if (Math.abs(x2 - x) < epsilon)
|
|
return t2;
|
|
if (x > x2)
|
|
t0 = t2;
|
|
else
|
|
t1 = t2;
|
|
t2 = (t1 - t0) * 0.5 + t0;
|
|
}
|
|
|
|
// Failure.
|
|
return t2;
|
|
}
|
|
};
|
|
|
|
WI.CubicBezierTimingFunction.keywordValues = {
|
|
"ease": [0.25, 0.1, 0.25, 1],
|
|
"ease-in": [0.42, 0, 1, 1],
|
|
"ease-out": [0, 0, 0.58, 1],
|
|
"ease-in-out": [0.42, 0, 0.58, 1],
|
|
"linear": [0, 0, 1, 1]
|
|
};
|
|
|
|
/* Models/DOMBreakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMBreakpoint = class DOMBreakpoint extends WI.Breakpoint
|
|
{
|
|
constructor(domNodeOrInfo, type, {disabled, actions, condition, ignoreCount, autoContinue} = {})
|
|
{
|
|
console.assert(domNodeOrInfo instanceof WI.DOMNode || typeof domNodeOrInfo === "object", domNodeOrInfo);
|
|
console.assert(Object.values(WI.DOMBreakpoint.Type).includes(type), type);
|
|
|
|
super({disabled, actions, condition, ignoreCount, autoContinue});
|
|
|
|
if (domNodeOrInfo instanceof WI.DOMNode) {
|
|
this._domNode = domNodeOrInfo;
|
|
this._path = domNodeOrInfo.path();
|
|
console.assert(WI.networkManager.mainFrame);
|
|
this._url = WI.networkManager.mainFrame.url;
|
|
} else if (domNodeOrInfo && typeof domNodeOrInfo === "object") {
|
|
this._domNode = null;
|
|
this._path = domNodeOrInfo.path;
|
|
this._url = domNodeOrInfo.url;
|
|
}
|
|
|
|
this._type = type;
|
|
}
|
|
|
|
// Static
|
|
|
|
static displayNameForType(type)
|
|
{
|
|
console.assert(Object.values(WI.DOMBreakpoint.Type).includes(type), type);
|
|
|
|
switch (type) {
|
|
case WI.DOMBreakpoint.Type.SubtreeModified:
|
|
return WI.UIString("Subtree Modified", "Subtree Modified @ DOM Breakpoint", "A submenu item of 'Break On' that breaks (pauses) before child DOM node is modified");
|
|
|
|
case WI.DOMBreakpoint.Type.AttributeModified:
|
|
return WI.UIString("Attribute Modified", "Attribute Modified @ DOM Breakpoint", "A submenu item of 'Break On' that breaks (pauses) before DOM attribute is modified");
|
|
|
|
case WI.DOMBreakpoint.Type.NodeRemoved:
|
|
return WI.UIString("Node Removed", "Node Removed @ DOM Breakpoint", "A submenu item of 'Break On' that breaks (pauses) before DOM node is removed");
|
|
}
|
|
|
|
console.assert(false, "Unknown DOM breakpoint type", type);
|
|
return WI.UIString("DOM");
|
|
}
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.DOMBreakpoint(json, json.type, {
|
|
disabled: json.disabled,
|
|
condition: json.condition,
|
|
actions: json.actions?.map((actionJSON) => WI.BreakpointAction.fromJSON(actionJSON)) || [],
|
|
ignoreCount: json.ignoreCount,
|
|
autoContinue: json.autoContinue,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get url() { return this._url; }
|
|
get path() { return this._path; }
|
|
|
|
get displayName()
|
|
{
|
|
return WI.DOMBreakpoint.displayNameForType(this._type);
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
// COMPATIBILITY (iOS 14): DOMDebugger.setDOMBreakpoint did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setDOMBreakpoint", "options");
|
|
}
|
|
|
|
get domNode()
|
|
{
|
|
return this._domNode;
|
|
}
|
|
|
|
set domNode(domNode)
|
|
{
|
|
console.assert(!domNode || domNode instanceof WI.DOMNode, domNode);
|
|
console.assert(!domNode || xor(domNode, this._domNode), "domNode should not change once set", domNode, this._domNode);
|
|
if (!xor(domNode, this._domNode))
|
|
return;
|
|
|
|
this.dispatchEventToListeners(WI.DOMBreakpoint.Event.DOMNodeWillChange);
|
|
this._domNode = domNode;
|
|
this.dispatchEventToListeners(WI.DOMBreakpoint.Event.DOMNodeDidChange);
|
|
}
|
|
|
|
remove()
|
|
{
|
|
super.remove();
|
|
|
|
WI.domDebuggerManager.removeDOMBreakpoint(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["dom-breakpoint-url"] = this._url;
|
|
cookie["dom-breakpoint-path"] = this._path;
|
|
cookie["dom-breakpoint-type"] = this._type;
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.url = this._url;
|
|
json.path = this._path;
|
|
json.type = this._type;
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.domBreakpoints.keyPath] = this._url + ":" + this._path + ":" + this._type;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.DOMBreakpoint.Type = {
|
|
SubtreeModified: "subtree-modified",
|
|
AttributeModified: "attribute-modified",
|
|
NodeRemoved: "node-removed",
|
|
};
|
|
|
|
WI.DOMBreakpoint.Event = {
|
|
DOMNodeDidChange: "dom-breakpoint-dom-node-did-change",
|
|
DOMNodeWillChange: "dom-breakpoint-dom-node-will-change",
|
|
};
|
|
|
|
WI.DOMBreakpoint.ReferencePage = WI.ReferencePage.DOMBreakpoints;
|
|
|
|
/* Models/DOMNode.js */
|
|
|
|
/*
|
|
* Copyright (C) 2009, 2010 Google Inc. All rights reserved.
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
* Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMNode = class DOMNode extends WI.Object
|
|
{
|
|
constructor(domManager, doc, isInShadowTree, payload)
|
|
{
|
|
super();
|
|
|
|
this._destroyed = false;
|
|
|
|
this._domManager = domManager;
|
|
this._isInShadowTree = isInShadowTree;
|
|
|
|
this.id = payload.nodeId;
|
|
this._domManager._idToDOMNode[this.id] = this;
|
|
|
|
this._nodeType = payload.nodeType;
|
|
this._nodeName = payload.nodeName;
|
|
this._localName = payload.localName;
|
|
this._nodeValue = payload.nodeValue;
|
|
this._pseudoType = payload.pseudoType;
|
|
this._shadowRootType = payload.shadowRootType;
|
|
this._computedRole = null;
|
|
this._contentSecurityPolicyHash = payload.contentSecurityPolicyHash;
|
|
|
|
this._layoutFlags = [];
|
|
this._layoutOverlayShowing = false;
|
|
this._layoutOverlayColorSetting = null;
|
|
|
|
if (this._nodeType === Node.DOCUMENT_NODE)
|
|
this.ownerDocument = this;
|
|
else
|
|
this.ownerDocument = doc;
|
|
|
|
this._frame = null;
|
|
|
|
// COMPATIBILITY (iOS 12.2): DOM.Node.frameId was changed to represent the owner frame, not the content frame.
|
|
// Since support can't be tested directly, check for Audit (iOS 13.0+).
|
|
// FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed.
|
|
if (InspectorBackend.hasDomain("Audit")) {
|
|
if (payload.frameId)
|
|
this._frame = WI.networkManager.frameForIdentifier(payload.frameId);
|
|
}
|
|
|
|
if (!this._frame && this.ownerDocument)
|
|
this._frame = WI.networkManager.frameForIdentifier(this.ownerDocument.frameIdentifier);
|
|
|
|
this._attributes = [];
|
|
this._attributesMap = new Map;
|
|
if (payload.attributes)
|
|
this._setAttributesPayload(payload.attributes);
|
|
|
|
this._childNodeCount = payload.childNodeCount;
|
|
this._children = null;
|
|
|
|
this._nextSibling = null;
|
|
this._previousSibling = null;
|
|
this.parentNode = null;
|
|
|
|
this._enabledPseudoClasses = [];
|
|
|
|
// FIXME: The logic around this._shadowRoots and this._children is very confusing.
|
|
// We eventually include shadow roots at the start of _children. However we might
|
|
// not have our actual children yet. So we try to defer initializing _children until
|
|
// we have both shadowRoots and child nodes.
|
|
this._shadowRoots = [];
|
|
if (payload.shadowRoots) {
|
|
for (var i = 0; i < payload.shadowRoots.length; ++i) {
|
|
var root = payload.shadowRoots[i];
|
|
var node = new WI.DOMNode(this._domManager, this.ownerDocument, true, root);
|
|
node.parentNode = this;
|
|
this._shadowRoots.push(node);
|
|
}
|
|
}
|
|
|
|
if (payload.children)
|
|
this._setChildrenPayload(payload.children);
|
|
else if (this._shadowRoots.length && !this._childNodeCount)
|
|
this._children = this._shadowRoots.slice();
|
|
|
|
if (this._nodeType === Node.ELEMENT_NODE)
|
|
this._customElementState = payload.customElementState || WI.DOMNode.CustomElementState.Builtin;
|
|
else
|
|
this._customElementState = null;
|
|
|
|
if (payload.templateContent) {
|
|
this._templateContent = new WI.DOMNode(this._domManager, this.ownerDocument, false, payload.templateContent);
|
|
this._templateContent.parentNode = this;
|
|
}
|
|
|
|
this._pseudoElements = new Map;
|
|
if (payload.pseudoElements) {
|
|
for (var i = 0; i < payload.pseudoElements.length; ++i) {
|
|
var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload.pseudoElements[i]);
|
|
node.parentNode = this;
|
|
this._pseudoElements.set(node.pseudoType(), node);
|
|
}
|
|
}
|
|
|
|
if (payload.contentDocument) {
|
|
this._contentDocument = new WI.DOMNode(this._domManager, null, false, payload.contentDocument);
|
|
this._children = [this._contentDocument];
|
|
this._renumber();
|
|
}
|
|
|
|
if (this._nodeType === Node.ELEMENT_NODE) {
|
|
// HTML and BODY from internal iframes should not overwrite top-level ones.
|
|
if (this.ownerDocument && !this.ownerDocument.documentElement && this._nodeName === "HTML")
|
|
this.ownerDocument.documentElement = this;
|
|
if (this.ownerDocument && !this.ownerDocument.body && this._nodeName === "BODY")
|
|
this.ownerDocument.body = this;
|
|
if (payload.documentURL)
|
|
this.documentURL = payload.documentURL;
|
|
} else if (this._nodeType === Node.DOCUMENT_TYPE_NODE) {
|
|
this.publicId = payload.publicId;
|
|
this.systemId = payload.systemId;
|
|
} else if (this._nodeType === Node.DOCUMENT_NODE) {
|
|
this.documentURL = payload.documentURL;
|
|
this.xmlVersion = payload.xmlVersion;
|
|
} else if (this._nodeType === Node.ATTRIBUTE_NODE) {
|
|
this.name = payload.name;
|
|
this.value = payload.value;
|
|
}
|
|
|
|
this._domEvents = [];
|
|
this._powerEfficientPlaybackRanges = [];
|
|
|
|
if (this.isMediaElement())
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): CSS.LayoutContextType was renamed/expanded to CSS.LayoutFlag.
|
|
if (!InspectorBackend.Enum.CSS.LayoutFlag) {
|
|
let layoutFlags = [WI.DOMNode.LayoutFlag.Rendered];
|
|
if (payload.layoutContextType)
|
|
layoutFlags.push(payload.layoutContextType);
|
|
this.layoutFlags = layoutFlags;
|
|
} else
|
|
this.layoutFlags = payload.layoutFlags;
|
|
}
|
|
|
|
// Static
|
|
|
|
static get defaultLayoutOverlayColor()
|
|
{
|
|
return new WI.Color(WI.Color.Format.HSL, WI.DOMNode._defaultLayoutOverlayConfiguration.colors[0]);
|
|
}
|
|
|
|
static resetDefaultLayoutOverlayConfiguration()
|
|
{
|
|
let configuration = WI.DOMNode._defaultLayoutOverlayConfiguration;
|
|
configuration.nextFlexColorIndex = 0;
|
|
configuration.nextGridColorIndex = 0;
|
|
}
|
|
|
|
static getFullscreenDOMEvents(domEvents)
|
|
{
|
|
return domEvents.reduce((accumulator, current) => {
|
|
if (current.eventName === "webkitfullscreenchange" && current.data && (!accumulator.length || accumulator.lastValue.data.enabled !== current.data.enabled))
|
|
accumulator.push(current);
|
|
return accumulator;
|
|
}, []);
|
|
}
|
|
|
|
static isPlayEvent(eventName)
|
|
{
|
|
return eventName === "play"
|
|
|| eventName === "playing";
|
|
}
|
|
|
|
static isPauseEvent(eventName)
|
|
{
|
|
return eventName === "pause"
|
|
|| eventName === "stall";
|
|
}
|
|
|
|
static isStopEvent(eventName)
|
|
{
|
|
return eventName === "emptied"
|
|
|| eventName === "ended"
|
|
|| eventName === "suspend";
|
|
}
|
|
|
|
|
|
// Public
|
|
|
|
get destroyed() { return this._destroyed; }
|
|
get frame() { return this._frame; }
|
|
get nextSibling() { return this._nextSibling; }
|
|
get previousSibling() { return this._previousSibling; }
|
|
get children() { return this._children; }
|
|
get domEvents() { return this._domEvents; }
|
|
get powerEfficientPlaybackRanges() { return this._powerEfficientPlaybackRanges; }
|
|
get layoutOverlayShowing() { return this._layoutOverlayShowing; }
|
|
|
|
get attached()
|
|
{
|
|
if (this._destroyed)
|
|
return false;
|
|
|
|
for (let node = this; node; node = node.parentNode) {
|
|
if (node.ownerDocument === node)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
get firstChild()
|
|
{
|
|
var children = this.children;
|
|
|
|
if (children && children.length > 0)
|
|
return children[0];
|
|
|
|
return null;
|
|
}
|
|
|
|
get lastChild()
|
|
{
|
|
var children = this.children;
|
|
|
|
if (children && children.length > 0)
|
|
return children.lastValue;
|
|
|
|
return null;
|
|
}
|
|
|
|
get childNodeCount()
|
|
{
|
|
var children = this.children;
|
|
if (children)
|
|
return children.length;
|
|
|
|
return this._childNodeCount + this._shadowRoots.length;
|
|
}
|
|
|
|
set childNodeCount(count)
|
|
{
|
|
this._childNodeCount = count;
|
|
}
|
|
|
|
get layoutFlags()
|
|
{
|
|
return this._layoutFlags;
|
|
}
|
|
|
|
set layoutFlags(layoutFlags)
|
|
{
|
|
layoutFlags ||= [];
|
|
console.assert(Array.isArray(layoutFlags), layoutFlags);
|
|
console.assert(layoutFlags.every((layoutFlag) => Object.values(WI.DOMNode.LayoutFlag).includes(layoutFlag)), layoutFlags);
|
|
console.assert(layoutFlags.filter((layoutFlag) => WI.DOMNode._LayoutContextTypes.includes(layoutFlag)).length <= 1, layoutFlags);
|
|
console.assert(!layoutFlags.length || !Array.shallowEqual(layoutFlags, this._layoutFlags), layoutFlags);
|
|
|
|
let oldLayoutContextType = this.layoutContextType;
|
|
|
|
this._layoutFlags = layoutFlags;
|
|
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.LayoutFlagsChanged);
|
|
|
|
if (!this._layoutOverlayShowing)
|
|
return;
|
|
|
|
// The overlay is automatically hidden on the backend when the context type changes.
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.LayoutOverlayHidden);
|
|
|
|
switch (oldLayoutContextType) {
|
|
case WI.DOMNode.LayoutFlag.Flex:
|
|
WI.settings.flexOverlayShowOrderNumbers.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
break;
|
|
|
|
case WI.DOMNode.LayoutFlag.Grid:
|
|
WI.settings.gridOverlayShowExtendedGridLines.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNames.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNumbers.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowTrackSizes.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowAreaNames.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
break;
|
|
}
|
|
}
|
|
|
|
get layoutContextType()
|
|
{
|
|
return this._layoutFlags.find((layoutFlag) => WI.DOMNode._LayoutContextTypes.includes(layoutFlag)) || null;
|
|
}
|
|
|
|
markDestroyed()
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
this._destroyed = true;
|
|
|
|
this.layoutFlags = [];
|
|
}
|
|
|
|
computedRole()
|
|
{
|
|
return this._computedRole;
|
|
}
|
|
|
|
contentSecurityPolicyHash()
|
|
{
|
|
return this._contentSecurityPolicyHash;
|
|
}
|
|
|
|
hasAttributes()
|
|
{
|
|
return this._attributes.length > 0;
|
|
}
|
|
|
|
hasChildNodes()
|
|
{
|
|
return this.childNodeCount > 0;
|
|
}
|
|
|
|
hasShadowRoots()
|
|
{
|
|
return !!this._shadowRoots.length;
|
|
}
|
|
|
|
isInShadowTree()
|
|
{
|
|
return this._isInShadowTree;
|
|
}
|
|
|
|
isInUserAgentShadowTree()
|
|
{
|
|
return this._isInShadowTree && this.ancestorShadowRoot().isUserAgentShadowRoot();
|
|
}
|
|
|
|
isCustomElement()
|
|
{
|
|
return this._customElementState === WI.DOMNode.CustomElementState.Custom;
|
|
}
|
|
|
|
customElementState()
|
|
{
|
|
return this._customElementState;
|
|
}
|
|
|
|
isShadowRoot()
|
|
{
|
|
return !!this._shadowRootType;
|
|
}
|
|
|
|
isUserAgentShadowRoot()
|
|
{
|
|
return this._shadowRootType === WI.DOMNode.ShadowRootType.UserAgent;
|
|
}
|
|
|
|
ancestorShadowRoot()
|
|
{
|
|
if (!this._isInShadowTree)
|
|
return null;
|
|
|
|
let node = this;
|
|
while (node && !node.isShadowRoot())
|
|
node = node.parentNode;
|
|
return node;
|
|
}
|
|
|
|
ancestorShadowHost()
|
|
{
|
|
let shadowRoot = this.ancestorShadowRoot();
|
|
return shadowRoot ? shadowRoot.parentNode : null;
|
|
}
|
|
|
|
isPseudoElement()
|
|
{
|
|
return this._pseudoType !== undefined;
|
|
}
|
|
|
|
nodeType()
|
|
{
|
|
return this._nodeType;
|
|
}
|
|
|
|
nodeName()
|
|
{
|
|
return this._nodeName;
|
|
}
|
|
|
|
nodeNameInCorrectCase()
|
|
{
|
|
return this.isXMLNode() ? this.nodeName() : this.nodeName().toLowerCase();
|
|
}
|
|
|
|
setNodeName(name, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setNodeName(this.id, name, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
localName()
|
|
{
|
|
return this._localName;
|
|
}
|
|
|
|
templateContent()
|
|
{
|
|
return this._templateContent || null;
|
|
}
|
|
|
|
pseudoType()
|
|
{
|
|
return this._pseudoType;
|
|
}
|
|
|
|
hasPseudoElements()
|
|
{
|
|
return this._pseudoElements.size > 0;
|
|
}
|
|
|
|
pseudoElements()
|
|
{
|
|
return this._pseudoElements;
|
|
}
|
|
|
|
beforePseudoElement()
|
|
{
|
|
return this._pseudoElements.get(WI.DOMNode.PseudoElementType.Before) || null;
|
|
}
|
|
|
|
afterPseudoElement()
|
|
{
|
|
return this._pseudoElements.get(WI.DOMNode.PseudoElementType.After) || null;
|
|
}
|
|
|
|
shadowRoots()
|
|
{
|
|
return this._shadowRoots;
|
|
}
|
|
|
|
shadowRootType()
|
|
{
|
|
return this._shadowRootType;
|
|
}
|
|
|
|
nodeValue()
|
|
{
|
|
return this._nodeValue;
|
|
}
|
|
|
|
setNodeValue(value, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setNodeValue(this.id, value, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
getAttribute(name)
|
|
{
|
|
let attr = this._attributesMap.get(name);
|
|
return attr ? attr.value : undefined;
|
|
}
|
|
|
|
setAttribute(name, text, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setAttributesAsText(this.id, text, name, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
setAttributeValue(name, value, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
if (!callback)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
if (!callback) {
|
|
return target.DOMAgent.setAttributeValue(this.id, name, value).then(() => {
|
|
this._markUndoableState();
|
|
});
|
|
}
|
|
|
|
target.DOMAgent.setAttributeValue(this.id, name, value, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
attributes()
|
|
{
|
|
return this._attributes;
|
|
}
|
|
|
|
removeAttribute(name, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
function mycallback(error, success)
|
|
{
|
|
if (!error) {
|
|
this._attributesMap.delete(name);
|
|
for (var i = 0; i < this._attributes.length; ++i) {
|
|
if (this._attributes[i].name === name) {
|
|
this._attributes.splice(i, 1);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._makeUndoableCallback(callback)(error);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.removeAttribute(this.id, name, mycallback.bind(this));
|
|
}
|
|
|
|
toggleClass(className, flag)
|
|
{
|
|
if (!className || !className.length)
|
|
return;
|
|
|
|
if (this.isPseudoElement()) {
|
|
this.parentNode.toggleClass(className, flag);
|
|
return;
|
|
}
|
|
|
|
if (this.nodeType() !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
WI.RemoteObject.resolveNode(this).then((object) => {
|
|
function inspectedPage_node_toggleClass(className, flag) {
|
|
this.classList.toggle(className, flag);
|
|
}
|
|
|
|
object.callFunction(inspectedPage_node_toggleClass, [className, flag]);
|
|
object.release();
|
|
});
|
|
}
|
|
|
|
querySelector(selector, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
if (typeof callback !== "function") {
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
return target.DOMAgent.querySelector(this.id, selector).then(({nodeId}) => nodeId);
|
|
}
|
|
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
target.DOMAgent.querySelector(this.id, selector, WI.DOMManager.wrapClientCallback(callback));
|
|
}
|
|
|
|
querySelectorAll(selector, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
if (typeof callback !== "function") {
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
return target.DOMAgent.querySelectorAll(this.id, selector).then(({nodeIds}) => nodeIds);
|
|
}
|
|
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
target.DOMAgent.querySelectorAll(this.id, selector, WI.DOMManager.wrapClientCallback(callback));
|
|
}
|
|
|
|
highlight(mode)
|
|
{
|
|
if (this._destroyed)
|
|
return;
|
|
|
|
if (this._hideDOMNodeHighlightTimeout) {
|
|
clearTimeout(this._hideDOMNodeHighlightTimeout);
|
|
this._hideDOMNodeHighlightTimeout = undefined;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.highlightNode.invoke({
|
|
nodeId: this.id,
|
|
...WI.DOMManager.buildHighlightConfigs(mode),
|
|
});
|
|
}
|
|
|
|
showLayoutOverlay({color} = {})
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
console.assert(Object.values(WI.DOMNode._LayoutContextTypes).includes(this.layoutContextType), this);
|
|
|
|
console.assert(!color || color instanceof WI.Color, color);
|
|
color ||= this.layoutOverlayColor;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
let agentCommandFunction = null;
|
|
let agentCommandArguments = {nodeId: this.id};
|
|
|
|
switch (this.layoutContextType) {
|
|
case WI.DOMNode.LayoutFlag.Grid:
|
|
agentCommandArguments.gridOverlayConfig = {
|
|
gridColor: color.toProtocol(),
|
|
showLineNames: WI.settings.gridOverlayShowLineNames.value,
|
|
showLineNumbers: WI.settings.gridOverlayShowLineNumbers.value,
|
|
showExtendedGridLines: WI.settings.gridOverlayShowExtendedGridLines.value,
|
|
showTrackSizes: WI.settings.gridOverlayShowTrackSizes.value,
|
|
showAreaNames: WI.settings.gridOverlayShowAreaNames.value,
|
|
};
|
|
|
|
// COMPATIBILITY (macOS 13.3, iOS 16.4): DOM.GridOverlayConfig did not exist yet.
|
|
if (!target.hasCommand("DOM.showGridOverlay", "gridOverlayConfig")) {
|
|
for (let [key, value] in Object.entries(agentCommandArguments.gridOverlayConfig))
|
|
agentCommandArguments[key] = value;
|
|
}
|
|
|
|
agentCommandFunction = target.DOMAgent.showGridOverlay;
|
|
|
|
if (!this._layoutOverlayShowing) {
|
|
WI.settings.gridOverlayShowExtendedGridLines.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNames.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNumbers.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowTrackSizes.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowAreaNames.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
}
|
|
break;
|
|
|
|
case WI.DOMNode.LayoutFlag.Flex:
|
|
agentCommandArguments.flexOverlayConfig = {
|
|
flexColor: color.toProtocol(),
|
|
showOrderNumbers: WI.settings.flexOverlayShowOrderNumbers.value,
|
|
};
|
|
|
|
// COMPATIBILITY (macOS 13.3, iOS 16.4): DOM.FlexOverlayConfig did not exist yet.
|
|
if (!target.hasCommand("DOM.showFlexOverlay", "flexOverlayConfig")) {
|
|
for (let [key, value] in Object.entries(agentCommandArguments.flexOverlayConfig))
|
|
agentCommandArguments[key] = value;
|
|
}
|
|
|
|
agentCommandFunction = target.DOMAgent.showFlexOverlay;
|
|
|
|
if (!this._layoutOverlayShowing)
|
|
WI.settings.flexOverlayShowOrderNumbers.addEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
break;
|
|
}
|
|
|
|
this._layoutOverlayShowing = true;
|
|
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.LayoutOverlayShown);
|
|
|
|
console.assert(agentCommandFunction);
|
|
return agentCommandFunction.invoke(agentCommandArguments);
|
|
}
|
|
|
|
hideLayoutOverlay()
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
console.assert(Object.values(WI.DOMNode._LayoutContextTypes).includes(this.layoutContextType), this);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
let agentCommandFunction;
|
|
let agentCommandArguments = {nodeId: this.id};
|
|
|
|
switch (this.layoutContextType) {
|
|
case WI.DOMNode.LayoutFlag.Grid:
|
|
WI.settings.gridOverlayShowExtendedGridLines.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNames.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowLineNumbers.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowTrackSizes.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
WI.settings.gridOverlayShowAreaNames.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
|
|
agentCommandFunction = target.DOMAgent.hideGridOverlay;
|
|
break;
|
|
|
|
case WI.DOMNode.LayoutFlag.Flex:
|
|
WI.settings.flexOverlayShowOrderNumbers.removeEventListener(WI.Setting.Event.Changed, this._handleLayoutOverlaySettingChanged, this);
|
|
|
|
agentCommandFunction = target.DOMAgent.hideFlexOverlay;
|
|
break;
|
|
}
|
|
|
|
console.assert(this._layoutOverlayShowing, this);
|
|
this._layoutOverlayShowing = false;
|
|
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.LayoutOverlayHidden);
|
|
|
|
console.assert(agentCommandFunction);
|
|
return agentCommandFunction.invoke(agentCommandArguments);
|
|
}
|
|
|
|
get layoutOverlayColor()
|
|
{
|
|
this._createLayoutOverlayColorSettingIfNeeded();
|
|
return new WI.Color(WI.Color.Format.HSL, this._layoutOverlayColorSetting.value);
|
|
}
|
|
|
|
set layoutOverlayColor(color)
|
|
{
|
|
console.assert(color instanceof WI.Color, color);
|
|
|
|
this._createLayoutOverlayColorSettingIfNeeded();
|
|
this._layoutOverlayColorSetting.value = color.hsl;
|
|
|
|
if (this._layoutOverlayShowing)
|
|
this.showLayoutOverlay({color});
|
|
}
|
|
|
|
scrollIntoView()
|
|
{
|
|
WI.RemoteObject.resolveNode(this).then((object) => {
|
|
function inspectedPage_node_scrollIntoView() {
|
|
this.scrollIntoViewIfNeeded(true);
|
|
}
|
|
|
|
object.callFunction(inspectedPage_node_scrollIntoView);
|
|
object.release();
|
|
});
|
|
}
|
|
|
|
getChildNodes(callback)
|
|
{
|
|
if (this.children) {
|
|
if (callback)
|
|
callback(this.children);
|
|
return;
|
|
}
|
|
|
|
if (this._destroyed) {
|
|
callback(this.children);
|
|
return;
|
|
}
|
|
|
|
function mycallback(error) {
|
|
if (!error && callback)
|
|
callback(this.children);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.requestChildNodes(this.id, mycallback.bind(this));
|
|
}
|
|
|
|
getSubtree(depth, callback)
|
|
{
|
|
if (this._destroyed) {
|
|
callback(this.children);
|
|
return;
|
|
}
|
|
|
|
function mycallback(error)
|
|
{
|
|
if (callback)
|
|
callback(error ? null : this.children);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.requestChildNodes(this.id, depth, mycallback.bind(this));
|
|
}
|
|
|
|
getOuterHTML(callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
if (typeof callback !== "function") {
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
return target.DOMAgent.getOuterHTML(this.id).then(({outerHTML}) => outerHTML);
|
|
}
|
|
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
target.DOMAgent.getOuterHTML(this.id, callback);
|
|
}
|
|
|
|
setOuterHTML(html, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setOuterHTML(this.id, html, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
insertAdjacentHTML(position, html)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed)
|
|
return;
|
|
|
|
if (this.nodeType() !== Node.ELEMENT_NODE)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.insertAdjacentHTML(this.id, position, html, this._makeUndoableCallback());
|
|
}
|
|
|
|
removeNode(callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.removeNode(this.id, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
getEventListeners({includeAncestors} = {})
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed)
|
|
return Promise.reject("ERROR: node is destroyed");
|
|
|
|
includeAncestors ??= true;
|
|
|
|
console.assert(WI.domManager.inspectedNode === this || !includeAncestors, this, includeAncestors);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.DOMAgent.getEventListenersForNode.invoke({
|
|
nodeId: this.id,
|
|
includeAncestors,
|
|
});
|
|
}
|
|
|
|
accessibilityProperties(callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback({});
|
|
return;
|
|
}
|
|
|
|
function accessibilityPropertiesCallback(error, accessibilityProperties)
|
|
{
|
|
if (!error && callback && accessibilityProperties) {
|
|
this._computedRole = accessibilityProperties.role;
|
|
|
|
callback({
|
|
activeDescendantNodeId: accessibilityProperties.activeDescendantNodeId,
|
|
busy: accessibilityProperties.busy,
|
|
checked: accessibilityProperties.checked,
|
|
childNodeIds: accessibilityProperties.childNodeIds,
|
|
controlledNodeIds: accessibilityProperties.controlledNodeIds,
|
|
current: accessibilityProperties.current,
|
|
disabled: accessibilityProperties.disabled,
|
|
exists: accessibilityProperties.exists,
|
|
expanded: accessibilityProperties.expanded,
|
|
flowedNodeIds: accessibilityProperties.flowedNodeIds,
|
|
focused: accessibilityProperties.focused,
|
|
ignored: accessibilityProperties.ignored,
|
|
ignoredByDefault: accessibilityProperties.ignoredByDefault,
|
|
invalid: accessibilityProperties.invalid,
|
|
isPopupButton: accessibilityProperties.isPopUpButton,
|
|
headingLevel: accessibilityProperties.headingLevel,
|
|
hierarchyLevel: accessibilityProperties.hierarchyLevel,
|
|
hidden: accessibilityProperties.hidden,
|
|
label: accessibilityProperties.label,
|
|
liveRegionAtomic: accessibilityProperties.liveRegionAtomic,
|
|
liveRegionRelevant: accessibilityProperties.liveRegionRelevant,
|
|
liveRegionStatus: accessibilityProperties.liveRegionStatus,
|
|
mouseEventNodeId: accessibilityProperties.mouseEventNodeId,
|
|
nodeId: accessibilityProperties.nodeId,
|
|
ownedNodeIds: accessibilityProperties.ownedNodeIds,
|
|
parentNodeId: accessibilityProperties.parentNodeId,
|
|
pressed: accessibilityProperties.pressed,
|
|
readonly: accessibilityProperties.readonly,
|
|
required: accessibilityProperties.required,
|
|
role: accessibilityProperties.role,
|
|
selected: accessibilityProperties.selected,
|
|
selectedChildNodeIds: accessibilityProperties.selectedChildNodeIds
|
|
});
|
|
}
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.getAccessibilityPropertiesForNode(this.id, accessibilityPropertiesCallback.bind(this));
|
|
}
|
|
|
|
path()
|
|
{
|
|
var path = [];
|
|
var node = this;
|
|
while (node && "index" in node && node._nodeName.length) {
|
|
path.push([node.index, node._nodeName]);
|
|
node = node.parentNode;
|
|
}
|
|
path.reverse();
|
|
return path.join(",");
|
|
}
|
|
|
|
get escapedIdSelector()
|
|
{
|
|
return this._idSelector(true);
|
|
}
|
|
|
|
get escapedClassSelector()
|
|
{
|
|
return this._classSelector(true);
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
if (this.isPseudoElement())
|
|
return "::" + this._pseudoType;
|
|
return this.nodeNameInCorrectCase() + this.escapedIdSelector + this.escapedClassSelector;
|
|
}
|
|
|
|
get unescapedSelector()
|
|
{
|
|
if (this.isPseudoElement())
|
|
return "::" + this._pseudoType;
|
|
|
|
const shouldEscape = false;
|
|
return this.nodeNameInCorrectCase() + this._idSelector(shouldEscape) + this._classSelector(shouldEscape);
|
|
}
|
|
|
|
appropriateSelectorFor(justSelector)
|
|
{
|
|
if (this.isPseudoElement())
|
|
return this.parentNode.appropriateSelectorFor() + "::" + this._pseudoType;
|
|
|
|
let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
|
|
|
|
let id = this.escapedIdSelector;
|
|
if (id.length)
|
|
return justSelector ? id : lowerCaseName + id;
|
|
|
|
let classes = this.escapedClassSelector;
|
|
if (classes.length)
|
|
return justSelector ? classes : lowerCaseName + classes;
|
|
|
|
if (lowerCaseName === "input" && this.getAttribute("type"))
|
|
return lowerCaseName + "[type=\"" + this.getAttribute("type") + "\"]";
|
|
|
|
return lowerCaseName;
|
|
}
|
|
|
|
isAncestor(node)
|
|
{
|
|
if (!node)
|
|
return false;
|
|
|
|
var currentNode = node.parentNode;
|
|
while (currentNode) {
|
|
if (this === currentNode)
|
|
return true;
|
|
currentNode = currentNode.parentNode;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isDescendant(descendant)
|
|
{
|
|
return descendant !== null && descendant.isAncestor(this);
|
|
}
|
|
|
|
get ownerSVGElement()
|
|
{
|
|
if (this._nodeName === "svg")
|
|
return this;
|
|
|
|
if (!this.parentNode)
|
|
return null;
|
|
|
|
return this.parentNode.ownerSVGElement;
|
|
}
|
|
|
|
isSVGElement()
|
|
{
|
|
return !!this.ownerSVGElement;
|
|
}
|
|
|
|
isMediaElement()
|
|
{
|
|
let lowerCaseName = this.localName() || this.nodeName().toLowerCase();
|
|
return lowerCaseName === "video" || lowerCaseName === "audio";
|
|
}
|
|
|
|
didFireEvent(eventName, timestamp, data)
|
|
{
|
|
// Called from WI.DOMManager.
|
|
|
|
this._addDOMEvent({
|
|
eventName,
|
|
timestamp: WI.timelineManager.computeElapsedTime(timestamp),
|
|
data,
|
|
});
|
|
}
|
|
|
|
powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient)
|
|
{
|
|
// Called from WI.DOMManager.
|
|
|
|
console.assert(this.canEnterPowerEfficientPlaybackState());
|
|
|
|
let lastValue = this._powerEfficientPlaybackRanges.lastValue;
|
|
|
|
if (isPowerEfficient) {
|
|
console.assert(!lastValue || lastValue.endTimestamp);
|
|
if (!lastValue || lastValue.endTimestamp)
|
|
this._powerEfficientPlaybackRanges.push({startTimestamp: timestamp});
|
|
} else {
|
|
console.assert(!lastValue || lastValue.startTimestamp);
|
|
if (!lastValue)
|
|
this._powerEfficientPlaybackRanges.push({endTimestamp: timestamp});
|
|
else if (lastValue.startTimestamp)
|
|
lastValue.endTimestamp = timestamp;
|
|
}
|
|
|
|
this.dispatchEventToListeners(DOMNode.Event.PowerEfficientPlaybackStateChanged, {isPowerEfficient, timestamp});
|
|
}
|
|
|
|
canEnterPowerEfficientPlaybackState()
|
|
{
|
|
return this.localName() === "video" || this.nodeName().toLowerCase() === "video";
|
|
}
|
|
|
|
_handleDOMNodeDidFireEvent(event)
|
|
{
|
|
if (event.target === this || !event.target.isAncestor(this))
|
|
return;
|
|
|
|
let domEvent = Object.shallowCopy(event.data.domEvent);
|
|
domEvent.originator = event.target;
|
|
|
|
this._addDOMEvent(domEvent);
|
|
}
|
|
|
|
_addDOMEvent(domEvent)
|
|
{
|
|
this._domEvents.push(domEvent);
|
|
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.DidFireEvent, {domEvent});
|
|
}
|
|
|
|
_setAttributesPayload(attrs)
|
|
{
|
|
this._attributes = [];
|
|
this._attributesMap = new Map;
|
|
for (var i = 0; i < attrs.length; i += 2)
|
|
this._addAttribute(attrs[i], attrs[i + 1]);
|
|
}
|
|
|
|
_insertChild(prev, payload)
|
|
{
|
|
var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payload);
|
|
if (!prev) {
|
|
if (!this._children) {
|
|
// First node
|
|
this._children = this._shadowRoots.concat([node]);
|
|
} else
|
|
this._children.unshift(node);
|
|
} else
|
|
this._children.splice(this._children.indexOf(prev) + 1, 0, node);
|
|
this._renumber();
|
|
return node;
|
|
}
|
|
|
|
_removeChild(node)
|
|
{
|
|
// FIXME: Handle removal if this is a shadow root.
|
|
if (node.isPseudoElement()) {
|
|
this._pseudoElements.delete(node.pseudoType());
|
|
node.parentNode = null;
|
|
} else {
|
|
this._children.splice(this._children.indexOf(node), 1);
|
|
node.parentNode = null;
|
|
this._renumber();
|
|
}
|
|
}
|
|
|
|
_setChildrenPayload(payloads)
|
|
{
|
|
// We set children in the constructor.
|
|
if (this._contentDocument)
|
|
return;
|
|
|
|
this._children = this._shadowRoots.slice();
|
|
for (var i = 0; i < payloads.length; ++i) {
|
|
var node = new WI.DOMNode(this._domManager, this.ownerDocument, this._isInShadowTree, payloads[i]);
|
|
this._children.push(node);
|
|
}
|
|
this._renumber();
|
|
}
|
|
|
|
_renumber()
|
|
{
|
|
var childNodeCount = this._children.length;
|
|
if (childNodeCount === 0)
|
|
return;
|
|
|
|
for (var i = 0; i < childNodeCount; ++i) {
|
|
var child = this._children[i];
|
|
child.index = i;
|
|
child._nextSibling = i + 1 < childNodeCount ? this._children[i + 1] : null;
|
|
child._previousSibling = i - 1 >= 0 ? this._children[i - 1] : null;
|
|
child.parentNode = this;
|
|
}
|
|
}
|
|
|
|
_addAttribute(name, value)
|
|
{
|
|
let attr = {name, value, _node: this};
|
|
this._attributesMap.set(name, attr);
|
|
this._attributes.push(attr);
|
|
}
|
|
|
|
_setAttribute(name, value)
|
|
{
|
|
let attr = this._attributesMap.get(name);
|
|
if (attr)
|
|
attr.value = value;
|
|
else
|
|
this._addAttribute(name, value);
|
|
}
|
|
|
|
_removeAttribute(name)
|
|
{
|
|
let attr = this._attributesMap.get(name);
|
|
if (attr) {
|
|
this._attributes.remove(attr);
|
|
this._attributesMap.delete(name);
|
|
}
|
|
}
|
|
|
|
moveTo(targetNode, anchorNode, callback)
|
|
{
|
|
console.assert(!this._destroyed, this);
|
|
if (this._destroyed) {
|
|
callback("ERROR: node is destroyed");
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.moveTo(this.id, targetNode.id, anchorNode ? anchorNode.id : undefined, this._makeUndoableCallback(callback));
|
|
}
|
|
|
|
isXMLNode()
|
|
{
|
|
return !!this.ownerDocument && !!this.ownerDocument.xmlVersion;
|
|
}
|
|
|
|
get enabledPseudoClasses()
|
|
{
|
|
return this._enabledPseudoClasses;
|
|
}
|
|
|
|
setPseudoClassEnabled(pseudoClass, enabled)
|
|
{
|
|
var pseudoClasses = this._enabledPseudoClasses;
|
|
if (enabled) {
|
|
if (pseudoClasses.includes(pseudoClass))
|
|
return;
|
|
pseudoClasses.push(pseudoClass);
|
|
} else {
|
|
if (!pseudoClasses.includes(pseudoClass))
|
|
return;
|
|
pseudoClasses.remove(pseudoClass);
|
|
}
|
|
|
|
function changed(error)
|
|
{
|
|
if (!error)
|
|
this.dispatchEventToListeners(WI.DOMNode.Event.EnabledPseudoClassesChanged);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CSSAgent.forcePseudoState(this.id, pseudoClasses, changed.bind(this));
|
|
}
|
|
|
|
_markUndoableState()
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
if (target.hasCommand("DOM.markUndoableState"))
|
|
target.DOMAgent.markUndoableState();
|
|
}
|
|
|
|
_makeUndoableCallback(callback)
|
|
{
|
|
return (...args) => {
|
|
if (!args[0]) // error
|
|
this._markUndoableState();
|
|
|
|
if (callback)
|
|
callback.apply(null, args);
|
|
};
|
|
}
|
|
|
|
_idSelector(shouldEscape)
|
|
{
|
|
let id = this.getAttribute("id");
|
|
if (!id)
|
|
return "";
|
|
|
|
id = id.trim();
|
|
if (!id.length)
|
|
return "";
|
|
|
|
if (shouldEscape)
|
|
id = CSS.escape(id);
|
|
if (/[\s'"]/.test(id))
|
|
return `[id="${id}"]`;
|
|
|
|
return `#${id}`;
|
|
}
|
|
|
|
_classSelector(shouldEscape) {
|
|
let classes = this.getAttribute("class");
|
|
if (!classes)
|
|
return "";
|
|
|
|
classes = classes.trim();
|
|
if (!classes.length)
|
|
return "";
|
|
|
|
let foundClasses = new Set;
|
|
return classes.split(/\s+/).reduce((selector, className) => {
|
|
if (!className.length || foundClasses.has(className))
|
|
return selector;
|
|
|
|
foundClasses.add(className);
|
|
return `${selector}.${(shouldEscape ? CSS.escape(className) : className)}`;
|
|
}, "");
|
|
}
|
|
|
|
_createLayoutOverlayColorSettingIfNeeded()
|
|
{
|
|
if (this._layoutOverlayColorSetting)
|
|
return;
|
|
|
|
let defaultConfiguration = WI.DOMNode._defaultLayoutOverlayConfiguration;
|
|
|
|
let url = this.ownerDocument.documentURL || WI.networkManager.mainFrame.url;
|
|
|
|
let nextColorIndex;
|
|
switch (this.layoutContextType) {
|
|
case WI.DOMNode.LayoutFlag.Grid:
|
|
nextColorIndex = defaultConfiguration.nextGridColorIndex;
|
|
defaultConfiguration.nextGridColorIndex = (nextColorIndex + 1) % defaultConfiguration.colors.length;
|
|
break;
|
|
|
|
case WI.DOMNode.LayoutFlag.Flex:
|
|
nextColorIndex = defaultConfiguration.nextFlexColorIndex;
|
|
defaultConfiguration.nextFlexColorIndex = (nextColorIndex + 1) % defaultConfiguration.colors.length;
|
|
break;
|
|
}
|
|
|
|
this._layoutOverlayColorSetting = new WI.Setting(`overlay-color-${url.hash}-${this.path().hash}`, defaultConfiguration.colors[nextColorIndex]);
|
|
}
|
|
|
|
_handleLayoutOverlaySettingChanged(event)
|
|
{
|
|
if (this._layoutOverlayShowing)
|
|
this.showLayoutOverlay();
|
|
}
|
|
|
|
async getMediaStats()
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
let {mediaStats} = await target.DOMAgent.getMediaStats(this.id);
|
|
return mediaStats;
|
|
}
|
|
};
|
|
|
|
WI.DOMNode._defaultLayoutOverlayConfiguration = {
|
|
colors: [
|
|
[329, 91, 70],
|
|
[207, 96, 69],
|
|
[92, 90, 64],
|
|
[291, 73, 68],
|
|
[40, 97, 57],
|
|
],
|
|
nextFlexColorIndex: 0,
|
|
nextGridColorIndex: 0,
|
|
};
|
|
|
|
WI.DOMNode.Event = {
|
|
EnabledPseudoClassesChanged: "dom-node-enabled-pseudo-classes-did-change",
|
|
AttributeModified: "dom-node-attribute-modified",
|
|
AttributeRemoved: "dom-node-attribute-removed",
|
|
EventListenersChanged: "dom-node-event-listeners-changed",
|
|
DidFireEvent: "dom-node-did-fire-event",
|
|
PowerEfficientPlaybackStateChanged: "dom-node-power-efficient-playback-state-changed",
|
|
LayoutFlagsChanged: "dom-node-layout-flags-changed",
|
|
LayoutOverlayShown: "dom-node-layout-overlay-shown",
|
|
LayoutOverlayHidden: "dom-node-layout-overlay-hidden",
|
|
};
|
|
|
|
WI.DOMNode.PseudoElementType = {
|
|
Before: "before",
|
|
After: "after",
|
|
};
|
|
|
|
WI.DOMNode.ShadowRootType = {
|
|
UserAgent: "user-agent",
|
|
Closed: "closed",
|
|
Open: "open",
|
|
};
|
|
|
|
WI.DOMNode.CustomElementState = {
|
|
Builtin: "builtin",
|
|
Custom: "custom",
|
|
Waiting: "waiting",
|
|
Failed: "failed",
|
|
};
|
|
|
|
// Corresponds to `CSS.LayoutFlag`.
|
|
WI.DOMNode.LayoutFlag = {
|
|
Rendered: "rendered",
|
|
Event: "event",
|
|
Scrollable: "scrollable",
|
|
|
|
// These are mutually exclusive.
|
|
Flex: "flex",
|
|
Grid: "grid",
|
|
};
|
|
|
|
WI.DOMNode._LayoutContextTypes = [
|
|
WI.DOMNode.LayoutFlag.Flex,
|
|
WI.DOMNode.LayoutFlag.Grid,
|
|
];
|
|
|
|
/* Models/DOMNodeStyles.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMNodeStyles = class DOMNodeStyles extends WI.Object
|
|
{
|
|
constructor(node)
|
|
{
|
|
super();
|
|
|
|
console.assert(node);
|
|
this._node = node || null;
|
|
|
|
this._rulesMap = new Map;
|
|
this._stylesMap = new Multimap;
|
|
this._groupingsMap = new Map;
|
|
|
|
this._matchedRules = [];
|
|
this._inheritedRules = [];
|
|
this._pseudoElements = new Map;
|
|
this._inlineStyle = null;
|
|
this._attributesStyle = null;
|
|
this._computedStyle = null;
|
|
this._orderedStyles = [];
|
|
|
|
this._computedPrimaryFont = null;
|
|
|
|
this._propertyNameToEffectivePropertyMap = {};
|
|
this._usedCSSVariables = new Set;
|
|
this._allCSSVariables = new Set;
|
|
|
|
this._pendingRefreshTask = null;
|
|
this.refresh();
|
|
|
|
this._trackedStyleSheets = new WeakSet;
|
|
WI.CSSStyleSheet.addEventListener(WI.CSSStyleSheet.Event.ContentDidChange, this._handleCSSStyleSheetContentDidChange, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static parseSelectorListPayload(selectorList)
|
|
{
|
|
let selectors = selectorList.selectors;
|
|
if (!selectors.length)
|
|
return [];
|
|
|
|
return selectors.map(function(selectorPayload) {
|
|
return new WI.CSSSelector(selectorPayload.text, selectorPayload.specificity, selectorPayload.dynamic);
|
|
});
|
|
}
|
|
|
|
static createSourceCodeLocation(sourceURL, {line, column, documentNode} = {})
|
|
{
|
|
if (!sourceURL)
|
|
return null;
|
|
|
|
let sourceCode = null;
|
|
|
|
// Try to use the node to find the frame which has the correct resource first.
|
|
if (documentNode) {
|
|
let mainResource = WI.networkManager.resourcesForURL(documentNode.documentURL).firstValue;
|
|
if (mainResource) {
|
|
let parentFrame = mainResource.parentFrame;
|
|
sourceCode = parentFrame.resourcesForURL(sourceURL).firstValue;
|
|
}
|
|
}
|
|
|
|
// If that didn't find the resource, then search all frames.
|
|
if (!sourceCode)
|
|
sourceCode = WI.networkManager.resourcesForURL(sourceURL).firstValue;
|
|
|
|
if (!sourceCode)
|
|
return null;
|
|
|
|
return sourceCode.createSourceCodeLocation(line || 0, column || 0);
|
|
}
|
|
|
|
static uniqueOrderedStyles(orderedStyles)
|
|
{
|
|
let uniqueOrderedStyles = [];
|
|
|
|
for (let style of orderedStyles) {
|
|
let rule = style.ownerRule;
|
|
if (!rule) {
|
|
uniqueOrderedStyles.push(style);
|
|
continue;
|
|
}
|
|
|
|
let found = false;
|
|
for (let existingStyle of uniqueOrderedStyles) {
|
|
if (rule.isEqualTo(existingStyle.ownerRule)) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
uniqueOrderedStyles.push(style);
|
|
}
|
|
|
|
return uniqueOrderedStyles;
|
|
}
|
|
|
|
// Public
|
|
|
|
get node() { return this._node; }
|
|
get matchedRules() { return this._matchedRules; }
|
|
get inheritedRules() { return this._inheritedRules; }
|
|
get inlineStyle() { return this._inlineStyle; }
|
|
get attributesStyle() { return this._attributesStyle; }
|
|
get pseudoElements() { return this._pseudoElements; }
|
|
get computedStyle() { return this._computedStyle; }
|
|
get orderedStyles() { return this._orderedStyles; }
|
|
get computedPrimaryFont() { return this._computedPrimaryFont; }
|
|
get usedCSSVariables() { return this._usedCSSVariables; }
|
|
get allCSSVariables() { return this._allCSSVariables; }
|
|
|
|
set ignoreNextContentDidChangeForStyleSheet(ignoreNextContentDidChangeForStyleSheet) { this._ignoreNextContentDidChangeForStyleSheet = ignoreNextContentDidChangeForStyleSheet; }
|
|
|
|
get needsRefresh()
|
|
{
|
|
return this._pendingRefreshTask || this._needsRefresh;
|
|
}
|
|
|
|
get uniqueOrderedStyles()
|
|
{
|
|
return WI.DOMNodeStyles.uniqueOrderedStyles(this._orderedStyles);
|
|
}
|
|
|
|
refreshIfNeeded()
|
|
{
|
|
if (this._pendingRefreshTask)
|
|
return this._pendingRefreshTask;
|
|
if (!this._needsRefresh)
|
|
return Promise.resolve(this);
|
|
return this.refresh();
|
|
}
|
|
|
|
refresh()
|
|
{
|
|
if (this._pendingRefreshTask)
|
|
return this._pendingRefreshTask;
|
|
|
|
this._needsRefresh = false;
|
|
|
|
let fetchedMatchedStylesPromise = new WI.WrappedPromise;
|
|
let fetchedInlineStylesPromise = new WI.WrappedPromise;
|
|
let fetchedComputedStylesPromise = new WI.WrappedPromise;
|
|
let fetchedFontDataPromise = new WI.WrappedPromise;
|
|
|
|
// Ensure we resolve these promises even in the case of an error.
|
|
function wrap(func, promise) {
|
|
return (...args) => {
|
|
try {
|
|
func.apply(this, args);
|
|
} catch (e) {
|
|
console.error(e);
|
|
promise.resolve();
|
|
}
|
|
};
|
|
}
|
|
|
|
let parseRuleMatchArrayPayload = (matchArray, node, inherited, pseudoId) => {
|
|
var result = [];
|
|
|
|
// Iterate in reverse order to match the cascade order.
|
|
var ruleOccurrences = {};
|
|
for (var i = matchArray.length - 1; i >= 0; --i) {
|
|
var rule = this._parseRulePayload(matchArray[i].rule, matchArray[i].matchingSelectors, node, inherited, pseudoId, ruleOccurrences);
|
|
if (!rule)
|
|
continue;
|
|
result.push(rule);
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
function fetchedMatchedStyles(error, matchedRulesPayload, pseudoElementRulesPayload, inheritedRulesPayload)
|
|
{
|
|
matchedRulesPayload = matchedRulesPayload || [];
|
|
pseudoElementRulesPayload = pseudoElementRulesPayload || [];
|
|
inheritedRulesPayload = inheritedRulesPayload || [];
|
|
|
|
this._previousStylesMap = this._stylesMap;
|
|
this._stylesMap = new Multimap;
|
|
this._groupingsMap = new Map;
|
|
|
|
this._matchedRules = parseRuleMatchArrayPayload(matchedRulesPayload, this._node);
|
|
|
|
this._pseudoElements.clear();
|
|
for (let {pseudoId, matches} of pseudoElementRulesPayload) {
|
|
let pseudoElementRules = parseRuleMatchArrayPayload(matches, this._node, false, pseudoId);
|
|
this._pseudoElements.set(pseudoId, {matchedRules: pseudoElementRules});
|
|
}
|
|
|
|
this._inheritedRules = [];
|
|
|
|
var i = 0;
|
|
var currentNode = this._node.parentNode;
|
|
while (currentNode && i < inheritedRulesPayload.length) {
|
|
var inheritedRulePayload = inheritedRulesPayload[i];
|
|
|
|
var inheritedRuleInfo = {node: currentNode};
|
|
inheritedRuleInfo.inlineStyle = inheritedRulePayload.inlineStyle ? this._parseStyleDeclarationPayload(inheritedRulePayload.inlineStyle, currentNode, true, null, WI.CSSStyleDeclaration.Type.Inline) : null;
|
|
inheritedRuleInfo.matchedRules = inheritedRulePayload.matchedCSSRules ? parseRuleMatchArrayPayload(inheritedRulePayload.matchedCSSRules, currentNode, true) : [];
|
|
|
|
if (inheritedRuleInfo.inlineStyle || inheritedRuleInfo.matchedRules.length)
|
|
this._inheritedRules.push(inheritedRuleInfo);
|
|
|
|
currentNode = currentNode.parentNode;
|
|
++i;
|
|
}
|
|
|
|
fetchedMatchedStylesPromise.resolve();
|
|
}
|
|
|
|
function fetchedInlineStyles(error, inlineStylePayload, attributesStylePayload)
|
|
{
|
|
this._inlineStyle = inlineStylePayload ? this._parseStyleDeclarationPayload(inlineStylePayload, this._node, false, null, WI.CSSStyleDeclaration.Type.Inline) : null;
|
|
this._attributesStyle = attributesStylePayload ? this._parseStyleDeclarationPayload(attributesStylePayload, this._node, false, null, WI.CSSStyleDeclaration.Type.Attribute) : null;
|
|
|
|
this._updateStyleCascade();
|
|
|
|
fetchedInlineStylesPromise.resolve();
|
|
}
|
|
|
|
function fetchedComputedStyle(error, computedPropertiesPayload)
|
|
{
|
|
var properties = [];
|
|
for (var i = 0; computedPropertiesPayload && i < computedPropertiesPayload.length; ++i) {
|
|
var propertyPayload = computedPropertiesPayload[i];
|
|
|
|
var canonicalName = WI.cssManager.canonicalNameForPropertyName(propertyPayload.name);
|
|
propertyPayload.implicit = !this._propertyNameToEffectivePropertyMap[canonicalName];
|
|
|
|
var property = this._parseStylePropertyPayload(propertyPayload, NaN, this._computedStyle);
|
|
if (!property.implicit)
|
|
property.implicit = !this._isPropertyFoundInMatchingRules(property.name);
|
|
properties.push(property);
|
|
}
|
|
|
|
if (this._computedStyle)
|
|
this._computedStyle.update(null, properties);
|
|
else
|
|
this._computedStyle = new WI.CSSStyleDeclaration(this, null, null, WI.CSSStyleDeclaration.Type.Computed, this._node, false, null, properties);
|
|
|
|
let significantChange = false;
|
|
for (let [key, styles] of this._stylesMap.sets()) {
|
|
// Check if the same key exists in the previous map and has the same style objects.
|
|
let previousStyles = this._previousStylesMap.get(key);
|
|
if (previousStyles) {
|
|
// Some styles have selectors such that they will match with the DOM node twice (for example "::before, ::after").
|
|
// In this case a second style for a second matching may be generated and added which will cause the shallowEqual
|
|
// to not return true, so in this case we just want to ensure that all the current styles existed previously.
|
|
let styleFound = false;
|
|
for (let style of styles) {
|
|
if (previousStyles.has(style)) {
|
|
styleFound = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (styleFound)
|
|
continue;
|
|
}
|
|
|
|
if (!this._includeUserAgentRulesOnNextRefresh) {
|
|
// We can assume all the styles with the same key are from the same stylesheet and rule, so we only check the first.
|
|
let firstStyle = styles.firstValue;
|
|
if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent) {
|
|
// User Agent styles get different identifiers after some edits. This would cause us to fire a significant refreshed
|
|
// event more than it is helpful. And since the user agent stylesheet is static it shouldn't match differently
|
|
// between refreshes for the same node. This issue is tracked by: https://webkit.org/b/110055
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// This key is new or has different style objects than before. This is a significant change.
|
|
significantChange = true;
|
|
break;
|
|
}
|
|
|
|
if (!significantChange) {
|
|
for (let [key, previousStyles] of this._previousStylesMap.sets()) {
|
|
// Check if the same key exists in current map. If it does exist it was already checked for equality above.
|
|
if (this._stylesMap.has(key))
|
|
continue;
|
|
|
|
if (!this._includeUserAgentRulesOnNextRefresh) {
|
|
// See above for why we skip user agent style rules.
|
|
let firstStyle = previousStyles.firstValue;
|
|
if (firstStyle && firstStyle.ownerRule && firstStyle.ownerRule.type === WI.CSSStyleSheet.Type.UserAgent)
|
|
continue;
|
|
}
|
|
|
|
// This key no longer exists. This is a significant change.
|
|
significantChange = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
this._previousStylesMap = null;
|
|
this._includeUserAgentRulesOnNextRefresh = false;
|
|
|
|
fetchedComputedStylesPromise.resolve({significantChange});
|
|
}
|
|
|
|
function fetchedFontData(error, fontDataPayload)
|
|
{
|
|
if (fontDataPayload)
|
|
this._computedPrimaryFont = WI.Font.fromPayload(fontDataPayload);
|
|
else
|
|
this._computedPrimaryFont = null;
|
|
|
|
fetchedFontDataPromise.resolve();
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CSSAgent.getMatchedStylesForNode.invoke({nodeId: this._node.id, includePseudo: true, includeInherited: true}, wrap.call(this, fetchedMatchedStyles, fetchedMatchedStylesPromise));
|
|
target.CSSAgent.getInlineStylesForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedInlineStyles, fetchedInlineStylesPromise));
|
|
target.CSSAgent.getComputedStyleForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedComputedStyle, fetchedComputedStylesPromise));
|
|
|
|
// COMPATIBILITY (iOS 14.0): `CSS.getFontDataForNode` did not exist yet.
|
|
if (InspectorBackend.hasCommand("CSS.getFontDataForNode"))
|
|
target.CSSAgent.getFontDataForNode.invoke({nodeId: this._node.id}, wrap.call(this, fetchedFontData, fetchedFontDataPromise));
|
|
else
|
|
fetchedFontDataPromise.resolve();
|
|
|
|
this._pendingRefreshTask = Promise.all([fetchedComputedStylesPromise.promise, fetchedMatchedStylesPromise.promise, fetchedInlineStylesPromise.promise, fetchedFontDataPromise.promise])
|
|
.then(([fetchComputedStylesResult]) => {
|
|
this._pendingRefreshTask = null;
|
|
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.Refreshed, {
|
|
significantChange: fetchComputedStylesResult.significantChange,
|
|
});
|
|
return this;
|
|
});
|
|
|
|
return this._pendingRefreshTask;
|
|
}
|
|
|
|
addRule(selector, text, styleSheetId)
|
|
{
|
|
selector = selector || this._node.appropriateSelectorFor(true);
|
|
|
|
let result = new WI.WrappedPromise;
|
|
let target = WI.assumingMainTarget();
|
|
|
|
function completed()
|
|
{
|
|
target.DOMAgent.markUndoableState();
|
|
|
|
// Wait for the refresh promise caused by injecting an empty inspector stylesheet to resolve
|
|
// (another call will be ignored while one is still pending),
|
|
// then refresh again to get the latest matching styles which include the newly created rule.
|
|
if (this._pendingRefreshTask)
|
|
this._pendingRefreshTask.then(this.refresh.bind(this));
|
|
else
|
|
this.refresh();
|
|
}
|
|
|
|
function styleChanged(error, stylePayload)
|
|
{
|
|
if (error)
|
|
return;
|
|
|
|
completed.call(this);
|
|
}
|
|
|
|
function addedRule(error, rulePayload)
|
|
{
|
|
if (error){
|
|
result.reject(error);
|
|
return;
|
|
}
|
|
|
|
result.resolve(rulePayload);
|
|
|
|
if (!text || !text.length) {
|
|
completed.call(this);
|
|
return;
|
|
}
|
|
|
|
target.CSSAgent.setStyleText(rulePayload.style.styleId, text, styleChanged.bind(this));
|
|
}
|
|
|
|
function inspectorStyleSheetAvailable(styleSheet)
|
|
{
|
|
if (!styleSheet)
|
|
return;
|
|
|
|
target.CSSAgent.addRule(styleSheet.id, selector, addedRule.bind(this));
|
|
}
|
|
|
|
if (styleSheetId)
|
|
inspectorStyleSheetAvailable.call(this, WI.cssManager.styleSheetForIdentifier(styleSheetId));
|
|
else
|
|
WI.cssManager.preferredInspectorStyleSheetForFrame(this._node.frame, inspectorStyleSheetAvailable.bind(this));
|
|
|
|
return result.promise;
|
|
}
|
|
|
|
effectivePropertyForName(name)
|
|
{
|
|
let property = this._propertyNameToEffectivePropertyMap[name];
|
|
if (property)
|
|
return property;
|
|
|
|
let canonicalName = WI.cssManager.canonicalNameForPropertyName(name);
|
|
return this._propertyNameToEffectivePropertyMap[canonicalName] || null;
|
|
}
|
|
|
|
// Protected
|
|
|
|
mediaQueryResultDidChange()
|
|
{
|
|
this._markAsNeedsRefresh();
|
|
}
|
|
|
|
pseudoClassesDidChange(node)
|
|
{
|
|
this._includeUserAgentRulesOnNextRefresh = true;
|
|
this._markAsNeedsRefresh();
|
|
}
|
|
|
|
attributeDidChange(node, attributeName)
|
|
{
|
|
this._markAsNeedsRefresh();
|
|
}
|
|
|
|
changeRuleSelector(rule, selector)
|
|
{
|
|
selector = selector || "";
|
|
let result = new WI.WrappedPromise;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
function ruleSelectorChanged(error, rulePayload)
|
|
{
|
|
if (error) {
|
|
result.reject(error);
|
|
return;
|
|
}
|
|
|
|
target.DOMAgent.markUndoableState();
|
|
|
|
// Do a full refresh incase the rule no longer matches the node or the
|
|
// matched selector indices changed.
|
|
this.refresh().then(() => {
|
|
result.resolve(rulePayload);
|
|
});
|
|
}
|
|
|
|
this._needsRefresh = true;
|
|
this._ignoreNextContentDidChangeForStyleSheet = rule.ownerStyleSheet;
|
|
|
|
target.CSSAgent.setRuleSelector(rule.id, selector, ruleSelectorChanged.bind(this));
|
|
return result.promise;
|
|
}
|
|
|
|
changeStyleText(style, text, callback)
|
|
{
|
|
if (!style.ownerStyleSheet || !style.styleSheetTextRange) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
text = text || "";
|
|
|
|
let didSetStyleText = (error, stylePayload) => {
|
|
if (error) {
|
|
callback(error);
|
|
return;
|
|
}
|
|
callback();
|
|
|
|
// Update validity of each property for rules that don't match the selected DOM node.
|
|
// These rules don't get updated by CSSAgent.getMatchedStylesForNode.
|
|
if (style.ownerRule && !style.ownerRule.matchedSelectorIndices.length)
|
|
this._parseStyleDeclarationPayload(stylePayload, this._node, false, null, style.type, style.ownerRule, false);
|
|
|
|
this.refresh();
|
|
};
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CSSAgent.setStyleText(style.id, text, didSetStyleText);
|
|
}
|
|
|
|
// Private
|
|
|
|
_parseSourceRangePayload(payload)
|
|
{
|
|
if (!payload)
|
|
return null;
|
|
|
|
return new WI.TextRange(payload.startLine, payload.startColumn, payload.endLine, payload.endColumn);
|
|
}
|
|
|
|
_parseStylePropertyPayload(payload, index, styleDeclaration)
|
|
{
|
|
var text = payload.text || "";
|
|
var name = payload.name;
|
|
var value = payload.value || "";
|
|
var priority = payload.priority || "";
|
|
let range = payload.range || null;
|
|
|
|
var enabled = true;
|
|
var overridden = false;
|
|
var implicit = payload.implicit || false;
|
|
var anonymous = false;
|
|
var valid = "parsedOk" in payload ? payload.parsedOk : true;
|
|
|
|
switch (payload.status || "style") {
|
|
case "active":
|
|
enabled = true;
|
|
break;
|
|
case "inactive":
|
|
overridden = true;
|
|
enabled = true;
|
|
break;
|
|
case "disabled":
|
|
enabled = false;
|
|
break;
|
|
case "style":
|
|
// FIXME: Is this still needed? This includes UserAgent styles and HTML attribute styles.
|
|
anonymous = true;
|
|
break;
|
|
}
|
|
|
|
if (range) {
|
|
// Last property of inline style has mismatching range.
|
|
// The actual text has one line, but the range spans two lines.
|
|
let rangeLineCount = 1 + range.endLine - range.startLine;
|
|
if (rangeLineCount > 1) {
|
|
let textLineCount = text.lineCount;
|
|
if (textLineCount === rangeLineCount - 1) {
|
|
range.endLine = range.startLine + (textLineCount - 1);
|
|
range.endColumn = range.startColumn + text.lastLine.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
|
|
|
|
if (styleDeclaration) {
|
|
// Use propertyForName when the index is NaN since propertyForName is fast in that case.
|
|
var property = isNaN(index) ? styleDeclaration.propertyForName(name) : styleDeclaration.properties[index];
|
|
|
|
// Reuse a property if the index and name matches. Otherwise it is a different property
|
|
// and should be created from scratch. This works in the simple cases where only existing
|
|
// properties change in place and no properties are inserted or deleted at the beginning.
|
|
// FIXME: This could be smarter by ignoring index and just go by name. However, that gets
|
|
// tricky for rules that have more than one property with the same name.
|
|
if (property && property.name === name && (property.index === index || (isNaN(property.index) && isNaN(index)))) {
|
|
property.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
|
|
return property;
|
|
}
|
|
|
|
// Reuse a pending property with the same name. These properties are pending being committed,
|
|
// so if we find a match that likely means it got committed and we should use it.
|
|
var pendingProperties = styleDeclaration.pendingProperties;
|
|
for (var i = 0; i < pendingProperties.length; ++i) {
|
|
var pendingProperty = pendingProperties[i];
|
|
if (pendingProperty.name === name && isNaN(pendingProperty.index)) {
|
|
pendingProperty.index = index;
|
|
pendingProperty.update(text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
|
|
return pendingProperty;
|
|
}
|
|
}
|
|
}
|
|
|
|
return new WI.CSSProperty(index, text, name, value, priority, enabled, overridden, implicit, anonymous, valid, styleSheetTextRange);
|
|
}
|
|
|
|
_parseStyleDeclarationPayload(payload, node, inherited, pseudoId, type, rule, matchesNode = true)
|
|
{
|
|
if (!payload)
|
|
return null;
|
|
|
|
rule = rule || null;
|
|
inherited = inherited || false;
|
|
|
|
var id = payload.styleId;
|
|
var mapKey = id ? id.styleSheetId + ":" + id.ordinal : null;
|
|
if (pseudoId)
|
|
mapKey += ":" + pseudoId;
|
|
if (type === WI.CSSStyleDeclaration.Type.Attribute)
|
|
mapKey += ":" + node.id + ":attribute";
|
|
|
|
let style = rule ? rule.style : null;
|
|
console.assert(matchesNode || style);
|
|
|
|
if (matchesNode) {
|
|
console.assert(this._previousStylesMap);
|
|
let existingStyles = this._previousStylesMap.get(mapKey);
|
|
if (existingStyles && !style) {
|
|
for (let existingStyle of existingStyles) {
|
|
if (existingStyle.node === node && existingStyle.inherited === inherited) {
|
|
style = existingStyle;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (style)
|
|
this._stylesMap.add(mapKey, style);
|
|
}
|
|
|
|
var inheritedPropertyCount = 0;
|
|
|
|
var properties = [];
|
|
for (var i = 0; payload.cssProperties && i < payload.cssProperties.length; ++i) {
|
|
var propertyPayload = payload.cssProperties[i];
|
|
|
|
if (inherited && WI.CSSProperty.isInheritedPropertyName(propertyPayload.name))
|
|
++inheritedPropertyCount;
|
|
|
|
let property = this._parseStylePropertyPayload(propertyPayload, i, style);
|
|
properties.push(property);
|
|
}
|
|
|
|
let text = payload.cssText;
|
|
var styleSheetTextRange = this._parseSourceRangePayload(payload.range);
|
|
|
|
if (style) {
|
|
style.update(text, properties, styleSheetTextRange);
|
|
return style;
|
|
}
|
|
|
|
if (!matchesNode)
|
|
return null;
|
|
|
|
var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
|
|
if (styleSheet) {
|
|
if (type === WI.CSSStyleDeclaration.Type.Inline)
|
|
styleSheet.markAsInlineStyleAttributeStyleSheet();
|
|
this._trackedStyleSheets.add(styleSheet);
|
|
}
|
|
|
|
if (inherited && !inheritedPropertyCount)
|
|
return null;
|
|
|
|
style = new WI.CSSStyleDeclaration(this, styleSheet, id, type, node, inherited, text, properties, styleSheetTextRange);
|
|
|
|
if (mapKey)
|
|
this._stylesMap.add(mapKey, style);
|
|
|
|
return style;
|
|
}
|
|
|
|
_parseRulePayload(payload, matchedSelectorIndices, node, inherited, pseudoId, ruleOccurrences)
|
|
{
|
|
if (!payload)
|
|
return null;
|
|
|
|
// User and User Agent rules don't have 'ruleId' in the payload. However, their style's have 'styleId' and
|
|
// 'styleId' is the same identifier the backend uses for Author rule identifiers, so do the same here.
|
|
// They are excluded by the backend because they are not editable, however our front-end does not determine
|
|
// editability solely based on the existence of the id like the open source front-end does.
|
|
var id = payload.ruleId || payload.style.styleId;
|
|
|
|
var mapKey = id ? id.styleSheetId + ":" + id.ordinal + ":" + (inherited ? "I" : "N") + ":" + (pseudoId ? pseudoId + ":" : "") + node.id : null;
|
|
|
|
// Rules can match multiple times if they have multiple selectors or because of inheritance. We keep a count
|
|
// of occurrences so we have unique rules per occurrence, that way properties will be correctly marked as overridden.
|
|
var occurrence = 0;
|
|
if (mapKey) {
|
|
if (mapKey in ruleOccurrences)
|
|
occurrence = ++ruleOccurrences[mapKey];
|
|
else
|
|
ruleOccurrences[mapKey] = occurrence;
|
|
|
|
// Append the occurrence number to the map key for lookup in the rules map.
|
|
mapKey += ":" + occurrence;
|
|
}
|
|
|
|
let rule = this._rulesMap.get(mapKey);
|
|
|
|
var style = this._parseStyleDeclarationPayload(payload.style, node, inherited, pseudoId, WI.CSSStyleDeclaration.Type.Rule, rule);
|
|
if (!style)
|
|
return null;
|
|
|
|
var styleSheet = id ? WI.cssManager.styleSheetForIdentifier(id.styleSheetId) : null;
|
|
|
|
var selectorText = payload.selectorList.text;
|
|
let selectors = DOMNodeStyles.parseSelectorListPayload(payload.selectorList);
|
|
var type = WI.CSSManager.protocolStyleSheetOriginToEnum(payload.origin);
|
|
|
|
var sourceCodeLocation = null;
|
|
var sourceRange = payload.selectorList.range;
|
|
if (sourceRange) {
|
|
sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
|
|
line: sourceRange.startLine,
|
|
column: sourceRange.startColumn,
|
|
documentNode: this._node.ownerDocument,
|
|
});
|
|
} else {
|
|
// FIXME: Is it possible for a CSSRule to have a sourceLine without its selectorList having a sourceRange? Fall back just in case.
|
|
sourceCodeLocation = DOMNodeStyles.createSourceCodeLocation(payload.sourceURL, {
|
|
line: payload.sourceLine,
|
|
documentNode: this._node.ownerDocument,
|
|
});
|
|
}
|
|
|
|
if (styleSheet) {
|
|
if (!sourceCodeLocation && sourceRange)
|
|
sourceCodeLocation = styleSheet.createSourceCodeLocation(sourceRange.startLine, sourceRange.startColumn);
|
|
sourceCodeLocation = styleSheet.offsetSourceCodeLocation(sourceCodeLocation);
|
|
}
|
|
|
|
// COMPATIBILITY (iOS 13): CSS.CSSRule.groupings did not exist yet.
|
|
let groupings = (payload.groupings || payload.media || []).map((grouping) => {
|
|
// COMPATIBILITY (macOS 13, iOS 16) CSS.CSSRule.ruleId did not exist yet.
|
|
let ruleId = grouping.ruleId;
|
|
|
|
let ruleIdForMap = null;
|
|
if (ruleId) {
|
|
ruleIdForMap = `${ruleId.styleSheetId}-${ruleId.ordinal}`;
|
|
|
|
let existingGroupingForRuleId = this._groupingsMap.get(ruleIdForMap);
|
|
if (existingGroupingForRuleId) {
|
|
console.assert(existingGroupingForRuleId.text === grouping.text);
|
|
console.assert(existingGroupingForRuleId.type === grouping.type);
|
|
return existingGroupingForRuleId;
|
|
}
|
|
}
|
|
|
|
let groupingType = WI.CSSManager.protocolGroupingTypeToEnum(grouping.type || grouping.source);
|
|
|
|
let location = {};
|
|
if (payload.range) {
|
|
location.line = payload.range.startLine;
|
|
location.column = payload.range.startColumn;
|
|
location.documentNode = this._node.ownerDocument;
|
|
}
|
|
|
|
// The style sheet may be different from the style rule's style sheet, since groupings are computed beyond
|
|
// `@import` boundaries, and an `@import` statement from another style sheet may have been wrapped in
|
|
// another `@` rule.
|
|
let groupingStyleSheet = ruleId ? WI.cssManager.styleSheetForIdentifier(ruleId.styleSheetId) : null;
|
|
|
|
let groupingSourceCodeLocation = WI.DOMNodeStyles.createSourceCodeLocation(grouping.sourceURL, location);
|
|
let offsetGroupingSourceCodeLocation = styleSheet?.offsetSourceCodeLocation(groupingSourceCodeLocation) ?? groupingSourceCodeLocation;
|
|
|
|
let cssGrouping = new WI.CSSGrouping(this, groupingType, {
|
|
ownerStyleSheet: groupingStyleSheet,
|
|
id: grouping.ruleId,
|
|
text: grouping.text,
|
|
sourceCodeLocation: offsetGroupingSourceCodeLocation,
|
|
});
|
|
|
|
if (ruleIdForMap)
|
|
this._groupingsMap.set(ruleIdForMap, cssGrouping);
|
|
|
|
return cssGrouping;
|
|
});
|
|
|
|
if (rule) {
|
|
rule.update(sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings);
|
|
return rule;
|
|
}
|
|
|
|
if (styleSheet)
|
|
this._trackedStyleSheets.add(styleSheet);
|
|
|
|
rule = new WI.CSSRule(this, styleSheet, id, type, sourceCodeLocation, selectorText, selectors, matchedSelectorIndices, style, groupings, payload.isImplicitlyNested);
|
|
|
|
if (mapKey)
|
|
this._rulesMap.set(mapKey, rule);
|
|
|
|
return rule;
|
|
}
|
|
|
|
_markAsNeedsRefresh()
|
|
{
|
|
this._needsRefresh = true;
|
|
this.dispatchEventToListeners(WI.DOMNodeStyles.Event.NeedsRefresh);
|
|
}
|
|
|
|
_handleCSSStyleSheetContentDidChange(event)
|
|
{
|
|
let styleSheet = event.target;
|
|
if (!this._trackedStyleSheets.has(styleSheet))
|
|
return;
|
|
|
|
// Ignore the stylesheet we know we just changed and handled above.
|
|
if (styleSheet === this._ignoreNextContentDidChangeForStyleSheet) {
|
|
this._ignoreNextContentDidChangeForStyleSheet = null;
|
|
return;
|
|
}
|
|
|
|
this._markAsNeedsRefresh();
|
|
}
|
|
|
|
_updateStyleCascade()
|
|
{
|
|
var cascadeOrderedStyleDeclarations = this._collectStylesInCascadeOrder(this._matchedRules, this._inlineStyle, this._attributesStyle);
|
|
|
|
for (var i = 0; i < this._inheritedRules.length; ++i) {
|
|
var inheritedStyleInfo = this._inheritedRules[i];
|
|
var inheritedCascadeOrder = this._collectStylesInCascadeOrder(inheritedStyleInfo.matchedRules, inheritedStyleInfo.inlineStyle, null);
|
|
cascadeOrderedStyleDeclarations.pushAll(inheritedCascadeOrder);
|
|
}
|
|
|
|
this._orderedStyles = cascadeOrderedStyleDeclarations;
|
|
|
|
this._propertyNameToEffectivePropertyMap = {};
|
|
|
|
this._associateRelatedProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
|
|
this._markOverriddenProperties(cascadeOrderedStyleDeclarations, this._propertyNameToEffectivePropertyMap);
|
|
this._collectCSSVariables(cascadeOrderedStyleDeclarations);
|
|
|
|
for (let pseudoElementInfo of this._pseudoElements.values()) {
|
|
pseudoElementInfo.orderedStyles = this._collectStylesInCascadeOrder(pseudoElementInfo.matchedRules, null, null);
|
|
this._associateRelatedProperties(pseudoElementInfo.orderedStyles);
|
|
this._markOverriddenProperties(pseudoElementInfo.orderedStyles);
|
|
}
|
|
}
|
|
|
|
_collectStylesInCascadeOrder(matchedRules, inlineStyle, attributesStyle)
|
|
{
|
|
var result = [];
|
|
|
|
// Inline style has the greatest specificity. So it goes first in the cascade order.
|
|
if (inlineStyle)
|
|
result.push(inlineStyle);
|
|
|
|
var userAndUserAgentStyles = [];
|
|
|
|
for (var i = 0; i < matchedRules.length; ++i) {
|
|
var rule = matchedRules[i];
|
|
|
|
// Only append to the result array here for author and inspector rules since attribute
|
|
// styles come between author rules and user/user agent rules.
|
|
switch (rule.type) {
|
|
case WI.CSSStyleSheet.Type.Inspector:
|
|
case WI.CSSStyleSheet.Type.Author:
|
|
result.push(rule.style);
|
|
break;
|
|
|
|
case WI.CSSStyleSheet.Type.User:
|
|
case WI.CSSStyleSheet.Type.UserAgent:
|
|
userAndUserAgentStyles.push(rule.style);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Style properties from HTML attributes are next.
|
|
if (attributesStyle)
|
|
result.push(attributesStyle);
|
|
|
|
// Finally add the user and user stylesheet's matched style rules we collected earlier.
|
|
result.pushAll(userAndUserAgentStyles);
|
|
|
|
return result;
|
|
}
|
|
|
|
_markOverriddenProperties(styles, propertyNameToEffectiveProperty)
|
|
{
|
|
propertyNameToEffectiveProperty = propertyNameToEffectiveProperty || {};
|
|
|
|
function isOverriddenByRelatedShorthand(property) {
|
|
let shorthand = property.relatedShorthandProperty;
|
|
if (!shorthand)
|
|
return false;
|
|
|
|
if (property.important && !shorthand.important)
|
|
return false;
|
|
|
|
if (!property.important && shorthand.important)
|
|
return true;
|
|
|
|
if (property.ownerStyle === shorthand.ownerStyle)
|
|
return shorthand.index > property.index;
|
|
|
|
let propertyStyleIndex = styles.indexOf(property.ownerStyle);
|
|
let shorthandStyleIndex = styles.indexOf(shorthand.ownerStyle);
|
|
return shorthandStyleIndex > propertyStyleIndex;
|
|
}
|
|
|
|
for (var i = 0; i < styles.length; ++i) {
|
|
var style = styles[i];
|
|
var properties = style.enabledProperties;
|
|
|
|
for (var j = 0; j < properties.length; ++j) {
|
|
var property = properties[j];
|
|
if (!property.attached || !property.valid) {
|
|
property.overridden = false;
|
|
continue;
|
|
}
|
|
|
|
if (style.inherited && !property.inherited) {
|
|
property.overridden = false;
|
|
continue;
|
|
}
|
|
|
|
var canonicalName = property.canonicalName;
|
|
if (canonicalName in propertyNameToEffectiveProperty) {
|
|
var effectiveProperty = propertyNameToEffectiveProperty[canonicalName];
|
|
|
|
if (effectiveProperty.ownerStyle === property.ownerStyle) {
|
|
if (effectiveProperty.important && !property.important) {
|
|
property.overridden = true;
|
|
property.overridingProperty = effectiveProperty;
|
|
continue;
|
|
}
|
|
} else if (effectiveProperty.important || !property.important || effectiveProperty.ownerStyle.node !== property.ownerStyle.node) {
|
|
property.overridden = true;
|
|
property.overridingProperty = effectiveProperty;
|
|
continue;
|
|
}
|
|
|
|
if (!property.anonymous) {
|
|
effectiveProperty.overridden = true;
|
|
effectiveProperty.overridingProperty = property;
|
|
}
|
|
}
|
|
|
|
if (isOverriddenByRelatedShorthand(property)) {
|
|
property.overridden = true;
|
|
property.overridingProperty = property.relatedShorthandProperty;
|
|
} else
|
|
property.overridden = false;
|
|
|
|
propertyNameToEffectiveProperty[canonicalName] = property;
|
|
}
|
|
}
|
|
}
|
|
|
|
_associateRelatedProperties(styles, propertyNameToEffectiveProperty)
|
|
{
|
|
for (var i = 0; i < styles.length; ++i) {
|
|
var properties = styles[i].enabledProperties;
|
|
|
|
var knownShorthands = {};
|
|
|
|
for (var j = 0; j < properties.length; ++j) {
|
|
var property = properties[j];
|
|
|
|
if (!property.valid)
|
|
continue;
|
|
|
|
if (!WI.CSSKeywordCompletions.LonghandNamesForShorthandProperty.has(property.name))
|
|
continue;
|
|
|
|
if (knownShorthands[property.canonicalName] && !knownShorthands[property.canonicalName].overridden) {
|
|
console.assert(property.overridden);
|
|
continue;
|
|
}
|
|
|
|
knownShorthands[property.canonicalName] = property;
|
|
}
|
|
|
|
for (var j = 0; j < properties.length; ++j) {
|
|
var property = properties[j];
|
|
|
|
if (!property.valid)
|
|
continue;
|
|
|
|
var shorthandProperty = null;
|
|
|
|
if (!isEmptyObject(knownShorthands)) {
|
|
var possibleShorthands = WI.CSSKeywordCompletions.ShorthandNamesForLongHandProperty.get(property.canonicalName) || [];
|
|
for (var k = 0; k < possibleShorthands.length; ++k) {
|
|
if (possibleShorthands[k] in knownShorthands) {
|
|
shorthandProperty = knownShorthands[possibleShorthands[k]];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!shorthandProperty || shorthandProperty.overridden !== property.overridden) {
|
|
property.relatedShorthandProperty = null;
|
|
property.clearRelatedLonghandProperties();
|
|
continue;
|
|
}
|
|
|
|
shorthandProperty.addRelatedLonghandProperty(property);
|
|
property.relatedShorthandProperty = shorthandProperty;
|
|
|
|
if (propertyNameToEffectiveProperty && propertyNameToEffectiveProperty[shorthandProperty.canonicalName] === shorthandProperty)
|
|
propertyNameToEffectiveProperty[property.canonicalName] = property;
|
|
}
|
|
}
|
|
}
|
|
|
|
_collectCSSVariables(styles)
|
|
{
|
|
this._allCSSVariables = new Set;
|
|
this._usedCSSVariables = new Set;
|
|
|
|
for (let style of styles) {
|
|
for (let property of style.enabledProperties) {
|
|
if (property.isVariable)
|
|
this._allCSSVariables.add(property.name);
|
|
|
|
let variables = WI.CSSProperty.findVariableNames(property.value);
|
|
|
|
if (!style.inherited) {
|
|
// FIXME: <https://webkit.org/b/226648> Support the case of variables declared on matching styles but not used anywhere.
|
|
this._usedCSSVariables.addAll(variables);
|
|
continue;
|
|
}
|
|
|
|
// Always collect variables used in values of inheritable properties.
|
|
if (WI.CSSKeywordCompletions.InheritedProperties.has(property.name)) {
|
|
this._usedCSSVariables.addAll(variables);
|
|
continue;
|
|
}
|
|
|
|
// For variables from inherited styles, leverage the fact that styles are already sorted in cascade order to support inherited variables referencing other variables.
|
|
// If the variable was found to be used before, collect any variables used in its declaration value
|
|
// (if any variables are found, this isn't the end of the variable reference chain in the inheritance stack).
|
|
if (property.isVariable && this._usedCSSVariables.has(property.name))
|
|
this._usedCSSVariables.addAll(variables);
|
|
}
|
|
}
|
|
}
|
|
|
|
_isPropertyFoundInMatchingRules(propertyName)
|
|
{
|
|
return this._orderedStyles.some((style) => {
|
|
return style.enabledProperties.some((property) => property.name === propertyName);
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.DOMNodeStyles.Event = {
|
|
NeedsRefresh: "dom-node-styles-needs-refresh",
|
|
Refreshed: "dom-node-styles-refreshed"
|
|
};
|
|
|
|
/* Models/DOMStorageObject.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMStorageObject = class DOMStorageObject extends WI.Object
|
|
{
|
|
constructor(id, host, isLocalStorage)
|
|
{
|
|
super();
|
|
|
|
this._id = id;
|
|
this._host = host;
|
|
this._isLocalStorage = isLocalStorage;
|
|
this._entries = new Map;
|
|
}
|
|
|
|
// Public
|
|
|
|
get id() { return this._id; }
|
|
get host() { return this._host; }
|
|
get entries() { return this._entries; }
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.DOMStorageObject.HostCookieKey] = this.host;
|
|
cookie[WI.DOMStorageObject.LocalStorageCookieKey] = this.isLocalStorage();
|
|
}
|
|
|
|
isLocalStorage()
|
|
{
|
|
return this._isLocalStorage;
|
|
}
|
|
|
|
getEntries(callback)
|
|
{
|
|
function innerCallback(error, entries)
|
|
{
|
|
if (error)
|
|
return;
|
|
|
|
for (let [key, value] of entries) {
|
|
if (!key || !value)
|
|
continue;
|
|
|
|
this._entries.set(key, value);
|
|
}
|
|
|
|
callback(error, entries);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMStorageAgent.getDOMStorageItems(this._id, innerCallback.bind(this));
|
|
}
|
|
|
|
removeItem(key)
|
|
{
|
|
console.assert(this._entries.has(key));
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.DOMStorageAgent.removeDOMStorageItem(this._id, key);
|
|
}
|
|
|
|
setItem(key, value)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
return target.DOMStorageAgent.setDOMStorageItem(this._id, key, value);
|
|
}
|
|
|
|
clear()
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (iOS 13.4): DOMStorage.clearDOMStorageItems did not exist yet.
|
|
if (!target.hasCommand("DOMStorage.clearDOMStorageItems")) {
|
|
let promises = [];
|
|
for (let key of this._entries.keys())
|
|
promises.push(this.removeItem(key));
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
return target.DOMStorageAgent.clearDOMStorageItems(this._id);
|
|
}
|
|
|
|
// DOMStorageManager
|
|
|
|
itemsCleared()
|
|
{
|
|
this._entries.clear();
|
|
|
|
this.dispatchEventToListeners(WI.DOMStorageObject.Event.ItemsCleared);
|
|
}
|
|
|
|
itemRemoved(key)
|
|
{
|
|
let removed = this._entries.delete(key);
|
|
console.assert(removed);
|
|
|
|
this.dispatchEventToListeners(WI.DOMStorageObject.Event.ItemRemoved, {key});
|
|
}
|
|
|
|
itemAdded(key, value)
|
|
{
|
|
console.assert(!this._entries.has(key));
|
|
this._entries.set(key, value);
|
|
|
|
this.dispatchEventToListeners(WI.DOMStorageObject.Event.ItemAdded, {key, value});
|
|
}
|
|
|
|
itemUpdated(key, oldValue, newValue)
|
|
{
|
|
console.assert(this._entries.get(key) === oldValue);
|
|
this._entries.set(key, newValue);
|
|
|
|
this.dispatchEventToListeners(WI.DOMStorageObject.Event.ItemUpdated, {key, oldValue, newValue});
|
|
}
|
|
};
|
|
|
|
WI.DOMStorageObject.TypeIdentifier = "dom-storage";
|
|
WI.DOMStorageObject.HostCookieKey = "dom-storage-object-host";
|
|
WI.DOMStorageObject.LocalStorageCookieKey = "dom-storage-object-local-storage";
|
|
|
|
WI.DOMStorageObject.Event = {
|
|
ItemsCleared: "dom-storage-object-items-cleared",
|
|
ItemAdded: "dom-storage-object-item-added",
|
|
ItemRemoved: "dom-storage-object-item-removed",
|
|
ItemUpdated: "dom-storage-object-updated",
|
|
};
|
|
|
|
/* Models/DOMStyleable.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMStyleable = class DOMStyleable
|
|
{
|
|
constructor(node, {pseudoId} = {})
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, node);
|
|
console.assert(!pseudoId || Object.values(WI.CSSManager.PseudoSelectorNames).includes(pseudoId), pseudoId);
|
|
|
|
this._node = node;
|
|
this._pseudoId = pseudoId || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload({nodeId, pseudoId})
|
|
{
|
|
return new WI.DOMStyleable(WI.domManager.nodeForId(nodeId), {pseudoId});
|
|
}
|
|
|
|
// Public
|
|
|
|
get node() { return this._node; }
|
|
get pseudoId() { return this._pseudoId; }
|
|
|
|
get displayName()
|
|
{
|
|
if (this._pseudoId)
|
|
return WI.CSSManager.displayNameForPseudoId(this._pseudoId);
|
|
return this._node.displayName;
|
|
}
|
|
};
|
|
|
|
/* Models/DOMTree.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMTree = class DOMTree extends WI.Object
|
|
{
|
|
constructor(frame)
|
|
{
|
|
super();
|
|
|
|
this._frame = frame;
|
|
|
|
this._rootDOMNode = null;
|
|
this._requestIdentifier = 0;
|
|
|
|
this._frame.addEventListener(WI.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextChanged, this);
|
|
|
|
WI.domManager.addEventListener(WI.DOMManager.Event.DocumentUpdated, this._documentUpdated, this);
|
|
|
|
// Only add extra event listeners when not the main frame. Since DocumentUpdated is enough for the main frame.
|
|
if (!this._frame.isMainFrame()) {
|
|
WI.domManager.addEventListener(WI.DOMManager.Event.NodeRemoved, this._nodeRemoved, this);
|
|
this._frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._frameMainResourceDidChange, this);
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get frame() { return this._frame; }
|
|
|
|
disconnect()
|
|
{
|
|
this._frame.removeEventListener(WI.Frame.Event.PageExecutionContextChanged, this._framePageExecutionContextChanged, this);
|
|
|
|
WI.domManager.removeEventListener(WI.DOMManager.Event.DocumentUpdated, this._documentUpdated, this);
|
|
|
|
if (!this._frame.isMainFrame()) {
|
|
WI.domManager.removeEventListener(WI.DOMManager.Event.NodeRemoved, this._nodeRemoved, this);
|
|
this._frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._frameMainResourceDidChange, this);
|
|
}
|
|
}
|
|
|
|
invalidate()
|
|
{
|
|
// Set to null so it is fetched again next time requestRootDOMNode is called.
|
|
this._rootDOMNode = null;
|
|
|
|
// Clear the pending callbacks. It is the responsibility of the client to listen for
|
|
// the RootDOMNodeInvalidated event and request the root DOM node again.
|
|
this._pendingRootDOMNodeRequests = null;
|
|
|
|
if (this._invalidateTimeoutIdentifier)
|
|
return;
|
|
|
|
function performInvalidate()
|
|
{
|
|
this._invalidateTimeoutIdentifier = undefined;
|
|
|
|
this.dispatchEventToListeners(WI.DOMTree.Event.RootDOMNodeInvalidated);
|
|
}
|
|
|
|
// Delay the invalidation on a timeout to coalesce multiple calls to invalidate.
|
|
this._invalidateTimeoutIdentifier = setTimeout(performInvalidate.bind(this), 0);
|
|
}
|
|
|
|
requestRootDOMNode(callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
if (typeof callback !== "function")
|
|
return;
|
|
|
|
if (this._rootDOMNode) {
|
|
callback(this._rootDOMNode);
|
|
return;
|
|
}
|
|
|
|
if (!this._frame.isMainFrame() && !this._frame.pageExecutionContext) {
|
|
this._rootDOMNodeRequestWaitingForExecutionContext = true;
|
|
if (!this._pendingRootDOMNodeRequests)
|
|
this._pendingRootDOMNodeRequests = [];
|
|
this._pendingRootDOMNodeRequests.push(callback);
|
|
return;
|
|
}
|
|
|
|
if (this._pendingRootDOMNodeRequests) {
|
|
this._pendingRootDOMNodeRequests.push(callback);
|
|
return;
|
|
}
|
|
|
|
this._pendingRootDOMNodeRequests = [callback];
|
|
this._requestRootDOMNode();
|
|
}
|
|
|
|
// Private
|
|
|
|
_requestRootDOMNode()
|
|
{
|
|
console.assert(this._frame.isMainFrame() || this._frame.pageExecutionContext);
|
|
console.assert(this._pendingRootDOMNodeRequests.length);
|
|
|
|
// Bump the request identifier. This prevents pending callbacks for previous requests from completing.
|
|
var requestIdentifier = ++this._requestIdentifier;
|
|
|
|
function rootObjectAvailable(error, result)
|
|
{
|
|
// Check to see if we have been invalidated (if the callbacks were cleared).
|
|
if (!this._pendingRootDOMNodeRequests || requestIdentifier !== this._requestIdentifier)
|
|
return;
|
|
|
|
if (error) {
|
|
console.error(JSON.stringify(error));
|
|
|
|
this._rootDOMNode = null;
|
|
dispatchCallbacks.call(this);
|
|
return;
|
|
}
|
|
|
|
// Convert the RemoteObject to a DOMNode by asking the backend to push it to us.
|
|
var remoteObject = WI.RemoteObject.fromPayload(result);
|
|
remoteObject.pushNodeToFrontend(rootDOMNodeAvailable.bind(this, remoteObject));
|
|
}
|
|
|
|
function rootDOMNodeAvailable(remoteObject, nodeId)
|
|
{
|
|
remoteObject.release();
|
|
|
|
// Check to see if we have been invalidated (if the callbacks were cleared).
|
|
if (!this._pendingRootDOMNodeRequests || requestIdentifier !== this._requestIdentifier)
|
|
return;
|
|
|
|
if (!nodeId) {
|
|
this._rootDOMNode = null;
|
|
dispatchCallbacks.call(this);
|
|
return;
|
|
}
|
|
|
|
this._rootDOMNode = WI.domManager.nodeForId(nodeId);
|
|
|
|
console.assert(this._rootDOMNode);
|
|
if (!this._rootDOMNode) {
|
|
dispatchCallbacks.call(this);
|
|
return;
|
|
}
|
|
|
|
// Request the child nodes since the root node is often not shown in the UI,
|
|
// and the child nodes will be needed immediately.
|
|
this._rootDOMNode.getChildNodes(dispatchCallbacks.bind(this));
|
|
}
|
|
|
|
function mainDocumentAvailable(document)
|
|
{
|
|
this._rootDOMNode = document;
|
|
|
|
dispatchCallbacks.call(this);
|
|
}
|
|
|
|
function dispatchCallbacks()
|
|
{
|
|
// Check to see if we have been invalidated (if the callbacks were cleared).
|
|
if (!this._pendingRootDOMNodeRequests || requestIdentifier !== this._requestIdentifier)
|
|
return;
|
|
|
|
for (var i = 0; i < this._pendingRootDOMNodeRequests.length; ++i)
|
|
this._pendingRootDOMNodeRequests[i](this._rootDOMNode);
|
|
this._pendingRootDOMNodeRequests = null;
|
|
}
|
|
|
|
// For the main frame we can use the more straight forward requestDocument function. For
|
|
// child frames we need to do a more roundabout approach since the protocol does not include
|
|
// a specific way to request a document given a frame identifier. The child frame approach
|
|
// involves evaluating the JavaScript "document" and resolving that into a DOMNode.
|
|
if (this._frame.isMainFrame())
|
|
WI.domManager.requestDocument(mainDocumentAvailable.bind(this));
|
|
else {
|
|
let target = WI.assumingMainTarget();
|
|
var contextId = this._frame.pageExecutionContext.id;
|
|
target.RuntimeAgent.evaluate.invoke({expression: appendWebInspectorSourceURL("document"), objectGroup: "", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, contextId, returnByValue: false, generatePreview: false}, rootObjectAvailable.bind(this));
|
|
}
|
|
}
|
|
|
|
_nodeRemoved(event)
|
|
{
|
|
console.assert(!this._frame.isMainFrame());
|
|
|
|
if (event.data.node !== this._rootDOMNode)
|
|
return;
|
|
|
|
this.invalidate();
|
|
}
|
|
|
|
_documentUpdated(event)
|
|
{
|
|
this.invalidate();
|
|
}
|
|
|
|
_frameMainResourceDidChange(event)
|
|
{
|
|
console.assert(!this._frame.isMainFrame());
|
|
|
|
this.invalidate();
|
|
}
|
|
|
|
_framePageExecutionContextChanged(event)
|
|
{
|
|
if (this._rootDOMNodeRequestWaitingForExecutionContext) {
|
|
console.assert(this._frame.pageExecutionContext);
|
|
console.assert(this._pendingRootDOMNodeRequests && this._pendingRootDOMNodeRequests.length);
|
|
|
|
this._rootDOMNodeRequestWaitingForExecutionContext = false;
|
|
|
|
this._requestRootDOMNode();
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.DOMTree.Event = {
|
|
RootDOMNodeInvalidated: "dom-tree-root-dom-node-invalidated",
|
|
};
|
|
|
|
/* Models/DebuggerData.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DebuggerData = class DebuggerData
|
|
{
|
|
constructor(target)
|
|
{
|
|
console.assert(target instanceof WI.Target);
|
|
|
|
this._target = target;
|
|
|
|
this._paused = false;
|
|
this._pausing = false;
|
|
this._pauseReason = null;
|
|
this._pauseData = null;
|
|
this._stackTrace = null;
|
|
|
|
this._scriptIdMap = new Map;
|
|
this._scriptContentIdentifierMap = new Map;
|
|
|
|
this._makePausingAfterNextResume = false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get paused() { return this._paused; }
|
|
get pausing() { return this._pausing; }
|
|
get pauseReason() { return this._pauseReason; }
|
|
get pauseData() { return this._pauseData; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
|
|
get scripts()
|
|
{
|
|
return Array.from(this._scriptIdMap.values());
|
|
}
|
|
|
|
scriptForIdentifier(id)
|
|
{
|
|
return this._scriptIdMap.get(id);
|
|
}
|
|
|
|
scriptsForURL(url)
|
|
{
|
|
return this._scriptContentIdentifierMap.get(WI.urlWithoutUserQueryOrFragment(url)) || [];
|
|
}
|
|
|
|
// Protected (Called by DebuggerManager)
|
|
|
|
reset()
|
|
{
|
|
this._scriptIdMap.clear();
|
|
}
|
|
|
|
addScript(script)
|
|
{
|
|
this._scriptIdMap.set(script.id, script);
|
|
|
|
if (script.contentIdentifier) {
|
|
let url = WI.urlWithoutUserQueryOrFragment(script.contentIdentifier);
|
|
let scripts = this._scriptContentIdentifierMap.get(url);
|
|
if (!scripts) {
|
|
scripts = [];
|
|
this._scriptContentIdentifierMap.set(url, scripts);
|
|
}
|
|
scripts.push(script);
|
|
}
|
|
}
|
|
|
|
pauseIfNeeded()
|
|
{
|
|
if (this._paused || this._pausing)
|
|
return Promise.resolve();
|
|
|
|
this._pausing = true;
|
|
|
|
return this._target.DebuggerAgent.pause();
|
|
}
|
|
|
|
resumeIfNeeded()
|
|
{
|
|
if (!this._paused && !this._pausing)
|
|
return Promise.resolve();
|
|
|
|
this._pausing = false;
|
|
|
|
return this._target.DebuggerAgent.resume();
|
|
}
|
|
|
|
continueUntilNextRunLoop()
|
|
{
|
|
if (!this._paused || this._pausing)
|
|
return Promise.resolve();
|
|
|
|
// The backend will automatically start pausing
|
|
// after resuming, so we need to match that here.
|
|
this._makePausingAfterNextResume = true;
|
|
|
|
return this._target.DebuggerAgent.continueUntilNextRunLoop();
|
|
}
|
|
|
|
updateForPause(stackTrace, pauseReason, pauseData)
|
|
{
|
|
this._paused = true;
|
|
this._pausing = false;
|
|
this._pauseReason = pauseReason;
|
|
this._pauseData = pauseData;
|
|
this._stackTrace = stackTrace;
|
|
|
|
// We paused, no need for auto-pausing.
|
|
this._makePausingAfterNextResume = false;
|
|
}
|
|
|
|
updateForResume()
|
|
{
|
|
this._paused = false;
|
|
this._pausing = false;
|
|
this._pauseReason = null;
|
|
this._pauseData = null;
|
|
this._stackTrace = null;
|
|
|
|
// We resumed, but may be auto-pausing.
|
|
if (this._makePausingAfterNextResume) {
|
|
this._makePausingAfterNextResume = false;
|
|
this._pausing = true;
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Models/EventBreakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.EventBreakpoint = class EventBreakpoint extends WI.Breakpoint
|
|
{
|
|
constructor(type, {eventName, caseSensitive, isRegex, eventListener, disabled, actions, condition, ignoreCount, autoContinue} = {})
|
|
{
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.EventBreakpointTypes.Timer was removed.
|
|
if (type === "timer") {
|
|
switch (eventName) {
|
|
case "setInterval":
|
|
type = WI.EventBreakpoint.Type.Interval;
|
|
break;
|
|
|
|
case "setTimeout":
|
|
type = WI.EventBreakpoint.Type.Timeout;
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.assert(Object.values(WI.EventBreakpoint.Type).includes(type), type);
|
|
console.assert(!eventName || type === WI.EventBreakpoint.Type.Listener, eventName);
|
|
console.assert(caseSensitive === undefined || (type === WI.EventBreakpoint.Type.Listener && eventName), caseSensitive);
|
|
console.assert(isRegex === undefined || (type === WI.EventBreakpoint.Type.Listener && eventName), isRegex);
|
|
console.assert(!eventListener || type === WI.EventBreakpoint.Type.Listener, eventListener);
|
|
|
|
super({disabled, condition, actions, ignoreCount, autoContinue});
|
|
|
|
this._type = type;
|
|
this._eventName = eventName || null;
|
|
this._caseSensitive = caseSensitive !== undefined ? !!caseSensitive : true;
|
|
this._isRegex = isRegex !== undefined ? !!isRegex : false;
|
|
this._eventListener = eventListener || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static get supportsEditing()
|
|
{
|
|
// COMPATIBILITY (iOS 14): DOMDebugger.setEventBreakpoint did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setEventBreakpoint", "options");
|
|
}
|
|
|
|
static get supportsCaseSensitive()
|
|
{
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): DOMDebugger.setEventBreakpoint did not have a "caseSensitive" parameter yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setEventBreakpoint", "caseSensitive");
|
|
}
|
|
|
|
static get supportsIsRegex()
|
|
{
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): DOMDebugger.setEventBreakpoint did not have a "isRegex" parameter yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setEventBreakpoint", "isRegex");
|
|
}
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.EventBreakpoint(json.type, {
|
|
eventName: json.eventName,
|
|
caseSensitive: json.caseSensitive,
|
|
isRegex: json.isRegex,
|
|
disabled: json.disabled,
|
|
condition: json.condition,
|
|
actions: json.actions?.map((actionJSON) => WI.BreakpointAction.fromJSON(actionJSON)) || [],
|
|
ignoreCount: json.ignoreCount,
|
|
autoContinue: json.autoContinue,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get eventName() { return this._eventName; }
|
|
get caseSensitive() { return this._caseSensitive; }
|
|
get isRegex() { return this._isRegex; }
|
|
get eventListener() { return this._eventListener; }
|
|
|
|
get displayName()
|
|
{
|
|
switch (this) {
|
|
case WI.domDebuggerManager.allAnimationFramesBreakpoint:
|
|
return WI.repeatedUIString.allAnimationFrames();
|
|
|
|
case WI.domDebuggerManager.allIntervalsBreakpoint:
|
|
return WI.repeatedUIString.allIntervals();
|
|
|
|
case WI.domDebuggerManager.allListenersBreakpoint:
|
|
return WI.repeatedUIString.allEvents();
|
|
|
|
case WI.domDebuggerManager.allTimeoutsBreakpoint:
|
|
return WI.repeatedUIString.allTimeouts();
|
|
}
|
|
|
|
console.assert(this._type === WI.EventBreakpoint.Type.Listener && this._eventName, this);
|
|
|
|
if (this._isRegex)
|
|
return "/" + this._eventName + "/" + (!this._caseSensitive ? "i" : "");
|
|
|
|
let displayName = this._eventName;
|
|
if (!this._caseSensitive)
|
|
displayName = WI.UIString("%s (Case Insensitive)", "%s (Case Insensitive) @ EventBreakpoint", "Label for case-insensitive match pattern of an event breakpoint.").format(displayName);
|
|
return displayName;
|
|
}
|
|
|
|
get special()
|
|
{
|
|
switch (this) {
|
|
case WI.domDebuggerManager.allAnimationFramesBreakpoint:
|
|
case WI.domDebuggerManager.allIntervalsBreakpoint:
|
|
case WI.domDebuggerManager.allListenersBreakpoint:
|
|
case WI.domDebuggerManager.allTimeoutsBreakpoint:
|
|
return true;
|
|
}
|
|
|
|
return super.special;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
if (this._eventListener) {
|
|
// COMPATIBILITY (iOS 14): DOM.setBreakpointForEventListener did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("DOM.setBreakpointForEventListener", "options");
|
|
}
|
|
|
|
return WI.EventBreakpoint.supportsEditing || super.editable;
|
|
}
|
|
|
|
matches(eventName)
|
|
{
|
|
if (!eventName || this.disabled)
|
|
return false;
|
|
|
|
if (this._isRegex)
|
|
return (new RegExp(this._eventName, !this._caseSensitive ? "i" : "")).test(eventName);
|
|
|
|
if (!this._caseSensitive)
|
|
return eventName.toLowerCase() === this._eventName.toLowerCase();
|
|
|
|
return eventName === this._eventName;
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
console.assert(other instanceof WI.EventBreakpoint, other);
|
|
|
|
return this._eventName === other.eventName
|
|
&& this._caseSensitive === other.caseSensitive
|
|
&& this._isRegex === other.isRegex;
|
|
}
|
|
|
|
remove()
|
|
{
|
|
super.remove();
|
|
|
|
if (this._eventListener)
|
|
WI.domManager.removeBreakpointForEventListener(this._eventListener);
|
|
else
|
|
WI.domDebuggerManager.removeEventBreakpoint(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["event-breakpoint-type"] = this._type;
|
|
if (this._eventName) {
|
|
cookie["event-breakpoint-event-name"] = this._eventName;
|
|
cookie["event-breakpoint-case-sensitive"] = this._caseSensitive;
|
|
cookie["event-breakpoint-is-regex"] = this._isRegex;
|
|
}
|
|
if (this._eventListener)
|
|
cookie["event-breakpoint-event-listener"] = this._eventListener.eventListenerId;
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.type = this._type;
|
|
if (this._eventName) {
|
|
json.eventName = this._eventName;
|
|
json.caseSensitive = this._caseSensitive;
|
|
json.isRegex = this._isRegex;
|
|
}
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.eventBreakpoints.keyPath] = this._type + (this._eventName ? ":" + this._eventName + "-" + this._caseSensitive + "-" + this._isRegex : "");
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.EventBreakpoint.Type = {
|
|
AnimationFrame: "animation-frame",
|
|
Interval: "interval",
|
|
Listener: "listener",
|
|
Timeout: "timeout",
|
|
};
|
|
|
|
WI.EventBreakpoint.ReferencePage = WI.ReferencePage.EventBreakpoints;
|
|
|
|
/* Models/ExecutionContext.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ExecutionContext = class ExecutionContext
|
|
{
|
|
constructor(target, id, type, name, frame)
|
|
{
|
|
console.assert(target instanceof WI.Target);
|
|
console.assert(typeof id === "number" || id === WI.RuntimeManager.TopLevelExecutionContextIdentifier);
|
|
console.assert(Object.values(WI.ExecutionContext.Type).includes(type));
|
|
console.assert(!name || typeof name === "string");
|
|
console.assert(frame instanceof WI.Frame || id === WI.RuntimeManager.TopLevelExecutionContextIdentifier);
|
|
|
|
this._target = target;
|
|
this._id = id;
|
|
this._type = type || WI.ExecutionContext.Type.Internal;
|
|
this._name = name || "";
|
|
this._frame = frame || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static typeFromPayload(payload)
|
|
{
|
|
// COMPATIBILITY (iOS 13.1): `Runtime.ExecutionContextType` did not exist yet.
|
|
if (!("type" in payload))
|
|
return payload.isPageContext ? WI.ExecutionContext.Type.Normal : WI.ExecutionContext.Type.Internal;
|
|
|
|
switch (payload.type) {
|
|
case InspectorBackend.Enum.Runtime.ExecutionContextType.Normal:
|
|
return WI.ExecutionContext.Type.Normal;
|
|
case InspectorBackend.Enum.Runtime.ExecutionContextType.User:
|
|
return WI.ExecutionContext.Type.User;
|
|
case InspectorBackend.Enum.Runtime.ExecutionContextType.Internal:
|
|
return WI.ExecutionContext.Type.Internal;
|
|
}
|
|
|
|
console.assert(false, "Unknown Runtime.ExecutionContextType", payload.type);
|
|
return WI.ExecutionContext.Type.Internal;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get id() { return this._id; }
|
|
get type() { return this._type; }
|
|
get name() { return this._name; }
|
|
get frame() { return this._frame; }
|
|
};
|
|
|
|
WI.ExecutionContext.Type = {
|
|
Normal: "normal",
|
|
User: "user",
|
|
Internal: "internal",
|
|
};
|
|
|
|
/* Models/ExecutionContextList.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ExecutionContextList = class ExecutionContextList
|
|
{
|
|
constructor()
|
|
{
|
|
this._contexts = [];
|
|
this._pageExecutionContext = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get pageExecutionContext()
|
|
{
|
|
return this._pageExecutionContext;
|
|
}
|
|
|
|
get contexts()
|
|
{
|
|
return this._contexts;
|
|
}
|
|
|
|
add(context)
|
|
{
|
|
// COMPATIBILITY (iOS 13.0): Older iOS releases will send duplicates.
|
|
// Newer releases will not and this check should be removed eventually.
|
|
if (context.type === WI.ExecutionContext.Type.Normal && this._pageExecutionContext) {
|
|
console.assert(context.id === this._pageExecutionContext.id);
|
|
return;
|
|
}
|
|
|
|
this._contexts.push(context);
|
|
|
|
if (context.type === WI.ExecutionContext.Type.Normal && context.target.type === WI.TargetType.Page) {
|
|
console.assert(!this._pageExecutionContext);
|
|
this._pageExecutionContext = context;
|
|
}
|
|
}
|
|
|
|
clear()
|
|
{
|
|
this._contexts = [];
|
|
this._pageExecutionContext = null;
|
|
}
|
|
};
|
|
|
|
/* Models/FPSInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FPSInstrument = class FPSInstrument extends WI.Instrument
|
|
{
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.RenderingFrame;
|
|
}
|
|
};
|
|
|
|
/* Models/Font.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020-2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Font = class Font
|
|
{
|
|
constructor(name, variationAxes, {synthesizedBold, synthesizedOblique} = {})
|
|
{
|
|
this._name = name;
|
|
this._variationAxes = variationAxes;
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): CSS.Font.synthesizedBold and CSS.Font.synthesizedOblique did not exist yet.
|
|
this._synthesizedBold = !!synthesizedBold;
|
|
this._synthesizedOblique = !!synthesizedOblique;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
let variationAxes = payload.variationAxes.map((axisPayload) => WI.FontVariationAxis.fromPayload(axisPayload));
|
|
|
|
let synthesizedBold = payload.synthesizedBold;
|
|
let synthesizedOblique = payload.synthesizedOblique;
|
|
|
|
return new WI.Font(payload.displayName, variationAxes, {synthesizedBold, synthesizedOblique});
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get variationAxes() { return this._variationAxes; }
|
|
get synthesizedBold() { return this._synthesizedBold; }
|
|
get synthesizedOblique() { return this._synthesizedOblique; }
|
|
|
|
variationAxis(tag)
|
|
{
|
|
return this._variationAxes.find((axis) => axis.tag === tag);
|
|
}
|
|
};
|
|
|
|
/* Models/FontStyles.js */
|
|
|
|
/*
|
|
* Copyright (C) 2023 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FontStyles = class FontStyles
|
|
{
|
|
constructor(nodeStyles)
|
|
{
|
|
this._nodeStyles = nodeStyles;
|
|
|
|
this._featuresMap = new Map;
|
|
this._variationsMap = new Map;
|
|
this._propertiesMap = new Map;
|
|
|
|
this._authoredFontVariationSettingsMap = new Map;
|
|
this._effectiveWritablePropertyForNameMap = new Map;
|
|
|
|
// A change in the number of axes or their tags is considered a significant change.
|
|
// A change to the value of a known axis is not considered a significant change.
|
|
this._significantChangeSinceLastRefresh = true;
|
|
|
|
this._variationAxesTags = [];
|
|
this._registeredAxesTags = [];
|
|
|
|
const forceSignificantChange = true;
|
|
this.refresh(forceSignificantChange);
|
|
}
|
|
|
|
// Public
|
|
|
|
get featuresMap() { return this._featuresMap; }
|
|
|
|
get variationsMap() { return this._variationsMap; }
|
|
|
|
get propertiesMap() { return this._propertiesMap; }
|
|
|
|
get significantChangeSinceLastRefresh() { return this._significantChangeSinceLastRefresh; }
|
|
|
|
// Static
|
|
|
|
static fontPropertyForAxisTag(tag) {
|
|
const tagToPropertyMap = {
|
|
"wght": "font-weight",
|
|
"wdth": "font-stretch",
|
|
"slnt": "font-style",
|
|
"ital": "font-style",
|
|
}
|
|
|
|
return tagToPropertyMap[tag];
|
|
}
|
|
|
|
static axisValueToFontPropertyValue(tag, value)
|
|
{
|
|
switch (tag) {
|
|
case "wdth":
|
|
return `${value}%`;
|
|
case "slnt":
|
|
return `oblique ${value}deg`;
|
|
case "ital":
|
|
return value >= 1 ? "italic" : "normal";
|
|
default:
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
static fontPropertyValueToAxisValue(tag, value)
|
|
{
|
|
switch (tag) {
|
|
case "wdth":
|
|
return parseFloat(value);
|
|
case "ital":
|
|
case "slnt":
|
|
// See: https://w3c.github.io/csswg-drafts/css-fonts/#valdef-font-style-oblique-angle--90deg-90deg
|
|
const obliqueAngleDefaultValue = 14;
|
|
|
|
if (value === "normal")
|
|
return 0;
|
|
|
|
if (tag === "ital" && (value === "oblique" || value === "italic"))
|
|
return 1;
|
|
|
|
if (tag === "slnt" && (value === "oblique" || value === "italic"))
|
|
return obliqueAngleDefaultValue;
|
|
|
|
let degrees = value.match(/oblique (?<degrees>-?\d+(\.\d+)?)deg/)?.groups?.degrees;
|
|
if (degrees && tag === "ital")
|
|
return parseFloat(degrees) >= obliqueAngleDefaultValue ? 1 : 0; // The `ital` variation axis acts as an on/off toggle (0 = off, 1 = on).
|
|
|
|
if (degrees && tag === "slnt")
|
|
return parseFloat(degrees);
|
|
|
|
console.assert(false, `Unexpected font property value associated with variation axis ${tag}`, value);
|
|
break;
|
|
default:
|
|
return parseFloat(value);
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
writeFontVariation(tag, value)
|
|
{
|
|
let targetPropertyName = WI.FontStyles.fontPropertyForAxisTag(tag);
|
|
let targetPropertyValue;
|
|
if (targetPropertyName && !this._authoredFontVariationSettingsMap.has(tag))
|
|
targetPropertyValue = WI.FontStyles.axisValueToFontPropertyValue(tag, value);
|
|
else {
|
|
this._authoredFontVariationSettingsMap.set(tag, value);
|
|
let axes = [];
|
|
for (let [tag, value] of this._authoredFontVariationSettingsMap) {
|
|
axes.push(`"${tag}" ${value}`);
|
|
}
|
|
|
|
targetPropertyName = "font-variation-settings";
|
|
targetPropertyValue = axes.join(", ");
|
|
}
|
|
|
|
const createIfMissing = true;
|
|
let cssProperty = this._effectiveWritablePropertyForName(targetPropertyName, createIfMissing);
|
|
cssProperty.rawValue = targetPropertyValue;
|
|
}
|
|
|
|
refresh(forceSignificantChange)
|
|
{
|
|
this._effectiveWritablePropertyForNameMap.clear();
|
|
|
|
let prevVariationAxisTags = this._variationAxesTags.slice();
|
|
let prevRegisteredAxisTags = this._registeredAxesTags.slice();
|
|
this._variationAxesTags = [];
|
|
this._registeredAxesTags = [];
|
|
|
|
this._calculateFontProperties();
|
|
|
|
if (forceSignificantChange)
|
|
this._significantChangeSinceLastRefresh = true;
|
|
else
|
|
this._significantChangeSinceLastRefresh = !Array.shallowEqual(prevRegisteredAxisTags, this._registeredAxesTags) || !Array.shallowEqual(prevVariationAxisTags, this._variationAxesTags);
|
|
}
|
|
|
|
// Private
|
|
|
|
_calculateFontProperties()
|
|
{
|
|
this._featuresMap = this._calculateFontFeatureAxes(this._nodeStyles);
|
|
this._variationsMap = this._calculateFontVariationAxes(this._nodeStyles);
|
|
this._propertiesMap = this._calculateProperties({domNodeStyle: this._nodeStyles, featuresMap: this._featuresMap, variationsMap: this._variationsMap});
|
|
}
|
|
|
|
_calculateProperties(style)
|
|
{
|
|
let resultProperties = new Map;
|
|
|
|
this._populateProperty("font-size", style, resultProperties, {
|
|
keywordComputedReplacements: ["larger", "smaller", "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large"],
|
|
});
|
|
this._populateProperty("font-style", style, resultProperties, {
|
|
variations: ["ital", "slnt"],
|
|
keywordReplacements: new Map([
|
|
["oblique", "oblique 14deg"],
|
|
]),
|
|
});
|
|
this._populateProperty("font-weight", style, resultProperties, {
|
|
variations: ["wght"],
|
|
keywordComputedReplacements: ["bolder", "lighter"],
|
|
keywordReplacements: new Map([
|
|
["normal", "400"],
|
|
["bold", "700"],
|
|
]),
|
|
});
|
|
this._populateProperty("font-stretch", style, resultProperties, {
|
|
variations: ["wdth"],
|
|
keywordReplacements: new Map([
|
|
["ultra-condensed", "50%"],
|
|
["extra-condensed", "62.5%"],
|
|
["condensed", "75%"],
|
|
["semi-condensed", "87.5%"],
|
|
["normal", "100%"],
|
|
["semi-expanded", "112.5%"],
|
|
["expanded", "125%"],
|
|
["extra-expanded", "150%"],
|
|
["ultra-expanded", "200%"],
|
|
]),
|
|
});
|
|
|
|
this._populateProperty("font-variant-ligatures", style, resultProperties, {features: ["liga", "clig", "dlig", "hlig", "calt"]});
|
|
this._populateProperty("font-variant-position", style, resultProperties, {features: ["subs", "sups"]});
|
|
this._populateProperty("font-variant-caps", style, resultProperties, {features: ["smcp", "c2sc", "pcap", "c2pc", "unic", "titl"]});
|
|
this._populateProperty("font-variant-numeric", style, resultProperties, {features: ["lnum", "onum", "pnum", "tnum", "frac", "afrc", "ordn", "zero"]});
|
|
this._populateProperty("font-variant-alternates", style, resultProperties, {features: ["hist"] });
|
|
this._populateProperty("font-variant-east-asian", style, resultProperties, {features: ["jp78", "jp83", "jp90", "jp04", "smpl", "trad", "fwid", "pwid", "ruby"]});
|
|
|
|
return resultProperties;
|
|
}
|
|
|
|
_calculateFontFeatureAxes(domNodeStyle)
|
|
{
|
|
return this._parseFontFeatureOrVariationSettings(domNodeStyle, "font-feature-settings");
|
|
}
|
|
|
|
_calculateFontVariationAxes(domNodeStyle)
|
|
{
|
|
this._authoredFontVariationSettingsMap = this._parseFontFeatureOrVariationSettings(domNodeStyle, "font-variation-settings");
|
|
let resultAxes = new Map;
|
|
|
|
if (!this._nodeStyles.computedPrimaryFont)
|
|
return resultAxes;
|
|
|
|
for (let axis of this._nodeStyles.computedPrimaryFont.variationAxes) {
|
|
// `value` can be undefined.
|
|
resultAxes.set(axis.tag, {
|
|
tag: axis.tag,
|
|
name: axis.name,
|
|
minimumValue: axis.minimumValue,
|
|
maximumValue: axis.maximumValue,
|
|
defaultValue: axis.defaultValue,
|
|
value: this._authoredFontVariationSettingsMap.get(axis.tag),
|
|
});
|
|
|
|
this._variationAxesTags.push(axis.tag);
|
|
}
|
|
|
|
return resultAxes;
|
|
}
|
|
|
|
_parseFontFeatureOrVariationSettings(domNodeStyle, property)
|
|
{
|
|
let cssSettings = new Map;
|
|
let cssSettingsRawValue = this._computedPropertyValueForName(domNodeStyle, property);
|
|
|
|
if (cssSettingsRawValue !== "normal") {
|
|
for (let axis of cssSettingsRawValue.split(",")) {
|
|
// Tags can contains upper and lowercase latin letters, numbers, and spaces (only ending with space(s)). Values will be numbers, `on`, or `off`.
|
|
let [tag, value] = axis.match(WI.FontStyles.SettingPattern);
|
|
tag = tag.replaceAll(/["']/g, "");
|
|
if (!value || value === "on")
|
|
value = 1;
|
|
else if (value === "off")
|
|
value = 0;
|
|
cssSettings.set(tag, parseFloat(value));
|
|
}
|
|
}
|
|
|
|
return cssSettings;
|
|
}
|
|
|
|
_populateProperty(name, style, resultProperties, {variations, features, keywordComputedReplacements, keywordReplacements})
|
|
{
|
|
resultProperties.set(name, this._computeProperty(name, style, {variations, features, keywordComputedReplacements, keywordReplacements}));
|
|
}
|
|
|
|
_computeProperty(name, style, {variations, features, keywordComputedReplacements, keywordReplacements})
|
|
{
|
|
variations ??= [];
|
|
features ??= [];
|
|
keywordComputedReplacements ??= [];
|
|
keywordReplacements ??= new Map;
|
|
|
|
let resultProperties = {};
|
|
|
|
let value = this._effectivePropertyValueForName(style.domNodeStyle, name);
|
|
|
|
if (!value || value === "inherit" || keywordComputedReplacements.includes(value))
|
|
value = this._computedPropertyValueForName(style.domNodeStyle, name);
|
|
|
|
if (keywordReplacements.has(value))
|
|
value = keywordReplacements.get(value);
|
|
|
|
resultProperties.value = value;
|
|
|
|
for (let fontVariationTag of variations) {
|
|
let fontVariationAxis = style.variationsMap.get(fontVariationTag);
|
|
if (fontVariationAxis) {
|
|
resultProperties.variations ??= new Map;
|
|
resultProperties.variations.set(fontVariationTag, fontVariationAxis);
|
|
|
|
// Remove the tag so it is not presented twice.
|
|
style.variationsMap.delete(fontVariationTag);
|
|
|
|
this._registeredAxesTags.push(fontVariationTag);
|
|
}
|
|
}
|
|
|
|
for (let fontFeatureSetting of features) {
|
|
let featureSettingValue = style.featuresMap.get(fontFeatureSetting);
|
|
if (featureSettingValue || featureSettingValue === 0) {
|
|
resultProperties.features ??= new Map;
|
|
resultProperties.features.set(fontFeatureSetting, featureSettingValue);
|
|
|
|
// Remove the tag so it is not presented twice.
|
|
style.featuresMap.delete(fontFeatureSetting);
|
|
}
|
|
}
|
|
|
|
return resultProperties;
|
|
}
|
|
|
|
_effectivePropertyValueForName(domNodeStyle, name)
|
|
{
|
|
return domNodeStyle.effectivePropertyForName(name)?.value || "";
|
|
}
|
|
|
|
_effectiveWritablePropertyForName(name, createIfMissing)
|
|
{
|
|
let cssProperty = this._effectiveWritablePropertyForNameMap.get(name);
|
|
if (cssProperty)
|
|
return cssProperty;
|
|
|
|
// FIXME: <webkit.org/b/250127> Value for edited variation axis should be written to ideal CSS rule in cascade
|
|
let inlineCSSStyleDeclaration = this._nodeStyles.inlineStyle;
|
|
let properties = inlineCSSStyleDeclaration.visibleProperties;
|
|
|
|
cssProperty = properties.find(property => property.name === name);
|
|
if (!cssProperty && createIfMissing) {
|
|
cssProperty = inlineCSSStyleDeclaration.newBlankProperty(properties.length);
|
|
cssProperty.name = name;
|
|
}
|
|
|
|
if (cssProperty)
|
|
this._effectiveWritablePropertyForNameMap.set(name, cssProperty);
|
|
|
|
return cssProperty;
|
|
}
|
|
|
|
_computedPropertyValueForName(domNodeStyle, name)
|
|
{
|
|
return domNodeStyle.computedStyle?.propertyForName(name)?.value || "";
|
|
}
|
|
};
|
|
|
|
WI.FontStyles.SettingPattern = /[^\s"']+|["']([^"']*)["']/g;
|
|
|
|
/* Models/FontVariationAxis.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FontVariationAxis = class FontVariationAxis
|
|
{
|
|
constructor(name, tag, minimumValue, maximumValue, defaultValue)
|
|
{
|
|
console.assert(typeof tag === "string" && tag.length === 4, "Invalid font variation axis tag", tag);
|
|
|
|
this._name = name;
|
|
this._tag = tag;
|
|
this._minimumValue = minimumValue;
|
|
this._maximumValue = maximumValue;
|
|
this._defaultValue = defaultValue;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
return new WI.FontVariationAxis(payload.name, payload.tag, payload.minimumValue, payload.maximumValue, payload.defaultValue);
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get tag() { return this._tag; }
|
|
get minimumValue() { return this._minimumValue; }
|
|
get maximumValue() { return this._maximumValue; }
|
|
get defaultValue() { return this._defaultValue; }
|
|
};
|
|
|
|
/* Models/Frame.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Frame = class Frame extends WI.Object
|
|
{
|
|
constructor(id, name, securityOrigin, loaderIdentifier, mainResource)
|
|
{
|
|
super();
|
|
|
|
console.assert(id);
|
|
|
|
this._id = id;
|
|
|
|
this._name = null;
|
|
this._securityOrigin = null;
|
|
|
|
this._resourceCollection = new WI.ResourceCollection;
|
|
this._provisionalResourceCollection = new WI.ResourceCollection;
|
|
this._extraScriptCollection = new WI.ScriptCollection;
|
|
|
|
this._childFrameCollection = new WI.FrameCollection;
|
|
this._childFrameIdentifierMap = new Map;
|
|
|
|
this._parentFrame = null;
|
|
this._isMainFrame = false;
|
|
|
|
this._domContentReadyEventTimestamp = NaN;
|
|
this._loadEventTimestamp = NaN;
|
|
|
|
this._executionContextList = new WI.ExecutionContextList;
|
|
|
|
this.initialize(name, securityOrigin, loaderIdentifier, mainResource);
|
|
}
|
|
|
|
// Public
|
|
|
|
get resourceCollection() { return this._resourceCollection; }
|
|
get extraScriptCollection() { return this._extraScriptCollection; }
|
|
get childFrameCollection() { return this._childFrameCollection; }
|
|
|
|
initialize(name, securityOrigin, loaderIdentifier, mainResource)
|
|
{
|
|
console.assert(loaderIdentifier);
|
|
console.assert(mainResource);
|
|
|
|
var oldName = this._name;
|
|
var oldSecurityOrigin = this._securityOrigin;
|
|
var oldMainResource = this._mainResource;
|
|
|
|
this._name = name || null;
|
|
this._securityOrigin = securityOrigin || null;
|
|
this._loaderIdentifier = loaderIdentifier || null;
|
|
|
|
this._mainResource = mainResource;
|
|
this._mainResource._parentFrame = this;
|
|
|
|
if (oldMainResource && this._mainResource !== oldMainResource)
|
|
this._disassociateWithResource(oldMainResource);
|
|
|
|
this.removeAllResources();
|
|
this.removeAllChildFrames();
|
|
this.clearExecutionContexts();
|
|
this.clearProvisionalLoad();
|
|
|
|
if (this._mainResource !== oldMainResource)
|
|
this._dispatchMainResourceDidChangeEvent(oldMainResource);
|
|
|
|
if (this._securityOrigin !== oldSecurityOrigin)
|
|
this.dispatchEventToListeners(WI.Frame.Event.SecurityOriginDidChange, {oldSecurityOrigin});
|
|
|
|
if (this._name !== oldName)
|
|
this.dispatchEventToListeners(WI.Frame.Event.NameDidChange, {oldName});
|
|
}
|
|
|
|
startProvisionalLoad(provisionalMainResource)
|
|
{
|
|
console.assert(provisionalMainResource);
|
|
|
|
this._provisionalMainResource = provisionalMainResource;
|
|
this._provisionalMainResource._parentFrame = this;
|
|
|
|
this._provisionalLoaderIdentifier = provisionalMainResource.loaderIdentifier;
|
|
|
|
this._provisionalResourceCollection.clear();
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ProvisionalLoadStarted);
|
|
}
|
|
|
|
commitProvisionalLoad(securityOrigin)
|
|
{
|
|
console.assert(this._provisionalMainResource);
|
|
console.assert(this._provisionalLoaderIdentifier);
|
|
if (!this._provisionalLoaderIdentifier)
|
|
return;
|
|
|
|
var oldSecurityOrigin = this._securityOrigin;
|
|
var oldMainResource = this._mainResource;
|
|
|
|
this._securityOrigin = securityOrigin || null;
|
|
this._loaderIdentifier = this._provisionalLoaderIdentifier;
|
|
this._mainResource = this._provisionalMainResource;
|
|
|
|
this._domContentReadyEventTimestamp = NaN;
|
|
this._loadEventTimestamp = NaN;
|
|
|
|
if (oldMainResource && this._mainResource !== oldMainResource)
|
|
this._disassociateWithResource(oldMainResource);
|
|
|
|
this.removeAllResources();
|
|
|
|
this._resourceCollection = this._provisionalResourceCollection;
|
|
this._provisionalResourceCollection = new WI.ResourceCollection;
|
|
this._extraScriptCollection.clear();
|
|
|
|
this.clearExecutionContexts(true);
|
|
this.clearProvisionalLoad(true);
|
|
this.removeAllChildFrames();
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ProvisionalLoadCommitted);
|
|
|
|
if (this._mainResource !== oldMainResource)
|
|
this._dispatchMainResourceDidChangeEvent(oldMainResource);
|
|
|
|
if (this._securityOrigin !== oldSecurityOrigin)
|
|
this.dispatchEventToListeners(WI.Frame.Event.SecurityOriginDidChange, {oldSecurityOrigin});
|
|
}
|
|
|
|
clearProvisionalLoad(skipProvisionalLoadClearedEvent)
|
|
{
|
|
if (!this._provisionalLoaderIdentifier)
|
|
return;
|
|
|
|
this._provisionalLoaderIdentifier = null;
|
|
this._provisionalMainResource = null;
|
|
this._provisionalResourceCollection.clear();
|
|
|
|
if (!skipProvisionalLoadClearedEvent)
|
|
this.dispatchEventToListeners(WI.Frame.Event.ProvisionalLoadCleared);
|
|
}
|
|
|
|
get id()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
get loaderIdentifier()
|
|
{
|
|
return this._loaderIdentifier;
|
|
}
|
|
|
|
get provisionalLoaderIdentifier()
|
|
{
|
|
return this._provisionalLoaderIdentifier;
|
|
}
|
|
|
|
get name()
|
|
{
|
|
return this._name;
|
|
}
|
|
|
|
get securityOrigin()
|
|
{
|
|
return this._securityOrigin;
|
|
}
|
|
|
|
get url()
|
|
{
|
|
return this._mainResource.url;
|
|
}
|
|
|
|
get urlComponents()
|
|
{
|
|
return this._mainResource.urlComponents;
|
|
}
|
|
|
|
get domTree()
|
|
{
|
|
if (!this._domTree)
|
|
this._domTree = new WI.DOMTree(this);
|
|
return this._domTree;
|
|
}
|
|
|
|
get pageExecutionContext()
|
|
{
|
|
return this._executionContextList.pageExecutionContext;
|
|
}
|
|
|
|
get executionContextList()
|
|
{
|
|
return this._executionContextList;
|
|
}
|
|
|
|
clearExecutionContexts(committingProvisionalLoad)
|
|
{
|
|
if (this._executionContextList.contexts.length) {
|
|
let contexts = this._executionContextList.contexts.slice();
|
|
this._executionContextList.clear();
|
|
this.dispatchEventToListeners(WI.Frame.Event.ExecutionContextsCleared, {committingProvisionalLoad: !!committingProvisionalLoad, contexts});
|
|
}
|
|
}
|
|
|
|
addExecutionContext(context)
|
|
{
|
|
this._executionContextList.add(context);
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ExecutionContextAdded, {context});
|
|
|
|
if (this._executionContextList.pageExecutionContext === context)
|
|
this.dispatchEventToListeners(WI.Frame.Event.PageExecutionContextChanged);
|
|
}
|
|
|
|
get mainResource()
|
|
{
|
|
return this._mainResource;
|
|
}
|
|
|
|
get provisionalMainResource()
|
|
{
|
|
return this._provisionalMainResource;
|
|
}
|
|
|
|
get parentFrame()
|
|
{
|
|
return this._parentFrame;
|
|
}
|
|
|
|
get domContentReadyEventTimestamp()
|
|
{
|
|
return this._domContentReadyEventTimestamp;
|
|
}
|
|
|
|
get loadEventTimestamp()
|
|
{
|
|
return this._loadEventTimestamp;
|
|
}
|
|
|
|
isMainFrame()
|
|
{
|
|
return this._isMainFrame;
|
|
}
|
|
|
|
markAsMainFrame()
|
|
{
|
|
this._isMainFrame = true;
|
|
}
|
|
|
|
unmarkAsMainFrame()
|
|
{
|
|
this._isMainFrame = false;
|
|
}
|
|
|
|
markDOMContentReadyEvent(timestamp)
|
|
{
|
|
console.assert(isNaN(this._domContentReadyEventTimestamp));
|
|
|
|
this._domContentReadyEventTimestamp = timestamp || NaN;
|
|
}
|
|
|
|
markLoadEvent(timestamp)
|
|
{
|
|
console.assert(isNaN(this._loadEventTimestamp));
|
|
|
|
this._loadEventTimestamp = timestamp || NaN;
|
|
}
|
|
|
|
isDetached()
|
|
{
|
|
var frame = this;
|
|
while (frame) {
|
|
if (frame.isMainFrame())
|
|
return false;
|
|
frame = frame.parentFrame;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
childFrameForIdentifier(frameId)
|
|
{
|
|
return this._childFrameIdentifierMap.get(frameId) || null;
|
|
}
|
|
|
|
addChildFrame(frame)
|
|
{
|
|
console.assert(frame instanceof WI.Frame);
|
|
if (!(frame instanceof WI.Frame))
|
|
return;
|
|
|
|
if (frame._parentFrame === this)
|
|
return;
|
|
|
|
if (frame._parentFrame)
|
|
frame._parentFrame.removeChildFrame(frame);
|
|
|
|
this._childFrameCollection.add(frame);
|
|
this._childFrameIdentifierMap.set(frame._id, frame);
|
|
|
|
frame._parentFrame = this;
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ChildFrameWasAdded, {childFrame: frame});
|
|
}
|
|
|
|
removeChildFrame(frameOrFrameId)
|
|
{
|
|
console.assert(frameOrFrameId);
|
|
|
|
let childFrameId = frameOrFrameId;
|
|
if (childFrameId instanceof WI.Frame)
|
|
childFrameId = frameOrFrameId._id;
|
|
|
|
// Fetch the frame by id even if we were passed a WI.Frame.
|
|
// We do this incase the WI.Frame is a new object that isn't
|
|
// in _childFrameCollection, but the id is a valid child frame.
|
|
let childFrame = this.childFrameForIdentifier(childFrameId);
|
|
console.assert(childFrame instanceof WI.Frame);
|
|
if (!(childFrame instanceof WI.Frame))
|
|
return;
|
|
|
|
console.assert(childFrame.parentFrame === this);
|
|
|
|
this._childFrameCollection.remove(childFrame);
|
|
this._childFrameIdentifierMap.delete(childFrame._id);
|
|
|
|
childFrame._detachFromParentFrame();
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ChildFrameWasRemoved, {childFrame});
|
|
}
|
|
|
|
removeAllChildFrames()
|
|
{
|
|
this._detachFromParentFrame();
|
|
|
|
for (let childFrame of this._childFrameCollection)
|
|
childFrame.removeAllChildFrames();
|
|
|
|
this._childFrameCollection.clear();
|
|
this._childFrameIdentifierMap.clear();
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.AllChildFramesRemoved);
|
|
}
|
|
|
|
resourcesForURL(url, recursivelySearchChildFrames)
|
|
{
|
|
let resources = this._resourceCollection.resourcesForURL(url);
|
|
|
|
// Check the main resources of the child frames for the requested URL.
|
|
for (let childFrame of this._childFrameCollection) {
|
|
if (childFrame.mainResource.url === url)
|
|
resources.add(childFrame.mainResource);
|
|
}
|
|
|
|
if (recursivelySearchChildFrames) {
|
|
for (let childFrame of this._childFrameCollection)
|
|
resources.addAll(childFrame.resourcesForURL(url, recursivelySearchChildFrames));
|
|
}
|
|
|
|
return resources;
|
|
}
|
|
|
|
resourceCollectionForType(type)
|
|
{
|
|
return this._resourceCollection.resourceCollectionForType(type);
|
|
}
|
|
|
|
addResource(resource)
|
|
{
|
|
console.assert(resource instanceof WI.Resource);
|
|
if (!(resource instanceof WI.Resource))
|
|
return;
|
|
|
|
if (resource.parentFrame === this)
|
|
return;
|
|
|
|
if (resource.parentFrame)
|
|
resource.parentFrame.remove(resource);
|
|
|
|
this._associateWithResource(resource);
|
|
|
|
if (this._isProvisionalResource(resource)) {
|
|
this._provisionalResourceCollection.add(resource);
|
|
this.dispatchEventToListeners(WI.Frame.Event.ProvisionalResourceWasAdded, {resource});
|
|
} else {
|
|
this._resourceCollection.add(resource);
|
|
this.dispatchEventToListeners(WI.Frame.Event.ResourceWasAdded, {resource});
|
|
}
|
|
}
|
|
|
|
removeResource(resource)
|
|
{
|
|
// This does not remove provisional resources.
|
|
|
|
this._resourceCollection.remove(resource);
|
|
|
|
this._disassociateWithResource(resource);
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ResourceWasRemoved, {resource});
|
|
}
|
|
|
|
removeAllResources()
|
|
{
|
|
// This does not remove provisional resources, use clearProvisionalLoad for that.
|
|
|
|
if (!this._resourceCollection.size)
|
|
return;
|
|
|
|
for (let resource of this._resourceCollection)
|
|
this._disassociateWithResource(resource);
|
|
|
|
this._resourceCollection.clear();
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.AllResourcesRemoved);
|
|
}
|
|
|
|
addExtraScript(script)
|
|
{
|
|
this._extraScriptCollection.add(script);
|
|
|
|
this.dispatchEventToListeners(WI.Frame.Event.ExtraScriptAdded, {script});
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.Frame.MainResourceURLCookieKey] = this.mainResource.url.hash;
|
|
cookie[WI.Frame.IsMainFrameCookieKey] = this._isMainFrame;
|
|
}
|
|
|
|
// Private
|
|
|
|
_detachFromParentFrame()
|
|
{
|
|
if (this._domTree) {
|
|
this._domTree.disconnect();
|
|
this._domTree = null;
|
|
}
|
|
|
|
this._parentFrame = null;
|
|
}
|
|
|
|
_isProvisionalResource(resource)
|
|
{
|
|
return resource.loaderIdentifier && this._provisionalLoaderIdentifier && resource.loaderIdentifier === this._provisionalLoaderIdentifier;
|
|
}
|
|
|
|
_associateWithResource(resource)
|
|
{
|
|
console.assert(!resource._parentFrame);
|
|
if (resource._parentFrame)
|
|
return;
|
|
|
|
resource._parentFrame = this;
|
|
}
|
|
|
|
_disassociateWithResource(resource)
|
|
{
|
|
console.assert(resource.parentFrame === this);
|
|
if (resource.parentFrame !== this)
|
|
return;
|
|
|
|
resource._parentFrame = null;
|
|
}
|
|
|
|
_dispatchMainResourceDidChangeEvent(oldMainResource)
|
|
{
|
|
this.dispatchEventToListeners(WI.Frame.Event.MainResourceDidChange, {oldMainResource});
|
|
}
|
|
};
|
|
|
|
WI.Frame.Event = {
|
|
NameDidChange: "frame-name-did-change",
|
|
SecurityOriginDidChange: "frame-security-origin-did-change",
|
|
MainResourceDidChange: "frame-main-resource-did-change",
|
|
ProvisionalLoadStarted: "frame-provisional-load-started",
|
|
ProvisionalLoadCommitted: "frame-provisional-load-committed",
|
|
ProvisionalLoadCleared: "frame-provisional-load-cleared",
|
|
ProvisionalResourceWasAdded: "frame-provisional-resource-was-added",
|
|
ResourceWasAdded: "frame-resource-was-added",
|
|
ResourceWasRemoved: "frame-resource-was-removed",
|
|
AllResourcesRemoved: "frame-all-resources-removed",
|
|
ExtraScriptAdded: "frame-extra-script-added",
|
|
ChildFrameWasAdded: "frame-child-frame-was-added",
|
|
ChildFrameWasRemoved: "frame-child-frame-was-removed",
|
|
AllChildFramesRemoved: "frame-all-child-frames-removed",
|
|
PageExecutionContextChanged: "frame-page-execution-context-changed",
|
|
ExecutionContextAdded: "frame-execution-context-added",
|
|
ExecutionContextsCleared: "frame-execution-contexts-cleared"
|
|
};
|
|
|
|
WI.Frame.TypeIdentifier = "Frame";
|
|
WI.Frame.MainResourceURLCookieKey = "frame-main-resource-url";
|
|
WI.Frame.IsMainFrameCookieKey = "frame-is-main-frame";
|
|
|
|
/* Models/GarbageCollection.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.GarbageCollection = class GarbageCollection
|
|
{
|
|
constructor(type, startTime, endTime)
|
|
{
|
|
console.assert(endTime >= startTime);
|
|
|
|
this._type = type;
|
|
this._startTime = startTime;
|
|
this._endTime = endTime;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
let type = WI.GarbageCollection.Type.Full;
|
|
if (payload.type === InspectorBackend.Enum.Heap.GarbageCollectionType.Partial)
|
|
type = WI.GarbageCollection.Type.Partial;
|
|
|
|
return new WI.GarbageCollection(type, payload.startTime, payload.endTime);
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
let {type, startTime, endTime} = json;
|
|
return new WI.GarbageCollection(type, startTime, endTime);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
__type: "GarbageCollection",
|
|
type: this.type,
|
|
startTime: this.startTime,
|
|
endTime: this.endTime,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get startTime() { return this._startTime; }
|
|
get endTime() { return this._endTime; }
|
|
|
|
get duration()
|
|
{
|
|
return this._endTime - this._startTime;
|
|
}
|
|
};
|
|
|
|
WI.GarbageCollection.Type = {
|
|
Partial: "partial",
|
|
Full: "full",
|
|
};
|
|
|
|
/* Models/Geometry.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Point = class Point
|
|
{
|
|
constructor(x, y)
|
|
{
|
|
this.x = x || 0;
|
|
this.y = y || 0;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromEvent(event)
|
|
{
|
|
return new WI.Point(event.pageX, event.pageY);
|
|
}
|
|
|
|
static fromEventInElement(event, element)
|
|
{
|
|
let rect = element.getBoundingClientRect();
|
|
return new WI.Point(event.pageX - rect.x, event.pageY - rect.y);
|
|
}
|
|
|
|
// Public
|
|
|
|
toString()
|
|
{
|
|
return "WI.Point[" + this.x + "," + this.y + "]";
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.Point(this.x, this.y);
|
|
}
|
|
|
|
equals(anotherPoint)
|
|
{
|
|
return this.x === anotherPoint.x && this.y === anotherPoint.y;
|
|
}
|
|
|
|
distance(anotherPoint)
|
|
{
|
|
let dx = anotherPoint.x - this.x;
|
|
let dy = anotherPoint.y - this.y;
|
|
return Math.sqrt((dx * dx) + (dy * dy));
|
|
}
|
|
};
|
|
|
|
WI.Size = class Size
|
|
{
|
|
constructor(width, height)
|
|
{
|
|
this.width = width || 0;
|
|
this.height = height || 0;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.Size(json.width, json.height);
|
|
}
|
|
|
|
// Public
|
|
|
|
toString()
|
|
{
|
|
return "WI.Size[" + this.width + "," + this.height + "]";
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.Size(this.width, this.height);
|
|
}
|
|
|
|
equals(anotherSize)
|
|
{
|
|
return this.width === anotherSize.width && this.height === anotherSize.height;
|
|
}
|
|
};
|
|
|
|
WI.Size.ZERO_SIZE = new WI.Size(0, 0);
|
|
|
|
|
|
WI.Rect = class Rect
|
|
{
|
|
constructor(x, y, width, height)
|
|
{
|
|
this.origin = new WI.Point(x || 0, y || 0);
|
|
this.size = new WI.Size(width || 0, height || 0);
|
|
}
|
|
|
|
// Static
|
|
|
|
static rectFromClientRect(clientRect)
|
|
{
|
|
return new WI.Rect(clientRect.left, clientRect.top, clientRect.width, clientRect.height);
|
|
}
|
|
|
|
static unionOfRects(rects)
|
|
{
|
|
var union = rects[0];
|
|
for (var i = 1; i < rects.length; ++i)
|
|
union = union.unionWithRect(rects[i]);
|
|
return union;
|
|
}
|
|
|
|
// Public
|
|
|
|
toString()
|
|
{
|
|
return "WI.Rect[" + [this.origin.x, this.origin.y, this.size.width, this.size.height].join(", ") + "]";
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.Rect(this.origin.x, this.origin.y, this.size.width, this.size.height);
|
|
}
|
|
|
|
equals(anotherRect)
|
|
{
|
|
return this.origin.equals(anotherRect.origin) && this.size.equals(anotherRect.size);
|
|
}
|
|
|
|
inset(insets)
|
|
{
|
|
return new WI.Rect(
|
|
this.origin.x + insets.left,
|
|
this.origin.y + insets.top,
|
|
this.size.width - insets.left - insets.right,
|
|
this.size.height - insets.top - insets.bottom
|
|
);
|
|
}
|
|
|
|
pad(padding)
|
|
{
|
|
return new WI.Rect(
|
|
this.origin.x - padding,
|
|
this.origin.y - padding,
|
|
this.size.width + padding * 2,
|
|
this.size.height + padding * 2
|
|
);
|
|
}
|
|
|
|
minX()
|
|
{
|
|
return this.origin.x;
|
|
}
|
|
|
|
minY()
|
|
{
|
|
return this.origin.y;
|
|
}
|
|
|
|
midX()
|
|
{
|
|
return this.origin.x + (this.size.width / 2);
|
|
}
|
|
|
|
midY()
|
|
{
|
|
return this.origin.y + (this.size.height / 2);
|
|
}
|
|
|
|
maxX()
|
|
{
|
|
return this.origin.x + this.size.width;
|
|
}
|
|
|
|
maxY()
|
|
{
|
|
return this.origin.y + this.size.height;
|
|
}
|
|
|
|
intersectionWithRect(rect)
|
|
{
|
|
var x1 = Math.max(this.minX(), rect.minX());
|
|
var x2 = Math.min(this.maxX(), rect.maxX());
|
|
if (x1 > x2)
|
|
return WI.Rect.ZERO_RECT;
|
|
var intersection = new WI.Rect;
|
|
intersection.origin.x = x1;
|
|
intersection.size.width = x2 - x1;
|
|
var y1 = Math.max(this.minY(), rect.minY());
|
|
var y2 = Math.min(this.maxY(), rect.maxY());
|
|
if (y1 > y2)
|
|
return WI.Rect.ZERO_RECT;
|
|
intersection.origin.y = y1;
|
|
intersection.size.height = y2 - y1;
|
|
return intersection;
|
|
}
|
|
|
|
unionWithRect(rect)
|
|
{
|
|
var x = Math.min(this.minX(), rect.minX());
|
|
var y = Math.min(this.minY(), rect.minY());
|
|
var width = Math.max(this.maxX(), rect.maxX()) - x;
|
|
var height = Math.max(this.maxY(), rect.maxY()) - y;
|
|
return new WI.Rect(x, y, width, height);
|
|
}
|
|
|
|
round()
|
|
{
|
|
return new WI.Rect(
|
|
Math.floor(this.origin.x),
|
|
Math.floor(this.origin.y),
|
|
Math.ceil(this.size.width),
|
|
Math.ceil(this.size.height)
|
|
);
|
|
}
|
|
};
|
|
|
|
WI.Rect.ZERO_RECT = new WI.Rect(0, 0, 0, 0);
|
|
|
|
|
|
WI.EdgeInsets = class EdgeInsets
|
|
{
|
|
constructor(top, right, bottom, left)
|
|
{
|
|
console.assert(arguments.length === 1 || arguments.length === 4);
|
|
|
|
if (arguments.length === 1) {
|
|
this.top = top;
|
|
this.right = top;
|
|
this.bottom = top;
|
|
this.left = top;
|
|
} else if (arguments.length === 4) {
|
|
this.top = top;
|
|
this.right = right;
|
|
this.bottom = bottom;
|
|
this.left = left;
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
equals(anotherInset)
|
|
{
|
|
return this.top === anotherInset.top && this.right === anotherInset.right
|
|
&& this.bottom === anotherInset.bottom && this.left === anotherInset.left;
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.EdgeInsets(this.top, this.right, this.bottom, this.left);
|
|
}
|
|
};
|
|
|
|
WI.RectEdge = {
|
|
MIN_X: 0,
|
|
MIN_Y: 1,
|
|
MAX_X: 2,
|
|
MAX_Y: 3
|
|
};
|
|
|
|
WI.Quad = class Quad
|
|
{
|
|
constructor(quad)
|
|
{
|
|
this.points = [
|
|
new WI.Point(quad[0], quad[1]), // top left
|
|
new WI.Point(quad[2], quad[3]), // top right
|
|
new WI.Point(quad[4], quad[5]), // bottom right
|
|
new WI.Point(quad[6], quad[7]) // bottom left
|
|
];
|
|
|
|
this.width = Math.round(Math.sqrt(Math.pow(quad[0] - quad[2], 2) + Math.pow(quad[1] - quad[3], 2)));
|
|
this.height = Math.round(Math.sqrt(Math.pow(quad[0] - quad[6], 2) + Math.pow(quad[1] - quad[7], 2)));
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.Quad(json);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return this.toProtocol();
|
|
}
|
|
|
|
// Public
|
|
|
|
toProtocol()
|
|
{
|
|
return [
|
|
this.points[0].x, this.points[0].y,
|
|
this.points[1].x, this.points[1].y,
|
|
this.points[2].x, this.points[2].y,
|
|
this.points[3].x, this.points[3].y
|
|
];
|
|
}
|
|
};
|
|
|
|
WI.Polygon = class Polygon
|
|
{
|
|
constructor(points)
|
|
{
|
|
this.points = points;
|
|
}
|
|
|
|
// Public
|
|
|
|
bounds()
|
|
{
|
|
var minX = Number.MAX_VALUE;
|
|
var minY = Number.MAX_VALUE;
|
|
var maxX = -Number.MAX_VALUE;
|
|
var maxY = -Number.MAX_VALUE;
|
|
for (var point of this.points) {
|
|
minX = Math.min(minX, point.x);
|
|
maxX = Math.max(maxX, point.x);
|
|
minY = Math.min(minY, point.y);
|
|
maxY = Math.max(maxY, point.y);
|
|
}
|
|
return new WI.Rect(minX, minY, maxX - minX, maxY - minY);
|
|
}
|
|
};
|
|
|
|
/* Models/Gradient.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014, 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Gradient = class Gradient
|
|
{
|
|
constructor(type, stops)
|
|
{
|
|
this.type = type;
|
|
this.stops = stops;
|
|
}
|
|
|
|
// Static
|
|
|
|
static angleFromString(string)
|
|
{
|
|
let match = string.match(/([-\d\.]+)(\w+)/);
|
|
if (!match || !Object.values(WI.Gradient.AngleUnits).includes(match[2]))
|
|
return null;
|
|
|
|
return {value: parseFloat(match[1]), units: match[2]};
|
|
}
|
|
|
|
static fromString(cssString)
|
|
{
|
|
var type;
|
|
var openingParenthesisIndex = cssString.indexOf("(");
|
|
var typeString = cssString.substring(0, openingParenthesisIndex);
|
|
if (typeString.includes(WI.Gradient.Types.Linear))
|
|
type = WI.Gradient.Types.Linear;
|
|
else if (typeString.includes(WI.Gradient.Types.Radial))
|
|
type = WI.Gradient.Types.Radial;
|
|
else if (typeString.includes(WI.Gradient.Types.Conic))
|
|
type = WI.Gradient.Types.Conic;
|
|
else
|
|
return null;
|
|
|
|
var components = [];
|
|
var currentParams = [];
|
|
var currentParam = "";
|
|
var openParentheses = 0;
|
|
var ch = openingParenthesisIndex + 1;
|
|
var c = null;
|
|
while (c = cssString[ch]) {
|
|
if (c === "(")
|
|
openParentheses++;
|
|
if (c === ")")
|
|
openParentheses--;
|
|
|
|
var isComma = c === ",";
|
|
var isSpace = /\s/.test(c);
|
|
|
|
if (openParentheses === 0) {
|
|
if (isSpace) {
|
|
if (currentParam !== "")
|
|
currentParams.push(currentParam);
|
|
currentParam = "";
|
|
} else if (isComma) {
|
|
currentParams.push(currentParam);
|
|
components.push(currentParams);
|
|
currentParams = [];
|
|
currentParam = "";
|
|
}
|
|
}
|
|
|
|
if (openParentheses === -1) {
|
|
currentParams.push(currentParam);
|
|
components.push(currentParams);
|
|
break;
|
|
}
|
|
|
|
if (openParentheses > 0 || (!isComma && !isSpace))
|
|
currentParam += c;
|
|
|
|
ch++;
|
|
}
|
|
|
|
if (openParentheses !== -1)
|
|
return null;
|
|
|
|
let gradient = null;
|
|
switch (type) {
|
|
case WI.Gradient.Types.Linear:
|
|
gradient = WI.LinearGradient.fromComponents(components);
|
|
break;
|
|
|
|
case WI.Gradient.Types.Radial:
|
|
gradient = WI.RadialGradient.fromComponents(components);
|
|
break;
|
|
|
|
case WI.Gradient.Types.Conic:
|
|
gradient = WI.ConicGradient.fromComponents(components);
|
|
break;
|
|
}
|
|
|
|
if (gradient)
|
|
gradient.repeats = typeString.startsWith("repeating");
|
|
|
|
return gradient;
|
|
}
|
|
|
|
static stopsWithComponents(components)
|
|
{
|
|
// FIXME: handle lengths.
|
|
var stops = components.map(function(component) {
|
|
while (component.length) {
|
|
var color = WI.Color.fromString(component.shift());
|
|
if (!color)
|
|
continue;
|
|
|
|
var stop = {color};
|
|
if (component.length && component[0].substr(-1) === "%")
|
|
stop.offset = parseFloat(component.shift()) / 100;
|
|
return stop;
|
|
}
|
|
});
|
|
|
|
if (!stops.length)
|
|
return null;
|
|
|
|
for (var i = 0, count = stops.length; i < count; ++i) {
|
|
var stop = stops[i];
|
|
|
|
// If one of the stops failed to parse, then this is not a valid
|
|
// set of components for a gradient. So the whole thing is invalid.
|
|
if (!stop)
|
|
return null;
|
|
|
|
if (!stop.offset)
|
|
stop.offset = i / (count - 1);
|
|
}
|
|
|
|
return stops;
|
|
}
|
|
|
|
// Public
|
|
|
|
stringFromStops(stops)
|
|
{
|
|
var count = stops.length - 1;
|
|
return stops.map(function(stop, index) {
|
|
var str = stop.color;
|
|
if (stop.offset !== index / count)
|
|
str += " " + Math.round(stop.offset * 10_000) / 100 + "%";
|
|
return str;
|
|
}).join(", ");
|
|
}
|
|
|
|
// Public
|
|
|
|
get angleValue()
|
|
{
|
|
return this._angle.value.maxDecimals(2);
|
|
}
|
|
|
|
set angleValue(value)
|
|
{
|
|
this._angle.value = value;
|
|
}
|
|
|
|
get angleUnits()
|
|
{
|
|
return this._angle.units;
|
|
}
|
|
|
|
set angleUnits(units)
|
|
{
|
|
if (units === this._angle.units)
|
|
return;
|
|
|
|
this._angle.value = this._angleValueForUnits(units);
|
|
this._angle.units = units;
|
|
}
|
|
|
|
copy()
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
toString()
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
// Private
|
|
|
|
_angleValueForUnits(units)
|
|
{
|
|
if (units === this._angle.units)
|
|
return this._angle.value;
|
|
|
|
let deg = 0;
|
|
|
|
switch (this._angle.units) {
|
|
case WI.Gradient.AngleUnits.DEG:
|
|
deg = this._angle.value;
|
|
break;
|
|
|
|
case WI.Gradient.AngleUnits.RAD:
|
|
deg = this._angle.value * 180 / Math.PI;
|
|
break;
|
|
|
|
case WI.Gradient.AngleUnits.GRAD:
|
|
deg = this._angle.value / 400 * 360;
|
|
break;
|
|
|
|
case WI.Gradient.AngleUnits.TURN:
|
|
deg = this._angle.value * 360;
|
|
break;
|
|
}
|
|
|
|
switch (units) {
|
|
case WI.Gradient.AngleUnits.DEG:
|
|
return deg;
|
|
|
|
case WI.Gradient.AngleUnits.RAD:
|
|
return deg * Math.PI / 180;
|
|
|
|
case WI.Gradient.AngleUnits.GRAD:
|
|
return deg / 360 * 400;
|
|
|
|
case WI.Gradient.AngleUnits.TURN:
|
|
return deg / 360;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
};
|
|
|
|
WI.Gradient.Types = {
|
|
Linear: "linear-gradient",
|
|
Radial: "radial-gradient",
|
|
Conic: "conic-gradient",
|
|
};
|
|
|
|
WI.Gradient.AngleUnits = {
|
|
DEG: "deg",
|
|
RAD: "rad",
|
|
GRAD: "grad",
|
|
TURN: "turn",
|
|
};
|
|
|
|
WI.LinearGradient = class LinearGradient extends WI.Gradient
|
|
{
|
|
constructor(angle, stops)
|
|
{
|
|
super(WI.Gradient.Types.Linear, stops);
|
|
this._angle = angle;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromComponents(components)
|
|
{
|
|
let angle = {value: 180, units: WI.Gradient.AngleUnits.DEG};
|
|
|
|
if (components[0].length === 1 && !WI.Color.fromString(components[0][0])) {
|
|
angle = WI.Gradient.angleFromString(components[0][0]);
|
|
|
|
if (!angle)
|
|
return null;
|
|
|
|
components.shift();
|
|
} else if (components[0][0] === "to") {
|
|
components[0].shift();
|
|
switch (components[0].sort().join(" ")) {
|
|
case "top":
|
|
angle.value = 0;
|
|
break;
|
|
case "right top":
|
|
angle.value = 45;
|
|
break;
|
|
case "right":
|
|
angle.value = 90;
|
|
break;
|
|
case "bottom right":
|
|
angle.value = 135;
|
|
break;
|
|
case "bottom":
|
|
angle.value = 180;
|
|
break;
|
|
case "bottom left":
|
|
angle.value = 225;
|
|
break;
|
|
case "left":
|
|
angle.value = 270;
|
|
break;
|
|
case "left top":
|
|
angle.value = 315;
|
|
break;
|
|
default:
|
|
return null;
|
|
}
|
|
|
|
components.shift();
|
|
} else if (components[0].length !== 1 && !WI.Color.fromString(components[0][0])) {
|
|
// If the first component is not a color, then we're dealing with a
|
|
// legacy linear gradient format that we don't support.
|
|
return null;
|
|
}
|
|
|
|
let stops = WI.Gradient.stopsWithComponents(components);
|
|
if (!stops)
|
|
return null;
|
|
|
|
return new WI.LinearGradient(angle, stops);
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.LinearGradient(this._angle, this.stops.concat());
|
|
}
|
|
|
|
toString()
|
|
{
|
|
let str = "";
|
|
|
|
let deg = this._angleValueForUnits(WI.LinearGradient.AngleUnits.DEG);
|
|
if (deg === 0)
|
|
str += "to top";
|
|
else if (deg === 45)
|
|
str += "to top right";
|
|
else if (deg === 90)
|
|
str += "to right";
|
|
else if (deg === 135)
|
|
str += "to bottom right";
|
|
else if (deg === 225)
|
|
str += "to bottom left";
|
|
else if (deg === 270)
|
|
str += "to left";
|
|
else if (deg === 315)
|
|
str += "to top left";
|
|
else if (deg !== 180)
|
|
str += this.angleValue + this.angleUnits;
|
|
|
|
if (str)
|
|
str += ", ";
|
|
|
|
str += this.stringFromStops(this.stops);
|
|
|
|
return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")";
|
|
}
|
|
};
|
|
|
|
WI.RadialGradient = class RadialGradient extends WI.Gradient
|
|
{
|
|
constructor(sizing, stops)
|
|
{
|
|
super(WI.Gradient.Types.Radial, stops);
|
|
this.sizing = sizing;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromComponents(components)
|
|
{
|
|
let sizing = !WI.Color.fromString(components[0].join(" ")) ? components.shift().join(" ") : "";
|
|
|
|
let stops = WI.Gradient.stopsWithComponents(components);
|
|
if (!stops)
|
|
return null;
|
|
|
|
return new WI.RadialGradient(sizing, stops);
|
|
}
|
|
|
|
// Public
|
|
|
|
get angleValue()
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
set angleValue(value)
|
|
{
|
|
console.assert(false, "CSS radial gradients do not have an angle");
|
|
}
|
|
|
|
get angleUnits()
|
|
{
|
|
return "";
|
|
}
|
|
|
|
set angleUnits(units)
|
|
{
|
|
console.assert(false, "CSS radial gradients do not have an angle");
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.RadialGradient(this.sizing, this.stops.concat());
|
|
}
|
|
|
|
toString()
|
|
{
|
|
let str = this.sizing;
|
|
|
|
if (str)
|
|
str += ", ";
|
|
|
|
str += this.stringFromStops(this.stops);
|
|
|
|
return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")";
|
|
}
|
|
};
|
|
|
|
WI.ConicGradient = class ConicGradient extends WI.Gradient
|
|
{
|
|
constructor(angle, position, stops)
|
|
{
|
|
super(WI.Gradient.Types.Conic, stops);
|
|
|
|
this._angle = angle;
|
|
this._position = position;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromComponents(components)
|
|
{
|
|
let angle = {value: 0, units: WI.Gradient.AngleUnits.DEG};
|
|
let position = null;
|
|
let hasCustomAngleOrPosition = false;
|
|
|
|
if (components[0][0] == "from") {
|
|
components[0].shift();
|
|
angle = WI.Gradient.angleFromString(components[0][0]);
|
|
if (!angle)
|
|
return null;
|
|
components[0].shift();
|
|
hasCustomAngleOrPosition = true;
|
|
}
|
|
if (components[0][0] == "at") {
|
|
components[0].shift();
|
|
// FIXME: <https://webkit.org/b/234643> (Web Inspector: allow editing positions in gradient editor)
|
|
if (components[0].length <= 0)
|
|
return null;
|
|
position = components[0].join(" ");
|
|
hasCustomAngleOrPosition = true;
|
|
}
|
|
if (hasCustomAngleOrPosition)
|
|
components.shift();
|
|
|
|
let stops = WI.Gradient.stopsWithComponents(components);
|
|
if (!stops)
|
|
return null;
|
|
|
|
return new WI.ConicGradient(angle, position, stops);
|
|
}
|
|
|
|
// Public
|
|
|
|
copy()
|
|
{
|
|
return new WI.ConicGradient(this._angle, this._position, this.stops.concat());
|
|
}
|
|
|
|
toString()
|
|
{
|
|
let str = "";
|
|
|
|
if (this._angle.value)
|
|
str += `from ${this._angle.value}${this._angle.units}`;
|
|
|
|
if (this._position) {
|
|
if (str)
|
|
str += " ";
|
|
str += `at ${this._position}`;
|
|
}
|
|
|
|
if (str)
|
|
str += ", ";
|
|
|
|
str += this.stringFromStops(this.stops);
|
|
|
|
return (this.repeats ? "repeating-" : "") + this.type + "(" + str + ")";
|
|
}
|
|
};
|
|
|
|
/* Models/HeapAllocationsInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapAllocationsInstrument = class HeapAllocationsInstrument extends WI.Instrument
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._snapshotIntervalIdentifier = undefined;
|
|
}
|
|
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.HeapAllocations;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
// FIXME: Include a "track allocations" option for this instrument.
|
|
// FIXME: Include a periodic snapshot interval option for this instrument.
|
|
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.HeapAgent.startTracking();
|
|
}
|
|
|
|
// Periodic snapshots.
|
|
const snapshotInterval = 10_000;
|
|
this._snapshotIntervalIdentifier = setInterval(this._takeHeapSnapshot.bind(this), snapshotInterval);
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.HeapAgent.stopTracking();
|
|
}
|
|
|
|
window.clearInterval(this._snapshotIntervalIdentifier);
|
|
this._snapshotIntervalIdentifier = undefined;
|
|
}
|
|
|
|
// Private
|
|
|
|
_takeHeapSnapshot()
|
|
{
|
|
WI.heapManager.snapshot((error, timestamp, snapshotStringData) => {
|
|
let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
|
|
workerProxy.createSnapshot(snapshotStringData, ({objectId, snapshot: serializedSnapshot}) => {
|
|
let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
|
|
snapshot.snapshotStringData = snapshotStringData;
|
|
WI.timelineManager.heapSnapshotAdded(timestamp, snapshot);
|
|
});
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Models/HeapAllocationsTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapAllocationsTimelineRecord = class HeapAllocationsTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(timestamp, heapSnapshot)
|
|
{
|
|
super(WI.TimelineRecord.Type.HeapAllocations, timestamp, timestamp);
|
|
|
|
console.assert(typeof timestamp === "number");
|
|
console.assert(heapSnapshot instanceof WI.HeapSnapshotProxy);
|
|
|
|
this._timestamp = timestamp;
|
|
this._heapSnapshot = heapSnapshot;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
// NOTE: This just goes through and generates a new heap snapshot,
|
|
// it is not perfect but does what we want. It asynchronously creates
|
|
// a heap snapshot at the right time, and insert it into the active
|
|
// recording, which on an import should be the newly imported recording.
|
|
let {timestamp, title, snapshotStringData} = json;
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
let workerProxy = WI.HeapSnapshotWorkerProxy.singleton();
|
|
workerProxy.createImportedSnapshot(snapshotStringData, title, ({objectId, snapshot: serializedSnapshot}) => {
|
|
let snapshot = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot);
|
|
snapshot.snapshotStringData = snapshotStringData;
|
|
resolve(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
|
|
});
|
|
});
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
type: this.type,
|
|
timestamp: this._timestamp,
|
|
title: WI.TimelineTabContentView.displayNameForRecord(this),
|
|
snapshotStringData: this._heapSnapshot.snapshotStringData,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get timestamp() { return this._timestamp; }
|
|
get heapSnapshot() { return this._heapSnapshot; }
|
|
};
|
|
|
|
/* Models/IndexedDatabase.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.IndexedDatabase = class IndexedDatabase
|
|
{
|
|
constructor(name, securityOrigin, version, objectStores)
|
|
{
|
|
this._name = name;
|
|
this._securityOrigin = securityOrigin;
|
|
this._host = parseSecurityOrigin(securityOrigin).host;
|
|
this._version = version;
|
|
this._objectStores = objectStores || [];
|
|
|
|
for (var objectStore of this._objectStores)
|
|
objectStore.establishRelationship(this);
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get securityOrigin() { return this._securityOrigin; }
|
|
get host() { return this._host; }
|
|
get version() { return this._version; }
|
|
get objectStores() { return this._objectStores; }
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.IndexedDatabase.NameCookieKey] = this._name;
|
|
cookie[WI.IndexedDatabase.HostCookieKey] = this._host;
|
|
}
|
|
};
|
|
|
|
WI.IndexedDatabase.TypeIdentifier = "indexed-database";
|
|
WI.IndexedDatabase.NameCookieKey = "indexed-database-name";
|
|
WI.IndexedDatabase.HostCookieKey = "indexed-database-host";
|
|
|
|
/* Models/IndexedDatabaseObjectStore.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.IndexedDatabaseObjectStore = class IndexedDatabaseObjectStore
|
|
{
|
|
constructor(name, keyPath, autoIncrement, indexes)
|
|
{
|
|
this._name = name;
|
|
this._keyPath = keyPath;
|
|
this._autoIncrement = autoIncrement || false;
|
|
this._indexes = indexes || [];
|
|
this._parentDatabase = null;
|
|
|
|
for (var index of this._indexes)
|
|
index.establishRelationship(this);
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get keyPath() { return this._keyPath; }
|
|
get autoIncrement() { return this._autoIncrement; }
|
|
get parentDatabase() { return this._parentDatabase; }
|
|
get indexes() { return this._indexes; }
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.IndexedDatabaseObjectStore.NameCookieKey] = this._name;
|
|
cookie[WI.IndexedDatabaseObjectStore.KeyPathCookieKey] = this._keyPath;
|
|
}
|
|
|
|
// Protected
|
|
|
|
establishRelationship(parentDatabase)
|
|
{
|
|
this._parentDatabase = parentDatabase || null;
|
|
}
|
|
};
|
|
|
|
WI.IndexedDatabaseObjectStore.TypeIdentifier = "indexed-database-object-store";
|
|
WI.IndexedDatabaseObjectStore.NameCookieKey = "indexed-database-object-store-name";
|
|
WI.IndexedDatabaseObjectStore.KeyPathCookieKey = "indexed-database-object-store-key-path";
|
|
|
|
/* Models/IndexedDatabaseObjectStoreIndex.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.IndexedDatabaseObjectStoreIndex = class IndexedDatabaseObjectStoreIndex
|
|
{
|
|
constructor(name, keyPath, unique, multiEntry)
|
|
{
|
|
this._name = name;
|
|
this._keyPath = keyPath;
|
|
this._unique = unique || false;
|
|
this._multiEntry = multiEntry || false;
|
|
this._parentObjectStore = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get keyPath() { return this._keyPath; }
|
|
get unique() { return this._unique; }
|
|
get multiEntry() { return this._multiEntry; }
|
|
get parentObjectStore() { return this._parentObjectStore; }
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.IndexedDatabaseObjectStoreIndex.NameCookieKey] = this._name;
|
|
cookie[WI.IndexedDatabaseObjectStoreIndex.KeyPathCookieKey] = this._keyPath;
|
|
}
|
|
|
|
// Protected
|
|
|
|
establishRelationship(parentObjectStore)
|
|
{
|
|
this._parentObjectStore = parentObjectStore || null;
|
|
}
|
|
};
|
|
|
|
WI.IndexedDatabaseObjectStoreIndex.TypeIdentifier = "indexed-database-object-store-index";
|
|
WI.IndexedDatabaseObjectStoreIndex.NameCookieKey = "indexed-database-object-store-index-name";
|
|
WI.IndexedDatabaseObjectStoreIndex.KeyPathCookieKey = "indexed-database-object-store-index-key-path";
|
|
|
|
/* Models/IssueMessage.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.IssueMessage = class IssueMessage extends WI.Object
|
|
{
|
|
constructor(consoleMessage)
|
|
{
|
|
super();
|
|
|
|
console.assert(consoleMessage instanceof WI.ConsoleMessage);
|
|
|
|
this._consoleMessage = consoleMessage;
|
|
|
|
this._text = this._issueText();
|
|
|
|
switch (this._consoleMessage.source) {
|
|
case WI.ConsoleMessage.MessageSource.JS:
|
|
// FIXME: It would be nice if we had this information (the specific type of JavaScript error)
|
|
// as part of the data passed from WebCore, instead of having to determine it ourselves.
|
|
var prefixRegex = /^([^:]+): (?:DOM Exception \d+: )?/;
|
|
var match = prefixRegex.exec(this._text);
|
|
if (match && match[1] in WI.IssueMessage.Type._prefixTypeMap) {
|
|
this._type = WI.IssueMessage.Type._prefixTypeMap[match[1]];
|
|
this._text = this._text.substring(match[0].length);
|
|
} else
|
|
this._type = WI.IssueMessage.Type.OtherIssue;
|
|
break;
|
|
|
|
case WI.ConsoleMessage.MessageSource.CSS:
|
|
case WI.ConsoleMessage.MessageSource.XML:
|
|
this._type = WI.IssueMessage.Type.PageIssue;
|
|
break;
|
|
|
|
case WI.ConsoleMessage.MessageSource.Network:
|
|
this._type = WI.IssueMessage.Type.NetworkIssue;
|
|
break;
|
|
|
|
case WI.ConsoleMessage.MessageSource.Security:
|
|
this._type = WI.IssueMessage.Type.SecurityIssue;
|
|
break;
|
|
|
|
case WI.ConsoleMessage.MessageSource.ConsoleAPI:
|
|
case WI.ConsoleMessage.MessageSource.Storage:
|
|
case WI.ConsoleMessage.MessageSource.Appcache:
|
|
case WI.ConsoleMessage.MessageSource.Rendering:
|
|
case WI.ConsoleMessage.MessageSource.Media:
|
|
case WI.ConsoleMessage.MessageSource.Mediasource:
|
|
case WI.ConsoleMessage.MessageSource.WebRTC:
|
|
case WI.ConsoleMessage.MessageSource.ITPDebug:
|
|
case WI.ConsoleMessage.MessageSource.PrivateClickMeasurement:
|
|
case WI.ConsoleMessage.MessageSource.PaymentRequest:
|
|
case WI.ConsoleMessage.MessageSource.AdClickAttribution: // COMPATIBILITY (iOS 14.0): `Console.ChannelSource.AdClickAttribution` was renamed to `Console.ChannelSource.PrivateClickMeasurement`.
|
|
case WI.ConsoleMessage.MessageSource.Other:
|
|
this._type = WI.IssueMessage.Type.OtherIssue;
|
|
break;
|
|
|
|
default:
|
|
console.error("Unknown issue source:", this._consoleMessage.source);
|
|
this._type = WI.IssueMessage.Type.OtherIssue;
|
|
}
|
|
|
|
this._sourceCodeLocation = consoleMessage.sourceCodeLocation;
|
|
if (this._sourceCodeLocation)
|
|
this._sourceCodeLocation.addEventListener(WI.SourceCodeLocation.Event.DisplayLocationChanged, this._sourceCodeLocationDisplayLocationChanged, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static displayName(type)
|
|
{
|
|
switch (type) {
|
|
case WI.IssueMessage.Type.SemanticIssue:
|
|
return WI.UIString("Semantic Issue");
|
|
case WI.IssueMessage.Type.RangeIssue:
|
|
return WI.UIString("Range Issue");
|
|
case WI.IssueMessage.Type.ReferenceIssue:
|
|
return WI.UIString("Reference Issue");
|
|
case WI.IssueMessage.Type.TypeIssue:
|
|
return WI.UIString("Type Issue");
|
|
case WI.IssueMessage.Type.PageIssue:
|
|
return WI.UIString("Page Issue");
|
|
case WI.IssueMessage.Type.NetworkIssue:
|
|
return WI.UIString("Network Issue");
|
|
case WI.IssueMessage.Type.SecurityIssue:
|
|
return WI.UIString("Security Issue");
|
|
case WI.IssueMessage.Type.OtherIssue:
|
|
return WI.UIString("Other Issue");
|
|
default:
|
|
console.error("Unknown issue message type:", type);
|
|
return WI.UIString("Other Issue");
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get text() { return this._text; }
|
|
get type() { return this._type; }
|
|
get level() { return this._consoleMessage.level; }
|
|
get source() { return this._consoleMessage.source; }
|
|
get url() { return this._consoleMessage.url; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
|
|
// Protected
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.IssueMessage.URLCookieKey] = this.url;
|
|
cookie[WI.IssueMessage.LineNumberCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.lineNumber : 0;
|
|
cookie[WI.IssueMessage.ColumnNumberCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.columnNumber : 0;
|
|
}
|
|
|
|
// Private
|
|
|
|
_issueText()
|
|
{
|
|
let parameters = this._consoleMessage.parameters;
|
|
if (!parameters)
|
|
return this._consoleMessage.messageText;
|
|
|
|
if (parameters[0].type !== "string")
|
|
return this._consoleMessage.messageText;
|
|
|
|
function valueFormatter(obj)
|
|
{
|
|
return obj.description;
|
|
}
|
|
|
|
let formatters = {};
|
|
formatters.o = valueFormatter;
|
|
formatters.s = valueFormatter;
|
|
formatters.f = valueFormatter;
|
|
formatters.i = valueFormatter;
|
|
formatters.d = valueFormatter;
|
|
|
|
function append(a, b)
|
|
{
|
|
a += b;
|
|
return a;
|
|
}
|
|
|
|
let result = String.format(parameters[0].description, parameters.slice(1), formatters, "", append);
|
|
let resultText = result.formattedResult;
|
|
|
|
for (let i = 0; i < result.unusedSubstitutions.length; ++i)
|
|
resultText += " " + result.unusedSubstitutions[i].description;
|
|
|
|
return resultText;
|
|
}
|
|
|
|
_sourceCodeLocationDisplayLocationChanged(event)
|
|
{
|
|
this.dispatchEventToListeners(WI.IssueMessage.Event.DisplayLocationDidChange, event.data);
|
|
}
|
|
};
|
|
|
|
WI.IssueMessage.Level = {
|
|
Error: "error",
|
|
Warning: "warning"
|
|
};
|
|
|
|
WI.IssueMessage.Type = {
|
|
SemanticIssue: "issue-message-type-semantic-issue",
|
|
RangeIssue: "issue-message-type-range-issue",
|
|
ReferenceIssue: "issue-message-type-reference-issue",
|
|
TypeIssue: "issue-message-type-type-issue",
|
|
PageIssue: "issue-message-type-page-issue",
|
|
NetworkIssue: "issue-message-type-network-issue",
|
|
SecurityIssue: "issue-message-type-security-issue",
|
|
OtherIssue: "issue-message-type-other-issue"
|
|
};
|
|
|
|
WI.IssueMessage.TypeIdentifier = "issue-message";
|
|
WI.IssueMessage.URLCookieKey = "issue-message-url";
|
|
WI.IssueMessage.LineNumberCookieKey = "issue-message-line-number";
|
|
WI.IssueMessage.ColumnNumberCookieKey = "issue-message-column-number";
|
|
|
|
WI.IssueMessage.Event = {
|
|
LocationDidChange: "issue-message-location-did-change",
|
|
DisplayLocationDidChange: "issue-message-display-location-did-change"
|
|
};
|
|
|
|
WI.IssueMessage.Type._prefixTypeMap = {
|
|
"SyntaxError": WI.IssueMessage.Type.SemanticIssue,
|
|
"URIError": WI.IssueMessage.Type.SemanticIssue,
|
|
"AggregateError": WI.IssueMessage.Type.SemanticIssue,
|
|
"EvalError": WI.IssueMessage.Type.SemanticIssue,
|
|
"INVALID_CHARACTER_ERR": WI.IssueMessage.Type.SemanticIssue,
|
|
"SYNTAX_ERR": WI.IssueMessage.Type.SemanticIssue,
|
|
|
|
"RangeError": WI.IssueMessage.Type.RangeIssue,
|
|
"INDEX_SIZE_ERR": WI.IssueMessage.Type.RangeIssue,
|
|
"DOMSTRING_SIZE_ERR": WI.IssueMessage.Type.RangeIssue,
|
|
|
|
"ReferenceError": WI.IssueMessage.Type.ReferenceIssue,
|
|
"HIERARCHY_REQUEST_ERR": WI.IssueMessage.Type.ReferenceIssue,
|
|
"INVALID_STATE_ERR": WI.IssueMessage.Type.ReferenceIssue,
|
|
"NOT_FOUND_ERR": WI.IssueMessage.Type.ReferenceIssue,
|
|
"WRONG_DOCUMENT_ERR": WI.IssueMessage.Type.ReferenceIssue,
|
|
|
|
"TypeError": WI.IssueMessage.Type.TypeIssue,
|
|
"INVALID_NODE_TYPE_ERR": WI.IssueMessage.Type.TypeIssue,
|
|
"TYPE_MISMATCH_ERR": WI.IssueMessage.Type.TypeIssue,
|
|
|
|
"SECURITY_ERR": WI.IssueMessage.Type.SecurityIssue,
|
|
|
|
"NETWORK_ERR": WI.IssueMessage.Type.NetworkIssue,
|
|
|
|
"ABORT_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"DATA_CLONE_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"INUSE_ATTRIBUTE_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"INVALID_ACCESS_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"INVALID_MODIFICATION_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"NAMESPACE_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"NOT_SUPPORTED_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"NO_DATA_ALLOWED_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"NO_MODIFICATION_ALLOWED_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"QUOTA_EXCEEDED_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"TIMEOUT_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"URL_MISMATCH_ERR": WI.IssueMessage.Type.OtherIssue,
|
|
"VALIDATION_ERR": WI.IssueMessage.Type.OtherIssue
|
|
};
|
|
|
|
/* Models/JavaScriptBreakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.JavaScriptBreakpoint = class JavaScriptBreakpoint extends WI.Breakpoint
|
|
{
|
|
constructor(sourceCodeLocation, {contentIdentifier, resolved, disabled, condition, actions, ignoreCount, autoContinue} = {})
|
|
{
|
|
console.assert(sourceCodeLocation instanceof WI.SourceCodeLocation, sourceCodeLocation);
|
|
console.assert(!contentIdentifier || typeof contentIdentifier === "string", contentIdentifier);
|
|
console.assert(resolved === undefined || typeof resolved === "boolean", resolved);
|
|
|
|
super({disabled, condition, actions, ignoreCount, autoContinue});
|
|
|
|
this._id = null;
|
|
this._sourceCodeLocation = sourceCodeLocation;
|
|
|
|
let sourceCode = this._sourceCodeLocation.sourceCode;
|
|
if (sourceCode) {
|
|
this._contentIdentifier = sourceCode.contentIdentifier;
|
|
console.assert(!contentIdentifier || contentIdentifier === this._contentIdentifier, "The content identifier from the source code should match the given value.");
|
|
} else
|
|
this._contentIdentifier = contentIdentifier || null;
|
|
console.assert(this._contentIdentifier || this._isSpecial(), "There should always be a content identifier for a breakpoint.");
|
|
|
|
this._scriptIdentifier = sourceCode instanceof WI.Script ? sourceCode.id : null;
|
|
this._target = sourceCode instanceof WI.Script ? sourceCode.target : null;
|
|
|
|
this._resolved = !!resolved;
|
|
this._resolvedLocations = [];
|
|
|
|
this._sourceCodeLocation.addEventListener(WI.SourceCodeLocation.Event.LocationChanged, this._sourceCodeLocationLocationChanged, this);
|
|
this._sourceCodeLocation.addEventListener(WI.SourceCodeLocation.Event.DisplayLocationChanged, this._sourceCodeLocationDisplayLocationChanged, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsMicrotasks(parameter)
|
|
{
|
|
// COMPATIBILITY (iOS 13): Debugger.setPauseOnMicrotasks did not exist yet.
|
|
return InspectorBackend.hasCommand("Debugger.setPauseOnMicrotasks", parameter);
|
|
}
|
|
|
|
static supportsDebuggerStatements(parameter)
|
|
{
|
|
// COMPATIBILITY (iOS 13.1): Debugger.setPauseOnDebuggerStatements did not exist yet.
|
|
return InspectorBackend.hasCommand("Debugger.setPauseOnDebuggerStatements", parameter);
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
const sourceCode = null;
|
|
return new WI.JavaScriptBreakpoint(new WI.SourceCodeLocation(sourceCode, json.lineNumber || 0, json.columnNumber || 0), {
|
|
// The 'url' fallback is for transitioning from older frontends and should be removed.
|
|
contentIdentifier: json.contentIdentifier || json.url,
|
|
resolved: json.resolved,
|
|
disabled: json.disabled,
|
|
condition: json.condition,
|
|
actions: json.actions?.map((actionJSON) => WI.BreakpointAction.fromJSON(actionJSON)) || [],
|
|
ignoreCount: json.ignoreCount,
|
|
autoContinue: json.autoContinue,
|
|
});
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
// The id, scriptIdentifier, target, and resolved state are tied to the current session, so don't include them for serialization.
|
|
let json = super.toJSON(key);
|
|
if (this._contentIdentifier)
|
|
json.contentIdentifier = this._contentIdentifier;
|
|
if (isFinite(this._sourceCodeLocation.lineNumber))
|
|
json.lineNumber = this._sourceCodeLocation.lineNumber;
|
|
if (isFinite(this._sourceCodeLocation.columnNumber))
|
|
json.columnNumber = this._sourceCodeLocation.columnNumber;
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.breakpoints.keyPath] = this._contentIdentifier + ":" + this._sourceCodeLocation.lineNumber + ":" + this._sourceCodeLocation.columnNumber;
|
|
return json;
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
get contentIdentifier() { return this._contentIdentifier; }
|
|
get scriptIdentifier() { return this._scriptIdentifier; }
|
|
get target() { return this._target; }
|
|
get resolvedLocations() { return this._resolvedLocations; }
|
|
|
|
get displayName()
|
|
{
|
|
switch (this) {
|
|
case WI.debuggerManager.debuggerStatementsBreakpoint:
|
|
return WI.repeatedUIString.debuggerStatements();
|
|
|
|
case WI.debuggerManager.allExceptionsBreakpoint:
|
|
return WI.repeatedUIString.allExceptions();
|
|
|
|
case WI.debuggerManager.uncaughtExceptionsBreakpoint:
|
|
return WI.repeatedUIString.uncaughtExceptions();
|
|
|
|
case WI.debuggerManager.assertionFailuresBreakpoint:
|
|
return WI.repeatedUIString.assertionFailures();
|
|
|
|
case WI.debuggerManager.allMicrotasksBreakpoint:
|
|
return WI.repeatedUIString.allMicrotasks();
|
|
}
|
|
|
|
return this._sourceCodeLocation.displayLocationString()
|
|
}
|
|
|
|
get special()
|
|
{
|
|
switch (this) {
|
|
case WI.debuggerManager.debuggerStatementsBreakpoint:
|
|
case WI.debuggerManager.allExceptionsBreakpoint:
|
|
case WI.debuggerManager.uncaughtExceptionsBreakpoint:
|
|
case WI.debuggerManager.assertionFailuresBreakpoint:
|
|
case WI.debuggerManager.allMicrotasksBreakpoint:
|
|
return true;
|
|
}
|
|
|
|
if (this._isSpecial())
|
|
return true;
|
|
|
|
return super.special;
|
|
}
|
|
|
|
get removable()
|
|
{
|
|
switch (this) {
|
|
case WI.debuggerManager.debuggerStatementsBreakpoint:
|
|
case WI.debuggerManager.allExceptionsBreakpoint:
|
|
case WI.debuggerManager.uncaughtExceptionsBreakpoint:
|
|
return false;
|
|
}
|
|
|
|
return super.removable;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
switch (this) {
|
|
case WI.debuggerManager.debuggerStatementsBreakpoint:
|
|
// COMPATIBILITY (iOS 14): Debugger.setPauseOnDebuggerStatements did not have an "options" parameter yet.
|
|
return WI.JavaScriptBreakpoint.supportsDebuggerStatements("options");
|
|
|
|
case WI.debuggerManager.allExceptionsBreakpoint:
|
|
case WI.debuggerManager.uncaughtExceptionsBreakpoint:
|
|
// COMPATIBILITY (iOS 14): Debugger.setPauseOnExceptions did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("Debugger.setPauseOnExceptions", "options");
|
|
|
|
case WI.debuggerManager.assertionFailuresBreakpoint:
|
|
// COMPATIBILITY (iOS 14): Debugger.setPauseOnAssertions did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("Debugger.setPauseOnAssertions", "options");
|
|
|
|
case WI.debuggerManager.allMicrotasksBreakpoint:
|
|
// COMPATIBILITY (iOS 14): Debugger.setPauseOnMicrotasks did not have an "options" parameter yet.
|
|
return WI.JavaScriptBreakpoint.supportsMicrotasks("options");
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
get identifier()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
set identifier(id)
|
|
{
|
|
this._id = id || null;
|
|
}
|
|
|
|
get resolved()
|
|
{
|
|
return super.resolved && this._resolved;
|
|
}
|
|
|
|
set resolved(resolved)
|
|
{
|
|
if (this._resolved === resolved)
|
|
return;
|
|
|
|
console.assert(!resolved || this._sourceCodeLocation.sourceCode || this._isSpecial(), "Breakpoints must have a SourceCode to be resolved.", this);
|
|
|
|
this._resolved = resolved || false;
|
|
|
|
this.dispatchEventToListeners(WI.JavaScriptBreakpoint.Event.ResolvedStateDidChange);
|
|
}
|
|
|
|
addResolvedLocation(location)
|
|
{
|
|
console.assert(!this._isSpecial(), this);
|
|
console.assert(location instanceof WI.SourceCodeLocation, location);
|
|
console.assert(!this.hasResolvedLocation(location), location, this._resolvedLocations);
|
|
|
|
this._resolvedLocations.push(location);
|
|
this.resolved = true;
|
|
}
|
|
|
|
hasResolvedLocation(location)
|
|
{
|
|
console.assert(!this._isSpecial(), this);
|
|
console.assert(location instanceof WI.SourceCodeLocation, location);
|
|
|
|
return this._resolvedLocations.some((resolvedLocation) => resolvedLocation.isEqual(location));
|
|
}
|
|
|
|
clearResolvedLocations()
|
|
{
|
|
console.assert(!this._isSpecial(), this);
|
|
|
|
this._resolvedLocations = [];
|
|
this.resolved = false;
|
|
}
|
|
|
|
remove()
|
|
{
|
|
super.remove();
|
|
|
|
WI.debuggerManager.removeBreakpoint(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["javascript-breakpoint-content-identifier"] = this._contentIdentifier;
|
|
cookie["javascript-breakpoint-line-number"] = this._sourceCodeLocation.lineNumber;
|
|
cookie["javascript-breakpoint-column-number"] = this._sourceCodeLocation.columnNumber;
|
|
}
|
|
|
|
// Private
|
|
|
|
_isSpecial()
|
|
{
|
|
return this._sourceCodeLocation.isEqual(WI.SourceCodeLocation.specialBreakpointLocation);
|
|
}
|
|
|
|
_sourceCodeLocationLocationChanged(event)
|
|
{
|
|
this.dispatchEventToListeners(WI.JavaScriptBreakpoint.Event.LocationDidChange, event.data);
|
|
}
|
|
|
|
_sourceCodeLocationDisplayLocationChanged(event)
|
|
{
|
|
this.dispatchEventToListeners(WI.JavaScriptBreakpoint.Event.DisplayLocationDidChange, event.data);
|
|
}
|
|
};
|
|
|
|
WI.JavaScriptBreakpoint.TypeIdentifier = "javascript-breakpoint";
|
|
|
|
WI.JavaScriptBreakpoint.Event = {
|
|
ResolvedStateDidChange: "javascript-breakpoint-resolved-state-did-change",
|
|
LocationDidChange: "javascript-breakpoint-location-did-change",
|
|
DisplayLocationDidChange: "javascript-breakpoint-display-location-did-change",
|
|
};
|
|
|
|
WI.JavaScriptBreakpoint.ReferencePage = WI.ReferencePage.JavaScriptBreakpoints;
|
|
|
|
/* Models/Layer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Sony Interactive Entertainment Inc.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Layer = class Layer {
|
|
constructor(layerId, nodeId, bounds, paintCount, memory, compositedBounds, isInShadowTree, isReflection, isGeneratedContent, isAnonymous, pseudoElementId, pseudoElement)
|
|
{
|
|
console.assert(typeof bounds === "object");
|
|
|
|
this._layerId = layerId;
|
|
this._nodeId = nodeId;
|
|
this._bounds = bounds;
|
|
this._paintCount = paintCount;
|
|
this._memory = memory;
|
|
this._compositedBounds = compositedBounds;
|
|
this._isInShadowTree = isInShadowTree;
|
|
this._isReflection = isReflection;
|
|
this._isGeneratedContent = isGeneratedContent;
|
|
this._isAnonymous = isAnonymous;
|
|
this._pseudoElementId = pseudoElementId;
|
|
this._pseudoElement = pseudoElement;
|
|
|
|
// Transform compositedBounds to the global position
|
|
this._compositedBounds.x += this._bounds.x;
|
|
this._compositedBounds.y += this._bounds.y;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
return new WI.Layer(
|
|
payload.layerId,
|
|
payload.nodeId,
|
|
payload.bounds,
|
|
payload.paintCount,
|
|
payload.memory,
|
|
payload.compositedBounds,
|
|
payload.isInShadowTree,
|
|
payload.isReflection,
|
|
payload.isGeneratedContent,
|
|
payload.isAnonymous,
|
|
payload.pseudoElementId,
|
|
payload.pseudoElement
|
|
);
|
|
}
|
|
|
|
// Public
|
|
|
|
get layerId() { return this._layerId; }
|
|
get nodeId() { return this._nodeId; }
|
|
get bounds() { return this._bounds; }
|
|
get paintCount() { return this._paintCount; }
|
|
get memory() { return this._memory; }
|
|
get compositedBounds() { return this._compositedBounds; }
|
|
get isInShadowTree() { return this._isInShadowTree; }
|
|
get isReflection() { return this._isReflection; }
|
|
get isGeneratedContent() { return this._isGeneratedContent; }
|
|
get isAnonymous() { return this._isAnonymous; }
|
|
get pseudoElementId() { return this._pseudoElementId; }
|
|
get pseudoElement() { return this._pseudoElement; }
|
|
};
|
|
|
|
/* Models/LayoutInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LayoutInstrument = class LayoutInstrument extends WI.Instrument
|
|
{
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Layout;
|
|
}
|
|
};
|
|
|
|
/* Models/LayoutTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LayoutTimelineRecord = class LayoutTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(eventType, startTime, endTime, stackTrace, sourceCodeLocation, quad)
|
|
{
|
|
super(WI.TimelineRecord.Type.Layout, startTime, endTime, stackTrace, sourceCodeLocation);
|
|
|
|
console.assert(eventType);
|
|
console.assert(!quad || quad instanceof WI.Quad);
|
|
|
|
if (eventType in WI.LayoutTimelineRecord.EventType)
|
|
eventType = WI.LayoutTimelineRecord.EventType[eventType];
|
|
|
|
this._eventType = eventType;
|
|
this._quad = quad || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static displayNameForEventType(eventType)
|
|
{
|
|
switch (eventType) {
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateStyles:
|
|
return WI.UIString("Styles Invalidated");
|
|
case WI.LayoutTimelineRecord.EventType.RecalculateStyles:
|
|
return WI.UIString("Styles Recalculated");
|
|
case WI.LayoutTimelineRecord.EventType.InvalidateLayout:
|
|
return WI.UIString("Layout Invalidated");
|
|
case WI.LayoutTimelineRecord.EventType.ForcedLayout:
|
|
return WI.UIString("Forced Layout", "Layout phase records that were imperative (forced)");
|
|
case WI.LayoutTimelineRecord.EventType.Layout:
|
|
return WI.repeatedUIString.timelineRecordLayout();
|
|
case WI.LayoutTimelineRecord.EventType.Paint:
|
|
return WI.repeatedUIString.timelineRecordPaint();
|
|
case WI.LayoutTimelineRecord.EventType.Composite:
|
|
return WI.repeatedUIString.timelineRecordComposite();
|
|
}
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {eventType, startTime, endTime, stackTrace, sourceCodeLocation, quad} = json;
|
|
quad = quad ? WI.Quad.fromJSON(quad) : null;
|
|
return new WI.LayoutTimelineRecord(eventType, startTime, endTime, stackTrace, sourceCodeLocation, quad);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
// FIXME: stackTrace
|
|
// FIXME: sourceCodeLocation
|
|
|
|
return {
|
|
type: this.type,
|
|
eventType: this._eventType,
|
|
startTime: this.startTime,
|
|
endTime: this.endTime,
|
|
quad: this._quad || undefined,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get eventType()
|
|
{
|
|
return this._eventType;
|
|
}
|
|
|
|
get width()
|
|
{
|
|
return this._quad ? this._quad.width : NaN;
|
|
}
|
|
|
|
get height()
|
|
{
|
|
return this._quad ? this._quad.height : NaN;
|
|
}
|
|
|
|
get area()
|
|
{
|
|
return this.width * this.height;
|
|
}
|
|
|
|
get quad()
|
|
{
|
|
return this._quad;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
super.saveIdentityToCookie(cookie);
|
|
|
|
cookie[WI.LayoutTimelineRecord.EventTypeCookieKey] = this._eventType;
|
|
}
|
|
};
|
|
|
|
WI.LayoutTimelineRecord.EventType = {
|
|
InvalidateStyles: "invalidate-styles",
|
|
RecalculateStyles: "recalculate-styles",
|
|
InvalidateLayout: "invalidate-layout",
|
|
ForcedLayout: "forced-layout",
|
|
Layout: "layout",
|
|
Paint: "paint",
|
|
Composite: "composite"
|
|
};
|
|
|
|
WI.LayoutTimelineRecord.TypeIdentifier = "layout-timeline-record";
|
|
WI.LayoutTimelineRecord.EventTypeCookieKey = "layout-timeline-record-event-type";
|
|
|
|
/* Models/LazySourceCodeLocation.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: Investigate folding this into SourceCodeLocation proper so it can always be as lazy as possible.
|
|
|
|
// Lazily compute the full SourceCodeLocation information only when such information is needed.
|
|
// - raw information doesn't require initialization, we have that information
|
|
// - formatted information does require initialization, done by overriding public APIs.
|
|
// - display information does require initialization, done by overriding private funnel API resolveMappedLocation.
|
|
|
|
WI.LazySourceCodeLocation = class LazySourceCodeLocation extends WI.SourceCodeLocation
|
|
{
|
|
constructor(sourceCode, lineNumber, columnNumber)
|
|
{
|
|
super(null, lineNumber, columnNumber);
|
|
|
|
console.assert(sourceCode);
|
|
|
|
this._initialized = false;
|
|
this._lazySourceCode = sourceCode;
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceCode()
|
|
{
|
|
return this._lazySourceCode;
|
|
}
|
|
|
|
set sourceCode(sourceCode)
|
|
{
|
|
// Getter and setter must be provided together.
|
|
this.setSourceCode(sourceCode);
|
|
}
|
|
|
|
get formattedLineNumber()
|
|
{
|
|
this._lazyInitialization();
|
|
return this._formattedLineNumber;
|
|
}
|
|
|
|
get formattedColumnNumber()
|
|
{
|
|
this._lazyInitialization();
|
|
return this._formattedColumnNumber;
|
|
}
|
|
|
|
formattedPosition()
|
|
{
|
|
this._lazyInitialization();
|
|
return new WI.SourceCodePosition(this._formattedLineNumber, this._formattedColumnNumber);
|
|
}
|
|
|
|
hasFormattedLocation()
|
|
{
|
|
this._lazyInitialization();
|
|
return super.hasFormattedLocation();
|
|
}
|
|
|
|
hasDifferentDisplayLocation()
|
|
{
|
|
this._lazyInitialization();
|
|
return super.hasDifferentDisplayLocation();
|
|
}
|
|
|
|
// Protected
|
|
|
|
resolveMappedLocation()
|
|
{
|
|
this._lazyInitialization();
|
|
super.resolveMappedLocation();
|
|
}
|
|
|
|
// Private
|
|
|
|
_lazyInitialization()
|
|
{
|
|
if (!this._initialized) {
|
|
this._initialized = true;
|
|
this.sourceCode = this._lazySourceCode;
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Models/LinearTimingFunction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2023 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LinearTimingFunction = class LinearTimingFunction
|
|
{
|
|
constructor(points)
|
|
{
|
|
console.assert(Array.isArray(points) && points.length >= 2 && points.every((point) => point instanceof WI.LinearTimingFunction.Point), points);
|
|
|
|
this._points = points;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromString(text)
|
|
{
|
|
if (!text || !text.length)
|
|
return null;
|
|
|
|
let trimmedText = text.toLowerCase().trim();
|
|
if (!trimmedText.length)
|
|
return null;
|
|
|
|
if (trimmedText in WI.LinearTimingFunction.keywordValues)
|
|
return new WI.LinearTimingFunction(WI.LinearTimingFunction.keywordValues[trimmedText].map((point) => new WI.LinearTimingFunction.Point(...point)));
|
|
|
|
let args = trimmedText.match(/^linear\(\s*([^\)]+)\s*\)$/)?.[1]?.split(/\s*,\s*/) ?? [];
|
|
if (args.length < 2)
|
|
return null;
|
|
|
|
let stops = [];
|
|
let largestInput = -Infinity;
|
|
let lastStopIsExtraPoint = false;
|
|
for (let arg of args) {
|
|
let [_, output, input, extraPoint] = arg.match(/([\d\.-]+)\s*(?:([\d\.-]+)%)?\s*(?:([\d\.-]+)%)?/) ?? [];
|
|
|
|
output = parseFloat(output);
|
|
if (isNaN(output))
|
|
return null;
|
|
|
|
let stop = {output};
|
|
stops.push(stop);
|
|
lastStopIsExtraPoint = false;
|
|
|
|
if (input !== undefined) {
|
|
input = parseFloat(input) / 100;
|
|
if (isNaN(input))
|
|
return null;
|
|
|
|
largestInput = Math.max(input, largestInput);
|
|
stop.input = largestInput;
|
|
|
|
if (extraPoint !== undefined) {
|
|
extraPoint = parseFloat(extraPoint) / 100;
|
|
if (isNaN(extraPoint))
|
|
return null;
|
|
|
|
largestInput = Math.max(extraPoint, largestInput);
|
|
stops.push({output, input: largestInput});
|
|
lastStopIsExtraPoint = true;
|
|
}
|
|
} else if (stops.length === 1) {
|
|
largestInput = 0;
|
|
stop.input = largestInput;
|
|
}
|
|
}
|
|
|
|
if (!lastStopIsExtraPoint && !("input" in stops.lastValue))
|
|
stops.lastValue.input = Math.max(1, largestInput);
|
|
|
|
let points = [];
|
|
let missingInputRunStart = -1;
|
|
for (let i = 0; i <= stops.length; ++i) {
|
|
if (i < stops.length && !("input" in stops[i])) {
|
|
if (missingInputRunStart === -1)
|
|
missingInputRunStart = i;
|
|
continue;
|
|
}
|
|
|
|
if (missingInputRunStart !== -1) {
|
|
let inputLow = missingInputRunStart > 0 ? stops[missingInputRunStart - 1].input : 0;
|
|
let inputHigh = i < stops.length ? stops[i].input : Math.max(1, largestInput);
|
|
let inputAverage = (inputLow + inputHigh) / (i - missingInputRunStart + 1);
|
|
for (let j = missingInputRunStart; j < i; ++j)
|
|
points.push(new WI.LinearTimingFunction.Point(stops[j].output, inputAverage * (j - missingInputRunStart + 1)));
|
|
|
|
missingInputRunStart = -1;
|
|
}
|
|
|
|
if (i < stops.length && "input" in stops[i])
|
|
points.push(new WI.LinearTimingFunction.Point(stops[i].output, stops[i].input));
|
|
}
|
|
console.assert(points.length === stops.length, points, stops);
|
|
console.assert(missingInputRunStart === -1, missingInputRunStart);
|
|
return new WI.LinearTimingFunction(points);
|
|
}
|
|
|
|
// Public
|
|
|
|
get points() { return this._points; }
|
|
|
|
copy()
|
|
{
|
|
return new WI.LinearTimingFunction(this._points.map((point) => point.copy()));
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
if (this._points.length !== other._points.length)
|
|
return false;
|
|
|
|
for (let i = 0; i < this._points.length; ++i) {
|
|
if (!this._points[i].equals(other._points[i]))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
toString()
|
|
{
|
|
for (let key in WI.LinearTimingFunction.keywordValues) {
|
|
if (this.equals(WI.LinearTimingFunction.fromString(key)))
|
|
return key;
|
|
}
|
|
|
|
return `linear(${this._points.map((point) => `${point.value} ${point.progress * 100}%`).join(", ")})`;
|
|
}
|
|
};
|
|
|
|
WI.LinearTimingFunction.Point = class LinearTimingFunctionPoint
|
|
{
|
|
constructor(value, progress)
|
|
{
|
|
console.assert(!isNaN(value), value);
|
|
console.assert(!isNaN(progress), progress);
|
|
|
|
this._value = value;
|
|
this._progress = progress;
|
|
}
|
|
|
|
// Public
|
|
|
|
get value() { return this._value; }
|
|
get progress() { return this._progress; }
|
|
|
|
copy()
|
|
{
|
|
return new WI.LinearTimingFunction.Point(this._value, this._progress);
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
return this._value === other._value && this._progress === other._progress;
|
|
}
|
|
};
|
|
|
|
WI.LinearTimingFunction.keywordValues = {
|
|
"linear": [[0, 0], [1, 1]],
|
|
};
|
|
|
|
/* Models/LocalResourceOverride.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LocalResourceOverride = class LocalResourceOverride extends WI.Object
|
|
{
|
|
constructor(url, type, localResource, {resourceErrorType, isCaseSensitive, isRegex, isPassthrough, disabled} = {})
|
|
{
|
|
console.assert(url && typeof url === "string", url);
|
|
console.assert(Object.values(WI.LocalResourceOverride.InterceptType).includes(type), type);
|
|
console.assert(localResource instanceof WI.LocalResource, localResource);
|
|
console.assert(!localResource.localResourceOverride, localResource);
|
|
console.assert(!resourceErrorType || Object.values(WI.LocalResourceOverride.ResourceErrorType).includes(resourceErrorType), resourceErrorType);
|
|
console.assert(isCaseSensitive === undefined || typeof isCaseSensitive === "boolean", isCaseSensitive);
|
|
console.assert(isRegex === undefined || typeof isRegex === "boolean", isRegex);
|
|
console.assert(isPassthrough === undefined || typeof isPassthrough === "boolean", isPassthrough);
|
|
console.assert(disabled === undefined || typeof disabled === "boolean", disabled);
|
|
|
|
console.assert(type !== WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory || localResource.isMappedToDirectory, localResource);
|
|
console.assert(type !== WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory || isRegex, isRegex);
|
|
console.assert(type !== WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory || isPassthrough, isPassthrough);
|
|
|
|
super();
|
|
|
|
this._url = WI.urlWithoutFragment(url);
|
|
this._urlComponents = null;
|
|
this._type = type;
|
|
this._localResource = localResource;
|
|
this._resourceErrorType = resourceErrorType || WI.LocalResourceOverride.ResourceErrorType.General;
|
|
this._isCaseSensitive = isCaseSensitive !== undefined ? isCaseSensitive : true;
|
|
this._isRegex = isRegex !== undefined ? isRegex : false;
|
|
this._isPassthrough = isPassthrough !== undefined ? isPassthrough : false;
|
|
this._disabled = disabled !== undefined ? disabled : false;
|
|
|
|
this._localResource._localResourceOverride = this;
|
|
}
|
|
|
|
// Static
|
|
|
|
static create(url, type, {requestURL, requestMethod, requestHeaders, requestData, responseMIMEType, responseContent, responseBase64Encoded, responseStatusCode, responseStatusText, responseHeaders, mappedFilePath, resourceErrorType, isCaseSensitive, isRegex, isPassthrough, disabled} = {})
|
|
{
|
|
let localResource = new WI.LocalResource({
|
|
request: {
|
|
url: requestURL || url || "",
|
|
method: requestMethod,
|
|
headers: requestHeaders,
|
|
data: requestData,
|
|
},
|
|
response: {
|
|
headers: responseHeaders,
|
|
mimeType: responseMIMEType,
|
|
statusCode: responseStatusCode,
|
|
statusText: responseStatusText,
|
|
content: responseContent,
|
|
base64Encoded: responseBase64Encoded,
|
|
},
|
|
mappedFilePath,
|
|
});
|
|
return new WI.LocalResourceOverride(url, type, localResource, {resourceErrorType, isCaseSensitive, isRegex, isPassthrough, disabled});
|
|
}
|
|
|
|
static displayNameForNetworkStageOfType(type)
|
|
{
|
|
switch (type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
return WI.UIString("Request Override", "Request Override @ Local Override Network Stage", "Text indicating that the local override replaces the request of the network activity.");
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
return WI.UIString("Response Override", "Response Override @ Local Override Network Stage", "Text indicating that the local override replaces the response of the network activity.");
|
|
}
|
|
|
|
console.assert(false, "Unknown type: ", type);
|
|
return "";
|
|
}
|
|
|
|
static displayNameForType(type)
|
|
{
|
|
switch (type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
return WI.UIString("Block", "Block @ Local Override Type", "Text indicating that the local override will always block the network activity.");
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
return WI.UIString("Request", "Request @ Local Override Type", "Text indicating that the local override intercepts the request phase of network activity.");
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
return WI.UIString("Response", "Response @ Local Override Type", "Text indicating that the local override intercepts the response phase of network activity.");
|
|
}
|
|
|
|
console.assert(false, "Unknown type: ", type);
|
|
return "";
|
|
}
|
|
|
|
static displayNameForResourceErrorType(resourceErrorType)
|
|
{
|
|
switch (resourceErrorType) {
|
|
case WI.LocalResourceOverride.ResourceErrorType.AccessControl:
|
|
return WI.UIString("Access Control", "Access Control @ Local Override Type", "Text indicating that the local override will block the network activity with an access error.");
|
|
|
|
case WI.LocalResourceOverride.ResourceErrorType.Cancellation:
|
|
return WI.UIString("Cancellation", "Cancellation @ Local Override Type", "Text indicating that the local override will block the network activity with a cancellation error.");
|
|
|
|
case WI.LocalResourceOverride.ResourceErrorType.General:
|
|
return WI.UIString("General", "General @ Local Override Type", "Text indicating that the local override will block the network activity with a general error.");
|
|
|
|
case WI.LocalResourceOverride.ResourceErrorType.Timeout:
|
|
return WI.UIString("Timeout", "Timeout @ Local Override Type", "Text indicating that the local override will block the network activity with an timeout error.");
|
|
}
|
|
|
|
console.assert(false, "Unknown resource error type: ", resourceErrorType);
|
|
return "";
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
let {url, type, localResource: localResourceJSON, resourceErrorType, isCaseSensitive, isRegex, isPassthrough, disabled} = json;
|
|
|
|
let localResource = WI.LocalResource.fromJSON(localResourceJSON);
|
|
|
|
// COMPATIBILITY (iOS 13.4): Network.interceptWithRequest/Network.interceptRequestWithResponse did not exist yet.
|
|
url ??= localResource.url;
|
|
type ??= WI.LocalResourceOverride.InterceptType.Response;
|
|
|
|
return new WI.LocalResourceOverride(url, type, localResource, {resourceErrorType, isCaseSensitive, isRegex, isPassthrough, disabled});
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = {
|
|
url: this._url,
|
|
type: this._type,
|
|
localResource: this._localResource.toJSON(key),
|
|
isCaseSensitive: this._isCaseSensitive,
|
|
isRegex: this._isRegex,
|
|
isPassthrough: this._isPassthrough,
|
|
disabled: this._disabled,
|
|
};
|
|
|
|
if (this._resourceErrorType)
|
|
json.resourceErrorType = this._resourceErrorType;
|
|
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.localResourceOverrides.keyPath] = this._url;
|
|
|
|
return json;
|
|
}
|
|
|
|
// Public
|
|
|
|
get url() { return this._url; }
|
|
get type() { return this._type; }
|
|
get localResource() { return this._localResource; }
|
|
get isCaseSensitive() { return this._isCaseSensitive; }
|
|
get isRegex() { return this._isRegex; }
|
|
get isPassthrough() { return this._isPassthrough; }
|
|
|
|
get urlComponents()
|
|
{
|
|
if (!this._urlComponents)
|
|
this._urlComponents = parseURL(this._url);
|
|
return this._urlComponents;
|
|
}
|
|
|
|
get networkStage()
|
|
{
|
|
switch (this._type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
return WI.NetworkManager.NetworkStage.Request;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory:
|
|
return WI.NetworkManager.NetworkStage.Response;
|
|
}
|
|
|
|
console.assert(false, "not reached");
|
|
return null;
|
|
}
|
|
|
|
get resourceErrorType()
|
|
{
|
|
return this._resourceErrorType;
|
|
}
|
|
|
|
set resourceErrorType(resourceErrorType)
|
|
{
|
|
console.assert(Object.values(WI.LocalResourceOverride.ResourceErrorType).includes(resourceErrorType), resourceErrorType);
|
|
|
|
if (this._resourceErrorType === resourceErrorType)
|
|
return;
|
|
|
|
this._resourceErrorType = resourceErrorType;
|
|
|
|
this.dispatchEventToListeners(WI.LocalResourceOverride.Event.ResourceErrorTypeChanged);
|
|
}
|
|
|
|
get disabled()
|
|
{
|
|
return this._disabled;
|
|
}
|
|
|
|
set disabled(disabled)
|
|
{
|
|
if (this._disabled === disabled)
|
|
return;
|
|
|
|
this._disabled = !!disabled;
|
|
|
|
this.dispatchEventToListeners(WI.LocalResourceOverride.Event.DisabledChanged);
|
|
}
|
|
|
|
get displayType()
|
|
{
|
|
let displayType = WI.LocalResourceOverride.displayNameForType(this._type);
|
|
|
|
if (this._localResource.isMappedToDirectory)
|
|
displayType = WI.UIString("%s (directory)", "%s (directory) @ Local Override Type", "Label modifier indicating that the local override maps to a directory on disk.").format(displayType);
|
|
else if (this._localResource.mappedFilePath)
|
|
displayType = WI.UIString("%s (file)", "%s (file) @ Local Override Type", "Label modifier indicating that the local override maps to a file on disk.").format(displayType);
|
|
|
|
return displayType;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
return this.displayURL();
|
|
}
|
|
|
|
displayURL({full} = {})
|
|
{
|
|
if (this._isRegex)
|
|
return "/" + this._url + "/" + (!this._isCaseSensitive ? "i" : "");
|
|
|
|
let displayName = full ? this._url : WI.displayNameForURL(this._url, this.urlComponents);
|
|
if (!this._isCaseSensitive)
|
|
displayName = WI.UIString("%s (Case Insensitive)", "%s (Case Insensitive) @ Local Override", "Label for case-insensitive URL match pattern of a local override.").format(displayName);
|
|
return displayName;
|
|
}
|
|
|
|
generateRequestRedirectURL(url)
|
|
{
|
|
console.assert(this._type === WI.LocalResourceOverride.InterceptType.Request, this);
|
|
|
|
let redirectURL = this._localResource.url;
|
|
if (!redirectURL)
|
|
return null;
|
|
|
|
if (this._isRegex)
|
|
return url.replace(this._urlRegex, redirectURL);
|
|
|
|
return redirectURL;
|
|
}
|
|
|
|
generateSubpathForMappedDirectory(url)
|
|
{
|
|
console.assert(this._type === WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory, this);
|
|
console.assert(this._isRegex, this);
|
|
console.assert(this._localResource.url, this._localResource);
|
|
|
|
return url.replace(this._urlRegex, this._localResource.url);
|
|
}
|
|
|
|
get canMapToFile()
|
|
{
|
|
if (!WI.LocalResource.canMapToFile())
|
|
return false;
|
|
|
|
switch (this._type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
return false;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory:
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
return true;
|
|
}
|
|
|
|
console.assert(false, "not reached");
|
|
return false;
|
|
}
|
|
|
|
matches(url)
|
|
{
|
|
url = WI.urlWithoutFragment(url);
|
|
|
|
if (this._isRegex)
|
|
return this._urlRegex.test(url);
|
|
|
|
if (!this._isCaseSensitive)
|
|
return url.toLowerCase() === this._url.toLowerCase();
|
|
|
|
return url === this._url;
|
|
}
|
|
|
|
equals(localResourceOverrideOrSerializedData)
|
|
{
|
|
console.assert(localResourceOverrideOrSerializedData instanceof WI.LocalResourceOverride || (localResourceOverrideOrSerializedData && typeof localResourceOverrideOrSerializedData === "object"), localResourceOverrideOrSerializedData);
|
|
|
|
return localResourceOverrideOrSerializedData.url === this._url
|
|
&& localResourceOverrideOrSerializedData.isCaseSensitive === this._isCaseSensitive
|
|
&& localResourceOverrideOrSerializedData.isRegex === this._isRegex;
|
|
}
|
|
|
|
// Protected
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["local-resource-override-url"] = this._url;
|
|
cookie["local-resource-override-type"] = this._type;
|
|
cookie["local-resource-override-is-case-sensitive"] = this._isCaseSensitive;
|
|
cookie["local-resource-override-is-regex"] = this._isRegex;
|
|
cookie["local-resource-override-disabled"] = this._disabled;
|
|
}
|
|
|
|
// Private
|
|
|
|
get _urlRegex()
|
|
{
|
|
console.assert(this._isRegex);
|
|
return new RegExp(this._url, !this._isCaseSensitive ? "i" : "");
|
|
}
|
|
};
|
|
|
|
WI.LocalResourceOverride.TypeIdentifier = "local-resource-override";
|
|
|
|
WI.LocalResourceOverride.InterceptType = {
|
|
Block: "block",
|
|
Request: "request",
|
|
Response: "response",
|
|
ResponseMappedDirectory: "response-mapped-directory",
|
|
ResponseSkippingNetwork: "response-skipping-network",
|
|
};
|
|
|
|
WI.LocalResourceOverride.ResourceErrorType = {
|
|
AccessControl: "AccessControl",
|
|
Cancellation: "Cancellation",
|
|
General: "General",
|
|
Timeout: "Timeout",
|
|
};
|
|
|
|
WI.LocalResourceOverride.Event = {
|
|
DisabledChanged: "local-resource-override-disabled-state-did-change",
|
|
ResourceErrorTypeChanged: "local-resource-override-resource-error-type-changed",
|
|
};
|
|
|
|
/* Models/LoggingChannel.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.LoggingChannel = class LoggingChannel
|
|
{
|
|
constructor(source, level)
|
|
{
|
|
console.assert(typeof source === "string");
|
|
console.assert(Object.values(WI.ConsoleMessage.MessageSource).includes(source), source);
|
|
|
|
console.assert(typeof level === "string");
|
|
console.assert(Object.values(WI.LoggingChannel.Level).includes(level), level);
|
|
|
|
this._source = source;
|
|
this._level = level;
|
|
}
|
|
|
|
// Payload
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
return new WI.LoggingChannel(payload.source, payload.level);
|
|
}
|
|
|
|
// Public
|
|
|
|
get source() { return this._source; }
|
|
get level() { return this._level; }
|
|
};
|
|
|
|
WI.LoggingChannel.Level = {
|
|
Off: "off",
|
|
Basic: "basic",
|
|
Verbose: "verbose",
|
|
};
|
|
|
|
/* Models/MediaInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MediaInstrument = class MediaInstrument extends WI.Instrument
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
console.assert(WI.MediaInstrument.supported());
|
|
}
|
|
|
|
// Static
|
|
|
|
static supported()
|
|
{
|
|
// COMPATIBILITY (iOS 12): DOM.didFireEvent did not exist yet.
|
|
return InspectorBackend.hasEvent("DOM.didFireEvent");
|
|
}
|
|
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Media;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
// Audio/Video/Picture instrumentation is always happening.
|
|
|
|
if (!initiatedByBackend) {
|
|
// COMPATIBILITY (iOS 13): Animation domain did not exist yet.
|
|
if (InspectorBackend.hasDomain("Animation")) {
|
|
let target = WI.assumingMainTarget();
|
|
target.AnimationAgent.startTracking();
|
|
}
|
|
}
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
// Audio/Video/Picture instrumentation is always happening.
|
|
|
|
if (!initiatedByBackend) {
|
|
// COMPATIBILITY (iOS 13): Animation domain did not exist yet.
|
|
if (InspectorBackend.hasDomain("Animation")) {
|
|
let target = WI.assumingMainTarget();
|
|
target.AnimationAgent.stopTracking();
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Models/MediaTimeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MediaTimeline = class MediaTimeline extends WI.Timeline
|
|
{
|
|
// Public
|
|
|
|
recordForTrackingAnimationId(trackingAnimationId)
|
|
{
|
|
return this._trackingAnimationIdRecordMap.get(trackingAnimationId) || null;
|
|
}
|
|
|
|
recordForMediaElementEvents(domNode)
|
|
{
|
|
console.assert(domNode.isMediaElement());
|
|
return this._mediaElementRecordMap.get(domNode) || null;
|
|
}
|
|
|
|
reset(suppressEvents)
|
|
{
|
|
this._trackingAnimationIdRecordMap = new Map;
|
|
this._mediaElementRecordMap = new Map;
|
|
|
|
super.reset(suppressEvents);
|
|
}
|
|
|
|
addRecord(record, options = {})
|
|
{
|
|
console.assert(record instanceof WI.MediaTimelineRecord);
|
|
|
|
if (record.trackingAnimationId) {
|
|
console.assert(!this._trackingAnimationIdRecordMap.has(record.trackingAnimationId));
|
|
this._trackingAnimationIdRecordMap.set(record.trackingAnimationId, record);
|
|
}
|
|
|
|
if (record.eventType === WI.MediaTimelineRecord.EventType.MediaElement) {
|
|
console.assert(!(record.domNode instanceof WI.DOMNode) || record.domNode.isMediaElement());
|
|
console.assert(!this._mediaElementRecordMap.has(record.domNode));
|
|
this._mediaElementRecordMap.set(record.domNode, record);
|
|
}
|
|
|
|
super.addRecord(record, options);
|
|
}
|
|
};
|
|
|
|
/* Models/MediaTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MediaTimelineRecord = class MediaTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(eventType, domNodeOrInfo, {trackingAnimationId, animationName, transitionProperty} = {})
|
|
{
|
|
console.assert(Object.values(MediaTimelineRecord.EventType).includes(eventType));
|
|
console.assert(domNodeOrInfo instanceof WI.DOMNode || (!isEmptyObject(domNodeOrInfo) && domNodeOrInfo.displayName && domNodeOrInfo.cssPath));
|
|
|
|
super(WI.TimelineRecord.Type.Media);
|
|
|
|
this._eventType = eventType;
|
|
this._domNode = domNodeOrInfo;
|
|
this._domNodeDisplayName = domNodeOrInfo?.displayName;
|
|
this._domNodeCSSPath = domNodeOrInfo instanceof WI.DOMNode ? WI.cssPath(domNodeOrInfo, {full: true}) : domNodeOrInfo?.cssPath;
|
|
|
|
// Web Animation
|
|
console.assert(trackingAnimationId === undefined || typeof trackingAnimationId === "string");
|
|
this._trackingAnimationId = trackingAnimationId || null;
|
|
|
|
// CSS Web Animation
|
|
console.assert(animationName === undefined || typeof animationName === "string");
|
|
console.assert(transitionProperty === undefined || typeof transitionProperty === "string");
|
|
this._animationName = animationName || null;
|
|
this._transitionProperty = transitionProperty || null;
|
|
|
|
this._timestamps = [];
|
|
this._activeStartTime = NaN;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {eventType, domNodeDisplayName, domNodeCSSPath, animationName, transitionProperty, timestamps} = json;
|
|
|
|
let documentNode = null;
|
|
if (InspectorBackend.hasDomain("DOM"))
|
|
documentNode = await new Promise((resolve) => WI.domManager.requestDocument(resolve));
|
|
|
|
let domNode = null;
|
|
if (documentNode && domNodeCSSPath) {
|
|
try {
|
|
let nodeId = await documentNode.querySelector(domNodeCSSPath);
|
|
if (nodeId)
|
|
domNode = WI.domManager.nodeForId(nodeId);
|
|
} catch { }
|
|
}
|
|
if (!domNode) {
|
|
domNode = {
|
|
displayName: domNodeDisplayName,
|
|
cssPath: domNodeCSSPath,
|
|
};
|
|
}
|
|
|
|
let record = new MediaTimelineRecord(eventType, domNode, {animationName, transitionProperty});
|
|
|
|
if (Array.isArray(timestamps) && timestamps.length) {
|
|
record._timestamps = [];
|
|
for (let item of timestamps) {
|
|
if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent) {
|
|
if (documentNode && item.originatorCSSPath) {
|
|
try {
|
|
let nodeId = await documentNode.querySelector(item.originatorCSSPath);
|
|
if (nodeId)
|
|
item.originator = WI.domManager.nodeForId(nodeId);
|
|
} catch { }
|
|
if (!item.originator) {
|
|
item.originator = {
|
|
displayName: item.originatorDisplayName,
|
|
cssPath: item.originatorCSSPath,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
record._timestamps.push(item);
|
|
}
|
|
}
|
|
|
|
return record;
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = {
|
|
eventType: this._eventType,
|
|
domNodeDisplayName: this._domNodeDisplayName,
|
|
domNodeCSSPath: this._domNodeCSSPath,
|
|
};
|
|
|
|
if (this._animationName)
|
|
json.animationName = this._animationName;
|
|
|
|
if (this._transitionProperty)
|
|
json.transitionProperty = this._transitionProperty;
|
|
|
|
if (this._timestamps.length) {
|
|
json.timestamps = this._timestamps.map((item) => {
|
|
if (item.type === MediaTimelineRecord.TimestampType.MediaElementDOMEvent && item.originator instanceof WI.DOMNode)
|
|
delete item.originator;
|
|
return item;
|
|
});
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
// Public
|
|
|
|
get eventType() { return this._eventType; }
|
|
get domNode() { return this._domNode; }
|
|
get trackingAnimationId() { return this._trackingAnimationId; }
|
|
get timestamps() { return this._timestamps; }
|
|
get activeStartTime() { return this._activeStartTime; }
|
|
|
|
get updatesDynamically()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
get usesActiveStartTime()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
get displayName()
|
|
{
|
|
switch (this._eventType) {
|
|
case MediaTimelineRecord.EventType.CSSAnimation:
|
|
return this._animationName;
|
|
|
|
case MediaTimelineRecord.EventType.CSSTransition:
|
|
return this._transitionProperty;
|
|
|
|
case MediaTimelineRecord.EventType.MediaElement:
|
|
return WI.UIString("Media Element");
|
|
}
|
|
|
|
console.error("Unknown media record event type: ", this._eventType, this);
|
|
return WI.UIString("Media Event");
|
|
}
|
|
|
|
get subtitle()
|
|
{
|
|
switch (this._eventType) {
|
|
case MediaTimelineRecord.EventType.CSSAnimation:
|
|
return WI.UIString("CSS Animation");
|
|
|
|
case MediaTimelineRecord.EventType.CSSTransition:
|
|
return WI.UIString("CSS Transition");
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
super.saveIdentityToCookie(cookie);
|
|
|
|
cookie["media-timeline-record-event-type"] = this._eventType;
|
|
cookie["media-timeline-record-dom-node"] = this._domNode instanceof WI.DOMNode ? this._domNode.path() : this._domNode;
|
|
if (this._animationName)
|
|
cookie["media-timeline-record-animation-name"] = this._animationName;
|
|
if (this._transitionProperty)
|
|
cookie["media-timeline-record-transition-property"] = this._transitionProperty;
|
|
}
|
|
|
|
// TimelineManager
|
|
|
|
updateAnimationState(timestamp, animationState)
|
|
{
|
|
console.assert(this._eventType === MediaTimelineRecord.EventType.CSSAnimation || this._eventType === MediaTimelineRecord.EventType.CSSTransition);
|
|
console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
|
|
|
|
let type;
|
|
switch (animationState) {
|
|
case InspectorBackend.Enum.Animation.AnimationState.Ready:
|
|
type = MediaTimelineRecord.TimestampType.CSSAnimationReady;
|
|
break;
|
|
case InspectorBackend.Enum.Animation.AnimationState.Delayed:
|
|
type = MediaTimelineRecord.TimestampType.CSSAnimationDelay;
|
|
break;
|
|
case InspectorBackend.Enum.Animation.AnimationState.Active:
|
|
type = MediaTimelineRecord.TimestampType.CSSAnimationActive;
|
|
break;
|
|
case InspectorBackend.Enum.Animation.AnimationState.Canceled:
|
|
type = MediaTimelineRecord.TimestampType.CSSAnimationCancel;
|
|
break;
|
|
case InspectorBackend.Enum.Animation.AnimationState.Done:
|
|
type = MediaTimelineRecord.TimestampType.CSSAnimationDone;
|
|
break;
|
|
}
|
|
console.assert(type);
|
|
|
|
this._timestamps.push({type, timestamp});
|
|
|
|
this._updateTimes();
|
|
}
|
|
|
|
addDOMEvent(timestamp, domEvent)
|
|
{
|
|
console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
|
|
console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
|
|
|
|
let data = {
|
|
type: MediaTimelineRecord.TimestampType.MediaElementDOMEvent,
|
|
timestamp,
|
|
eventName: domEvent.eventName,
|
|
};
|
|
if (domEvent.originator instanceof WI.DOMNode) {
|
|
data.originator = domEvent.originator;
|
|
data.originatorDisplayName = data.originator.displayName;
|
|
data.originatorCSSPath = WI.cssPath(data.originator, {full: true});
|
|
}
|
|
if (!isEmptyObject(domEvent.data))
|
|
data.data = domEvent.data;
|
|
this._timestamps.push(data);
|
|
|
|
this._updateTimes();
|
|
}
|
|
|
|
powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient)
|
|
{
|
|
console.assert(this._eventType === MediaTimelineRecord.EventType.MediaElement);
|
|
console.assert(!this._timestamps.length || timestamp > this._timestamps.lastValue.timestamp);
|
|
|
|
this._timestamps.push({
|
|
type: MediaTimelineRecord.TimestampType.MediaElementPowerEfficientPlaybackStateChange,
|
|
timestamp,
|
|
isPowerEfficient,
|
|
});
|
|
|
|
this._updateTimes();
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateTimes()
|
|
{
|
|
let oldStartTime = this.startTime;
|
|
let oldEndTime = this.endTime;
|
|
|
|
let firstItem = this._timestamps[0];
|
|
let lastItem = this._timestamps.lastValue;
|
|
|
|
if (isNaN(this._startTime))
|
|
this._startTime = firstItem.timestamp;
|
|
|
|
if (isNaN(this._activeStartTime)) {
|
|
if (this._eventType === MediaTimelineRecord.EventType.MediaElement)
|
|
this._activeStartTime = firstItem.timestamp;
|
|
else if (firstItem.type === MediaTimelineRecord.TimestampType.CSSAnimationActive)
|
|
this._activeStartTime = firstItem.timestamp;
|
|
}
|
|
|
|
switch (lastItem.type) {
|
|
case MediaTimelineRecord.TimestampType.CSSAnimationCancel:
|
|
case MediaTimelineRecord.TimestampType.CSSAnimationDone:
|
|
this._endTime = lastItem.timestamp;
|
|
break;
|
|
|
|
case MediaTimelineRecord.TimestampType.MediaElementDOMEvent:
|
|
if (WI.DOMNode.isPlayEvent(lastItem.eventName))
|
|
this._endTime = NaN;
|
|
else if (!isNaN(this._endTime) || WI.DOMNode.isPauseEvent(lastItem.eventName) || WI.DOMNode.isStopEvent(lastItem.eventName))
|
|
this._endTime = lastItem.timestamp;
|
|
break;
|
|
}
|
|
|
|
if (this.startTime !== oldStartTime || this.endTime !== oldEndTime)
|
|
this.dispatchEventToListeners(WI.TimelineRecord.Event.Updated);
|
|
}
|
|
};
|
|
|
|
WI.MediaTimelineRecord.EventType = {
|
|
CSSAnimation: "css-animation",
|
|
CSSTransition: "css-transition",
|
|
MediaElement: "media-element",
|
|
};
|
|
|
|
WI.MediaTimelineRecord.TimestampType = {
|
|
CSSAnimationReady: "css-animation-ready",
|
|
CSSAnimationDelay: "css-animation-delay",
|
|
CSSAnimationActive: "css-animation-active",
|
|
CSSAnimationCancel: "css-animation-cancel",
|
|
CSSAnimationDone: "css-animation-done",
|
|
// CSS transitions share the same timestamp types.
|
|
|
|
MediaElementDOMEvent: "media-element-dom-event",
|
|
MediaElementPowerEfficientPlaybackStateChange: "media-element-power-efficient-playback-state-change",
|
|
};
|
|
|
|
/* Models/MemoryCategory.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryCategory = class MemoryCategory
|
|
{
|
|
constructor(type, size)
|
|
{
|
|
console.assert(typeof type === "string");
|
|
console.assert(typeof size === "number");
|
|
console.assert(size >= 0);
|
|
|
|
this.type = type;
|
|
this.size = size;
|
|
}
|
|
};
|
|
|
|
WI.MemoryCategory.Type = {
|
|
JavaScript: "javascript",
|
|
Images: "images",
|
|
Layers: "layers",
|
|
Page: "page",
|
|
};
|
|
|
|
/* Models/MemoryInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryInstrument = class MemoryInstrument extends WI.Instrument
|
|
{
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Memory;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.MemoryAgent.startTracking();
|
|
}
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
if (!initiatedByBackend) {
|
|
let target = WI.assumingMainTarget();
|
|
target.MemoryAgent.stopTracking();
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Models/MemoryPressureEvent.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryPressureEvent = class MemoryPressureEvent
|
|
{
|
|
constructor(timestamp, severity)
|
|
{
|
|
this._timestamp = timestamp;
|
|
this._severity = severity;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(timestamp, protocolSeverity)
|
|
{
|
|
let severity;
|
|
switch (protocolSeverity) {
|
|
case InspectorBackend.Enum.Memory.MemoryPressureSeverity.Critical:
|
|
severity = WI.MemoryPressureEvent.Severity.Critical;
|
|
break;
|
|
case InspectorBackend.Enum.Memory.MemoryPressureSeverity.NonCritical:
|
|
severity = WI.MemoryPressureEvent.Severity.NonCritical;
|
|
break;
|
|
default:
|
|
console.error("Unexpected memory pressure severity", protocolSeverity);
|
|
severity = WI.MemoryPressureEvent.Severity.NonCritical;
|
|
break;
|
|
}
|
|
|
|
return new WI.MemoryPressureEvent(timestamp, severity);
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
|
|
static fromJSON(json)
|
|
{
|
|
let {timestamp, severity} = json;
|
|
return new WI.MemoryPressureEvent(timestamp, severity);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
timestamp: this._timestamp,
|
|
severity: this._severity,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get timestamp() { return this._timestamp; }
|
|
get severity() { return this._severity; }
|
|
};
|
|
|
|
WI.MemoryPressureEvent.Severity = {
|
|
Critical: "critical",
|
|
NonCritical: "non-critical",
|
|
};
|
|
|
|
/* Models/MemoryTimeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryTimeline = class MemoryTimeline extends WI.Timeline
|
|
{
|
|
// Public
|
|
|
|
get memoryPressureEvents() { return this._pressureEvents; }
|
|
|
|
addMemoryPressureEvent(memoryPressureEvent)
|
|
{
|
|
console.assert(memoryPressureEvent instanceof WI.MemoryPressureEvent);
|
|
|
|
this._pressureEvents.push(memoryPressureEvent);
|
|
|
|
this.dispatchEventToListeners(WI.MemoryTimeline.Event.MemoryPressureEventAdded, {memoryPressureEvent});
|
|
}
|
|
|
|
// Protected
|
|
|
|
reset(suppressEvents)
|
|
{
|
|
super.reset(suppressEvents);
|
|
|
|
this._pressureEvents = [];
|
|
}
|
|
|
|
addRecord(record, options = {})
|
|
{
|
|
let lastRecord = this.records.lastValue;
|
|
if (lastRecord) {
|
|
let startTime = lastRecord.endTime;
|
|
if (options.discontinuity)
|
|
startTime = options.discontinuity.endTime;
|
|
record.adjustStartTime(startTime);
|
|
}
|
|
|
|
super.addRecord(record, options);
|
|
}
|
|
};
|
|
|
|
WI.MemoryTimeline.Event = {
|
|
MemoryPressureEventAdded: "memory-timeline-memory-pressure-event-added",
|
|
};
|
|
|
|
/* Models/MemoryTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryTimelineRecord = class MemoryTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(timestamp, categories)
|
|
{
|
|
super(WI.TimelineRecord.Type.Memory, timestamp - MemoryTimelineRecord.samplingRatePerSecond, timestamp);
|
|
|
|
console.assert(typeof timestamp === "number");
|
|
console.assert(categories instanceof Array);
|
|
|
|
this._timestamp = timestamp;
|
|
this._categories = WI.MemoryTimelineRecord.memoryCategoriesFromProtocol(categories);
|
|
this._exportCategories = categories;
|
|
|
|
this._totalSize = 0;
|
|
for (let {size} of categories)
|
|
this._totalSize += size;
|
|
}
|
|
|
|
// Static
|
|
|
|
static get samplingRatePerSecond()
|
|
{
|
|
// 500ms. This matches the ResourceUsageThread sampling frequency in the backend.
|
|
return 0.5;
|
|
}
|
|
|
|
static memoryCategoriesFromProtocol(categories)
|
|
{
|
|
let javascriptSize = 0;
|
|
let imagesSize = 0;
|
|
let layersSize = 0;
|
|
let pageSize = 0;
|
|
|
|
for (let {type, size} of categories) {
|
|
switch (type) {
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.JavaScript:
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.JIT:
|
|
javascriptSize += size;
|
|
break;
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.Images:
|
|
imagesSize += size;
|
|
break;
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.Layers:
|
|
layersSize += size;
|
|
break;
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.Page:
|
|
case InspectorBackend.Enum.Memory.CategoryDataType.Other:
|
|
pageSize += size;
|
|
break;
|
|
default:
|
|
console.warn("Unhandled Memory.CategoryDataType: " + type);
|
|
break;
|
|
}
|
|
}
|
|
|
|
let memoryCategories = [];
|
|
if (javascriptSize)
|
|
memoryCategories.push({type: WI.MemoryCategory.Type.JavaScript, size: javascriptSize});
|
|
if (imagesSize)
|
|
memoryCategories.push({type: WI.MemoryCategory.Type.Images, size: imagesSize});
|
|
if (layersSize)
|
|
memoryCategories.push({type: WI.MemoryCategory.Type.Layers, size: layersSize});
|
|
if (pageSize)
|
|
memoryCategories.push({type: WI.MemoryCategory.Type.Page, size: pageSize});
|
|
return memoryCategories;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {timestamp, categories} = json;
|
|
return new WI.MemoryTimelineRecord(timestamp, categories);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
type: this.type,
|
|
timestamp: this.startTime,
|
|
categories: this._exportCategories,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get timestamp() { return this._timestamp; }
|
|
get categories() { return this._categories; }
|
|
get totalSize() { return this._totalSize; }
|
|
|
|
get unadjustedStartTime() { return this._timestamp; }
|
|
|
|
adjustStartTime(startTime)
|
|
{
|
|
console.assert(startTime < this._endTime);
|
|
this._startTime = startTime;
|
|
}
|
|
};
|
|
|
|
/* Models/NetworkInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.NetworkInstrument = class NetworkInstrument extends WI.Instrument
|
|
{
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Network;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
// Nothing to do, network instrumentation is always happening.
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
// Nothing to do, network instrumentation is always happening.
|
|
}
|
|
};
|
|
|
|
/* Models/NetworkTimeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.NetworkTimeline = class NetworkTimeline extends WI.Timeline
|
|
{
|
|
// Public
|
|
|
|
recordForResource(resource)
|
|
{
|
|
console.assert(resource instanceof WI.Resource);
|
|
|
|
return this._resourceRecordMap.get(resource) || null;
|
|
}
|
|
|
|
reset(suppressEvents)
|
|
{
|
|
this._resourceRecordMap = new Map;
|
|
|
|
super.reset(suppressEvents);
|
|
}
|
|
|
|
addRecord(record, options = {})
|
|
{
|
|
console.assert(record instanceof WI.ResourceTimelineRecord);
|
|
|
|
// Don't allow duplicate records for a resource.
|
|
if (this._resourceRecordMap.has(record.resource))
|
|
return;
|
|
|
|
this._resourceRecordMap.set(record.resource, record);
|
|
|
|
super.addRecord(record, options);
|
|
}
|
|
};
|
|
|
|
/* Models/ObjectPreview.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ObjectPreview = class ObjectPreview
|
|
{
|
|
constructor(type, subtype, description, lossless, overflow, properties, entries, size)
|
|
{
|
|
console.assert(type);
|
|
console.assert(typeof lossless === "boolean");
|
|
console.assert(!properties || !properties.length || properties[0] instanceof WI.PropertyPreview);
|
|
console.assert(!entries || !entries.length || entries[0] instanceof WI.CollectionEntryPreview);
|
|
|
|
this._type = type;
|
|
this._subtype = subtype;
|
|
this._description = description || "";
|
|
this._lossless = lossless;
|
|
this._overflow = overflow || false;
|
|
this._size = size;
|
|
|
|
this._properties = properties || null;
|
|
this._entries = entries || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Runtime.ObjectPreview.
|
|
static fromPayload(payload)
|
|
{
|
|
if (payload.properties)
|
|
payload.properties = payload.properties.map(WI.PropertyPreview.fromPayload);
|
|
if (payload.entries)
|
|
payload.entries = payload.entries.map(WI.CollectionEntryPreview.fromPayload);
|
|
return new WI.ObjectPreview(payload.type, payload.subtype, payload.description, payload.lossless, payload.overflow, payload.properties, payload.entries, payload.size);
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get subtype() { return this._subtype; }
|
|
get description() { return this._description; }
|
|
get lossless() { return this._lossless; }
|
|
get overflow() { return this._overflow; }
|
|
get propertyPreviews() { return this._properties; }
|
|
get collectionEntryPreviews() { return this._entries; }
|
|
get size() { return this._size; }
|
|
|
|
hasSize()
|
|
{
|
|
return this._size !== undefined && (this._subtype === "array" || this._subtype === "set" || this._subtype === "map" || this._subtype === "weakmap" || this._subtype === "weakset");
|
|
}
|
|
};
|
|
|
|
/* Models/Probe.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 University of Washington. All rights reserved.
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProbeSample = class ProbeSample
|
|
{
|
|
constructor(sampleId, batchId, elapsedTime, object)
|
|
{
|
|
console.assert(object instanceof WI.RemoteObject);
|
|
|
|
this.sampleId = sampleId;
|
|
this.batchId = batchId;
|
|
this.timestamp = elapsedTime;
|
|
this.object = object;
|
|
}
|
|
};
|
|
|
|
WI.Probe = class Probe extends WI.Object
|
|
{
|
|
constructor(id, breakpoint, expression)
|
|
{
|
|
super();
|
|
|
|
console.assert(id);
|
|
console.assert(breakpoint instanceof WI.Breakpoint);
|
|
|
|
this._id = id;
|
|
this._breakpoint = breakpoint;
|
|
this._expression = expression;
|
|
this._samples = [];
|
|
}
|
|
|
|
// Public
|
|
|
|
get id()
|
|
{
|
|
return this._id;
|
|
}
|
|
|
|
get breakpoint()
|
|
{
|
|
return this._breakpoint;
|
|
}
|
|
|
|
get expression()
|
|
{
|
|
return this._expression;
|
|
}
|
|
|
|
set expression(value)
|
|
{
|
|
if (this._expression === value)
|
|
return;
|
|
|
|
var data = {oldValue: this._expression, newValue: value};
|
|
this._expression = value;
|
|
this.clearSamples();
|
|
this.dispatchEventToListeners(WI.Probe.Event.ExpressionChanged, data);
|
|
}
|
|
|
|
get samples()
|
|
{
|
|
return this._samples.slice();
|
|
}
|
|
|
|
clearSamples()
|
|
{
|
|
this._samples = [];
|
|
this.dispatchEventToListeners(WI.Probe.Event.SamplesCleared);
|
|
}
|
|
|
|
addSample(sample)
|
|
{
|
|
console.assert(sample instanceof WI.ProbeSample, "Wrong object type passed as probe sample: ", sample);
|
|
this._samples.push(sample);
|
|
this.dispatchEventToListeners(WI.Probe.Event.SampleAdded, sample);
|
|
}
|
|
};
|
|
|
|
WI.Probe.Event = {
|
|
ExpressionChanged: "probe-object-expression-changed",
|
|
SampleAdded: "probe-object-sample-added",
|
|
SamplesCleared: "probe-object-samples-cleared"
|
|
};
|
|
|
|
/* Models/ProbeSet.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 University of Washington. All rights reserved.
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// A ProbeSet clusters Probes from the same Breakpoint and their samples.
|
|
|
|
WI.ProbeSet = class ProbeSet extends WI.Object
|
|
{
|
|
constructor(breakpoint)
|
|
{
|
|
super();
|
|
|
|
console.assert(breakpoint instanceof WI.Breakpoint, "Unknown breakpoint argument: ", breakpoint);
|
|
|
|
this._breakpoint = breakpoint;
|
|
this._probes = [];
|
|
this._probesByIdentifier = new Map;
|
|
|
|
this._createDataTable();
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceChanged, this);
|
|
WI.Probe.addEventListener(WI.Probe.Event.SampleAdded, this._sampleCollected, this);
|
|
}
|
|
|
|
// Public
|
|
|
|
get breakpoint() { return this._breakpoint; }
|
|
get probes() { return this._probes.slice(); }
|
|
get dataTable() { return this._dataTable; }
|
|
|
|
clear()
|
|
{
|
|
this._breakpoint.clearActions(WI.BreakpointAction.Type.Probe);
|
|
}
|
|
|
|
clearSamples()
|
|
{
|
|
for (var probe of this._probes)
|
|
probe.clearSamples();
|
|
|
|
var oldTable = this._dataTable;
|
|
this._createDataTable();
|
|
this.dispatchEventToListeners(WI.ProbeSet.Event.SamplesCleared, {oldTable});
|
|
}
|
|
|
|
createProbe(expression)
|
|
{
|
|
this._breakpoint.addAction(new WI.BreakpointAction(WI.BreakpointAction.Type.Probe, {data: expression}));
|
|
}
|
|
|
|
addProbe(probe)
|
|
{
|
|
console.assert(probe instanceof WI.Probe, "Tried to add non-probe ", probe, " to probe group", this);
|
|
console.assert(probe.breakpoint === this.breakpoint, "Probe and ProbeSet must have same breakpoint.", probe, this);
|
|
|
|
this._probes.push(probe);
|
|
this._probesByIdentifier.set(probe.id, probe);
|
|
|
|
this.dataTable.addProbe(probe);
|
|
this.dispatchEventToListeners(WI.ProbeSet.Event.ProbeAdded, probe);
|
|
}
|
|
|
|
removeProbe(probe)
|
|
{
|
|
console.assert(probe instanceof WI.Probe, "Tried to remove non-probe ", probe, " to probe group", this);
|
|
console.assert(this._probes.indexOf(probe) !== -1, "Tried to remove probe", probe, " not in group ", this);
|
|
console.assert(this._probesByIdentifier.has(probe.id), "Tried to remove probe", probe, " not in group ", this);
|
|
|
|
this._probes.splice(this._probes.indexOf(probe), 1);
|
|
this._probesByIdentifier.delete(probe.id);
|
|
this.dataTable.removeProbe(probe);
|
|
this.dispatchEventToListeners(WI.ProbeSet.Event.ProbeRemoved, probe);
|
|
}
|
|
|
|
willRemove()
|
|
{
|
|
console.assert(!this._probes.length, "ProbeSet.willRemove called, but probes still associated with group: ", this._probes);
|
|
|
|
WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceChanged, this);
|
|
WI.Probe.removeEventListener(WI.Probe.Event.SampleAdded, this._sampleCollected, this);
|
|
}
|
|
|
|
// Private
|
|
|
|
_mainResourceChanged()
|
|
{
|
|
this.dataTable.mainResourceChanged();
|
|
}
|
|
|
|
_createDataTable()
|
|
{
|
|
if (this.dataTable)
|
|
this.dataTable.willRemove();
|
|
|
|
this._dataTable = new WI.ProbeSetDataTable(this);
|
|
}
|
|
|
|
_sampleCollected(event)
|
|
{
|
|
var sample = event.data;
|
|
console.assert(sample instanceof WI.ProbeSample, "Tried to add non-sample to probe group: ", sample);
|
|
|
|
var probe = event.target;
|
|
if (!this._probesByIdentifier.has(probe.id))
|
|
return;
|
|
|
|
console.assert(this.dataTable);
|
|
this.dataTable.addSampleForProbe(probe, sample);
|
|
this.dispatchEventToListeners(WI.ProbeSet.Event.SampleAdded, {probe, sample});
|
|
}
|
|
};
|
|
|
|
WI.ProbeSet.Event = {
|
|
ProbeAdded: "probe-set-probe-added",
|
|
ProbeRemoved: "probe-set-probe-removed",
|
|
SampleAdded: "probe-set-sample-added",
|
|
SamplesCleared: "probe-set-samples-cleared",
|
|
};
|
|
|
|
/* Models/ProbeSetDataFrame.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 University of Washington. All rights reserved.
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProbeSetDataFrame = class ProbeSetDataFrame
|
|
{
|
|
constructor(index)
|
|
{
|
|
this._count = 0;
|
|
this._index = index;
|
|
this._separator = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static compare(a, b)
|
|
{
|
|
console.assert(a instanceof WI.ProbeSetDataFrame, a);
|
|
console.assert(b instanceof WI.ProbeSetDataFrame, b);
|
|
|
|
return a.index - b.index;
|
|
}
|
|
|
|
// Public
|
|
|
|
get key()
|
|
{
|
|
return String(this._index);
|
|
}
|
|
|
|
get count()
|
|
{
|
|
return this._count;
|
|
}
|
|
|
|
get index()
|
|
{
|
|
return this._index;
|
|
}
|
|
|
|
get isSeparator()
|
|
{
|
|
return this._separator;
|
|
}
|
|
|
|
// The last data frame before a main frame navigation is marked as a "separator" frame.
|
|
set isSeparator(value)
|
|
{
|
|
this._separator = !!value;
|
|
}
|
|
|
|
addSampleForProbe(probe, sample)
|
|
{
|
|
this[probe.id] = sample;
|
|
this._count++;
|
|
}
|
|
|
|
missingKeys(probeSet)
|
|
{
|
|
return probeSet.probes.filter(function(probe) {
|
|
return !this.hasOwnProperty(probe.id);
|
|
}, this);
|
|
}
|
|
|
|
isComplete(probeSet)
|
|
{
|
|
return !this.missingKeys(probeSet).length;
|
|
}
|
|
|
|
fillMissingValues(probeSet)
|
|
{
|
|
for (var key of this.missingKeys(probeSet))
|
|
this[key] = WI.ProbeSetDataFrame.MissingValue;
|
|
}
|
|
};
|
|
|
|
WI.ProbeSetDataFrame.MissingValue = "?";
|
|
|
|
/* Models/ProbeSetDataTable.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 University of Washington. All rights reserved.
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
|
* IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
|
|
* TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
|
|
* PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProbeSetDataTable = class ProbeSetDataTable extends WI.Object
|
|
{
|
|
constructor(probeSet)
|
|
{
|
|
super();
|
|
|
|
this._probeSet = probeSet;
|
|
this._frames = [];
|
|
this._previousBatchIdentifier = WI.ProbeSetDataTable.SentinelValue;
|
|
}
|
|
|
|
// Public
|
|
|
|
get frames()
|
|
{
|
|
return this._frames.slice();
|
|
}
|
|
|
|
get separators()
|
|
{
|
|
return this._frames.filter(function(frame) { return frame.isSeparator; });
|
|
}
|
|
|
|
willRemove()
|
|
{
|
|
this.dispatchEventToListeners(WI.ProbeSetDataTable.Event.WillRemove);
|
|
this._frames = [];
|
|
delete this._probeSet;
|
|
}
|
|
|
|
mainResourceChanged()
|
|
{
|
|
this.addSeparator();
|
|
}
|
|
|
|
addSampleForProbe(probe, sample)
|
|
{
|
|
// Eagerly save the frame if the batch identifier differs, or we know the frame is full.
|
|
// Create a new frame when the batch identifier differs.
|
|
if (sample.batchId !== this._previousBatchIdentifier) {
|
|
if (this._openFrame) {
|
|
this._openFrame.fillMissingValues(this._probeSet);
|
|
this.addFrame(this._openFrame);
|
|
}
|
|
this._openFrame = this.createFrame();
|
|
this._previousBatchIdentifier = sample.batchId;
|
|
}
|
|
|
|
console.assert(this._openFrame, "Should always have an open frame before adding sample.", this, probe, sample);
|
|
this._openFrame.addSampleForProbe(probe, sample);
|
|
if (this._openFrame.count === this._probeSet.probes.length) {
|
|
this.addFrame(this._openFrame);
|
|
this._openFrame = null;
|
|
}
|
|
}
|
|
|
|
addProbe(probe)
|
|
{
|
|
for (var frame of this.frames)
|
|
if (!frame[probe.id])
|
|
frame[probe.id] = WI.ProbeSetDataTable.UnknownValue;
|
|
}
|
|
|
|
removeProbe(probe)
|
|
{
|
|
for (var frame of this.frames)
|
|
delete frame[probe.id];
|
|
}
|
|
|
|
// Protected - can be overridden by subclasses.
|
|
|
|
createFrame()
|
|
{
|
|
return new WI.ProbeSetDataFrame(this._frames.length);
|
|
}
|
|
|
|
addFrame(frame)
|
|
{
|
|
this._frames.push(frame);
|
|
this.dispatchEventToListeners(WI.ProbeSetDataTable.Event.FrameInserted, frame);
|
|
}
|
|
|
|
addSeparator()
|
|
{
|
|
// Separators must be associated with a frame.
|
|
if (!this._frames.length)
|
|
return;
|
|
|
|
var previousFrame = this._frames.lastValue;
|
|
// Don't send out duplicate events for adjacent separators.
|
|
if (previousFrame.isSeparator)
|
|
return;
|
|
|
|
previousFrame.isSeparator = true;
|
|
this.dispatchEventToListeners(WI.ProbeSetDataTable.Event.SeparatorInserted, previousFrame);
|
|
}
|
|
};
|
|
|
|
WI.ProbeSetDataTable.Event = {
|
|
FrameInserted: "probe-set-data-table-frame-inserted",
|
|
SeparatorInserted: "probe-set-data-table-separator-inserted",
|
|
WillRemove: "probe-set-data-table-will-remove"
|
|
};
|
|
|
|
WI.ProbeSetDataTable.SentinelValue = -1;
|
|
WI.ProbeSetDataTable.UnknownValue = "?";
|
|
|
|
/* Models/Profile.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Profile = class Profile
|
|
{
|
|
constructor(topDownRootNodes)
|
|
{
|
|
topDownRootNodes = topDownRootNodes || [];
|
|
|
|
console.assert(topDownRootNodes instanceof Array);
|
|
console.assert(topDownRootNodes.reduce(function(previousValue, node) { return previousValue && node instanceof WI.ProfileNode; }, true));
|
|
|
|
this._topDownRootNodes = topDownRootNodes;
|
|
}
|
|
|
|
// Public
|
|
|
|
get topDownRootNodes()
|
|
{
|
|
return this._topDownRootNodes;
|
|
}
|
|
|
|
get bottomUpRootNodes()
|
|
{
|
|
// FIXME: Implement.
|
|
return [];
|
|
}
|
|
};
|
|
|
|
/* Models/ProfileNode.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProfileNode = class ProfileNode
|
|
{
|
|
constructor(id, type, functionName, sourceCodeLocation, callInfo, childNodes)
|
|
{
|
|
childNodes = childNodes || [];
|
|
|
|
console.assert(id);
|
|
console.assert(callInfo && !isEmptyObject(callInfo));
|
|
console.assert(childNodes instanceof Array);
|
|
console.assert(childNodes.every((node) => node instanceof WI.ProfileNode));
|
|
|
|
this._id = id;
|
|
this._type = type || WI.ProfileNode.Type.Function;
|
|
this._functionName = functionName || null;
|
|
this._sourceCodeLocation = sourceCodeLocation || null;
|
|
this._callInfo = callInfo;
|
|
this._childNodes = childNodes;
|
|
this._parentNode = null;
|
|
this._previousSibling = null;
|
|
this._nextSibling = null;
|
|
this._computedTotalTimes = false;
|
|
|
|
this._startTime = this._callInfo.startTime;
|
|
this._endTime = this._callInfo.endTime;
|
|
this._totalTime = this._callInfo.totalTime;
|
|
this._callCount = this._callInfo.callCount;
|
|
|
|
for (var i = 0; i < this._childNodes.length; ++i)
|
|
this._childNodes[i].establishRelationships(this, this._childNodes[i - 1], this._childNodes[i + 1]);
|
|
}
|
|
|
|
// Public
|
|
|
|
get id() { return this._id; }
|
|
get type() { return this._type; }
|
|
get functionName() { return this._functionName; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
get callInfo() { return this._callInfo; }
|
|
get previousSibling() { return this._previousSibling; }
|
|
get nextSibling() { return this._nextSibling; }
|
|
get parentNode() { return this._parentNode; }
|
|
get childNodes() { return this._childNodes; }
|
|
get startTime() { return this._startTime; }
|
|
get endTime() { return this._endTime; }
|
|
|
|
get selfTime()
|
|
{
|
|
this._computeTotalTimesIfNeeded();
|
|
return this._selfTime;
|
|
}
|
|
|
|
get totalTime()
|
|
{
|
|
this._computeTotalTimesIfNeeded();
|
|
return this._totalTime;
|
|
}
|
|
|
|
computeCallInfoForTimeRange(rangeStartTime, rangeEndTime)
|
|
{
|
|
console.assert(typeof rangeStartTime === "number");
|
|
console.assert(typeof rangeEndTime === "number");
|
|
|
|
// With aggregate call info we can't accurately partition self/total/average time
|
|
// in partial ranges because we don't know exactly when each call started. So we
|
|
// always return the entire range.
|
|
if (this._selfTime === undefined) {
|
|
var childNodesTotalTime = 0;
|
|
for (var childNode of this._childNodes)
|
|
childNodesTotalTime += childNode.totalTime;
|
|
this._selfTime = this._totalTime - childNodesTotalTime;
|
|
// selfTime can negative or very close to zero due to floating point error.
|
|
// Since we show at most four decimal places, treat anything less as zero.
|
|
if (this._selfTime < 0.0001)
|
|
this._selfTime = 0.0;
|
|
}
|
|
|
|
return {
|
|
callCount: this._callCount,
|
|
startTime: this._startTime,
|
|
endTime: this._endTime,
|
|
selfTime: this._selfTime,
|
|
totalTime: this._totalTime,
|
|
averageTime: this._selfTime / this._callCount,
|
|
};
|
|
}
|
|
|
|
traverseNextProfileNode(stayWithin)
|
|
{
|
|
var profileNode = this._childNodes[0];
|
|
if (profileNode)
|
|
return profileNode;
|
|
|
|
if (this === stayWithin)
|
|
return null;
|
|
|
|
profileNode = this._nextSibling;
|
|
if (profileNode)
|
|
return profileNode;
|
|
|
|
profileNode = this;
|
|
while (profileNode && !profileNode.nextSibling && profileNode.parentNode !== stayWithin)
|
|
profileNode = profileNode.parentNode;
|
|
|
|
if (!profileNode)
|
|
return null;
|
|
|
|
return profileNode.nextSibling;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.ProfileNode.TypeCookieKey] = this._type || null;
|
|
cookie[WI.ProfileNode.FunctionNameCookieKey] = this._functionName || null;
|
|
cookie[WI.ProfileNode.SourceCodeURLCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.sourceCode.url ? this._sourceCodeLocation.sourceCode.url.hash : null : null;
|
|
cookie[WI.ProfileNode.SourceCodeLocationLineCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.lineNumber : null;
|
|
cookie[WI.ProfileNode.SourceCodeLocationColumnCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.columnNumber : null;
|
|
}
|
|
|
|
// Protected
|
|
|
|
establishRelationships(parentNode, previousSibling, nextSibling)
|
|
{
|
|
this._parentNode = parentNode || null;
|
|
this._previousSibling = previousSibling || null;
|
|
this._nextSibling = nextSibling || null;
|
|
}
|
|
|
|
// Private
|
|
|
|
_computeTotalTimesIfNeeded()
|
|
{
|
|
if (this._computedTotalTimes)
|
|
return;
|
|
|
|
this._computedTotalTimes = true;
|
|
|
|
var info = this.computeCallInfoForTimeRange(0, Infinity);
|
|
this._startTime = info.startTime;
|
|
this._endTime = info.endTime;
|
|
this._selfTime = info.selfTime;
|
|
this._totalTime = info.totalTime;
|
|
}
|
|
};
|
|
|
|
WI.ProfileNode.Type = {
|
|
Function: "profile-node-type-function",
|
|
Program: "profile-node-type-program"
|
|
};
|
|
|
|
WI.ProfileNode.TypeIdentifier = "profile-node";
|
|
WI.ProfileNode.TypeCookieKey = "profile-node-type";
|
|
WI.ProfileNode.FunctionNameCookieKey = "profile-node-function-name";
|
|
WI.ProfileNode.SourceCodeURLCookieKey = "profile-node-source-code-url";
|
|
WI.ProfileNode.SourceCodeLocationLineCookieKey = "profile-node-source-code-location-line";
|
|
WI.ProfileNode.SourceCodeLocationColumnCookieKey = "profile-node-source-code-location-column";
|
|
|
|
/* Models/ProfileNodeCall.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ProfileNodeCall = class ProfileNodeCall
|
|
{
|
|
constructor(startTime, totalTime)
|
|
{
|
|
console.assert(startTime);
|
|
|
|
this._startTime = startTime;
|
|
this._totalTime = totalTime || 0;
|
|
this._parentNode = null;
|
|
this._previousSibling = null;
|
|
this._nextSibling = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get startTime()
|
|
{
|
|
return this._startTime;
|
|
}
|
|
|
|
get totalTime()
|
|
{
|
|
return this._totalTime;
|
|
}
|
|
|
|
get endTime()
|
|
{
|
|
return this._startTime + this._totalTime;
|
|
}
|
|
|
|
// Protected
|
|
|
|
establishRelationships(parentNode, previousSibling, nextSibling)
|
|
{
|
|
this._parentNode = parentNode || null;
|
|
this._previousSibling = previousSibling || null;
|
|
this._nextSibling = nextSibling || null;
|
|
}
|
|
};
|
|
|
|
/* Models/PropertyDescriptor.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.PropertyDescriptor = class PropertyDescriptor
|
|
{
|
|
constructor(descriptor, symbol, isOwnProperty, wasThrown, nativeGetter, isPrivateProperty, isInternalProperty)
|
|
{
|
|
console.assert(descriptor);
|
|
console.assert(descriptor.name);
|
|
console.assert(!descriptor.value || descriptor.value instanceof WI.RemoteObject);
|
|
console.assert(!descriptor.get || descriptor.get instanceof WI.RemoteObject);
|
|
console.assert(!descriptor.set || descriptor.set instanceof WI.RemoteObject);
|
|
console.assert(!symbol || symbol instanceof WI.RemoteObject);
|
|
|
|
this._name = descriptor.name;
|
|
this._value = descriptor.value;
|
|
this._hasValue = "value" in descriptor;
|
|
this._get = descriptor.get;
|
|
this._set = descriptor.set;
|
|
this._symbol = symbol;
|
|
|
|
this._writable = descriptor.writable || false;
|
|
this._configurable = descriptor.configurable || false;
|
|
this._enumerable = descriptor.enumerable || false;
|
|
|
|
this._own = isOwnProperty || false;
|
|
this._wasThrown = wasThrown || false;
|
|
this._nativeGetterValue = nativeGetter || false;
|
|
this._private = isPrivateProperty || false;
|
|
this._internal = isInternalProperty || false;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Runtime.PropertyDescriptor or Runtime.InternalPropertyDescriptor (second argument).
|
|
static fromPayload(payload, internal, target)
|
|
{
|
|
if (payload.value)
|
|
payload.value = WI.RemoteObject.fromPayload(payload.value, target);
|
|
if (payload.get)
|
|
payload.get = WI.RemoteObject.fromPayload(payload.get, target);
|
|
if (payload.set)
|
|
payload.set = WI.RemoteObject.fromPayload(payload.set, target);
|
|
|
|
if (payload.symbol)
|
|
payload.symbol = WI.RemoteObject.fromPayload(payload.symbol, target);
|
|
|
|
if (internal) {
|
|
console.assert(payload.value);
|
|
payload.writable = payload.configurable = payload.enumerable = false;
|
|
payload.isOwn = true;
|
|
}
|
|
|
|
return new WI.PropertyDescriptor(payload, payload.symbol, payload.isOwn, payload.wasThrown, payload.nativeGetter, payload.isPrivate, internal);
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get value() { return this._value; }
|
|
get get() { return this._get; }
|
|
get set() { return this._set; }
|
|
get writable() { return this._writable; }
|
|
get configurable() { return this._configurable; }
|
|
get enumerable() { return this._enumerable; }
|
|
get symbol() { return this._symbol; }
|
|
get isOwnProperty() { return this._own; }
|
|
get wasThrown() { return this._wasThrown; }
|
|
get nativeGetter() { return this._nativeGetterValue; }
|
|
get isPrivateProperty() { return this._private; }
|
|
get isInternalProperty() { return this._internal; }
|
|
|
|
hasValue()
|
|
{
|
|
return this._hasValue;
|
|
}
|
|
|
|
hasGetter()
|
|
{
|
|
return this._get && this._get.type === "function";
|
|
}
|
|
|
|
hasSetter()
|
|
{
|
|
return this._set && this._set.type === "function";
|
|
}
|
|
|
|
isIndexProperty()
|
|
{
|
|
return !isNaN(Number(this._name));
|
|
}
|
|
|
|
isSymbolProperty()
|
|
{
|
|
return !!this._symbol;
|
|
}
|
|
};
|
|
|
|
/* Models/PropertyPreview.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.PropertyPreview = class PropertyPreview
|
|
{
|
|
constructor(name, type, {subtype, value, valuePreview, isPrivateProperty, isInternalProperty} = {})
|
|
{
|
|
console.assert(typeof name === "string");
|
|
console.assert(type);
|
|
console.assert(!subtype || typeof subtype === "string");
|
|
console.assert(!value || typeof value === "string");
|
|
console.assert(!valuePreview || valuePreview instanceof WI.ObjectPreview);
|
|
|
|
this._name = name;
|
|
this._type = type;
|
|
this._subtype = subtype;
|
|
this._value = value;
|
|
this._valuePreview = valuePreview;
|
|
this._private = isPrivateProperty;
|
|
this._internal = isInternalProperty;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Runtime.PropertyPreview.
|
|
static fromPayload(payload)
|
|
{
|
|
return new WI.PropertyPreview(payload.name, payload.type, {
|
|
subtype: payload.subtype,
|
|
value: payload.value,
|
|
valuePreview: payload.valuePreview ? WI.ObjectPreview.fromPayload(payload.valuePreview) : undefined,
|
|
isPrivateProperty: payload.isPrivate,
|
|
isInternalProperty: payload.internal,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get type() { return this._type; }
|
|
get subtype() { return this._subtype; }
|
|
get value() { return this._value; }
|
|
get valuePreview() { return this._valuePreview; }
|
|
get private() { return this._private; }
|
|
get internal() { return this._internal; }
|
|
};
|
|
|
|
/* Models/QueryMatch.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.QueryMatch = class QueryMatch
|
|
{
|
|
constructor(type, index, queryIndex)
|
|
{
|
|
console.assert(Object.values(WI.QueryMatch.Type).includes(type), type);
|
|
console.assert(index >= 0, index);
|
|
console.assert(queryIndex >= 0, queryIndex);
|
|
this._type = type;
|
|
this._index = index;
|
|
this._queryIndex = queryIndex;
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get index() { return this._index; }
|
|
get queryIndex() { return this._queryIndex; }
|
|
};
|
|
|
|
WI.QueryMatch.Type = {
|
|
Normal: Symbol("normal"),
|
|
Special: Symbol("special"),
|
|
};
|
|
|
|
/* Models/QueryResult.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.QueryResult = class QueryResult
|
|
{
|
|
constructor(value, matches)
|
|
{
|
|
console.assert(matches.length, "Query matches list can't be empty.");
|
|
|
|
this._value = value;
|
|
this._matches = matches;
|
|
}
|
|
|
|
// Public
|
|
|
|
get value() { return this._value; }
|
|
get matches() { return this._matches; }
|
|
|
|
get rank()
|
|
{
|
|
if (this._rank === undefined)
|
|
this._calculateRank();
|
|
|
|
return this._rank;
|
|
}
|
|
|
|
get matchingTextRanges()
|
|
{
|
|
this._matchingTextRanges ??= this._createMatchingTextRanges();
|
|
|
|
return this._matchingTextRanges;
|
|
}
|
|
|
|
// Private
|
|
|
|
_calculateRank()
|
|
{
|
|
const normalWeight = 10;
|
|
const consecutiveWeight = 5;
|
|
const specialMultiplier = 5;
|
|
|
|
function getMultiplier(match) {
|
|
if (match.type === WI.QueryMatch.Type.Special)
|
|
return specialMultiplier;
|
|
|
|
return 1;
|
|
}
|
|
|
|
this._rank = 0;
|
|
|
|
let previousMatch = null;
|
|
let consecutiveMatchStart = null;
|
|
for (let match of this._matches) {
|
|
this._rank += normalWeight * getMultiplier(match);
|
|
|
|
let consecutive = previousMatch && previousMatch.index === match.index - 1;
|
|
if (consecutive) {
|
|
if (!consecutiveMatchStart)
|
|
consecutiveMatchStart = previousMatch;
|
|
|
|
// If the first match in this consecutive series was a special character, give a
|
|
// bonus (more likely to match a specific word in the text). Otherwise, multiply
|
|
// by the current length of the consecutive sequence (gives priority to fewer
|
|
// longer sequences instead of more short sequences).
|
|
this._rank += consecutiveWeight * getMultiplier(consecutiveMatchStart) * (match.index - consecutiveMatchStart.index);
|
|
} else if (consecutiveMatchStart)
|
|
consecutiveMatchStart = null;
|
|
|
|
previousMatch = match;
|
|
|
|
// The match index is deducted from the total rank, so matches that occur closer to
|
|
// the beginning of the string are ranked higher. Increase the amount subtracted if
|
|
// the match is special, so as to favor matches towards the beginning of the string.
|
|
if (!consecutive)
|
|
this._rank -= match.index * getMultiplier(match);
|
|
}
|
|
}
|
|
|
|
_createMatchingTextRanges()
|
|
{
|
|
if (!this._matches.length)
|
|
return [];
|
|
|
|
let ranges = [];
|
|
let startIndex = this._matches[0].index;
|
|
let endIndex = startIndex;
|
|
for (let i = 1; i < this._matches.length; ++i) {
|
|
let match = this._matches[i];
|
|
|
|
// Increment endIndex for consecutive match.
|
|
if (match.index === endIndex + 1) {
|
|
endIndex++;
|
|
continue;
|
|
}
|
|
|
|
// Begin a new range when a gap between this match and the previous match is found.
|
|
ranges.push(new WI.TextRange(0, startIndex, 0, endIndex + 1));
|
|
startIndex = match.index;
|
|
endIndex = startIndex;
|
|
}
|
|
|
|
ranges.push(new WI.TextRange(0, startIndex, 0, endIndex + 1));
|
|
return ranges;
|
|
}
|
|
|
|
// Testing
|
|
|
|
set rankForTesting(rank) { this._rank = rank; }
|
|
};
|
|
|
|
/* Models/Recording.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Recording = class Recording extends WI.Object
|
|
{
|
|
constructor(version, type, initialState, frames, data)
|
|
{
|
|
super();
|
|
|
|
this._version = version;
|
|
this._type = type;
|
|
this._initialState = initialState;
|
|
this._frames = frames;
|
|
this._data = data;
|
|
this._displayName = WI.UIString("Recording");
|
|
|
|
this._swizzle = null;
|
|
this._actions = [new WI.RecordingInitialStateAction].concat(...this._frames.map((frame) => frame.actions));
|
|
this._visualActionIndexes = [];
|
|
this._source = null;
|
|
|
|
this._processContext = null;
|
|
this._processStates = [];
|
|
this._processing = false;
|
|
}
|
|
|
|
static fromPayload(payload, frames)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (typeof payload.version !== "number") {
|
|
WI.Recording.synthesizeError(WI.UIString("non-number %s").format(WI.unlocalizedString("version")));
|
|
return null;
|
|
}
|
|
|
|
if (payload.version < 1 || payload.version > WI.Recording.Version) {
|
|
WI.Recording.synthesizeError(WI.UIString("unsupported %s").format(WI.unlocalizedString("version")));
|
|
return null;
|
|
}
|
|
|
|
if (parseInt(payload.version) !== payload.version) {
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-integer %s").format(WI.unlocalizedString("version")));
|
|
payload.version = parseInt(payload.version);
|
|
}
|
|
|
|
let type = null;
|
|
switch (payload.type) {
|
|
case InspectorBackend.Enum.Recording.Type.Canvas2D:
|
|
type = WI.Recording.Type.Canvas2D;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.OffscreenCanvas2D:
|
|
type = WI.Recording.Type.OffscreenCanvas2D;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.CanvasBitmapRenderer:
|
|
type = WI.Recording.Type.CanvasBitmapRenderer;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.OffscreenCanvasBitmapRenderer:
|
|
type = WI.Recording.Type.OffscreenCanvasBitmapRenderer;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.CanvasWebGL:
|
|
type = WI.Recording.Type.CanvasWebGL;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.OffscreenCanvasWebGL:
|
|
type = WI.Recording.Type.OffscreenCanvasWebGL;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.CanvasWebGL2:
|
|
type = WI.Recording.Type.CanvasWebGL2;
|
|
break;
|
|
case InspectorBackend.Enum.Recording.Type.OffscreenCanvasWebGL2:
|
|
type = WI.Recording.Type.OffscreenCanvasWebGL2;
|
|
break;
|
|
default:
|
|
WI.Recording.synthesizeWarning(WI.UIString("unknown %s \u0022%s\u0022").format(WI.unlocalizedString("type"), payload.type));
|
|
type = String(payload.type);
|
|
break;
|
|
}
|
|
|
|
if (typeof payload.initialState !== "object" || payload.initialState === null) {
|
|
if ("initialState" in payload)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-object %s").format(WI.unlocalizedString("initialState")));
|
|
|
|
payload.initialState = {};
|
|
}
|
|
|
|
if (typeof payload.initialState.attributes !== "object" || payload.initialState.attributes === null) {
|
|
if ("attributes" in payload.initialState)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-object %s").format(WI.unlocalizedString("initialState.attributes")));
|
|
|
|
payload.initialState.attributes = {};
|
|
}
|
|
|
|
if (!Array.isArray(payload.initialState.states) || payload.initialState.states.some((item) => typeof item !== "object" || item === null)) {
|
|
if ("states" in payload.initialState)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("initialState.states")));
|
|
|
|
payload.initialState.states = [];
|
|
|
|
// COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
|
|
if (!isEmptyObject(payload.initialState.attributes)) {
|
|
let {width, height, ...state} = payload.initialState.attributes;
|
|
if (!isEmptyObject(state))
|
|
payload.initialState.states.push(state);
|
|
}
|
|
}
|
|
|
|
if (!Array.isArray(payload.initialState.parameters)) {
|
|
if ("parameters" in payload.initialState)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("initialState.attributes")));
|
|
|
|
payload.initialState.parameters = [];
|
|
}
|
|
|
|
if (typeof payload.initialState.content !== "string") {
|
|
if ("content" in payload.initialState)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-string %s").format(WI.unlocalizedString("initialState.content")));
|
|
|
|
payload.initialState.content = "";
|
|
}
|
|
|
|
if (!Array.isArray(payload.frames)) {
|
|
if ("frames" in payload)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("frames")));
|
|
|
|
payload.frames = [];
|
|
}
|
|
|
|
if (!Array.isArray(payload.data)) {
|
|
if ("data" in payload)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("data")));
|
|
|
|
payload.data = [];
|
|
}
|
|
|
|
if (!frames)
|
|
frames = payload.frames.map(WI.RecordingFrame.fromPayload)
|
|
|
|
return new WI.Recording(payload.version, type, payload.initialState, frames, payload.data);
|
|
}
|
|
|
|
static displayNameForRecordingType(recordingType)
|
|
{
|
|
switch (recordingType) {
|
|
case Recording.Type.Canvas2D:
|
|
return WI.UIString("2D", "Recording Type Canvas 2D", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.OffscreenCanvas2D:
|
|
return WI.UIString("Offscreen2D", "Recording Type Offscreen Canvas 2D", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.CanvasBitmapRenderer:
|
|
return WI.UIString("Bitmap Renderer", "Recording Type Canvas Bitmap Renderer", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.OffscreenCanvasBitmapRenderer:
|
|
return WI.UIString("Bitmap Renderer (Offscreen)", "Recording Type Offscreen Canvas Bitmap Renderer", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.CanvasWebGL:
|
|
return WI.UIString("WebGL", "Recording Type Canvas WebGL", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.OffscreenCanvasWebGL:
|
|
return WI.UIString("WebGL (Offscreen)", "Recording Type Offscreen Canvas WebGL", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.CanvasWebGL2:
|
|
return WI.UIString("WebGL2", "Recording Type Canvas WebGL2", "A type of canvas recording in the Graphics Tab.");
|
|
case Recording.Type.OffscreenCanvasWebGL2:
|
|
return WI.UIString("WebGL2 (Offscreen)", "Recording Type Offscreen Canvas WebGL2", "A type of canvas recording in the Graphics Tab.");
|
|
}
|
|
|
|
console.assert(false, "Unknown recording type", recordingType);
|
|
return null;
|
|
}
|
|
|
|
static displayNameForSwizzleType(swizzleType)
|
|
{
|
|
switch (swizzleType) {
|
|
case WI.Recording.Swizzle.None:
|
|
return WI.unlocalizedString("None");
|
|
case WI.Recording.Swizzle.Number:
|
|
return WI.unlocalizedString("Number");
|
|
case WI.Recording.Swizzle.Boolean:
|
|
return WI.unlocalizedString("Boolean");
|
|
case WI.Recording.Swizzle.String:
|
|
return WI.unlocalizedString("String");
|
|
case WI.Recording.Swizzle.Array:
|
|
return WI.unlocalizedString("Array");
|
|
case WI.Recording.Swizzle.TypedArray:
|
|
return WI.unlocalizedString("TypedArray");
|
|
case WI.Recording.Swizzle.Image:
|
|
return WI.unlocalizedString("Image");
|
|
case WI.Recording.Swizzle.ImageData:
|
|
return WI.unlocalizedString("ImageData");
|
|
case WI.Recording.Swizzle.DOMMatrix:
|
|
return WI.unlocalizedString("DOMMatrix");
|
|
case WI.Recording.Swizzle.Path2D:
|
|
return WI.unlocalizedString("Path2D");
|
|
case WI.Recording.Swizzle.CanvasGradient:
|
|
return WI.unlocalizedString("CanvasGradient");
|
|
case WI.Recording.Swizzle.CanvasPattern:
|
|
return WI.unlocalizedString("CanvasPattern");
|
|
case WI.Recording.Swizzle.WebGLBuffer:
|
|
return WI.unlocalizedString("WebGLBuffer");
|
|
case WI.Recording.Swizzle.WebGLFramebuffer:
|
|
return WI.unlocalizedString("WebGLFramebuffer");
|
|
case WI.Recording.Swizzle.WebGLRenderbuffer:
|
|
return WI.unlocalizedString("WebGLRenderbuffer");
|
|
case WI.Recording.Swizzle.WebGLTexture:
|
|
return WI.unlocalizedString("WebGLTexture");
|
|
case WI.Recording.Swizzle.WebGLShader:
|
|
return WI.unlocalizedString("WebGLShader");
|
|
case WI.Recording.Swizzle.WebGLProgram:
|
|
return WI.unlocalizedString("WebGLProgram");
|
|
case WI.Recording.Swizzle.WebGLUniformLocation:
|
|
return WI.unlocalizedString("WebGLUniformLocation");
|
|
case WI.Recording.Swizzle.ImageBitmap:
|
|
return WI.unlocalizedString("ImageBitmap");
|
|
case WI.Recording.Swizzle.WebGLQuery:
|
|
return WI.unlocalizedString("WebGLQuery");
|
|
case WI.Recording.Swizzle.WebGLSampler:
|
|
return WI.unlocalizedString("WebGLSampler");
|
|
case WI.Recording.Swizzle.WebGLSync:
|
|
return WI.unlocalizedString("WebGLSync");
|
|
case WI.Recording.Swizzle.WebGLTransformFeedback:
|
|
return WI.unlocalizedString("WebGLTransformFeedback");
|
|
case WI.Recording.Swizzle.WebGLVertexArrayObject:
|
|
return WI.unlocalizedString("WebGLVertexArrayObject");
|
|
case WI.Recording.Swizzle.DOMPointInit:
|
|
return WI.unlocalizedString("DOMPointInit");
|
|
default:
|
|
console.error("Unknown swizzle type", swizzleType);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
static synthesizeWarning(message)
|
|
{
|
|
message = WI.UIString("Recording Warning: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.warn(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
static synthesizeError(message)
|
|
{
|
|
message = WI.UIString("Recording Error: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.error(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
// Public
|
|
|
|
get displayName() { return this._displayName; }
|
|
get type() { return this._type; }
|
|
get initialState() { return this._initialState; }
|
|
get frames() { return this._frames; }
|
|
get data() { return this._data; }
|
|
get actions() { return this._actions; }
|
|
get visualActionIndexes() { return this._visualActionIndexes; }
|
|
|
|
get source() { return this._source; }
|
|
set source(source) { this._source = source; }
|
|
|
|
get processing() { return this._processing; }
|
|
|
|
get ready()
|
|
{
|
|
return this._actions.lastValue.ready;
|
|
}
|
|
|
|
get isCanvas()
|
|
{
|
|
return this.isCanvas2D || this.isCanvasBitmapRender || this.isCanvasWebGL || this.isCanvasWebGL2;
|
|
}
|
|
|
|
get isCanvas2D()
|
|
{
|
|
return this._type === WI.Recording.Type.Canvas2D || this._type === WI.Recording.Type.OffscreenCanvas2D;
|
|
}
|
|
|
|
get isCanvasBitmapRender()
|
|
{
|
|
return this._type === WI.Recording.Type.CanvasBitmapRenderer || this._type === WI.Recording.Type.OffscreenCanvasBitmapRenderer;
|
|
}
|
|
|
|
get isCanvasWebGL()
|
|
{
|
|
return this._type === WI.Recording.Type.CanvasWebGL || this._type === WI.Recording.Type.OffscreenCanvasWebGL;
|
|
}
|
|
|
|
get isCanvasWebGL2()
|
|
{
|
|
return this._type === WI.Recording.Type.CanvasWebGL2 || this._type === WI.Recording.Type.OffscreenCanvasWebGL2;
|
|
}
|
|
|
|
startProcessing()
|
|
{
|
|
console.assert(!this._processing, "Cannot start an already started process().");
|
|
console.assert(!this.ready, "Cannot start a completed process().");
|
|
if (this._processing || this.ready)
|
|
return;
|
|
|
|
this._processing = true;
|
|
|
|
this._process();
|
|
}
|
|
|
|
stopProcessing()
|
|
{
|
|
console.assert(this._processing, "Cannot stop an already stopped process().");
|
|
console.assert(!this.ready, "Cannot stop a completed process().");
|
|
if (!this._processing || this.ready)
|
|
return;
|
|
|
|
this._processing = false;
|
|
}
|
|
|
|
createDisplayName(suggestedName)
|
|
{
|
|
let recordingNameSet;
|
|
if (this._source) {
|
|
recordingNameSet = this._source[WI.Recording.CanvasRecordingNamesSymbol];
|
|
if (!recordingNameSet)
|
|
this._source[WI.Recording.CanvasRecordingNamesSymbol] = recordingNameSet = new Set;
|
|
} else
|
|
recordingNameSet = WI.Recording._importedRecordingNameSet;
|
|
|
|
let name;
|
|
if (suggestedName) {
|
|
name = suggestedName;
|
|
let duplicateNumber = 2;
|
|
while (recordingNameSet.has(name))
|
|
name = `${suggestedName} (${duplicateNumber++})`;
|
|
} else {
|
|
let recordingNumber = 1;
|
|
do {
|
|
name = WI.UIString("Recording %d").format(recordingNumber++);
|
|
} while (recordingNameSet.has(name));
|
|
}
|
|
|
|
recordingNameSet.add(name);
|
|
this._displayName = name;
|
|
}
|
|
|
|
is2D()
|
|
{
|
|
return WI.Recording.is2D(this._type);
|
|
}
|
|
|
|
async swizzle(index, type)
|
|
{
|
|
if (!this._swizzle)
|
|
this._swizzle = {};
|
|
|
|
if (typeof this._swizzle[index] !== "object")
|
|
this._swizzle[index] = {};
|
|
|
|
if (type === WI.Recording.Swizzle.Number)
|
|
return parseFloat(index);
|
|
|
|
if (type === WI.Recording.Swizzle.Boolean)
|
|
return !!index;
|
|
|
|
if (type === WI.Recording.Swizzle.Array)
|
|
return Array.isArray(index) ? index : [];
|
|
|
|
if (type === WI.Recording.Swizzle.DOMMatrix)
|
|
return new DOMMatrix(index);
|
|
|
|
// FIXME: <https://webkit.org/b/176009> Web Inspector: send data for WebGL objects during a recording instead of a placeholder string
|
|
if (type === WI.Recording.Swizzle.TypedArray
|
|
|| type === WI.Recording.Swizzle.WebGLBuffer
|
|
|| type === WI.Recording.Swizzle.WebGLFramebuffer
|
|
|| type === WI.Recording.Swizzle.WebGLRenderbuffer
|
|
|| type === WI.Recording.Swizzle.WebGLTexture
|
|
|| type === WI.Recording.Swizzle.WebGLShader
|
|
|| type === WI.Recording.Swizzle.WebGLProgram
|
|
|| type === WI.Recording.Swizzle.WebGLUniformLocation
|
|
|| type === WI.Recording.Swizzle.WebGLQuery
|
|
|| type === WI.Recording.Swizzle.WebGLSampler
|
|
|| type === WI.Recording.Swizzle.WebGLSync
|
|
|| type === WI.Recording.Swizzle.WebGLTransformFeedback
|
|
|| type === WI.Recording.Swizzle.WebGLVertexArrayObject) {
|
|
return index;
|
|
}
|
|
|
|
if (!(type in this._swizzle[index])) {
|
|
try {
|
|
let data = this._data[index];
|
|
switch (type) {
|
|
case WI.Recording.Swizzle.None:
|
|
this._swizzle[index][type] = data;
|
|
break;
|
|
|
|
case WI.Recording.Swizzle.String:
|
|
if (Array.isArray(data))
|
|
this._swizzle[index][type] = await Promise.all(data.map((item) => this.swizzle(item, WI.Recording.Swizzle.String)));
|
|
else
|
|
this._swizzle[index][type] = String(data);
|
|
break;
|
|
|
|
case WI.Recording.Swizzle.Image:
|
|
this._swizzle[index][type] = await WI.ImageUtilities.promisifyLoad(data);
|
|
this._swizzle[index][type].__data = data;
|
|
break;
|
|
|
|
case WI.Recording.Swizzle.ImageData: {
|
|
let [object, width, height] = await Promise.all([
|
|
this.swizzle(data[0], WI.Recording.Swizzle.Array),
|
|
this.swizzle(data[1], WI.Recording.Swizzle.Number),
|
|
this.swizzle(data[2], WI.Recording.Swizzle.Number),
|
|
]);
|
|
|
|
object = await Promise.all(object.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number)));
|
|
|
|
this._swizzle[index][type] = new ImageData(new Uint8ClampedArray(object), width, height);
|
|
this._swizzle[index][type].__data = {data: object, width, height};
|
|
break;
|
|
}
|
|
|
|
case WI.Recording.Swizzle.Path2D:
|
|
this._swizzle[index][type] = new Path2D(data);
|
|
this._swizzle[index][type].__data = data;
|
|
break;
|
|
|
|
case WI.Recording.Swizzle.CanvasGradient: {
|
|
let [gradientType, points] = await Promise.all([
|
|
this.swizzle(data[0], WI.Recording.Swizzle.String),
|
|
this.swizzle(data[1], WI.Recording.Swizzle.Array),
|
|
]);
|
|
|
|
points = await Promise.all(points.map((item) => this.swizzle(item, WI.Recording.Swizzle.Number)));
|
|
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
if (gradientType == "radial-gradient")
|
|
this._swizzle[index][type] = context.createRadialGradient(...points);
|
|
else if (gradientType == "linear-gradient")
|
|
this._swizzle[index][type] = context.createLinearGradient(...points);
|
|
else
|
|
this._swizzle[index][type] = context.createConicGradient(...points);
|
|
});
|
|
|
|
let stops = [];
|
|
for (let stop of data[2]) {
|
|
let [offset, color] = await Promise.all([
|
|
this.swizzle(stop[0], WI.Recording.Swizzle.Number),
|
|
this.swizzle(stop[1], WI.Recording.Swizzle.String),
|
|
]);
|
|
this._swizzle[index][type].addColorStop(offset, color);
|
|
|
|
stops.push({offset, color});
|
|
}
|
|
|
|
this._swizzle[index][type].__data = {type: gradientType, points, stops};
|
|
break;
|
|
}
|
|
|
|
case WI.Recording.Swizzle.CanvasPattern: {
|
|
let [image, repeat] = await Promise.all([
|
|
this.swizzle(data[0], WI.Recording.Swizzle.Image),
|
|
this.swizzle(data[1], WI.Recording.Swizzle.String),
|
|
]);
|
|
|
|
WI.ImageUtilities.scratchCanvasContext2D((context) => {
|
|
this._swizzle[index][type] = context.createPattern(image, repeat);
|
|
this._swizzle[index][type].__image = image;
|
|
});
|
|
|
|
this._swizzle[index][type].__data = {image: image.__data, repeat};
|
|
break;
|
|
}
|
|
|
|
case WI.Recording.Swizzle.ImageBitmap: {
|
|
let image = await this.swizzle(index, WI.Recording.Swizzle.Image);
|
|
this._swizzle[index][type] = await createImageBitmap(image);
|
|
this._swizzle[index][type].__data = data;
|
|
break;
|
|
}
|
|
|
|
case WI.Recording.Swizzle.CallStack: {
|
|
let array = await this.swizzle(data, WI.Recording.Swizzle.Array);
|
|
if (!isNaN(array[0])) {
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): "stackTrace" was sent as an array of call frames instead of a single call stack
|
|
array = [array];
|
|
}
|
|
|
|
let promises = [];
|
|
|
|
// callFrames
|
|
promises.push(Promise.all(array[0].map((item) => this.swizzle(item, WI.Recording.Swizzle.CallFrame))));
|
|
|
|
// topCallFrameIsBoundary
|
|
if (array.length > 1)
|
|
promises.push(this.swizzle(array[1], WI.Recording.Swizzle.Boolean));
|
|
|
|
// truncated
|
|
if (array.length > 2)
|
|
promises.push(this.swizzle(array[2], WI.Recording.Swizzle.Boolean));
|
|
|
|
// parentStackTrace
|
|
if (array.length > 3)
|
|
promises.push(this.swizzle(array[3], WI.Recording.Swizzle.StackTrace));
|
|
|
|
let [callFrames, topCallFrameIsBoundary, truncated, parentStackTrace] = await Promise.all(promises);
|
|
this._swizzle[index][type] = WI.StackTrace.fromPayload(WI.assumingMainTarget(), {callFrames, topCallFrameIsBoundary, truncated, parentStackTrace});
|
|
break;
|
|
}
|
|
|
|
case WI.Recording.Swizzle.CallFrame: {
|
|
let array = await this.swizzle(data, WI.Recording.Swizzle.Array);
|
|
let [functionName, url] = await Promise.all([
|
|
this.swizzle(array[0], WI.Recording.Swizzle.String),
|
|
this.swizzle(array[1], WI.Recording.Swizzle.String),
|
|
]);
|
|
this._swizzle[index][type] = WI.CallFrame.fromPayload(WI.assumingMainTarget(), {
|
|
functionName,
|
|
url,
|
|
lineNumber: array[2],
|
|
columnNumber: array[3],
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
} catch { }
|
|
}
|
|
|
|
return this._swizzle[index][type];
|
|
}
|
|
|
|
createContext()
|
|
{
|
|
let createCanvasContext = (type) => {
|
|
let canvas = document.createElement("canvas");
|
|
if ("width" in this._initialState.attributes)
|
|
canvas.width = this._initialState.attributes.width;
|
|
if ("height" in this._initialState.attributes)
|
|
canvas.height = this._initialState.attributes.height;
|
|
return canvas.getContext(type, ...this._initialState.parameters);
|
|
};
|
|
let createOffscreenCanvasContext = (type) => {
|
|
let width = 1;
|
|
let height = 1;
|
|
if ("width" in this._initialState.attributes)
|
|
width = this._initialState.attributes.width;
|
|
if ("height" in this._initialState.attributes)
|
|
height = this._initialState.attributes.height;
|
|
let canvas = new OffscreenCanvas(width, height);
|
|
return canvas.getContext(type, ...this._initialState.parameters);
|
|
};
|
|
|
|
switch (this._type) {
|
|
case WI.Recording.Type.Canvas2D:
|
|
return createCanvasContext("2d");
|
|
case WI.Recording.Type.OffscreenCanvas2D:
|
|
return createOffscreenCanvasContext("2d");
|
|
case WI.Recording.Type.CanvasBitmapRenderer:
|
|
return createCanvasContext("bitmaprenderer");
|
|
case WI.Recording.Type.OffscreenCanvasBitmapRenderer:
|
|
return createOffscreenCanvasContext("bitmaprenderer");
|
|
case WI.Recording.Type.CanvasWebGL:
|
|
return createCanvasContext("webgl");
|
|
case WI.Recording.Type.OffscreenCanvasWebGL:
|
|
return createOffscreenCanvasContext("webgl");
|
|
case WI.Recording.Type.CanvasWebGL2:
|
|
return createCanvasContext("webgl2");
|
|
case WI.Recording.Type.OffscreenCanvasWebGL2:
|
|
return createOffscreenCanvasContext("webgl2");
|
|
}
|
|
|
|
console.error("Unknown recording type", this._type);
|
|
return null;
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let initialState = {};
|
|
if (!isEmptyObject(this._initialState.attributes))
|
|
initialState.attributes = this._initialState.attributes;
|
|
if (this._initialState.states.length)
|
|
initialState.states = this._initialState.states;
|
|
if (this._initialState.parameters.length)
|
|
initialState.parameters = this._initialState.parameters;
|
|
if (this._initialState.content && this._initialState.content.length)
|
|
initialState.content = this._initialState.content;
|
|
|
|
return {
|
|
version: this._version,
|
|
type: this._type,
|
|
initialState,
|
|
frames: this._frames.map((frame) => frame.toJSON()),
|
|
data: this._data,
|
|
};
|
|
}
|
|
|
|
toHTML()
|
|
{
|
|
console.assert(this.isCanvas2D);
|
|
console.assert(this.ready);
|
|
|
|
let lines = [];
|
|
let objects = [];
|
|
|
|
function processObject(object) {
|
|
objects.push({object, index: objects.length});
|
|
return `objects[${objects.length - 1}]`;
|
|
}
|
|
|
|
function processValue(value) {
|
|
if (typeof value === "object" && !Array.isArray(value))
|
|
return processObject(value);
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function escapeHTML(s) {
|
|
return s.replace(/[^0-9A-Za-z ]/g, (c) => {
|
|
return `&#${c.charCodeAt(0)};`;
|
|
});
|
|
}
|
|
|
|
lines.push(`<!DOCTYPE html>`);
|
|
lines.push(`<head>`);
|
|
lines.push(`<title>${escapeHTML(this._displayName)}</title>`);
|
|
lines.push(`<style>`);
|
|
lines.push(` body {`);
|
|
lines.push(` margin: 0;`);
|
|
lines.push(` }`);
|
|
lines.push(` canvas {`);
|
|
lines.push(` max-width: calc(100% - 40px);`);
|
|
lines.push(` max-height: calc(100% - 40px);`);
|
|
lines.push(` padding: 20px;`);
|
|
lines.push(` }`);
|
|
lines.push(`</style>`);
|
|
lines.push(`</head>`);
|
|
lines.push(`<body>`);
|
|
lines.push(`<script>`);
|
|
lines.push(`"use strict";`);
|
|
|
|
lines.push(``);
|
|
|
|
lines.push(`let promises = [];`);
|
|
lines.push(`let objects = {};`);
|
|
|
|
lines.push(``);
|
|
|
|
lines.push(`let canvas = document.body.appendChild(document.createElement("canvas"));`);
|
|
for (let [attribute, value] of Object.entries(this._initialState.attributes))
|
|
lines.push(`canvas.${attribute} = ${JSON.stringify(value)};`);
|
|
|
|
lines.push(``);
|
|
|
|
let parametersString = this._initialState.parameters.map(processValue).join(`, `);
|
|
lines.push(`let context = canvas.getContext("2d"${parametersString ? ", " + parametersString : ""});`);
|
|
|
|
lines.push(``);
|
|
|
|
lines.push(`let frames = [`);
|
|
|
|
lines.push(` function initialState() {`);
|
|
if (this._initialState.content) {
|
|
let image = new Image;
|
|
image.__data = this._initialState.content;
|
|
lines.push(` context.drawImage(${processObject(image)}, 0, 0);`);
|
|
lines.push(``);
|
|
}
|
|
for (let state of this._actions[0].states) {
|
|
for (let [name, value] of state) {
|
|
if (name === "getPath" || name === "currentX" || name === "currentY")
|
|
continue;
|
|
|
|
let contextString = `context`;
|
|
if (name === "setPath") {
|
|
lines.push(` if (${JSON.stringify(name)} in context)`);
|
|
contextString = ` ` + contextString;
|
|
}
|
|
|
|
let callString = ``;
|
|
if (WI.RecordingAction.isFunctionForType(this._type, name))
|
|
callString = `(` + value.map(processValue).join(`, `) + `)`;
|
|
else
|
|
callString = ` = ${processValue(value)}`;
|
|
|
|
lines.push(` ${contextString}.${name}${callString};`);
|
|
}
|
|
|
|
if (state !== this._actions[0].states.lastValue) {
|
|
lines.push(` context.save();`);
|
|
lines.push(``);
|
|
}
|
|
}
|
|
lines.push(` },`);
|
|
|
|
lines.push(` function startRecording() {`);
|
|
lines.push(` if (typeof console.record === "function")`);
|
|
lines.push(` console.record(context, {name: ${JSON.stringify(this._displayName)}});`);
|
|
lines.push(` },`);
|
|
|
|
for (let i = 0; i < this._frames.length; ++i) {
|
|
lines.push(` function frame${i + 1}() {`);
|
|
|
|
for (let action of this._frames[i].actions) {
|
|
let contextString = `context`;
|
|
if (action.contextReplacer)
|
|
contextString += `.${action.contextReplacer}`;
|
|
|
|
if (!action.valid)
|
|
contextString = `// ` + contextString;
|
|
|
|
let callString = ``;
|
|
if (action.isFunction)
|
|
callString += `(` + action.parameters.map(processValue).join(`, `) + `)`;
|
|
else if (!action.isGetter)
|
|
callString += ` = ` + processValue(action.parameters[0]);
|
|
|
|
lines.push(` ${contextString}.${action.name}${callString};`);
|
|
}
|
|
|
|
lines.push(` },`);
|
|
}
|
|
|
|
lines.push(` function stopRecording() {`);
|
|
lines.push(` if (typeof console.recordEnd === "function")`);
|
|
lines.push(` console.recordEnd(context);`);
|
|
lines.push(` },`);
|
|
|
|
lines.push(`];`);
|
|
|
|
lines.push(``);
|
|
|
|
if (objects.length) {
|
|
if (objects.some(({object}) => object instanceof CanvasGradient)) {
|
|
lines.push(`function rebuildCanvasGradient(key, data) {`);
|
|
lines.push(` let gradient = null;`);
|
|
lines.push(` if (data.type === "radial-gradient")`);
|
|
lines.push(` gradient = context.createRadialGradient(data.points[0], data.points[1], data.points[2], data.points[3], data.points[4], data.points[5]);`);
|
|
lines.push(` else if (data.type === "linear-gradient")`);
|
|
lines.push(` gradient = context.createLinearGradient(data.points[0], data.points[1], data.points[2], data.points[3]);`);
|
|
lines.push(` else`);
|
|
lines.push(` gradient = context.createConicGradient(data.points[0], data.points[1], data.points[2]);`);
|
|
lines.push(` for (let stop of data.stops)`);
|
|
lines.push(` gradient.addColorStop(stop.offset, stop.color);`);
|
|
lines.push(` objects[key] = gradient;`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof CanvasPattern)) {
|
|
lines.push(`function rebuildCanvasPattern(key, data) {`);
|
|
lines.push(` promises.push(new Promise(function(resolve, reject) {`);
|
|
lines.push(` let image = new Image;`);
|
|
lines.push(` function resolveWithImage(event) {`);
|
|
lines.push(` objects[key] = context.createPattern(image, data.repeat);`);
|
|
lines.push(` resolve();`);
|
|
lines.push(` }`);
|
|
lines.push(` image.addEventListener("load", resolveWithImage);`);
|
|
lines.push(` image.addEventListener("error", resolveWithImage);`);
|
|
lines.push(` image.src = data.image;`);
|
|
lines.push(` }));`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof DOMMatrix)) {
|
|
lines.push(`function rebuildDOMMatrix(key, data) {`);
|
|
lines.push(` objects[key] = new DOMMatrix(data);`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof Image)) {
|
|
lines.push(`function rebuildImage(key, data) {`);
|
|
lines.push(` promises.push(new Promise(function(resolve, reject) {`);
|
|
lines.push(` let image = new Image;`);
|
|
lines.push(` function resolveWithImage(event) {`);
|
|
lines.push(` objects[key] = image;`);
|
|
lines.push(` resolve();`);
|
|
lines.push(` }`);
|
|
lines.push(` image.addEventListener("load", resolveWithImage);`);
|
|
lines.push(` image.addEventListener("error", resolveWithImage);`);
|
|
lines.push(` image.src = data;`);
|
|
lines.push(` }));`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof ImageBitmap)) {
|
|
lines.push(`function rebuildImageBitmap(key, data) {`);
|
|
lines.push(` promises.push(new Promise(function(resolve, reject) {`);
|
|
lines.push(` let image = new Image;`);
|
|
lines.push(` function resolveWithImage(event) {`);
|
|
lines.push(` createImageBitmap(image).then(function(imageBitmap) {`);
|
|
lines.push(` objects[key] = imageBitmap;`);
|
|
lines.push(` resolve();`);
|
|
lines.push(` });`);
|
|
lines.push(` }`);
|
|
lines.push(` image.addEventListener("load", resolveWithImage);`);
|
|
lines.push(` image.addEventListener("error", resolveWithImage);`);
|
|
lines.push(` image.src = data;`);
|
|
lines.push(` }));`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof ImageData)) {
|
|
lines.push(`function rebuildImageData(key, data) {`);
|
|
lines.push(` objects[key] = new ImageData(new Uint8ClampedArray(data.data), parseInt(data.width), parseInt(data.height));`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
if (objects.some(({object}) => object instanceof Path2D)) {
|
|
lines.push(`function rebuildPath2D(key, data) {`);
|
|
lines.push(` objects[key] = new Path2D(data);`);
|
|
lines.push(`}`);
|
|
}
|
|
|
|
lines.push(``);
|
|
|
|
for (let {object, index} of objects) {
|
|
if (object instanceof CanvasGradient) {
|
|
lines.push(`rebuildCanvasGradient(${index}, ${JSON.stringify(object.__data)});`);
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof CanvasPattern) {
|
|
lines.push(`rebuildCanvasPattern(${index}, ${JSON.stringify(object.__data)});`);
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof DOMMatrix) {
|
|
lines.push(`rebuildDOMMatrix(${index}, ${JSON.stringify(object.toString())});`)
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof Image) {
|
|
lines.push(`rebuildImage(${index}, ${JSON.stringify(object.__data)});`)
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof ImageBitmap) {
|
|
lines.push(`rebuildImageBitmap(${index}, ${JSON.stringify(object.__data)});`)
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof ImageData) {
|
|
lines.push(`rebuildImageData(${index}, ${JSON.stringify(object.__data)});`);
|
|
continue;
|
|
}
|
|
|
|
if (object instanceof Path2D) {
|
|
lines.push(`rebuildPath2D(${index}, ${JSON.stringify(object.__data || "")});`)
|
|
continue;
|
|
}
|
|
}
|
|
|
|
lines.push(``);
|
|
}
|
|
|
|
lines.push(`Promise.all(promises).then(function() {`);
|
|
lines.push(` window.requestAnimationFrame(function executeFrame() {`);
|
|
lines.push(` frames.shift()();`);
|
|
lines.push(` if (frames.length)`);
|
|
lines.push(` window.requestAnimationFrame(executeFrame);`);
|
|
lines.push(` });`);
|
|
lines.push(`});`);
|
|
|
|
lines.push(`</script>`);
|
|
lines.push(`</body>`);
|
|
return lines.join(`\n`);
|
|
}
|
|
|
|
// Private
|
|
|
|
async _process()
|
|
{
|
|
if (!this._processContext) {
|
|
this._processContext = this.createContext();
|
|
|
|
if (this.isCanvas2D) {
|
|
let initialContent = await WI.ImageUtilities.promisifyLoad(this._initialState.content);
|
|
this._processContext.drawImage(initialContent, 0, 0);
|
|
|
|
for (let initialState of this._initialState.states) {
|
|
let state = await WI.RecordingState.swizzleInitialState(this, initialState);
|
|
state.apply(this._type, this._processContext);
|
|
|
|
// The last state represents the current state, which should not be saved.
|
|
if (initialState !== this._initialState.states.lastValue) {
|
|
this._processContext.save();
|
|
this._processStates.push(WI.RecordingState.fromCanvasContext2D(this._processContext));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// The first action is always a WI.RecordingInitialStateAction, which doesn't need to swizzle().
|
|
// Since it is not associated with a WI.RecordingFrame, it has to manually process().
|
|
if (!this._actions[0].ready) {
|
|
this._actions[0].process(this, this._processContext, this._processStates);
|
|
this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action: this._actions[0], index: 0});
|
|
}
|
|
|
|
const workInterval = 10;
|
|
let startTime = Date.now();
|
|
|
|
let cumulativeActionIndex = 0;
|
|
let lastAction = this._actions[cumulativeActionIndex];
|
|
for (let frameIndex = 0; frameIndex < this._frames.length; ++frameIndex) {
|
|
let frame = this._frames[frameIndex];
|
|
|
|
if (frame.actions.lastValue.ready) {
|
|
cumulativeActionIndex += frame.actions.length;
|
|
lastAction = frame.actions.lastValue;
|
|
continue;
|
|
}
|
|
|
|
for (let actionIndex = 0; actionIndex < frame.actions.length; ++actionIndex) {
|
|
++cumulativeActionIndex;
|
|
|
|
let action = frame.actions[actionIndex];
|
|
if (action.ready) {
|
|
lastAction = action;
|
|
continue;
|
|
}
|
|
|
|
await action.swizzle(this);
|
|
|
|
action.process(this, this._processContext, this._processStates, {lastAction});
|
|
|
|
if (action.isVisual)
|
|
this._visualActionIndexes.push(cumulativeActionIndex);
|
|
|
|
if (!actionIndex)
|
|
this.dispatchEventToListeners(WI.Recording.Event.StartProcessingFrame, {frame, index: frameIndex});
|
|
|
|
this.dispatchEventToListeners(WI.Recording.Event.ProcessedAction, {action, index: cumulativeActionIndex});
|
|
|
|
if (Date.now() - startTime > workInterval) {
|
|
await Promise.delay(); // yield
|
|
|
|
startTime = Date.now();
|
|
}
|
|
|
|
lastAction = action;
|
|
|
|
if (!this._processing)
|
|
return;
|
|
}
|
|
|
|
if (!this._processing)
|
|
return;
|
|
}
|
|
|
|
this._swizzle = null;
|
|
this._processContext = null;
|
|
this._processing = false;
|
|
}
|
|
};
|
|
|
|
// Keep this in sync with Inspector::Protocol::Recording::VERSION.
|
|
WI.Recording.Version = 2;
|
|
|
|
WI.Recording.Event = {
|
|
ProcessedAction: "recording-processed-action",
|
|
StartProcessingFrame: "recording-start-processing-frame",
|
|
};
|
|
|
|
WI.Recording._importedRecordingNameSet = new Set;
|
|
|
|
WI.Recording.CanvasRecordingNamesSymbol = Symbol("canvas-recording-names");
|
|
|
|
WI.Recording.Type = {
|
|
Canvas2D: "canvas-2d",
|
|
OffscreenCanvas2D: "offscreen-canvas-2d",
|
|
CanvasBitmapRenderer: "canvas-bitmaprenderer",
|
|
OffscreenCanvasBitmapRenderer: "offscreen-canvas-bitmaprenderer",
|
|
CanvasWebGL: "canvas-webgl",
|
|
OffscreenCanvasWebGL: "offscreen-canvas-webgl",
|
|
CanvasWebGL2: "canvas-webgl2",
|
|
OffscreenCanvasWebGL2: "offscreen-canvas-webgl2",
|
|
};
|
|
|
|
// Keep this in sync with WebCore::RecordingSwizzleType.
|
|
WI.Recording.Swizzle = {
|
|
None: 0,
|
|
Number: 1,
|
|
Boolean: 2,
|
|
String: 3,
|
|
Array: 4,
|
|
TypedArray: 5,
|
|
Image: 6,
|
|
ImageData: 7,
|
|
DOMMatrix: 8,
|
|
Path2D: 9,
|
|
CanvasGradient: 10,
|
|
CanvasPattern: 11,
|
|
WebGLBuffer: 12,
|
|
WebGLFramebuffer: 13,
|
|
WebGLRenderbuffer: 14,
|
|
WebGLTexture: 15,
|
|
WebGLShader: 16,
|
|
WebGLProgram: 17,
|
|
WebGLUniformLocation: 18,
|
|
ImageBitmap: 19,
|
|
WebGLQuery: 20,
|
|
WebGLSampler: 21,
|
|
WebGLSync: 22,
|
|
WebGLTransformFeedback: 23,
|
|
WebGLVertexArrayObject: 24,
|
|
|
|
// Special frontend-only swizzle types.
|
|
CallStack: Symbol("CallStack"),
|
|
CallFrame: Symbol("CallFrame"),
|
|
};
|
|
|
|
/* Models/RecordingAction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RecordingAction = class RecordingAction extends WI.Object
|
|
{
|
|
constructor(name, parameters, swizzleTypes, stackTrace, snapshot)
|
|
{
|
|
super();
|
|
|
|
this._payloadName = name;
|
|
this._payloadParameters = parameters;
|
|
this._payloadSwizzleTypes = swizzleTypes;
|
|
this._payloadStackTrace = stackTrace;
|
|
this._payloadSnapshot = snapshot ?? -1;
|
|
|
|
this._name = "";
|
|
this._parameters = [];
|
|
this._stackTrace = null;
|
|
this._snapshot = "";
|
|
|
|
this._valid = true;
|
|
this._isFunction = false;
|
|
this._isGetter = false;
|
|
this._isVisual = false;
|
|
|
|
this._contextReplacer = null;
|
|
|
|
this._states = [];
|
|
this._stateModifiers = new Set;
|
|
|
|
this._warning = null;
|
|
this._swizzled = false;
|
|
this._processed = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
// Payload format: (name, parameters, swizzleTypes, [stackTrace, [snapshot]])
|
|
static fromPayload(payload)
|
|
{
|
|
if (!Array.isArray(payload))
|
|
payload = [];
|
|
|
|
if (typeof payload[0] !== "number") {
|
|
if (payload.length > 0)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("name")));
|
|
|
|
payload[0] = -1;
|
|
}
|
|
|
|
if (!Array.isArray(payload[1])) {
|
|
if (payload.length > 1)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("parameters")));
|
|
|
|
payload[1] = [];
|
|
}
|
|
|
|
if (!Array.isArray(payload[2])) {
|
|
if (payload.length > 2)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("swizzleTypes")));
|
|
|
|
payload[2] = [];
|
|
}
|
|
|
|
if (typeof payload[3] !== "number" || isNaN(payload[3]) || (!payload[3] && payload[3] !== 0)) {
|
|
// COMPATIBILITY (iOS 12.1): "stackTrace" was sent as an array of call frames instead of a single call stack
|
|
if (!Array.isArray(payload[3])) {
|
|
if (payload.length > 3)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("stackTrace")));
|
|
|
|
payload[3] = [];
|
|
}
|
|
}
|
|
|
|
if (typeof payload[4] !== "number" || isNaN(payload[4])) {
|
|
if (payload.length > 4)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-number %s").format(WI.unlocalizedString("snapshot")));
|
|
|
|
payload[4] = -1;
|
|
}
|
|
|
|
return new WI.RecordingAction(...payload);
|
|
}
|
|
|
|
static isFunctionForType(type, name)
|
|
{
|
|
let prototype = WI.RecordingAction._prototypeForType(type);
|
|
if (!prototype)
|
|
return false;
|
|
let propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, name);
|
|
if (!propertyDescriptor)
|
|
return false;
|
|
return typeof propertyDescriptor.value === "function";
|
|
}
|
|
|
|
static bitfieldNamesForParameter(type, name, value, index, count)
|
|
{
|
|
if (!value)
|
|
return null;
|
|
|
|
let prototype = WI.RecordingAction._prototypeForType(type);
|
|
if (!prototype)
|
|
return null;
|
|
|
|
function testAndClearBit(name) {
|
|
let bit = prototype[name];
|
|
if (!bit)
|
|
return;
|
|
|
|
if (value & bit)
|
|
names.push(name);
|
|
|
|
value = value & ~bit;
|
|
}
|
|
|
|
function hexString(value) {
|
|
return "0x" + value.toString(16);
|
|
}
|
|
|
|
let names = [];
|
|
|
|
if ((name === "clear" && index === 0 && (type === WI.Recording.Type.CanvasWebGL || type === WI.Recording.Type.CanvasWebGL2)) ||
|
|
(name === "blitFramebuffer" && index === 8 && type === WI.Recording.Type.CanvasWebGL2)) {
|
|
testAndClearBit("COLOR_BUFFER_BIT");
|
|
testAndClearBit("DEPTH_BUFFER_BIT");
|
|
testAndClearBit("STENCIL_BUFFER_BIT");
|
|
if (value)
|
|
names.push(hexString(value));
|
|
}
|
|
|
|
if (name === "clientWaitSync" && index === 1 && type === WI.Recording.Type.CanvasWebGL2) {
|
|
testAndClearBit("SYNC_FLUSH_COMMANDS_BIT");
|
|
if (value)
|
|
names.push(hexString(value));
|
|
}
|
|
|
|
if (!names.length)
|
|
return null;
|
|
|
|
return names;
|
|
}
|
|
|
|
static constantNameForParameter(type, name, value, index, count)
|
|
{
|
|
let indexesForType = WI.RecordingAction._constantIndexes[type];
|
|
if (!indexesForType)
|
|
return null;
|
|
|
|
let indexesForAction = indexesForType[name];
|
|
if (!indexesForAction)
|
|
return null;
|
|
|
|
if (Array.isArray(indexesForAction)) {
|
|
if (!indexesForAction.includes(index))
|
|
return null;
|
|
} else if (typeof indexesForAction === "object") {
|
|
let indexesForActionVariant = indexesForAction[count];
|
|
if (!indexesForActionVariant)
|
|
return null;
|
|
|
|
if (Array.isArray(indexesForActionVariant) && !indexesForActionVariant.includes(index))
|
|
return null;
|
|
}
|
|
|
|
if (value === 0 && (type === WI.Recording.Type.CanvasWebGL || type === WI.Recording.Type.CanvasWebGL2)) {
|
|
if (name === "blendFunc" || name === "blendFuncSeparate")
|
|
return "ZERO";
|
|
if (index === 0) {
|
|
if (name === "drawArrays" || name === "drawElements")
|
|
return "POINTS";
|
|
if (name === "pixelStorei")
|
|
return "NONE";
|
|
}
|
|
}
|
|
|
|
let prototype = WI.RecordingAction._prototypeForType(type);
|
|
if (prototype) {
|
|
for (let key in prototype) {
|
|
let descriptor = Object.getOwnPropertyDescriptor(prototype, key);
|
|
if (descriptor && descriptor.value === value)
|
|
return key;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
static _prototypeForType(type)
|
|
{
|
|
switch (type) {
|
|
case WI.Recording.Type.Canvas2D:
|
|
return CanvasRenderingContext2D.prototype;
|
|
case WI.Recording.Type.OffscreenCanvas2D:
|
|
if (window.OffscreenCanvasRenderingContext2D)
|
|
return OffscreenCanvasRenderingContext2D.prototype;
|
|
break;
|
|
case WI.Recording.Type.CanvasBitmapRenderer:
|
|
case WI.Recording.Type.OffscreenCanvasBitmapRenderer:
|
|
if (window.ImageBitmapRenderingContext)
|
|
return ImageBitmapRenderingContext.prototype;
|
|
break;
|
|
case WI.Recording.Type.CanvasWebGL:
|
|
case WI.Recording.Type.OffscreenCanvasWebGL:
|
|
if (window.WebGLRenderingContext)
|
|
return WebGLRenderingContext.prototype;
|
|
break;
|
|
case WI.Recording.Type.CanvasWebGL2:
|
|
case WI.Recording.Type.OffscreenCanvasWebGL2:
|
|
if (window.WebGL2RenderingContext)
|
|
return WebGL2RenderingContext.prototype;
|
|
break;
|
|
}
|
|
|
|
WI.reportInternalError("Unknown recording type: " + type);
|
|
return null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get parameters() { return this._parameters; }
|
|
get swizzleTypes() { return this._payloadSwizzleTypes; }
|
|
get stackTrace() { return this._stackTrace; }
|
|
get snapshot() { return this._snapshot; }
|
|
get valid() { return this._valid; }
|
|
get isFunction() { return this._isFunction; }
|
|
get isGetter() { return this._isGetter; }
|
|
get isVisual() { return this._isVisual; }
|
|
get contextReplacer() { return this._contextReplacer; }
|
|
get states() { return this._states; }
|
|
get stateModifiers() { return this._stateModifiers; }
|
|
get warning() { return this._warning; }
|
|
|
|
get ready()
|
|
{
|
|
return this._swizzled && this._processed;
|
|
}
|
|
|
|
process(recording, context, states, {lastAction} = {})
|
|
{
|
|
console.assert(this._swizzled, "You must swizzle() before you can process().");
|
|
console.assert(!this._processed, "You should only process() once.");
|
|
|
|
this._processed = true;
|
|
|
|
if (recording.type === WI.Recording.Type.CanvasWebGL || recording.type === WI.Recording.Type.CanvasWebGL2) {
|
|
// We add each RecordingAction to the list of visualActionIndexes after it is processed.
|
|
if (this._valid && this._isVisual) {
|
|
let contentBefore = recording.visualActionIndexes.length ? recording.actions[recording.visualActionIndexes.lastValue].snapshot : recording.initialState.content;
|
|
if (this._snapshot === contentBefore)
|
|
this._warning = WI.UIString("This action causes no visual change");
|
|
}
|
|
return;
|
|
}
|
|
|
|
function getContent() {
|
|
if (context instanceof CanvasRenderingContext2D || (window.OffscreenCanvasRenderingContext2D && context instanceof OffscreenCanvasRenderingContext2D))
|
|
return context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
|
|
|
|
if ((window.WebGLRenderingContext && context instanceof WebGLRenderingContext) || (window.WebGL2RenderingContext && context instanceof WebGL2RenderingContext)) {
|
|
let pixels = new Uint8Array(context.drawingBufferWidth * context.drawingBufferHeight * 4);
|
|
context.readPixels(0, 0, context.canvas.width, context.canvas.height, context.RGBA, context.UNSIGNED_BYTE, pixels);
|
|
return pixels;
|
|
}
|
|
|
|
if (context.canvas instanceof HTMLCanvasElement)
|
|
return [context.canvas.toDataURL()];
|
|
|
|
console.assert("Unknown context type", context);
|
|
return [];
|
|
}
|
|
|
|
let contentBefore = null;
|
|
let shouldCheckHasVisualEffect = this._valid && this._isVisual;
|
|
if (shouldCheckHasVisualEffect)
|
|
contentBefore = getContent();
|
|
|
|
this.apply(context);
|
|
|
|
if (shouldCheckHasVisualEffect) {
|
|
let contentAfter = getContent();
|
|
if (Array.shallowEqual(contentBefore, contentAfter))
|
|
this._warning = WI.UIString("This action causes no visual change");
|
|
}
|
|
|
|
if (recording.isCanvas2D) {
|
|
let currentState = WI.RecordingState.fromCanvasContext2D(context, {source: this});
|
|
console.assert(currentState);
|
|
|
|
if (this.name === "save")
|
|
states.push(currentState);
|
|
else if (this.name === "restore")
|
|
states.pop();
|
|
|
|
this._states = states.slice();
|
|
this._states.push(currentState);
|
|
|
|
let lastState = null;
|
|
if (lastAction) {
|
|
lastState = lastAction.states.lastValue;
|
|
for (let [name, value] of currentState) {
|
|
let previousValue = lastState.get(name);
|
|
if (value !== previousValue && !Object.shallowEqual(value, previousValue))
|
|
this._stateModifiers.add(name);
|
|
}
|
|
}
|
|
|
|
let currentX = currentState.get("currentX");
|
|
let invalidX = (currentX < 0 || currentX >= context.canvas.width) && (!lastState || currentX !== lastState.get("currentX"));
|
|
|
|
let currentY = currentState.get("currentY");
|
|
let invalidY = (currentY < 0 || currentY >= context.canvas.height) && (!lastState || currentY !== lastState.get("currentY"));
|
|
|
|
if (invalidX || invalidY)
|
|
this._warning = WI.UIString("This action moves the path outside the visible area");
|
|
}
|
|
}
|
|
|
|
async swizzle(recording, lastAction)
|
|
{
|
|
console.assert(!this._swizzled, "You should only swizzle() once.");
|
|
|
|
if (!this._valid) {
|
|
this._swizzled = true;
|
|
return;
|
|
}
|
|
|
|
let swizzleParameter = (item, index) => {
|
|
return recording.swizzle(item, this._payloadSwizzleTypes[index]);
|
|
};
|
|
|
|
let swizzlePromises = [
|
|
recording.swizzle(this._payloadName, WI.Recording.Swizzle.String),
|
|
Promise.all(this._payloadParameters.map(swizzleParameter)),
|
|
];
|
|
|
|
if (!isNaN(this._payloadStackTrace))
|
|
swizzlePromises.push(recording.swizzle(this._payloadStackTrace, WI.Recording.Swizzle.CallStack));
|
|
else {
|
|
// COMPATIBILITY (iOS 12.1): "stackTrace" was sent as an array of call frames instead of a single call stack
|
|
let stackTracePromise = Promise.all(this._payloadStackTrace.map((item) => recording.swizzle(item, WI.Recording.Swizzle.CallFrame)))
|
|
.then((callFrames) => WI.StackTrace.fromPayload(WI.assumingMainTarget(), callFrames));
|
|
swizzlePromises.push(stackTracePromise);
|
|
}
|
|
|
|
if (this._payloadSnapshot >= 0)
|
|
swizzlePromises.push(recording.swizzle(this._payloadSnapshot, WI.Recording.Swizzle.String));
|
|
|
|
let [name, parameters, stackTrace, snapshot] = await Promise.all(swizzlePromises);
|
|
this._name = name;
|
|
this._parameters = parameters;
|
|
this._stackTrace = stackTrace;
|
|
if (this._payloadSnapshot >= 0)
|
|
this._snapshot = snapshot;
|
|
|
|
if (recording.isCanvas) {
|
|
if (this._name === "width" || this._name === "height") {
|
|
this._contextReplacer = "canvas";
|
|
this._isFunction = false;
|
|
this._isGetter = !this._parameters.length;
|
|
this._isVisual = !this._isGetter;
|
|
}
|
|
}
|
|
|
|
if (!this._contextReplacer) {
|
|
this._isFunction = WI.RecordingAction.isFunctionForType(recording.type, this._name);
|
|
this._isGetter = !this._isFunction && !this._parameters.length;
|
|
|
|
if (this._snapshot)
|
|
this._isVisual = true;
|
|
else {
|
|
let visualNames = WI.RecordingAction._visualNames[recording.type];
|
|
this._isVisual = visualNames ? visualNames.has(this._name) : false;
|
|
}
|
|
|
|
if (this._valid) {
|
|
let prototype = WI.RecordingAction._prototypeForType(recording.type);
|
|
if (prototype && !(name in prototype)) {
|
|
this.markInvalid();
|
|
|
|
WI.Recording.synthesizeWarning(WI.UIString("\u0022%s\u0022 is not valid for %s").format(name, prototype.constructor.name));
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._valid) {
|
|
let parametersSpecified = this._parameters.every((parameter) => parameter !== undefined);
|
|
let parametersCanBeSwizzled = this._payloadSwizzleTypes.every((swizzleType) => swizzleType !== WI.Recording.Swizzle.None);
|
|
if (!parametersSpecified || !parametersCanBeSwizzled)
|
|
this.markInvalid();
|
|
}
|
|
|
|
if (this._valid) {
|
|
let stateModifiers = WI.RecordingAction._stateModifiers[recording.type];
|
|
if (stateModifiers) {
|
|
this._stateModifiers.add(this._name);
|
|
let modifiedByAction = stateModifiers[this._name] || [];
|
|
for (let item of modifiedByAction)
|
|
this._stateModifiers.add(item);
|
|
}
|
|
}
|
|
|
|
this._swizzled = true;
|
|
}
|
|
|
|
apply(context, options = {})
|
|
{
|
|
console.assert(this._swizzled, "You must swizzle() before you can apply().");
|
|
console.assert(this._processed, "You must process() before you can apply().");
|
|
|
|
if (!this.valid)
|
|
return;
|
|
|
|
try {
|
|
let name = options.nameOverride || this._name;
|
|
|
|
if (this._contextReplacer)
|
|
context = context[this._contextReplacer];
|
|
|
|
if (this.isFunction)
|
|
context[name](...this._parameters);
|
|
else {
|
|
if (this.isGetter)
|
|
context[name];
|
|
else
|
|
context[name] = this._parameters[0];
|
|
}
|
|
} catch {
|
|
this.markInvalid();
|
|
|
|
WI.Recording.synthesizeWarning(WI.UIString("\u0022%s\u0022 threw an error").format(this._name));
|
|
}
|
|
}
|
|
|
|
markInvalid()
|
|
{
|
|
if (!this._valid)
|
|
return;
|
|
|
|
this._valid = false;
|
|
|
|
this.dispatchEventToListeners(WI.RecordingAction.Event.ValidityChanged);
|
|
}
|
|
|
|
getColorParameters()
|
|
{
|
|
switch (this._name) {
|
|
// 2D
|
|
case "fillStyle":
|
|
case "strokeStyle":
|
|
case "shadowColor":
|
|
// 2D (non-standard, legacy)
|
|
case "setFillColor":
|
|
case "setStrokeColor":
|
|
// WebGL
|
|
case "blendColor":
|
|
case "clearColor":
|
|
case "colorMask":
|
|
return this._parameters;
|
|
|
|
// 2D (non-standard, legacy)
|
|
case "setShadow":
|
|
return this._parameters.slice(3);
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
getImageParameters()
|
|
{
|
|
switch (this._name) {
|
|
// 2D
|
|
case "createImageData":
|
|
case "createPattern":
|
|
case "drawImage":
|
|
case "fillStyle":
|
|
case "putImageData":
|
|
case "strokeStyle":
|
|
// 2D (non-standard)
|
|
case "drawImageFromRect":
|
|
// BitmapRenderer
|
|
case "transferFromImageBitmap":
|
|
return this._parameters.slice(0, 1);
|
|
|
|
// WebGL
|
|
case "texImage2D":
|
|
case "texSubImage2D":
|
|
case "compressedTexImage2D":
|
|
return [this._parameters.lastValue];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = [this._payloadName, this._payloadParameters, this._payloadSwizzleTypes, this._payloadStackTrace];
|
|
if (this._payloadSnapshot >= 0)
|
|
json.push(this._payloadSnapshot);
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.RecordingAction.Event = {
|
|
ValidityChanged: "recording-action-marked-invalid",
|
|
};
|
|
|
|
WI.RecordingAction._constantIndexes = {
|
|
[WI.Recording.Type.CanvasWebGL]: {
|
|
"activeTexture": true,
|
|
"bindBuffer": true,
|
|
"bindFramebuffer": true,
|
|
"bindRenderbuffer": true,
|
|
"bindTexture": true,
|
|
"blendEquation": true,
|
|
"blendEquationSeparate": true,
|
|
"blendFunc": true,
|
|
"blendFuncSeparate": true,
|
|
"bufferData": [0, 2],
|
|
"bufferSubData": [0],
|
|
"checkFramebufferStatus": true,
|
|
"compressedTexImage2D": [0, 2],
|
|
"compressedTexSubImage2D": [0],
|
|
"copyTexImage2D": [0, 2],
|
|
"copyTexSubImage2D": [0],
|
|
"createShader": true,
|
|
"cullFace": true,
|
|
"depthFunc": true,
|
|
"disable": true,
|
|
"drawArrays": [0],
|
|
"drawElements": [0, 2],
|
|
"enable": true,
|
|
"framebufferRenderbuffer": true,
|
|
"framebufferTexture2D": [0, 1, 2],
|
|
"frontFace": true,
|
|
"generateMipmap": true,
|
|
"getBufferParameter": true,
|
|
"getFramebufferAttachmentParameter": true,
|
|
"getParameter": true,
|
|
"getProgramParameter": true,
|
|
"getRenderbufferParameter": true,
|
|
"getShaderParameter": true,
|
|
"getShaderPrecisionFormat": true,
|
|
"getTexParameter": true,
|
|
"getVertexAttrib": [1],
|
|
"getVertexAttribOffset": [1],
|
|
"hint": true,
|
|
"isEnabled": true,
|
|
"pixelStorei": [0],
|
|
"readPixels": [4, 5],
|
|
"renderbufferStorage": [0, 1],
|
|
"stencilFunc": [0],
|
|
"stencilFuncSeparate": [0, 1],
|
|
"stencilMaskSeparate": [0],
|
|
"stencilOp": true,
|
|
"stencilOpSeparate": true,
|
|
"texImage2D": {
|
|
5: [0, 2, 3, 4],
|
|
6: [0, 2, 3, 4],
|
|
8: [0, 2, 6, 7],
|
|
9: [0, 2, 6, 7],
|
|
},
|
|
"texParameterf": [0, 1],
|
|
"texParameteri": [0, 1],
|
|
"texSubImage2D": {
|
|
6: [0, 4, 5],
|
|
7: [0, 4, 5],
|
|
8: [0, 6, 7],
|
|
9: [0, 6, 7],
|
|
},
|
|
"vertexAttribPointer": [2],
|
|
},
|
|
[WI.Recording.Type.CanvasWebGL2]: {
|
|
"activeTexture": true,
|
|
"beginQuery": [0],
|
|
"beginTransformFeedback": true,
|
|
"bindBuffer": true,
|
|
"bindBufferBase": [0],
|
|
"bindBufferRange": [0],
|
|
"bindFramebuffer": true,
|
|
"bindRenderbuffer": true,
|
|
"bindTexture": true,
|
|
"bindTransformFeedback": [0],
|
|
"blendEquation": true,
|
|
"blendEquationSeparate": true,
|
|
"blendFunc": true,
|
|
"blendFuncSeparate": true,
|
|
"blitFramebuffer": [10],
|
|
"bufferData": [0, 2],
|
|
"bufferSubData": [0],
|
|
"checkFramebufferStatus": true,
|
|
"clearBufferfi": [0],
|
|
"clearBufferfv": [0],
|
|
"clearBufferiv": [0],
|
|
"clearBufferuiv": [0],
|
|
"compressedTexImage2D": [0, 2],
|
|
"compressedTexSubImage2D": [0],
|
|
"compressedTexSubImage3D": [0],
|
|
"copyBufferSubData": [0, 1],
|
|
"copyTexImage2D": [0, 2],
|
|
"copyTexSubImage2D": [0],
|
|
"copyTexSubImage3D": [0],
|
|
"createShader": true,
|
|
"cullFace": true,
|
|
"depthFunc": true,
|
|
"disable": true,
|
|
"drawArrays": [0],
|
|
"drawArraysInstanced": [0],
|
|
"drawBuffers": true,
|
|
"drawElements": [0, 2],
|
|
"drawElementsInstanced": [0, 2],
|
|
"drawRangeElements": [0, 4],
|
|
"enable": true,
|
|
"endQuery": true,
|
|
"fenceSync": [0],
|
|
"framebufferRenderbuffer": true,
|
|
"framebufferTexture2D": [0, 1, 2],
|
|
"framebufferTextureLayer": [0, 1],
|
|
"frontFace": true,
|
|
"generateMipmap": true,
|
|
"getActiveUniformBlockParameter": [2],
|
|
"getActiveUniforms": [2],
|
|
"getBufferParameter": true,
|
|
"getBufferSubData": [0],
|
|
"getFramebufferAttachmentParameter": true,
|
|
"getIndexedParameter": [0],
|
|
"getInternalformatParameter": true,
|
|
"getParameter": true,
|
|
"getProgramParameter": true,
|
|
"getQuery": true,
|
|
"getQueryParameter": [1],
|
|
"getRenderbufferParameter": true,
|
|
"getSamplerParameter": [1],
|
|
"getShaderParameter": true,
|
|
"getShaderPrecisionFormat": true,
|
|
"getSyncParameter": [1],
|
|
"getTexParameter": true,
|
|
"getVertexAttrib": [1],
|
|
"getVertexAttribOffset": [1],
|
|
"hint": true,
|
|
"invalidateFramebuffer": [0, 1],
|
|
"invalidateSubFramebuffer": [0, 1],
|
|
"isEnabled": true,
|
|
"pixelStorei": [0],
|
|
"readBuffer": true,
|
|
"readPixels": [4, 5],
|
|
"renderbufferStorage": [0, 1],
|
|
"renderbufferStorageMultisample": [0, 2],
|
|
"samplerParameterf": [1],
|
|
"samplerParameteri": [1],
|
|
"stencilFunc": [0],
|
|
"stencilFuncSeparate": [0, 1],
|
|
"stencilMaskSeparate": [0],
|
|
"stencilOp": true,
|
|
"stencilOpSeparate": true,
|
|
"texImage2D": {
|
|
5: [0, 2, 3, 4],
|
|
6: [0, 2, 3, 4],
|
|
8: [0, 2, 6, 7],
|
|
9: [0, 2, 6, 7],
|
|
10: [0, 2, 6, 7],
|
|
11: [0, 2, 7, 8],
|
|
},
|
|
"texParameterf": [0, 1],
|
|
"texParameteri": [0, 1],
|
|
"texStorage2D":[0, 2],
|
|
"texSubImage2D": {
|
|
6: [0, 4, 5],
|
|
7: [0, 4, 5],
|
|
8: [0, 6, 7],
|
|
9: [0, 6, 7],
|
|
10: [0, 6, 7],
|
|
11: [0, 8, 9],
|
|
12: [0, 8, 9],
|
|
},
|
|
"transformFeedbackVaryings": [2],
|
|
"vertexAttribIPointer": [2],
|
|
"vertexAttribPointer": [2],
|
|
},
|
|
};
|
|
WI.RecordingAction._constantIndexes[WI.Recording.Type.OffscreenCanvasWebGL] = WI.RecordingAction._constantIndexes[WI.Recording.Type.CanvasWebGL];
|
|
WI.RecordingAction._constantIndexes[WI.Recording.Type.OffscreenCanvasWebGL2] = WI.RecordingAction._constantIndexes[WI.Recording.Type.CanvasWebGL2];
|
|
|
|
WI.RecordingAction._visualNames = {
|
|
[WI.Recording.Type.Canvas2D]: new Set([
|
|
"clearRect",
|
|
"drawFocusIfNeeded",
|
|
"drawImage",
|
|
"drawImageFromRect",
|
|
"fill",
|
|
"fillRect",
|
|
"fillText",
|
|
"putImageData",
|
|
"stroke",
|
|
"strokeRect",
|
|
"strokeText",
|
|
]),
|
|
[WI.Recording.Type.OffscreenCanvas2D]: new Set([
|
|
"clearRect",
|
|
"drawImage",
|
|
"fill",
|
|
"fillRect",
|
|
"fillText",
|
|
"putImageData",
|
|
"strokeRect",
|
|
"strokeText",
|
|
]),
|
|
[WI.Recording.Type.CanvasBitmapRenderer]: new Set([
|
|
"transferFromImageBitmap",
|
|
]),
|
|
[WI.Recording.Type.CanvasWebGL]: new Set([
|
|
"clear",
|
|
"drawArrays",
|
|
"drawElements",
|
|
]),
|
|
[WI.Recording.Type.CanvasWebGL2]: new Set([
|
|
"clear",
|
|
"drawArrays",
|
|
"drawArraysInstanced",
|
|
"drawElements",
|
|
"drawElementsInstanced",
|
|
]),
|
|
};
|
|
WI.RecordingAction._visualNames[WI.Recording.Type.OffscreenCanvasBitmapRenderer] = WI.RecordingAction._visualNames[WI.Recording.Type.CanvasBitmapRenderer];
|
|
WI.RecordingAction._visualNames[WI.Recording.Type.OffscreenCanvasWebGL] = WI.RecordingAction._visualNames[WI.Recording.Type.CanvasWebGL];
|
|
WI.RecordingAction._visualNames[WI.Recording.Type.OffscreenCanvasWebGL2] = WI.RecordingAction._visualNames[WI.Recording.Type.CanvasWebGL2];
|
|
|
|
WI.RecordingAction._stateModifiers = {
|
|
[WI.Recording.Type.Canvas2D]: {
|
|
arc: ["currentX", "currentY"],
|
|
arcTo: ["currentX", "currentY"],
|
|
beginPath: ["currentX", "currentY"],
|
|
bezierCurveTo: ["currentX", "currentY"],
|
|
clearShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
|
|
closePath: ["currentX", "currentY"],
|
|
ellipse: ["currentX", "currentY"],
|
|
lineTo: ["currentX", "currentY"],
|
|
moveTo: ["currentX", "currentY"],
|
|
quadraticCurveTo: ["currentX", "currentY"],
|
|
rect: ["currentX", "currentY"],
|
|
resetTransform: ["transform"],
|
|
rotate: ["transform"],
|
|
scale: ["transform"],
|
|
setAlpha: ["globalAlpha"],
|
|
setCompositeOperation: ["globalCompositeOperation"],
|
|
setFillColor: ["fillStyle"],
|
|
setLineCap: ["lineCap"],
|
|
setLineJoin: ["lineJoin"],
|
|
setLineWidth: ["lineWidth"],
|
|
setMiterLimit: ["miterLimit"],
|
|
setShadow: ["shadowOffsetX", "shadowOffsetY", "shadowBlur", "shadowColor"],
|
|
setStrokeColor: ["strokeStyle"],
|
|
setTransform: ["transform"],
|
|
translate: ["transform"],
|
|
},
|
|
[WI.Recording.Type.OffscreenCanvas2D]: {
|
|
arc: ["currentX", "currentY"],
|
|
arcTo: ["currentX", "currentY"],
|
|
beginPath: ["currentX", "currentY"],
|
|
bezierCurveTo: ["currentX", "currentY"],
|
|
closePath: ["currentX", "currentY"],
|
|
ellipse: ["currentX", "currentY"],
|
|
lineTo: ["currentX", "currentY"],
|
|
moveTo: ["currentX", "currentY"],
|
|
quadraticCurveTo: ["currentX", "currentY"],
|
|
rect: ["currentX", "currentY"],
|
|
resetTransform: ["transform"],
|
|
rotate: ["transform"],
|
|
scale: ["transform"],
|
|
setTransform: ["transform"],
|
|
translate: ["transform"],
|
|
},
|
|
};
|
|
|
|
/* Models/RecordingFrame.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RecordingFrame = class RecordingFrame
|
|
{
|
|
constructor(actions, {duration, incomplete} = {})
|
|
{
|
|
this._actions = actions;
|
|
this._duration = duration || NaN;
|
|
this._incomplete = incomplete || false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
payload = {};
|
|
|
|
if (!Array.isArray(payload.actions)) {
|
|
if ("actions" in payload)
|
|
WI.Recording.synthesizeWarning(WI.UIString("non-array %s").format(WI.unlocalizedString("actions")));
|
|
|
|
payload.actions = [];
|
|
}
|
|
|
|
let actions = payload.actions.map(WI.RecordingAction.fromPayload);
|
|
return new WI.RecordingFrame(actions, {
|
|
duration: payload.duration || NaN,
|
|
incomplete: !!payload.incomplete,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get actions() { return this._actions; }
|
|
get duration() { return this._duration; }
|
|
get incomplete() { return this._incomplete; }
|
|
|
|
toJSON()
|
|
{
|
|
let json = {
|
|
actions: this._actions.map((action) => action.toJSON()),
|
|
};
|
|
if (!isNaN(this._duration))
|
|
json.duration = this._duration;
|
|
if (this._incomplete)
|
|
json.incomplete = this._incomplete;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
/* Models/RecordingInitialStateAction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RecordingInitialStateAction = class RecordingInitialStateAction extends WI.RecordingAction
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._name = WI.UIString("Initial State");
|
|
|
|
this._valid = false;
|
|
|
|
this._swizzled = true;
|
|
}
|
|
};
|
|
|
|
/* Models/RecordingState.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RecordingState = class RecordingState
|
|
{
|
|
constructor(data, {source} = {})
|
|
{
|
|
this._data = data;
|
|
this._source = source || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromCanvasContext2D(context, options = {})
|
|
{
|
|
console.assert(context instanceof CanvasRenderingContext2D || (window.OffscreenCanvasRenderingContext2D && context instanceof OffscreenCanvasRenderingContext2D), context);
|
|
|
|
let matrix = context.getTransform();
|
|
|
|
let data = {};
|
|
|
|
data.direction = context.direction;
|
|
data.fillStyle = context.fillStyle;
|
|
data.font = context.font;
|
|
data.globalAlpha = context.globalAlpha;
|
|
data.globalCompositeOperation = context.globalCompositeOperation;
|
|
data.imageSmoothingEnabled = context.imageSmoothingEnabled;
|
|
data.imageSmoothingQuality = context.imageSmoothingQuality;
|
|
data.lineCap = context.lineCap;
|
|
data.lineDash = context.getLineDash();
|
|
data.lineDashOffset = context.lineDashOffset;
|
|
data.lineJoin = context.lineJoin;
|
|
data.lineWidth = context.lineWidth;
|
|
data.miterLimit = context.miterLimit;
|
|
data.shadowBlur = context.shadowBlur;
|
|
data.shadowColor = context.shadowColor;
|
|
data.shadowOffsetX = context.shadowOffsetX;
|
|
data.shadowOffsetY = context.shadowOffsetY;
|
|
data.strokeStyle = context.strokeStyle;
|
|
data.textAlign = context.textAlign;
|
|
data.textBaseline = context.textBaseline;
|
|
data.transform = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f];
|
|
data.webkitImageSmoothingEnabled = context.webkitImageSmoothingEnabled;
|
|
data.webkitLineDash = context.webkitLineDash;
|
|
data.webkitLineDashOffset = context.webkitLineDashOffset;
|
|
|
|
data.currentX = context.currentX;
|
|
data.currentY = context.currentY;
|
|
data.setPath = [context.getPath()];
|
|
|
|
return new WI.RecordingState(data, options);
|
|
}
|
|
|
|
static async swizzleInitialState(recording, initialState)
|
|
{
|
|
if (recording.isCanvas2D) {
|
|
let swizzledState = {};
|
|
|
|
for (let [name, value] of Object.entries(initialState)) {
|
|
// COMPATIBILITY (iOS 12.0): Recording.InitialState.states did not exist yet
|
|
let nameIndex = parseInt(name);
|
|
if (!isNaN(nameIndex))
|
|
name = await recording.swizzle(nameIndex, WI.Recording.Swizzle.String);
|
|
|
|
switch (name) {
|
|
case "setTransform":
|
|
value = [await recording.swizzle(value, WI.Recording.Swizzle.DOMMatrix)];
|
|
break;
|
|
|
|
case "fillStyle":
|
|
case "strokeStyle":
|
|
var [gradient, pattern, string] = await Promise.all([
|
|
recording.swizzle(value, WI.Recording.Swizzle.CanvasGradient),
|
|
recording.swizzle(value, WI.Recording.Swizzle.CanvasPattern),
|
|
recording.swizzle(value, WI.Recording.Swizzle.String),
|
|
]);
|
|
if (gradient && !pattern)
|
|
value = gradient;
|
|
else if (pattern && !gradient)
|
|
value = pattern;
|
|
else
|
|
value = string;
|
|
break;
|
|
|
|
case "direction":
|
|
case "font":
|
|
case "globalCompositeOperation":
|
|
case "imageSmoothingQuality":
|
|
case "lineCap":
|
|
case "lineJoin":
|
|
case "shadowColor":
|
|
case "textAlign":
|
|
case "textBaseline":
|
|
value = await recording.swizzle(value, WI.Recording.Swizzle.String);
|
|
break;
|
|
|
|
case "globalAlpha":
|
|
case "lineWidth":
|
|
case "miterLimit":
|
|
case "shadowOffsetX":
|
|
case "shadowOffsetY":
|
|
case "shadowBlur":
|
|
case "lineDashOffset":
|
|
value = await recording.swizzle(value, WI.Recording.Swizzle.Number);
|
|
break;
|
|
|
|
case "setPath":
|
|
value = [await recording.swizzle(value[0], WI.Recording.Swizzle.Path2D)];
|
|
break;
|
|
}
|
|
|
|
if (value === undefined || (Array.isArray(value) && value.includes(undefined)))
|
|
continue;
|
|
|
|
swizzledState[name] = value;
|
|
}
|
|
|
|
return new WI.RecordingState(swizzledState);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get source() { return this._source; }
|
|
|
|
has(name)
|
|
{
|
|
return name in this._data;
|
|
}
|
|
|
|
get(name)
|
|
{
|
|
return this._data[name];
|
|
}
|
|
|
|
apply(type, context)
|
|
{
|
|
for (let [name, value] of this) {
|
|
if (!(name in context))
|
|
continue;
|
|
|
|
// Skip internal state used for path debugging.
|
|
if (name === "currentX" || name === "currentY")
|
|
continue;
|
|
|
|
try {
|
|
if (WI.RecordingAction.isFunctionForType(type, name))
|
|
context[name](...value);
|
|
else
|
|
context[name] = value;
|
|
} catch { }
|
|
}
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return this._data;
|
|
}
|
|
|
|
[Symbol.iterator]()
|
|
{
|
|
return Object.entries(this._data)[Symbol.iterator]();
|
|
}
|
|
};
|
|
|
|
/* Models/Redirect.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Redirect = class Redirect
|
|
{
|
|
constructor(url, requestMethod, requestHeaders, responseStatusCode, responseStatusText, responseHeaders, timestamp)
|
|
{
|
|
console.assert(typeof url === "string");
|
|
console.assert(typeof requestMethod === "string");
|
|
console.assert(typeof requestHeaders === "object");
|
|
console.assert(!isNaN(responseStatusCode));
|
|
console.assert(typeof responseStatusText === "string");
|
|
console.assert(typeof responseHeaders === "object");
|
|
console.assert(!isNaN(timestamp));
|
|
|
|
this._url = url;
|
|
this._urlComponents = null;
|
|
this._requestMethod = requestMethod;
|
|
this._requestHeaders = requestHeaders;
|
|
this._responseStatusCode = responseStatusCode;
|
|
this._responseStatusText = responseStatusText;
|
|
this._responseHeaders = responseHeaders;
|
|
this._timestamp = timestamp;
|
|
}
|
|
|
|
// Public
|
|
|
|
get url() { return this._url; }
|
|
get requestMethod() { return this._requestMethod; }
|
|
get requestHeaders() { return this._requestHeaders; }
|
|
get responseStatusCode() { return this._responseStatusCode; }
|
|
get responseStatusText() { return this._responseStatusText; }
|
|
get responseHeaders() { return this._responseHeaders; }
|
|
get timestamp() { return this._timestamp; }
|
|
|
|
get urlComponents()
|
|
{
|
|
if (!this._urlComponents)
|
|
this._urlComponents = parseURL(this._url);
|
|
return this._urlComponents;
|
|
}
|
|
};
|
|
|
|
/* Models/RenderingFrameTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RenderingFrameTimelineRecord = class RenderingFrameTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(startTime, endTime, name)
|
|
{
|
|
super(WI.TimelineRecord.Type.RenderingFrame, startTime, endTime);
|
|
|
|
this._name = name || "";
|
|
this._durationByTaskType = new Map;
|
|
this._frameIndex = -1;
|
|
}
|
|
|
|
// Static
|
|
|
|
static resetFrameIndex()
|
|
{
|
|
WI.RenderingFrameTimelineRecord._nextFrameIndex = 0;
|
|
}
|
|
|
|
static displayNameForTaskType(taskType)
|
|
{
|
|
switch (taskType) {
|
|
case WI.RenderingFrameTimelineRecord.TaskType.Script:
|
|
return WI.UIString("Script");
|
|
case WI.RenderingFrameTimelineRecord.TaskType.Layout:
|
|
return WI.repeatedUIString.timelineRecordLayout();
|
|
case WI.RenderingFrameTimelineRecord.TaskType.Paint:
|
|
return WI.repeatedUIString.timelineRecordPaint();
|
|
case WI.RenderingFrameTimelineRecord.TaskType.Other:
|
|
return WI.UIString("Other");
|
|
}
|
|
}
|
|
|
|
static taskTypeForTimelineRecord(record)
|
|
{
|
|
switch (record.type) {
|
|
case WI.TimelineRecord.Type.Script:
|
|
return WI.RenderingFrameTimelineRecord.TaskType.Script;
|
|
case WI.TimelineRecord.Type.Layout:
|
|
if (record.eventType === WI.LayoutTimelineRecord.EventType.Paint || record.eventType === WI.LayoutTimelineRecord.EventType.Composite)
|
|
return WI.RenderingFrameTimelineRecord.TaskType.Paint;
|
|
return WI.RenderingFrameTimelineRecord.TaskType.Layout;
|
|
default:
|
|
console.error("Unsupported timeline record type: " + record.type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {startTime, endTime} = json;
|
|
let record = new WI.RenderingFrameTimelineRecord(startTime, endTime);
|
|
record.setupFrameIndex();
|
|
return record;
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
// FIXME: durationByTaskType data cannot be calculated if this does not have children.
|
|
|
|
return {
|
|
type: this.type,
|
|
startTime: this.startTime,
|
|
endTime: this.endTime,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get frameIndex()
|
|
{
|
|
return this._frameIndex;
|
|
}
|
|
|
|
get frameNumber()
|
|
{
|
|
return this._frameIndex + 1;
|
|
}
|
|
|
|
get name()
|
|
{
|
|
return this._name;
|
|
}
|
|
|
|
setupFrameIndex()
|
|
{
|
|
console.assert(this._frameIndex === -1, "Frame index should only be set once.");
|
|
if (this._frameIndex >= 0)
|
|
return;
|
|
this._frameIndex = WI.RenderingFrameTimelineRecord._nextFrameIndex++;
|
|
}
|
|
|
|
durationForTask(taskType)
|
|
{
|
|
if (this._durationByTaskType.has(taskType))
|
|
return this._durationByTaskType.get(taskType);
|
|
|
|
var duration;
|
|
if (taskType === WI.RenderingFrameTimelineRecord.TaskType.Other)
|
|
duration = this._calculateDurationRemainder();
|
|
else {
|
|
duration = this.children.reduce(function(previousValue, currentValue) {
|
|
if (taskType !== WI.RenderingFrameTimelineRecord.taskTypeForTimelineRecord(currentValue))
|
|
return previousValue;
|
|
|
|
var currentDuration = currentValue.duration;
|
|
if (currentValue.usesActiveStartTime)
|
|
currentDuration -= currentValue.inactiveDuration;
|
|
return previousValue + currentDuration;
|
|
}, 0);
|
|
|
|
if (taskType === WI.RenderingFrameTimelineRecord.TaskType.Script) {
|
|
// Layout events synchronously triggered from JavaScript must be subtracted from the total
|
|
// script time, to prevent the time from being counted twice.
|
|
duration -= this.children.reduce(function(previousValue, currentValue) {
|
|
if (currentValue.type === WI.TimelineRecord.Type.Layout && (currentValue.sourceCodeLocation || currentValue.stackTrace))
|
|
return previousValue + currentValue.duration;
|
|
return previousValue;
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
this._durationByTaskType.set(taskType, duration);
|
|
return duration;
|
|
}
|
|
|
|
// Private
|
|
|
|
_calculateDurationRemainder()
|
|
{
|
|
return Object.keys(WI.RenderingFrameTimelineRecord.TaskType).reduce((previousValue, key) => {
|
|
let taskType = WI.RenderingFrameTimelineRecord.TaskType[key];
|
|
if (taskType === WI.RenderingFrameTimelineRecord.TaskType.Other)
|
|
return previousValue;
|
|
return previousValue - this.durationForTask(taskType);
|
|
}, this.duration);
|
|
}
|
|
};
|
|
|
|
WI.RenderingFrameTimelineRecord.TaskType = {
|
|
Script: "rendering-frame-timeline-record-script",
|
|
Layout: "rendering-frame-timeline-record-layout",
|
|
Paint: "rendering-frame-timeline-record-paint",
|
|
Other: "rendering-frame-timeline-record-other"
|
|
};
|
|
|
|
WI.RenderingFrameTimelineRecord.TypeIdentifier = "rendering-frame-timeline-record";
|
|
|
|
WI.RenderingFrameTimelineRecord._nextFrameIndex = 0;
|
|
|
|
/* Models/ResourceCollection.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2016 Devin Rousso <webkit@devinrousso.com>. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ResourceCollection = class ResourceCollection extends WI.Collection
|
|
{
|
|
constructor(resourceType)
|
|
{
|
|
super();
|
|
|
|
this._resourceType = resourceType || null;
|
|
this._resourceURLMap = new Multimap;
|
|
this._resourcesTypeMap = new Map;
|
|
}
|
|
|
|
// Public
|
|
|
|
get resourceType() { return this._resourceType; }
|
|
|
|
get displayName()
|
|
{
|
|
const plural = true;
|
|
return this._resourceType ? WI.Resource.displayNameForType(this._resourceType, plural) : WI.UIString("Resources");
|
|
}
|
|
|
|
objectIsRequiredType(object)
|
|
{
|
|
if (this._resourceType === WI.Resource.Type.StyleSheet && object instanceof WI.CSSStyleSheet)
|
|
return true;
|
|
|
|
if (!(object instanceof WI.Resource))
|
|
return false;
|
|
|
|
if (!this._resourceType)
|
|
return true;
|
|
|
|
return object.type === this._resourceType;
|
|
}
|
|
|
|
resourcesForURL(url)
|
|
{
|
|
return this._resourceURLMap.get(url) || new Set;
|
|
}
|
|
|
|
resourceCollectionForType(type)
|
|
{
|
|
if (this._resourceType) {
|
|
console.assert(type === this._resourceType);
|
|
return this;
|
|
}
|
|
|
|
let resourcesCollectionForType = this._resourcesTypeMap.get(type);
|
|
if (!resourcesCollectionForType) {
|
|
resourcesCollectionForType = new WI.ResourceCollection(type);
|
|
this._resourcesTypeMap.set(type, resourcesCollectionForType);
|
|
}
|
|
|
|
return resourcesCollectionForType;
|
|
}
|
|
|
|
clear()
|
|
{
|
|
super.clear();
|
|
|
|
this._resourceURLMap.clear();
|
|
|
|
if (!this._resourceType)
|
|
this._resourcesTypeMap.clear();
|
|
}
|
|
|
|
// Protected
|
|
|
|
itemAdded(item)
|
|
{
|
|
this._associateWithResource(item);
|
|
}
|
|
|
|
itemRemoved(item)
|
|
{
|
|
this._disassociateWithResource(item);
|
|
}
|
|
|
|
itemsCleared(items)
|
|
{
|
|
const skipRemoval = true;
|
|
|
|
for (let item of items)
|
|
this._disassociateWithResource(item, skipRemoval);
|
|
}
|
|
|
|
// Private
|
|
|
|
_associateWithResource(resource)
|
|
{
|
|
this._resourceURLMap.add(resource.url, resource);
|
|
|
|
if (!this._resourceType) {
|
|
let resourcesCollectionForType = this.resourceCollectionForType(resource.type);
|
|
resourcesCollectionForType.add(resource);
|
|
}
|
|
|
|
resource.addEventListener(WI.Resource.Event.URLDidChange, this._resourceURLDidChange, this);
|
|
resource.addEventListener(WI.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);
|
|
}
|
|
|
|
_disassociateWithResource(resource, skipRemoval)
|
|
{
|
|
resource.removeEventListener(WI.Resource.Event.URLDidChange, this._resourceURLDidChange, this);
|
|
resource.removeEventListener(WI.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);
|
|
|
|
if (skipRemoval)
|
|
return;
|
|
|
|
if (!this._resourceType) {
|
|
let resourcesCollectionForType = this.resourceCollectionForType(resource.type);
|
|
resourcesCollectionForType.remove(resource);
|
|
}
|
|
|
|
this._resourceURLMap.delete(resource.url, resource);
|
|
}
|
|
|
|
_resourceURLDidChange(event)
|
|
{
|
|
let resource = event.target;
|
|
console.assert(resource instanceof WI.Resource);
|
|
if (!(resource instanceof WI.Resource))
|
|
return;
|
|
|
|
let oldURL = event.data.oldURL;
|
|
console.assert(oldURL);
|
|
if (!oldURL)
|
|
return;
|
|
|
|
this._resourceURLMap.add(resource.url, resource);
|
|
this._resourceURLMap.delete(oldURL, resource);
|
|
}
|
|
|
|
_resourceTypeDidChange(event)
|
|
{
|
|
let resource = event.target;
|
|
console.assert(resource instanceof WI.Resource);
|
|
if (!(resource instanceof WI.Resource))
|
|
return;
|
|
|
|
if (this._resourceType) {
|
|
console.assert(resource.type !== this._resourceType);
|
|
this.remove(resource);
|
|
return;
|
|
}
|
|
|
|
console.assert(event.data.oldType);
|
|
|
|
let resourcesWithNewType = this.resourceCollectionForType(resource.type);
|
|
resourcesWithNewType.add(resource);
|
|
|
|
// It is not necessary to remove the resource from the sub-collection for the old type since
|
|
// this is handled by that sub-collection's own _resourceTypeDidChange handler (via the
|
|
// above if statement).
|
|
}
|
|
};
|
|
|
|
/* Models/ResourceQueryResult.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ResourceQueryResult = class ResourceQueryResult extends WI.QueryResult
|
|
{
|
|
constructor(resource, searchString, matches, cookie)
|
|
{
|
|
console.assert(resource instanceof WI.Resource, resource);
|
|
super(resource, matches);
|
|
|
|
this._searchString = searchString;
|
|
this._cookie = cookie || null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get resource() { return this.value; }
|
|
get searchString() { return this._searchString; }
|
|
get cookie() { return this._cookie; }
|
|
|
|
// Testing
|
|
|
|
__test_createMatchesMask()
|
|
{
|
|
let filename = this.resource.displayName;
|
|
let lastIndex = -1;
|
|
let result = "";
|
|
|
|
for (let match of this._matches) {
|
|
let gap = " ".repeat(match.index - lastIndex - 1);
|
|
result += gap;
|
|
result += filename[match.index];
|
|
lastIndex = match.index;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
/* Models/ResourceTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ResourceTimelineRecord = class ResourceTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(resource)
|
|
{
|
|
super(WI.TimelineRecord.Type.Network);
|
|
|
|
this._resource = resource;
|
|
this._resource.addEventListener(WI.Resource.Event.TimestampsDidChange, this._dispatchUpdatedEvent, this);
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {entry, archiveStartTime} = json;
|
|
let localResource = WI.LocalResource.fromHAREntry(entry, archiveStartTime);
|
|
return new WI.ResourceTimelineRecord(localResource);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
const content = "";
|
|
|
|
return {
|
|
type: this.type,
|
|
archiveStartTime: this._resource.requestSentWalltime - this.startTime,
|
|
entry: WI.HARBuilder.entry(this._resource, content),
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get resource()
|
|
{
|
|
return this._resource;
|
|
}
|
|
|
|
get updatesDynamically()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
get usesActiveStartTime()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
get startTime()
|
|
{
|
|
return this._resource.timingData.startTime;
|
|
}
|
|
|
|
get activeStartTime()
|
|
{
|
|
return this._resource.timingData.responseStart;
|
|
}
|
|
|
|
get endTime()
|
|
{
|
|
return this._resource.timingData.responseEnd;
|
|
}
|
|
|
|
// Private
|
|
|
|
_dispatchUpdatedEvent()
|
|
{
|
|
this.dispatchEventToListeners(WI.TimelineRecord.Event.Updated);
|
|
}
|
|
};
|
|
|
|
/* Models/ResourceTimingData.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ResourceTimingData = class ResourceTimingData
|
|
{
|
|
constructor(resource, data)
|
|
{
|
|
data = data || {};
|
|
|
|
console.assert(isNaN(data.startTime) || data.startTime <= data.fetchStart);
|
|
console.assert(isNaN(data.redirectStart) === isNaN(data.redirectEnd));
|
|
console.assert(isNaN(data.domainLookupStart) === isNaN(data.domainLookupEnd));
|
|
console.assert(isNaN(data.connectStart) === isNaN(data.connectEnd));
|
|
|
|
this._resource = resource;
|
|
|
|
this._startTime = data.startTime || NaN;
|
|
this._redirectStart = data.redirectStart || NaN;
|
|
this._redirectEnd = data.redirectEnd || NaN;
|
|
this._fetchStart = data.fetchStart || NaN;
|
|
this._domainLookupStart = data.domainLookupStart || NaN;
|
|
this._domainLookupEnd = data.domainLookupEnd || NaN;
|
|
this._connectStart = data.connectStart || NaN;
|
|
this._connectEnd = data.connectEnd || NaN;
|
|
this._secureConnectionStart = data.secureConnectionStart || NaN;
|
|
this._requestStart = data.requestStart || NaN;
|
|
this._responseStart = data.responseStart || NaN;
|
|
this._responseEnd = data.responseEnd || NaN;
|
|
|
|
if (this._domainLookupStart >= this._domainLookupEnd)
|
|
this._domainLookupStart = this._domainLookupEnd = NaN;
|
|
|
|
if (this._connectStart >= this._connectEnd)
|
|
this._connectStart = this._connectEnd = NaN;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(payload, resource)
|
|
{
|
|
payload = payload || {};
|
|
|
|
// COMPATIBILITY (iOS 12.0): Resource Timing data was based on startTime, not fetchStart.
|
|
let startTime = payload.startTime;
|
|
let fetchStart = payload.fetchStart;
|
|
let redirectStart = payload.redirectStart;
|
|
let redirectEnd = payload.redirectEnd;
|
|
|
|
if (isNaN(fetchStart) || fetchStart < startTime)
|
|
fetchStart = startTime;
|
|
|
|
if (redirectStart < startTime || redirectStart > fetchStart || redirectStart > redirectEnd)
|
|
redirectStart = NaN;
|
|
|
|
if (redirectEnd < startTime || redirectEnd > fetchStart || redirectEnd < redirectStart)
|
|
redirectEnd = NaN;
|
|
|
|
function offsetToTimestamp(offset) {
|
|
return offset > 0 ? fetchStart + (offset / 1000) : NaN;
|
|
}
|
|
|
|
let data = {
|
|
startTime,
|
|
redirectStart,
|
|
redirectEnd,
|
|
fetchStart,
|
|
domainLookupStart: offsetToTimestamp(payload.domainLookupStart),
|
|
domainLookupEnd: offsetToTimestamp(payload.domainLookupEnd),
|
|
connectStart: offsetToTimestamp(payload.connectStart),
|
|
connectEnd: offsetToTimestamp(payload.connectEnd),
|
|
secureConnectionStart: offsetToTimestamp(payload.secureConnectionStart),
|
|
requestStart: offsetToTimestamp(payload.requestStart),
|
|
responseStart: offsetToTimestamp(payload.responseStart),
|
|
responseEnd: offsetToTimestamp(payload.responseEnd)
|
|
};
|
|
|
|
return new WI.ResourceTimingData(resource, data);
|
|
}
|
|
|
|
// Public
|
|
|
|
get startTime() { return this._startTime || this._resource.requestSentTimestamp; }
|
|
get redirectStart() { return this._redirectStart; }
|
|
get redirectEnd() { return this._redirectEnd; }
|
|
get fetchStart() { return this._fetchStart || this._resource.requestSentTimestamp; }
|
|
get domainLookupStart() { return this._domainLookupStart; }
|
|
get domainLookupEnd() { return this._domainLookupEnd; }
|
|
get connectStart() { return this._connectStart; }
|
|
get connectEnd() { return this._connectEnd; }
|
|
get secureConnectionStart() { return this._secureConnectionStart; }
|
|
get requestStart() { return this._requestStart || this._startTime || this._resource.requestSentTimestamp; }
|
|
get responseStart() { return this._responseStart || this._startTime || this._resource.responseReceivedTimestamp || this._resource.finishedOrFailedTimestamp; }
|
|
get responseEnd() { return this._responseEnd || this._resource.finishedOrFailedTimestamp; }
|
|
|
|
markResponseEndTime(responseEnd)
|
|
{
|
|
console.assert(typeof responseEnd === "number");
|
|
console.assert(isNaN(responseEnd) || responseEnd >= this.startTime, "responseEnd time should be greater than the start time", this.startTime, responseEnd);
|
|
console.assert(isNaN(responseEnd) || responseEnd >= this.requestStart, "responseEnd time should be greater than the request time", this.requestStart, responseEnd);
|
|
this._responseEnd = responseEnd;
|
|
}
|
|
};
|
|
|
|
/* Models/Revision.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Revision = class Revision
|
|
{
|
|
// Public
|
|
|
|
apply()
|
|
{
|
|
// Implemented by subclasses.
|
|
console.error("Needs to be implemented by a subclass.");
|
|
}
|
|
|
|
revert()
|
|
{
|
|
// Implemented by subclasses.
|
|
console.error("Needs to be implemented by a subclass.");
|
|
}
|
|
|
|
copy()
|
|
{
|
|
// Override by subclasses.
|
|
return this;
|
|
}
|
|
};
|
|
|
|
/* Models/ScopeChainNode.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScopeChainNode = class ScopeChainNode
|
|
{
|
|
constructor(type, objects, name, location, empty)
|
|
{
|
|
console.assert(typeof type === "string");
|
|
console.assert(objects.every((x) => x instanceof WI.RemoteObject));
|
|
|
|
if (type in WI.ScopeChainNode.Type)
|
|
type = WI.ScopeChainNode.Type[type];
|
|
|
|
this._type = type || null;
|
|
this._objects = objects || [];
|
|
this._name = name || "";
|
|
this._location = location || null;
|
|
this._empty = empty || false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get objects() { return this._objects; }
|
|
get name() { return this._name; }
|
|
get location() { return this._location; }
|
|
get empty() { return this._empty; }
|
|
|
|
get hash()
|
|
{
|
|
if (this._hash)
|
|
return this._hash;
|
|
|
|
this._hash = this._name;
|
|
if (this._location)
|
|
this._hash += `:${this._location.scriptId}:${this._location.lineNumber}:${this._location.columnNumber}`;
|
|
return this._hash;
|
|
}
|
|
|
|
convertToLocalScope()
|
|
{
|
|
this._type = WI.ScopeChainNode.Type.Local;
|
|
}
|
|
};
|
|
|
|
WI.ScopeChainNode.Type = {
|
|
Local: "scope-chain-type-local",
|
|
Global: "scope-chain-type-global",
|
|
GlobalLexicalEnvironment: "scope-chain-type-global-lexical-environment",
|
|
With: "scope-chain-type-with",
|
|
Closure: "scope-chain-type-closure",
|
|
Catch: "scope-chain-type-catch",
|
|
FunctionName: "scope-chain-type-function-name",
|
|
Block: "scope-chain-type-block",
|
|
};
|
|
|
|
/* Models/ScreenshotsInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScreenshotsInstrument = class ScreenshotsInstrument extends WI.Instrument
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
console.assert(WI.ScreenshotsInstrument.supported());
|
|
}
|
|
|
|
// Static
|
|
|
|
static supported()
|
|
{
|
|
// FIXME: <webkit.org/b/251113> Screenshots timeline does not capture screenshots for changes in WebKitLegacy.
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.Page)
|
|
return false;
|
|
|
|
// COMPATIBILITY (macOS 12.3, iOS 15.4): Timeline.Instrument.Screenshot did not exist yet.
|
|
return InspectorBackend.Enum.Timeline.Instrument.Screenshot;
|
|
}
|
|
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Screenshots;
|
|
}
|
|
};
|
|
|
|
/* Models/ScreenshotsTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScreenshotsTimelineRecord = class ScreenshotsTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(timestamp, imageData)
|
|
{
|
|
console.assert(timestamp);
|
|
console.assert(imageData && typeof imageData === "string", imageData);
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
super(WI.TimelineRecord.Type.Screenshots, timestamp, timestamp);
|
|
|
|
this._imageData = imageData;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
return new WI.ScreenshotsTimelineRecord(json.timestamp, json.imageData);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
type: this.type,
|
|
timestamp: this.startTime,
|
|
imageData: this._imageData,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get imageData() { return this._imageData; }
|
|
};
|
|
|
|
/* Models/ScriptInstrument.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScriptInstrument = class ScriptInstrument extends WI.Instrument
|
|
{
|
|
// Protected
|
|
|
|
get timelineRecordType()
|
|
{
|
|
return WI.TimelineRecord.Type.Script;
|
|
}
|
|
|
|
startInstrumentation(initiatedByBackend)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// FIXME: Make this some UI visible option.
|
|
const includeSamples = true;
|
|
|
|
if (!initiatedByBackend)
|
|
target.ScriptProfilerAgent.startTracking(includeSamples);
|
|
}
|
|
|
|
stopInstrumentation(initiatedByBackend)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
|
|
if (!initiatedByBackend)
|
|
target.ScriptProfilerAgent.stopTracking();
|
|
}
|
|
};
|
|
|
|
/* Models/ScriptSyntaxTree.js */
|
|
|
|
/*
|
|
* Copyright (C) 2014-2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScriptSyntaxTree = class ScriptSyntaxTree
|
|
{
|
|
constructor(sourceText, script)
|
|
{
|
|
console.assert(script && script instanceof WI.Script, script);
|
|
|
|
this._script = script;
|
|
|
|
try {
|
|
let sourceType = this._script.sourceType === WI.Script.SourceType.Module ? "module" : "script";
|
|
let esprimaSyntaxTree = esprima.parse(sourceText, {loc: true, range: true, sourceType});
|
|
this._syntaxTree = this._createInternalSyntaxTree(esprimaSyntaxTree);
|
|
this._parsedSuccessfully = true;
|
|
} catch (error) {
|
|
this._parsedSuccessfully = false;
|
|
this._syntaxTree = null;
|
|
console.error("Couldn't parse JavaScript File: " + script.url, error);
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get parsedSuccessfully()
|
|
{
|
|
return this._parsedSuccessfully;
|
|
}
|
|
|
|
forEachNode(callback)
|
|
{
|
|
console.assert(this._parsedSuccessfully);
|
|
if (!this._parsedSuccessfully)
|
|
return;
|
|
|
|
this._recurse(this._syntaxTree, callback, this._defaultParserState());
|
|
}
|
|
|
|
filter(predicate, startNode)
|
|
{
|
|
console.assert(startNode && this._parsedSuccessfully);
|
|
if (!this._parsedSuccessfully)
|
|
return [];
|
|
|
|
var nodes = [];
|
|
function filter(node, state)
|
|
{
|
|
if (predicate(node))
|
|
nodes.push(node);
|
|
else
|
|
state.skipChildNodes = true;
|
|
}
|
|
|
|
this._recurse(startNode, filter, this._defaultParserState());
|
|
|
|
return nodes;
|
|
}
|
|
|
|
containersOfPosition(position)
|
|
{
|
|
console.assert(this._parsedSuccessfully);
|
|
if (!this._parsedSuccessfully)
|
|
return [];
|
|
|
|
let allNodes = [];
|
|
|
|
this.forEachNode((node, state) => {
|
|
if (node.endPosition.isBefore(position))
|
|
state.skipChildNodes = true;
|
|
else if (node.startPosition.isAfter(position))
|
|
state.shouldStopEarly = true;
|
|
else
|
|
allNodes.push(node);
|
|
});
|
|
|
|
return allNodes;
|
|
}
|
|
|
|
filterByRange(startPosition, endPosition)
|
|
{
|
|
console.assert(this._parsedSuccessfully);
|
|
if (!this._parsedSuccessfully)
|
|
return [];
|
|
|
|
var allNodes = [];
|
|
function filterForNodesInRange(node, state)
|
|
{
|
|
// program start range program end
|
|
// [ [ ] ]
|
|
// [ ] [ [ ] ] [ ]
|
|
|
|
// If a node's range ends before the range we're interested in starts, we don't need to search any of its
|
|
// enclosing ranges, because, by definition, those enclosing ranges are contained within this node's range.
|
|
if (node.endPosition.isBefore(startPosition)) {
|
|
state.skipChildNodes = true;
|
|
return;
|
|
}
|
|
|
|
// We are only interested in nodes whose start position is within our range.
|
|
if (node.startPosition.isWithin(startPosition, endPosition)) {
|
|
allNodes.push(node);
|
|
return;
|
|
}
|
|
|
|
// Once we see nodes that start beyond our range, we can quit traversing the AST. We can do this safely
|
|
// because we know the AST is traversed using depth first search, so it will traverse into enclosing ranges
|
|
// before it traverses into adjacent ranges.
|
|
if (node.startPosition.isAfter(endPosition))
|
|
state.shouldStopEarly = true;
|
|
}
|
|
|
|
this.forEachNode(filterForNodesInRange);
|
|
|
|
return allNodes;
|
|
}
|
|
|
|
containsNonEmptyReturnStatement(startNode)
|
|
{
|
|
console.assert(startNode && this._parsedSuccessfully);
|
|
if (!this._parsedSuccessfully)
|
|
return false;
|
|
|
|
if (startNode.attachments._hasNonEmptyReturnStatement !== undefined)
|
|
return startNode.attachments._hasNonEmptyReturnStatement;
|
|
|
|
function removeFunctionsFilter(node)
|
|
{
|
|
return node.type !== WI.ScriptSyntaxTree.NodeType.FunctionExpression
|
|
&& node.type !== WI.ScriptSyntaxTree.NodeType.FunctionDeclaration
|
|
&& node.type !== WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression;
|
|
}
|
|
|
|
var nodes = this.filter(removeFunctionsFilter, startNode);
|
|
var hasNonEmptyReturnStatement = false;
|
|
var returnStatementType = WI.ScriptSyntaxTree.NodeType.ReturnStatement;
|
|
for (var node of nodes) {
|
|
if (node.type === returnStatementType && node.argument) {
|
|
hasNonEmptyReturnStatement = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
startNode.attachments._hasNonEmptyReturnStatement = hasNonEmptyReturnStatement;
|
|
|
|
return hasNonEmptyReturnStatement;
|
|
}
|
|
|
|
static functionReturnDivot(node)
|
|
{
|
|
console.assert(node.type === WI.ScriptSyntaxTree.NodeType.FunctionDeclaration || node.type === WI.ScriptSyntaxTree.NodeType.FunctionExpression || node.type === WI.ScriptSyntaxTree.NodeType.MethodDefinition || node.type === WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression);
|
|
|
|
// "f" in "function". "s" in "set". "g" in "get". First letter in any method name for classes and object literals.
|
|
// The "[" for computed methods in classes and object literals.
|
|
return node.typeProfilingReturnDivot;
|
|
}
|
|
|
|
updateTypes(nodesToUpdate, callback)
|
|
{
|
|
console.assert(this._script.target.hasCommand("Runtime.getRuntimeTypesForVariablesAtOffsets"));
|
|
console.assert(Array.isArray(nodesToUpdate) && this._parsedSuccessfully);
|
|
|
|
if (!this._parsedSuccessfully)
|
|
return;
|
|
|
|
var allRequests = [];
|
|
var allRequestNodes = [];
|
|
var sourceID = this._script.id;
|
|
|
|
for (var node of nodesToUpdate) {
|
|
switch (node.type) {
|
|
case WI.ScriptSyntaxTree.NodeType.FunctionDeclaration:
|
|
case WI.ScriptSyntaxTree.NodeType.FunctionExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression:
|
|
for (var param of node.params) {
|
|
for (var identifier of this._gatherIdentifiersInDeclaration(param)) {
|
|
allRequests.push({
|
|
typeInformationDescriptor: WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor.NormalExpression,
|
|
sourceID,
|
|
divot: identifier.range[0]
|
|
});
|
|
allRequestNodes.push(identifier);
|
|
}
|
|
}
|
|
|
|
allRequests.push({
|
|
typeInformationDescriptor: WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor.FunctionReturn,
|
|
sourceID,
|
|
divot: WI.ScriptSyntaxTree.functionReturnDivot(node)
|
|
});
|
|
allRequestNodes.push(node);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.VariableDeclarator:
|
|
for (var identifier of this._gatherIdentifiersInDeclaration(node.id)) {
|
|
allRequests.push({
|
|
typeInformationDescriptor: WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor.NormalExpression,
|
|
sourceID,
|
|
divot: identifier.range[0]
|
|
});
|
|
allRequestNodes.push(identifier);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
console.assert(allRequests.length === allRequestNodes.length);
|
|
|
|
function handleTypes(error, typeInformationArray)
|
|
{
|
|
if (error)
|
|
return;
|
|
|
|
console.assert(typeInformationArray.length === allRequests.length);
|
|
|
|
for (var i = 0; i < typeInformationArray.length; i++) {
|
|
var node = allRequestNodes[i];
|
|
var typeInformation = WI.TypeDescription.fromPayload(typeInformationArray[i]);
|
|
if (allRequests[i].typeInformationDescriptor === WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor.FunctionReturn)
|
|
node.attachments.returnTypes = typeInformation;
|
|
else
|
|
node.attachments.types = typeInformation;
|
|
}
|
|
|
|
callback(allRequestNodes);
|
|
}
|
|
|
|
this._script.target.RuntimeAgent.getRuntimeTypesForVariablesAtOffsets(allRequests, handleTypes);
|
|
}
|
|
|
|
// Private
|
|
|
|
_gatherIdentifiersInDeclaration(node)
|
|
{
|
|
function gatherIdentifiers(node)
|
|
{
|
|
switch (node.type) {
|
|
case WI.ScriptSyntaxTree.NodeType.Identifier:
|
|
return [node];
|
|
case WI.ScriptSyntaxTree.NodeType.Property:
|
|
return gatherIdentifiers(node.value);
|
|
case WI.ScriptSyntaxTree.NodeType.ObjectPattern:
|
|
var identifiers = [];
|
|
for (var property of node.properties) {
|
|
for (var identifier of gatherIdentifiers(property))
|
|
identifiers.push(identifier);
|
|
}
|
|
return identifiers;
|
|
case WI.ScriptSyntaxTree.NodeType.ArrayPattern:
|
|
var identifiers = [];
|
|
for (var element of node.elements) {
|
|
for (var identifier of gatherIdentifiers(element))
|
|
identifiers.push(identifier);
|
|
}
|
|
return identifiers;
|
|
case WI.ScriptSyntaxTree.NodeType.AssignmentPattern:
|
|
return gatherIdentifiers(node.left);
|
|
case WI.ScriptSyntaxTree.NodeType.RestElement:
|
|
return gatherIdentifiers(node.argument);
|
|
default:
|
|
console.assert(false, "Unexpected node type in variable declarator: " + node.type);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
console.assert(node.type === WI.ScriptSyntaxTree.NodeType.Identifier || node.type === WI.ScriptSyntaxTree.NodeType.ObjectPattern || node.type === WI.ScriptSyntaxTree.NodeType.ArrayPattern || node.type === WI.ScriptSyntaxTree.NodeType.RestElement);
|
|
|
|
return gatherIdentifiers(node);
|
|
}
|
|
|
|
_defaultParserState()
|
|
{
|
|
return {
|
|
shouldStopEarly: false,
|
|
skipChildNodes: false
|
|
};
|
|
}
|
|
|
|
_recurse(node, callback, state)
|
|
{
|
|
if (!node)
|
|
return;
|
|
|
|
if (state.shouldStopEarly || state.skipChildNodes)
|
|
return;
|
|
|
|
callback(node, state);
|
|
|
|
switch (node.type) {
|
|
case WI.ScriptSyntaxTree.NodeType.AssignmentExpression:
|
|
this._recurse(node.left, callback, state);
|
|
this._recurse(node.right, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ArrayExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.ArrayPattern:
|
|
this._recurseArray(node.elements, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.AssignmentPattern:
|
|
this._recurse(node.left, callback, state);
|
|
this._recurse(node.right, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.AwaitExpression:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.BlockStatement:
|
|
case WI.ScriptSyntaxTree.NodeType.StaticBlock:
|
|
this._recurseArray(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.BinaryExpression:
|
|
this._recurse(node.left, callback, state);
|
|
this._recurse(node.right, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.BreakStatement:
|
|
this._recurse(node.label, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.CatchClause:
|
|
this._recurse(node.param, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.CallExpression:
|
|
this._recurse(node.callee, callback, state);
|
|
this._recurseArray(node.arguments, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ClassBody:
|
|
this._recurseArray(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ClassDeclaration:
|
|
case WI.ScriptSyntaxTree.NodeType.ClassExpression:
|
|
this._recurse(node.id, callback, state);
|
|
this._recurse(node.superClass, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ContinueStatement:
|
|
this._recurse(node.label, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.DoWhileStatement:
|
|
this._recurse(node.body, callback, state);
|
|
this._recurse(node.test, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ChainExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.ExpressionStatement:
|
|
this._recurse(node.expression, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ForStatement:
|
|
this._recurse(node.init, callback, state);
|
|
this._recurse(node.test, callback, state);
|
|
this._recurse(node.update, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ForInStatement:
|
|
case WI.ScriptSyntaxTree.NodeType.ForOfStatement:
|
|
this._recurse(node.left, callback, state);
|
|
this._recurse(node.right, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.FunctionDeclaration:
|
|
case WI.ScriptSyntaxTree.NodeType.FunctionExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression:
|
|
this._recurse(node.id, callback, state);
|
|
this._recurseArray(node.params, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.IfStatement:
|
|
this._recurse(node.test, callback, state);
|
|
this._recurse(node.consequent, callback, state);
|
|
this._recurse(node.alternate, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.LabeledStatement:
|
|
this._recurse(node.label, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.LogicalExpression:
|
|
this._recurse(node.left, callback, state);
|
|
this._recurse(node.right, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.MemberExpression:
|
|
this._recurse(node.object, callback, state);
|
|
this._recurse(node.property, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.MethodDefinition:
|
|
this._recurse(node.key, callback, state);
|
|
this._recurse(node.value, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.NewExpression:
|
|
this._recurse(node.callee, callback, state);
|
|
this._recurseArray(node.arguments, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ObjectExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.ObjectPattern:
|
|
this._recurseArray(node.properties, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.Program:
|
|
this._recurseArray(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.Property:
|
|
this._recurse(node.key, callback, state);
|
|
this._recurse(node.value, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.RestElement:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ReturnStatement:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.SequenceExpression:
|
|
this._recurseArray(node.expressions, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.SpreadElement:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.SwitchStatement:
|
|
this._recurse(node.discriminant, callback, state);
|
|
this._recurseArray(node.cases, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.SwitchCase:
|
|
this._recurse(node.test, callback, state);
|
|
this._recurseArray(node.consequent, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ConditionalExpression:
|
|
this._recurse(node.test, callback, state);
|
|
this._recurse(node.consequent, callback, state);
|
|
this._recurse(node.alternate, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.TaggedTemplateExpression:
|
|
this._recurse(node.tag, callback, state);
|
|
this._recurse(node.quasi, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.TemplateLiteral:
|
|
this._recurseArray(node.quasis, callback, state);
|
|
this._recurseArray(node.expressions, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ThrowStatement:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.TryStatement:
|
|
this._recurse(node.block, callback, state);
|
|
this._recurse(node.handler, callback, state);
|
|
this._recurse(node.finalizer, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.UnaryExpression:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.UpdateExpression:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.VariableDeclaration:
|
|
this._recurseArray(node.declarations, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.VariableDeclarator:
|
|
this._recurse(node.id, callback, state);
|
|
this._recurse(node.init, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.WhileStatement:
|
|
this._recurse(node.test, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.WithStatement:
|
|
this._recurse(node.object, callback, state);
|
|
this._recurse(node.body, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.YieldExpression:
|
|
this._recurse(node.argument, callback, state);
|
|
break;
|
|
|
|
// Modules.
|
|
|
|
case WI.ScriptSyntaxTree.NodeType.ExportAllDeclaration:
|
|
this._recurse(node.source, callback, state);
|
|
this._recurseArray(node.assertions, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ExportNamedDeclaration:
|
|
this._recurse(node.declaration, callback, state);
|
|
this._recurseArray(node.specifiers, callback, state);
|
|
this._recurse(node.source, callback, state);
|
|
this._recurseArray(node.assertions, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ExportDefaultDeclaration:
|
|
this._recurse(node.declaration, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ExportSpecifier:
|
|
this._recurse(node.local, callback, state);
|
|
this._recurse(node.exported, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportAttribute:
|
|
this._recurse(node.key, callback, state);
|
|
this._recurse(node.value, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportDeclaration:
|
|
this._recurseArray(node.specifiers, callback, state);
|
|
this._recurse(node.source, callback, state);
|
|
this._recurseArray(node.assertions, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportDefaultSpecifier:
|
|
this._recurse(node.local, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportExpression:
|
|
this._recurse(node.source, callback, state);
|
|
this._recurse(node.attributes, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportNamespaceSpecifier:
|
|
this._recurse(node.local, callback, state);
|
|
break;
|
|
case WI.ScriptSyntaxTree.NodeType.ImportSpecifier:
|
|
this._recurse(node.imported, callback, state);
|
|
this._recurse(node.local, callback, state);
|
|
break;
|
|
|
|
// All the leaf nodes go here.
|
|
case WI.ScriptSyntaxTree.NodeType.DebuggerStatement:
|
|
case WI.ScriptSyntaxTree.NodeType.EmptyStatement:
|
|
case WI.ScriptSyntaxTree.NodeType.Identifier:
|
|
case WI.ScriptSyntaxTree.NodeType.Literal:
|
|
case WI.ScriptSyntaxTree.NodeType.MetaProperty:
|
|
case WI.ScriptSyntaxTree.NodeType.Super:
|
|
case WI.ScriptSyntaxTree.NodeType.ThisExpression:
|
|
case WI.ScriptSyntaxTree.NodeType.TemplateElement:
|
|
break;
|
|
}
|
|
|
|
state.skipChildNodes = false;
|
|
}
|
|
|
|
_recurseArray(array, callback, state)
|
|
{
|
|
for (var node of array)
|
|
this._recurse(node, callback, state);
|
|
}
|
|
|
|
// This function translates from esprima's Abstract Syntax Tree to ours.
|
|
// Mostly, this is just the identity function. We've added an extra typeProfilingReturnDivot property for functions/methods.
|
|
// Our AST complies with the Mozilla parser API:
|
|
// https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API
|
|
_createInternalSyntaxTree(node)
|
|
{
|
|
if (!node)
|
|
return null;
|
|
|
|
var result = null;
|
|
switch (node.type) {
|
|
case "ArrayExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ArrayExpression,
|
|
elements: node.elements.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ArrayPattern":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ArrayPattern,
|
|
elements: node.elements.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ArrowFunctionExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
params: node.params.map(this._createInternalSyntaxTree, this),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
generator: node.generator,
|
|
expression: node.expression, // Boolean indicating if the body a single expression or a block statement.
|
|
async: node.async,
|
|
typeProfilingReturnDivot: node.range[0]
|
|
};
|
|
break;
|
|
case "AssignmentExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.AssignmentExpression,
|
|
operator: node.operator,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right)
|
|
};
|
|
break;
|
|
case "AssignmentPattern":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.AssignmentPattern,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right),
|
|
};
|
|
break;
|
|
case "AwaitExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.AwaitExpression,
|
|
argument: this._createInternalSyntaxTree(node.argument),
|
|
};
|
|
break;
|
|
case "BlockStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.BlockStatement,
|
|
body: node.body.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "BinaryExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.BinaryExpression,
|
|
operator: node.operator,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right)
|
|
};
|
|
break;
|
|
case "BreakStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.BreakStatement,
|
|
label: this._createInternalSyntaxTree(node.label)
|
|
};
|
|
break;
|
|
case "CallExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.CallExpression,
|
|
callee: this._createInternalSyntaxTree(node.callee),
|
|
arguments: node.arguments.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "CatchClause":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.CatchClause,
|
|
param: this._createInternalSyntaxTree(node.param),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "ChainExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ChainExpression,
|
|
expression: this._createInternalSyntaxTree(node.expression),
|
|
};
|
|
break;
|
|
case "ClassBody":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ClassBody,
|
|
body: node.body.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ClassDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ClassDeclaration,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
superClass: this._createInternalSyntaxTree(node.superClass),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
};
|
|
break;
|
|
case "ClassExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ClassExpression,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
superClass: this._createInternalSyntaxTree(node.superClass),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
};
|
|
break;
|
|
case "ConditionalExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ConditionalExpression,
|
|
test: this._createInternalSyntaxTree(node.test),
|
|
consequent: this._createInternalSyntaxTree(node.consequent),
|
|
alternate: this._createInternalSyntaxTree(node.alternate)
|
|
};
|
|
break;
|
|
case "ContinueStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ContinueStatement,
|
|
label: this._createInternalSyntaxTree(node.label)
|
|
};
|
|
break;
|
|
case "DoWhileStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.DoWhileStatement,
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
test: this._createInternalSyntaxTree(node.test)
|
|
};
|
|
break;
|
|
case "DebuggerStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.DebuggerStatement
|
|
};
|
|
break;
|
|
case "EmptyStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.EmptyStatement
|
|
};
|
|
break;
|
|
case "ExpressionStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ExpressionStatement,
|
|
expression: this._createInternalSyntaxTree(node.expression)
|
|
};
|
|
break;
|
|
case "ForStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ForStatement,
|
|
init: this._createInternalSyntaxTree(node.init),
|
|
test: this._createInternalSyntaxTree(node.test),
|
|
update: this._createInternalSyntaxTree(node.update),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "ForInStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ForInStatement,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "ForOfStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ForOfStatement,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
await: node.await
|
|
};
|
|
break;
|
|
case "FunctionDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.FunctionDeclaration,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
params: node.params.map(this._createInternalSyntaxTree, this),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
generator: node.generator,
|
|
async: node.async,
|
|
typeProfilingReturnDivot: node.range[0]
|
|
};
|
|
break;
|
|
case "FunctionExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.FunctionExpression,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
params: node.params.map(this._createInternalSyntaxTree, this),
|
|
body: this._createInternalSyntaxTree(node.body),
|
|
generator: node.generator,
|
|
async: node.async,
|
|
typeProfilingReturnDivot: node.range[0] // This may be overridden in the Property AST node.
|
|
};
|
|
break;
|
|
case "Identifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.Identifier,
|
|
name: node.name
|
|
};
|
|
break;
|
|
case "IfStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.IfStatement,
|
|
test: this._createInternalSyntaxTree(node.test),
|
|
consequent: this._createInternalSyntaxTree(node.consequent),
|
|
alternate: this._createInternalSyntaxTree(node.alternate)
|
|
};
|
|
break;
|
|
case "Literal":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.Literal,
|
|
value: node.value,
|
|
raw: node.raw
|
|
};
|
|
break;
|
|
case "LabeledStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.LabeledStatement,
|
|
label: this._createInternalSyntaxTree(node.label),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "LogicalExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.LogicalExpression,
|
|
left: this._createInternalSyntaxTree(node.left),
|
|
right: this._createInternalSyntaxTree(node.right),
|
|
operator: node.operator
|
|
};
|
|
break;
|
|
case "MemberExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.MemberExpression,
|
|
object: this._createInternalSyntaxTree(node.object),
|
|
property: this._createInternalSyntaxTree(node.property),
|
|
computed: node.computed
|
|
};
|
|
break;
|
|
case "MetaProperty":
|
|
// i.e: new.target produces {meta: "new", property: "target"}
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.MetaProperty,
|
|
meta: this._createInternalSyntaxTree(node.meta),
|
|
property: this._createInternalSyntaxTree(node.property),
|
|
};
|
|
break;
|
|
case "MethodDefinition":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.MethodDefinition,
|
|
key: this._createInternalSyntaxTree(node.key),
|
|
value: this._createInternalSyntaxTree(node.value),
|
|
computed: node.computed,
|
|
kind: node.kind,
|
|
static: node.static
|
|
};
|
|
result.value.typeProfilingReturnDivot = node.range[0]; // "g" in "get" or "s" in "set" or "[" in "['computed']" or "m" in "methodName".
|
|
break;
|
|
case "NewExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.NewExpression,
|
|
callee: this._createInternalSyntaxTree(node.callee),
|
|
arguments: node.arguments.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ObjectExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ObjectExpression,
|
|
properties: node.properties.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ObjectPattern":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ObjectPattern,
|
|
properties: node.properties.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "PrivateIdentifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.PrivateIdentifier,
|
|
name: node.name,
|
|
};
|
|
break;
|
|
case "Program":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.Program,
|
|
sourceType: node.sourceType,
|
|
body: node.body.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "Property":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.Property,
|
|
key: this._createInternalSyntaxTree(node.key),
|
|
value: this._createInternalSyntaxTree(node.value),
|
|
kind: node.kind,
|
|
method: node.method,
|
|
computed: node.computed
|
|
};
|
|
if (result.kind === "get" || result.kind === "set" || result.method)
|
|
result.value.typeProfilingReturnDivot = node.range[0]; // "g" in "get" or "s" in "set" or "[" in "['computed']" method or "m" in "methodName".
|
|
break;
|
|
case "RestElement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.RestElement,
|
|
argument: this._createInternalSyntaxTree(node.argument)
|
|
};
|
|
break;
|
|
case "ReturnStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ReturnStatement,
|
|
argument: this._createInternalSyntaxTree(node.argument)
|
|
};
|
|
break;
|
|
case "SequenceExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.SequenceExpression,
|
|
expressions: node.expressions.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "SpreadElement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.SpreadElement,
|
|
argument: this._createInternalSyntaxTree(node.argument),
|
|
};
|
|
break;
|
|
case "StaticBlock":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.StaticBlock,
|
|
body: node.body.map(this._createInternalSyntaxTree, this),
|
|
};
|
|
break;
|
|
case "Super":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.Super
|
|
};
|
|
break;
|
|
case "SwitchStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.SwitchStatement,
|
|
discriminant: this._createInternalSyntaxTree(node.discriminant),
|
|
cases: node.cases.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "SwitchCase":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.SwitchCase,
|
|
test: this._createInternalSyntaxTree(node.test),
|
|
consequent: node.consequent.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "TaggedTemplateExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.TaggedTemplateExpression,
|
|
tag: this._createInternalSyntaxTree(node.tag),
|
|
quasi: this._createInternalSyntaxTree(node.quasi)
|
|
};
|
|
break;
|
|
case "TemplateElement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.TemplateElement,
|
|
value: node.value,
|
|
tail: node.tail
|
|
};
|
|
break;
|
|
case "TemplateLiteral":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.TemplateLiteral,
|
|
quasis: node.quasis.map(this._createInternalSyntaxTree, this),
|
|
expressions: node.expressions.map(this._createInternalSyntaxTree, this)
|
|
};
|
|
break;
|
|
case "ThisExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ThisExpression
|
|
};
|
|
break;
|
|
case "ThrowStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ThrowStatement,
|
|
argument: this._createInternalSyntaxTree(node.argument)
|
|
};
|
|
break;
|
|
case "TryStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.TryStatement,
|
|
block: this._createInternalSyntaxTree(node.block),
|
|
handler: this._createInternalSyntaxTree(node.handler),
|
|
finalizer: this._createInternalSyntaxTree(node.finalizer)
|
|
};
|
|
break;
|
|
case "UnaryExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.UnaryExpression,
|
|
operator: node.operator,
|
|
argument: this._createInternalSyntaxTree(node.argument)
|
|
};
|
|
break;
|
|
case "UpdateExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.UpdateExpression,
|
|
operator: node.operator,
|
|
prefix: node.prefix,
|
|
argument: this._createInternalSyntaxTree(node.argument)
|
|
};
|
|
break;
|
|
case "VariableDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.VariableDeclaration,
|
|
declarations: node.declarations.map(this._createInternalSyntaxTree, this),
|
|
kind: node.kind
|
|
};
|
|
break;
|
|
case "VariableDeclarator":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.VariableDeclarator,
|
|
id: this._createInternalSyntaxTree(node.id),
|
|
init: this._createInternalSyntaxTree(node.init)
|
|
};
|
|
break;
|
|
case "WhileStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.WhileStatement,
|
|
test: this._createInternalSyntaxTree(node.test),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "WithStatement":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.WithStatement,
|
|
object: this._createInternalSyntaxTree(node.object),
|
|
body: this._createInternalSyntaxTree(node.body)
|
|
};
|
|
break;
|
|
case "YieldExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.YieldExpression,
|
|
argument: this._createInternalSyntaxTree(node.argument),
|
|
delegate: node.delegate
|
|
};
|
|
break;
|
|
|
|
// Modules.
|
|
|
|
case "ExportAllDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ExportAllDeclaration,
|
|
source: this._createInternalSyntaxTree(node.source),
|
|
assertions: node.assertions?.map(this._createInternalSyntaxTree, this) ?? [],
|
|
};
|
|
break;
|
|
case "ExportNamedDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ExportNamedDeclaration,
|
|
declaration: this._createInternalSyntaxTree(node.declaration),
|
|
specifiers: node.specifiers.map(this._createInternalSyntaxTree, this),
|
|
source: this._createInternalSyntaxTree(node.source),
|
|
assertions: node.assertions?.map(this._createInternalSyntaxTree, this) ?? [],
|
|
};
|
|
break;
|
|
case "ExportDefaultDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ExportDefaultDeclaration,
|
|
declaration: this._createInternalSyntaxTree(node.declaration),
|
|
};
|
|
break;
|
|
case "ExportSpecifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ExportSpecifier,
|
|
local: this._createInternalSyntaxTree(node.local),
|
|
exported: this._createInternalSyntaxTree(node.exported),
|
|
};
|
|
break;
|
|
case "ImportAttribute":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportAttribute,
|
|
key: this._createInternalSyntaxTree(node.key),
|
|
value: this._createInternalSyntaxTree(node.value),
|
|
};
|
|
break;
|
|
case "ImportExpression":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportExpression,
|
|
source: this._createInternalSyntaxTree(node.source),
|
|
attributes: this._createInternalSyntaxTree(node.attributes),
|
|
};
|
|
break;
|
|
case "ImportDeclaration":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportDeclaration,
|
|
specifiers: node.specifiers.map(this._createInternalSyntaxTree, this),
|
|
source: this._createInternalSyntaxTree(node.source),
|
|
assertions: node.assertions?.map(this._createInternalSyntaxTree, this) ?? [],
|
|
};
|
|
break;
|
|
case "ImportDefaultSpecifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportDefaultSpecifier,
|
|
local: this._createInternalSyntaxTree(node.local),
|
|
};
|
|
break;
|
|
case "ImportNamespaceSpecifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportNamespaceSpecifier,
|
|
local: this._createInternalSyntaxTree(node.local),
|
|
};
|
|
break;
|
|
case "ImportSpecifier":
|
|
result = {
|
|
type: WI.ScriptSyntaxTree.NodeType.ImportSpecifier,
|
|
imported: this._createInternalSyntaxTree(node.imported),
|
|
local: this._createInternalSyntaxTree(node.local),
|
|
};
|
|
break;
|
|
|
|
default:
|
|
console.error("Unsupported Syntax Tree Node: " + node.type, JSON.stringify(node));
|
|
return null;
|
|
}
|
|
|
|
console.assert(node.loc || node.type === "ChainExpression");
|
|
if (node.loc) {
|
|
let {start, end} = node.loc;
|
|
result.startPosition = new WI.SourceCodePosition(start.line - 1, start.column);
|
|
result.endPosition = new WI.SourceCodePosition(end.line - 1, end.column);
|
|
}
|
|
|
|
result.range = node.range;
|
|
// This is an object for which you can add fields to an AST node without worrying about polluting the syntax-related fields of the node.
|
|
result.attachments = {};
|
|
|
|
return result;
|
|
}
|
|
};
|
|
|
|
// This should be kept in sync with an enum in JavaSciptCore/runtime/TypeProfiler.h
|
|
WI.ScriptSyntaxTree.TypeProfilerSearchDescriptor = {
|
|
NormalExpression: 1,
|
|
FunctionReturn: 2
|
|
};
|
|
|
|
WI.ScriptSyntaxTree.NodeType = {
|
|
ArrayExpression: Symbol("array-expression"),
|
|
ArrayPattern: Symbol("array-pattern"),
|
|
ArrowFunctionExpression: Symbol("arrow-function-expression"),
|
|
AssignmentExpression: Symbol("assignment-expression"),
|
|
AssignmentPattern: Symbol("assignment-pattern"),
|
|
AwaitExpression: Symbol("await-expression"),
|
|
BinaryExpression: Symbol("binary-expression"),
|
|
BlockStatement: Symbol("block-statement"),
|
|
BreakStatement: Symbol("break-statement"),
|
|
CallExpression: Symbol("call-expression"),
|
|
CatchClause: Symbol("catch-clause"),
|
|
ChainExpression: Symbol("chain-expression"),
|
|
ClassBody: Symbol("class-body"),
|
|
ClassDeclaration: Symbol("class-declaration"),
|
|
ClassExpression: Symbol("class-expression"),
|
|
ConditionalExpression: Symbol("conditional-expression"),
|
|
ContinueStatement: Symbol("continue-statement"),
|
|
DebuggerStatement: Symbol("debugger-statement"),
|
|
DoWhileStatement: Symbol("do-while-statement"),
|
|
EmptyStatement: Symbol("empty-statement"),
|
|
ExportAllDeclaration: Symbol("export-all-declaration"),
|
|
ExportDefaultDeclaration: Symbol("export-default-declaration"),
|
|
ExportNamedDeclaration: Symbol("export-named-declaration"),
|
|
ExportSpecifier: Symbol("export-specifier"),
|
|
ExpressionStatement: Symbol("expression-statement"),
|
|
ForInStatement: Symbol("for-in-statement"),
|
|
ForOfStatement: Symbol("for-of-statement"),
|
|
ForStatement: Symbol("for-statement"),
|
|
FunctionDeclaration: Symbol("function-declaration"),
|
|
FunctionExpression: Symbol("function-expression"),
|
|
Identifier: Symbol("identifier"),
|
|
IfStatement: Symbol("if-statement"),
|
|
ImportAttribute: Symbol("import-attribute"),
|
|
ImportDeclaration: Symbol("import-declaration"),
|
|
ImportDefaultSpecifier: Symbol("import-default-specifier"),
|
|
ImportExpression: Symbol("import-expression"),
|
|
ImportNamespaceSpecifier: Symbol("import-namespace-specifier"),
|
|
ImportSpecifier: Symbol("import-specifier"),
|
|
LabeledStatement: Symbol("labeled-statement"),
|
|
Literal: Symbol("literal"),
|
|
LogicalExpression: Symbol("logical-expression"),
|
|
MemberExpression: Symbol("member-expression"),
|
|
MetaProperty: Symbol("meta-property"),
|
|
MethodDefinition: Symbol("method-definition"),
|
|
NewExpression: Symbol("new-expression"),
|
|
ObjectExpression: Symbol("object-expression"),
|
|
ObjectPattern: Symbol("object-pattern"),
|
|
PrivateIdentifier: Symbol("private-identifier"),
|
|
Program: Symbol("program"),
|
|
Property: Symbol("property"),
|
|
RestElement: Symbol("rest-element"),
|
|
ReturnStatement: Symbol("return-statement"),
|
|
SequenceExpression: Symbol("sequence-expression"),
|
|
SpreadElement: Symbol("spread-element"),
|
|
StaticBlock: Symbol("static-block"),
|
|
Super: Symbol("super"),
|
|
SwitchCase: Symbol("switch-case"),
|
|
SwitchStatement: Symbol("switch-statement"),
|
|
TaggedTemplateExpression: Symbol("tagged-template-expression"),
|
|
TemplateElement: Symbol("template-element"),
|
|
TemplateLiteral: Symbol("template-literal"),
|
|
ThisExpression: Symbol("this-expression"),
|
|
ThrowStatement: Symbol("throw-statement"),
|
|
TryStatement: Symbol("try-statement"),
|
|
UnaryExpression: Symbol("unary-expression"),
|
|
UpdateExpression: Symbol("update-expression"),
|
|
VariableDeclaration: Symbol("variable-declaration"),
|
|
VariableDeclarator: Symbol("variable-declarator"),
|
|
WhileStatement: Symbol("while-statement"),
|
|
WithStatement: Symbol("with-statement"),
|
|
YieldExpression: Symbol("yield-expression"),
|
|
};
|
|
|
|
/* Models/ScriptTimelineRecord.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ScriptTimelineRecord = class ScriptTimelineRecord extends WI.TimelineRecord
|
|
{
|
|
constructor(eventType, startTime, endTime, stackTrace, sourceCodeLocation, details, profilePayload, extraDetails)
|
|
{
|
|
super(WI.TimelineRecord.Type.Script, startTime, endTime, stackTrace, sourceCodeLocation);
|
|
|
|
console.assert(eventType);
|
|
|
|
if (eventType in WI.ScriptTimelineRecord.EventType)
|
|
eventType = WI.ScriptTimelineRecord.EventType[eventType];
|
|
|
|
this._eventType = eventType;
|
|
this._details = details || "";
|
|
this._profilePayload = profilePayload || null;
|
|
this._profile = null;
|
|
this._extraDetails = extraDetails || null;
|
|
|
|
// NOTE: _callCountOrSamples is being treated as the number of samples.
|
|
this._callCountOrSamples = 0;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async fromJSON(json)
|
|
{
|
|
let {eventType, startTime, endTime, stackTrace, sourceCodeLocation, details, profilePayload, extraDetails} = json;
|
|
|
|
if (typeof details === "object" && details.__type === "GarbageCollection")
|
|
details = WI.GarbageCollection.fromJSON(details);
|
|
|
|
return new WI.ScriptTimelineRecord(eventType, startTime, endTime, stackTrace, sourceCodeLocation, details, profilePayload, extraDetails);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
// FIXME: stackTrace
|
|
// FIXME: sourceCodeLocation
|
|
// FIXME: profilePayload
|
|
|
|
return {
|
|
type: this.type,
|
|
eventType: this._eventType,
|
|
startTime: this.startTime,
|
|
endTime: this.endTime,
|
|
details: this._details,
|
|
extraDetails: this._extraDetails,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get eventType() { return this._eventType; }
|
|
get details() { return this._details; }
|
|
get extraDetails() { return this._extraDetails; }
|
|
get callCountOrSamples() { return this._callCountOrSamples; }
|
|
|
|
get profile()
|
|
{
|
|
this._initializeProfileFromPayload();
|
|
return this._profile;
|
|
}
|
|
|
|
isGarbageCollection()
|
|
{
|
|
return this._eventType === WI.ScriptTimelineRecord.EventType.GarbageCollected;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
super.saveIdentityToCookie(cookie);
|
|
|
|
cookie[WI.ScriptTimelineRecord.EventTypeCookieKey] = this._eventType;
|
|
cookie[WI.ScriptTimelineRecord.DetailsCookieKey] = this._details;
|
|
}
|
|
|
|
get profilePayload()
|
|
{
|
|
return this._profilePayload;
|
|
}
|
|
|
|
set profilePayload(payload)
|
|
{
|
|
this._profilePayload = payload;
|
|
}
|
|
|
|
// Private
|
|
|
|
_initializeProfileFromPayload(payload)
|
|
{
|
|
if (this._profile || !this._profilePayload)
|
|
return;
|
|
|
|
var payload = this._profilePayload;
|
|
this._profilePayload = undefined;
|
|
|
|
console.assert(payload.rootNodes instanceof Array);
|
|
|
|
function profileNodeFromPayload(nodePayload)
|
|
{
|
|
console.assert("id" in nodePayload);
|
|
|
|
if (nodePayload.url) {
|
|
let sourceCode = WI.networkManager.resourcesForURL(nodePayload.url).firstValue;
|
|
if (!sourceCode)
|
|
sourceCode = WI.debuggerManager.scriptsForURL(nodePayload.url, WI.assumingMainTarget())[0];
|
|
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
var lineNumber = nodePayload.lineNumber - 1;
|
|
|
|
var sourceCodeLocation = sourceCode ? sourceCode.createLazySourceCodeLocation(lineNumber, nodePayload.columnNumber) : null;
|
|
}
|
|
|
|
var isProgramCode = nodePayload.functionName === "(program)";
|
|
var isAnonymousFunction = nodePayload.functionName === "(anonymous function)";
|
|
|
|
var type = isProgramCode ? WI.ProfileNode.Type.Program : WI.ProfileNode.Type.Function;
|
|
var functionName = !isProgramCode && !isAnonymousFunction && nodePayload.functionName !== "(unknown)" ? nodePayload.functionName : null;
|
|
|
|
return new WI.ProfileNode(nodePayload.id, type, functionName, sourceCodeLocation, nodePayload.callInfo, nodePayload.children);
|
|
}
|
|
|
|
function profileNodeCallFromPayload(nodeCallPayload)
|
|
{
|
|
console.assert("startTime" in nodeCallPayload);
|
|
console.assert("totalTime" in nodeCallPayload);
|
|
|
|
var startTime = WI.timelineManager.computeElapsedTime(nodeCallPayload.startTime);
|
|
|
|
return new WI.ProfileNodeCall(startTime, nodeCallPayload.totalTime);
|
|
}
|
|
|
|
var rootNodes = payload.rootNodes;
|
|
|
|
// Iterate over the node tree using a stack. Doing this recursively can easily cause a stack overflow.
|
|
// We traverse the profile in post-order and convert the payloads in place until we get back to the root.
|
|
var stack = [{parent: {children: rootNodes}, index: 0, root: true}];
|
|
while (stack.length) {
|
|
var entry = stack.lastValue;
|
|
|
|
if (entry.index < entry.parent.children.length) {
|
|
var childNodePayload = entry.parent.children[entry.index];
|
|
if (childNodePayload.children && childNodePayload.children.length)
|
|
stack.push({parent: childNodePayload, index: 0});
|
|
|
|
++entry.index;
|
|
} else {
|
|
if (!entry.root)
|
|
entry.parent.children = entry.parent.children.map(profileNodeFromPayload);
|
|
else
|
|
rootNodes = rootNodes.map(profileNodeFromPayload);
|
|
|
|
stack.pop();
|
|
}
|
|
}
|
|
|
|
for (let rootNode of rootNodes)
|
|
this._callCountOrSamples += rootNode.callInfo.callCount;
|
|
|
|
this._profile = new WI.Profile(rootNodes);
|
|
}
|
|
};
|
|
|
|
WI.ScriptTimelineRecord.EventType = {
|
|
ScriptEvaluated: "script-evaluated",
|
|
APIScriptEvaluated: "api-script-evaluated",
|
|
MicrotaskDispatched: "microtask-dispatched",
|
|
EventDispatched: "event-dispatched",
|
|
ProbeSampleRecorded: "probe-sample-recorded",
|
|
TimerFired: "timer-fired",
|
|
TimerInstalled: "timer-installed",
|
|
TimerRemoved: "timer-removed",
|
|
AnimationFrameFired: "animation-frame-fired",
|
|
AnimationFrameRequested: "animation-frame-requested",
|
|
AnimationFrameCanceled: "animation-frame-canceled",
|
|
ObserverCallback: "observer-callback",
|
|
ConsoleProfileRecorded: "console-profile-recorded",
|
|
GarbageCollected: "garbage-collected",
|
|
};
|
|
|
|
WI.ScriptTimelineRecord.EventType.displayName = function(eventType, details, includeDetailsInMainTitle)
|
|
{
|
|
if (details && !WI.ScriptTimelineRecord._eventDisplayNames) {
|
|
// These display names are not localized because they closely represent
|
|
// the real API name, just with word spaces and Title Case.
|
|
|
|
var nameMap = new Map;
|
|
nameMap.set("DOMActivate", "DOM Activate");
|
|
nameMap.set("DOMCharacterDataModified", "DOM Character Data Modified");
|
|
nameMap.set("DOMContentLoaded", "DOM Content Loaded");
|
|
nameMap.set("DOMNodeInserted", "DOM Node Inserted");
|
|
nameMap.set("DOMNodeInsertedIntoDocument", "DOM Node Inserted Into Document");
|
|
nameMap.set("DOMNodeRemoved", "DOM Node Removed");
|
|
nameMap.set("DOMNodeRemovedFromDocument", "DOM Node Removed From Document");
|
|
nameMap.set("DOMSubtreeModified", "DOM Sub-Tree Modified");
|
|
nameMap.set("addsourcebuffer", "Add Source Buffer");
|
|
nameMap.set("addstream", "Add Stream");
|
|
nameMap.set("addtrack", "Add Track");
|
|
nameMap.set("animationcancel", "Animation Cancel");
|
|
nameMap.set("animationend", "Animation End");
|
|
nameMap.set("animationiteration", "Animation Iteration");
|
|
nameMap.set("animationstart", "Animation Start");
|
|
nameMap.set("audioend", "Audio End");
|
|
nameMap.set("audioprocess", "Audio Process");
|
|
nameMap.set("audiostart", "Audio Start");
|
|
nameMap.set("beforecopy", "Before Copy");
|
|
nameMap.set("beforecut", "Before Cut");
|
|
nameMap.set("beforeload", "Before Load");
|
|
nameMap.set("beforepaste", "Before Paste");
|
|
nameMap.set("beforeunload", "Before Unload");
|
|
nameMap.set("cancel", "Animation Cancel");
|
|
nameMap.set("canplay", "Can Play");
|
|
nameMap.set("canplaythrough", "Can Play Through");
|
|
nameMap.set("chargingchange", "Charging Change");
|
|
nameMap.set("chargingtimechange", "Charging Time Change");
|
|
nameMap.set("compositionend", "Composition End");
|
|
nameMap.set("compositionstart", "Composition Start");
|
|
nameMap.set("compositionupdate", "Composition Update");
|
|
nameMap.set("contextmenu", "Context Menu");
|
|
nameMap.set("cuechange", "Cue Change");
|
|
nameMap.set("datachannel", "Data Channel");
|
|
nameMap.set("dblclick", "Double Click");
|
|
nameMap.set("devicemotion", "Device Motion");
|
|
nameMap.set("deviceorientation", "Device Orientation");
|
|
nameMap.set("dischargingtimechange", "Discharging Time Change");
|
|
nameMap.set("dragend", "Drag End");
|
|
nameMap.set("dragenter", "Drag Enter");
|
|
nameMap.set("dragleave", "Drag Leave");
|
|
nameMap.set("dragover", "Drag Over");
|
|
nameMap.set("dragstart", "Drag Start");
|
|
nameMap.set("durationchange", "Duration Change");
|
|
nameMap.set("finish", "Animation Finish");
|
|
nameMap.set("focusin", "Focus In");
|
|
nameMap.set("focusout", "Focus Out");
|
|
nameMap.set("formdata", "Form submission or invocation of FormData()");
|
|
nameMap.set("gesturechange", "Gesture Change");
|
|
nameMap.set("gestureend", "Gesture End");
|
|
nameMap.set("gesturescrollend", "Gesture Scroll End");
|
|
nameMap.set("gesturescrollstart", "Gesture Scroll Start");
|
|
nameMap.set("gesturescrollupdate", "Gesture Scroll Update");
|
|
nameMap.set("gesturestart", "Gesture Start");
|
|
nameMap.set("gesturetap", "Gesture Tap");
|
|
nameMap.set("gesturetapdown", "Gesture Tap Down");
|
|
nameMap.set("hashchange", "Hash Change");
|
|
nameMap.set("icecandidate", "ICE Candidate");
|
|
nameMap.set("iceconnectionstatechange", "ICE Connection State Change");
|
|
nameMap.set("keydown", "Key Down");
|
|
nameMap.set("keypress", "Key Press");
|
|
nameMap.set("keyup", "Key Up");
|
|
nameMap.set("levelchange", "Level Change");
|
|
nameMap.set("loadeddata", "Loaded Data");
|
|
nameMap.set("loadedmetadata", "Loaded Metadata");
|
|
nameMap.set("loadend", "Load End");
|
|
nameMap.set("loadingdone", "Loading Done");
|
|
nameMap.set("loadstart", "Load Start");
|
|
nameMap.set("mousedown", "Mouse Down");
|
|
nameMap.set("mouseenter", "Mouse Enter");
|
|
nameMap.set("mouseleave", "Mouse Leave");
|
|
nameMap.set("mousemove", "Mouse Move");
|
|
nameMap.set("mouseout", "Mouse Out");
|
|
nameMap.set("mouseover", "Mouse Over");
|
|
nameMap.set("mouseup", "Mouse Up");
|
|
nameMap.set("mousewheel", "Mouse Wheel");
|
|
nameMap.set("negotiationneeded", "Negotiation Needed");
|
|
nameMap.set("nomatch", "No Match");
|
|
nameMap.set("noupdate", "No Update");
|
|
nameMap.set("orientationchange", "Orientation Change");
|
|
nameMap.set("overflowchanged", "Overflow Changed");
|
|
nameMap.set("pagehide", "Page Hide");
|
|
nameMap.set("pageshow", "Page Show");
|
|
nameMap.set("popstate", "Pop State");
|
|
nameMap.set("ratechange", "Rate Change");
|
|
nameMap.set("readystatechange", "Ready State Change");
|
|
nameMap.set("remove", "Animation Remove");
|
|
nameMap.set("removesourcebuffer", "Remove Source Buffer");
|
|
nameMap.set("removestream", "Remove Stream");
|
|
nameMap.set("removetrack", "Remove Track");
|
|
nameMap.set("resize", "Resize");
|
|
nameMap.set("securitypolicyviolation", "Security Policy Violation");
|
|
nameMap.set("selectionchange", "Selection Change");
|
|
nameMap.set("selectstart", "Select Start");
|
|
nameMap.set("signalingstatechange", "Signaling State Change");
|
|
nameMap.set("soundend", "Sound End");
|
|
nameMap.set("soundstart", "Sound Start");
|
|
nameMap.set("sourceclose", "Source Close");
|
|
nameMap.set("sourceended", "Source Ended");
|
|
nameMap.set("sourceopen", "Source Open");
|
|
nameMap.set("speechend", "Speech End");
|
|
nameMap.set("speechstart", "Speech Start");
|
|
nameMap.set("textInput", "Text Input");
|
|
nameMap.set("timeupdate", "Time Update");
|
|
nameMap.set("tonechange", "Tone Change");
|
|
nameMap.set("touchcancel", "Touch Cancel");
|
|
nameMap.set("touchend", "Touch End");
|
|
nameMap.set("touchmove", "Touch Move");
|
|
nameMap.set("touchstart", "Touch Start");
|
|
nameMap.set("transitioncancel", "Transition Cancel");
|
|
nameMap.set("transitionend", "Transition End");
|
|
nameMap.set("transitionrun", "Transition Run");
|
|
nameMap.set("transitionstart", "Transition Start");
|
|
nameMap.set("updateend", "Update End");
|
|
nameMap.set("updateready", "Update Ready");
|
|
nameMap.set("updatestart", "Update Start");
|
|
nameMap.set("upgradeneeded", "Upgrade Needed");
|
|
nameMap.set("versionchange", "Version Change");
|
|
nameMap.set("visibilitychange", "Visibility Change");
|
|
nameMap.set("volumechange", "Volume Change");
|
|
nameMap.set("webglcontextcreationerror", "WebGL Context Creation Error");
|
|
nameMap.set("webglcontextlost", "WebGL Context Lost");
|
|
nameMap.set("webglcontextrestored", "WebGL Context Restored");
|
|
nameMap.set("webkitAnimationEnd", "Animation End");
|
|
nameMap.set("webkitAnimationIteration", "Animation Iteration");
|
|
nameMap.set("webkitAnimationStart", "Animation Start");
|
|
nameMap.set("webkitBeforeTextInserted", "Before Text Inserted");
|
|
nameMap.set("webkitEditableContentChanged", "Editable Content Changed");
|
|
nameMap.set("webkitTransitionEnd", "Transition End");
|
|
nameMap.set("webkitaddsourcebuffer", "Add Source Buffer");
|
|
nameMap.set("webkitbeginfullscreen", "Begin Full-Screen");
|
|
nameMap.set("webkitcurrentplaybacktargetiswirelesschanged", "Current Playback Target Is Wireless Changed");
|
|
nameMap.set("webkitendfullscreen", "End Full-Screen");
|
|
nameMap.set("webkitfullscreenchange", "Full-Screen Change");
|
|
nameMap.set("webkitfullscreenerror", "Full-Screen Error");
|
|
nameMap.set("webkitkeyadded", "Key Added");
|
|
nameMap.set("webkitkeyerror", "Key Error");
|
|
nameMap.set("webkitkeymessage", "Key Message");
|
|
nameMap.set("webkitneedkey", "Need Key");
|
|
nameMap.set("webkitnetworkinfochange", "Network Info Change");
|
|
nameMap.set("webkitplaybacktargetavailabilitychanged", "Playback Target Availability Changed");
|
|
nameMap.set("webkitpointerlockchange", "Pointer Lock Change");
|
|
nameMap.set("webkitpointerlockerror", "Pointer Lock Error");
|
|
nameMap.set("webkitremovesourcebuffer", "Remove Source Buffer");
|
|
nameMap.set("webkitresourcetimingbufferfull", "Resource Timing Buffer Full");
|
|
nameMap.set("webkitsourceclose", "Source Close");
|
|
nameMap.set("webkitsourceended", "Source Ended");
|
|
nameMap.set("webkitsourceopen", "Source Open");
|
|
nameMap.set("writeend", "Write End");
|
|
nameMap.set("writestart", "Write Start");
|
|
|
|
WI.ScriptTimelineRecord._eventDisplayNames = nameMap;
|
|
}
|
|
|
|
switch (eventType) {
|
|
case WI.ScriptTimelineRecord.EventType.ScriptEvaluated:
|
|
case WI.ScriptTimelineRecord.EventType.APIScriptEvaluated:
|
|
return WI.UIString("Script Evaluated");
|
|
case WI.ScriptTimelineRecord.EventType.MicrotaskDispatched:
|
|
return WI.UIString("Microtask Dispatched");
|
|
case WI.ScriptTimelineRecord.EventType.EventDispatched:
|
|
if (details && (details instanceof String || typeof details === "string")) {
|
|
var eventDisplayName = WI.ScriptTimelineRecord._eventDisplayNames.get(details) || details.capitalize();
|
|
return WI.UIString("%s Event Dispatched").format(eventDisplayName);
|
|
}
|
|
return WI.UIString("Event Dispatched");
|
|
case WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded:
|
|
return WI.UIString("Probe Sample Recorded");
|
|
case WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded:
|
|
if (details && (details instanceof String || typeof details === "string"))
|
|
return WI.UIString("\u201C%s\u201D Profile Recorded").format(details);
|
|
return WI.UIString("Console Profile Recorded");
|
|
case WI.ScriptTimelineRecord.EventType.GarbageCollected:
|
|
console.assert(details);
|
|
if (details && (details instanceof WI.GarbageCollection) && includeDetailsInMainTitle) {
|
|
switch (details.type) {
|
|
case WI.GarbageCollection.Type.Partial:
|
|
return WI.UIString("Partial Garbage Collection");
|
|
case WI.GarbageCollection.Type.Full:
|
|
return WI.UIString("Full Garbage Collection");
|
|
}
|
|
}
|
|
return WI.UIString("Garbage Collection");
|
|
case WI.ScriptTimelineRecord.EventType.TimerFired:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Timer %d Fired").format(details);
|
|
return WI.UIString("Timer Fired");
|
|
case WI.ScriptTimelineRecord.EventType.TimerInstalled:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Timer %d Installed").format(details.timerId);
|
|
return WI.UIString("Timer Installed");
|
|
case WI.ScriptTimelineRecord.EventType.TimerRemoved:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Timer %d Removed").format(details);
|
|
return WI.UIString("Timer Removed");
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameFired:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Animation Frame %d Fired").format(details);
|
|
return WI.UIString("Animation Frame Fired");
|
|
case WI.ScriptTimelineRecord.EventType.ObserverCallback:
|
|
if (details && (details instanceof String || typeof details === "string"))
|
|
return WI.UIString("%s Callback").format(details);
|
|
return WI.UIString("Observer Callback");
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameRequested:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Animation Frame %d Requested").format(details);
|
|
return WI.UIString("Animation Frame Requested");
|
|
case WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled:
|
|
if (details && includeDetailsInMainTitle)
|
|
return WI.UIString("Animation Frame %d Canceled").format(details);
|
|
return WI.UIString("Animation Frame Canceled");
|
|
}
|
|
};
|
|
|
|
WI.ScriptTimelineRecord.TypeIdentifier = "script-timeline-record";
|
|
WI.ScriptTimelineRecord.EventTypeCookieKey = "script-timeline-record-event-type";
|
|
WI.ScriptTimelineRecord.DetailsCookieKey = "script-timeline-record-details";
|
|
|
|
/* Models/ServerTimingEntry.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
* Copyright 2017 The Chromium Authors. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ServerTimingEntry = class ServerTimingEntry
|
|
{
|
|
constructor(name)
|
|
{
|
|
this._name = name;
|
|
this._duration = undefined;
|
|
this._description = undefined;
|
|
}
|
|
|
|
// Static
|
|
|
|
static parseHeaders(valueString = "")
|
|
{
|
|
// https://w3c.github.io/server-timing/#the-server-timing-header-field
|
|
function trimLeadingWhiteSpace() {
|
|
valueString = valueString.trimStart();
|
|
}
|
|
|
|
function consumeDelimiter(char) {
|
|
console.assert(char.length === 1);
|
|
trimLeadingWhiteSpace();
|
|
if (valueString.charAt(0) !== char)
|
|
return false;
|
|
|
|
valueString = valueString.substring(1);
|
|
return true;
|
|
}
|
|
|
|
function consumeToken() {
|
|
// https://tools.ietf.org/html/rfc7230#appendix-B
|
|
let result = /^(?:\s*)([\w!#$%&'*+\-.^`|~]+)(?:\s*)(.*)/.exec(valueString);
|
|
if (!result)
|
|
return null;
|
|
|
|
valueString = result[2];
|
|
return result[1];
|
|
}
|
|
|
|
function consumeTokenOrQuotedString() {
|
|
trimLeadingWhiteSpace();
|
|
if (valueString.charAt(0) === "\"")
|
|
return consumeQuotedString();
|
|
|
|
return consumeToken();
|
|
}
|
|
|
|
function consumeQuotedString() {
|
|
// https://tools.ietf.org/html/rfc7230#section-3.2.6
|
|
console.assert(valueString.charAt(0) === "\"");
|
|
|
|
// Consume the leading DQUOTE.
|
|
valueString = valueString.substring(1);
|
|
|
|
let unescapedValueString = "";
|
|
for (let i = 0; i < valueString.length; ++i) {
|
|
let char = valueString.charAt(i);
|
|
switch (char) {
|
|
case "\\":
|
|
// Backslash character found, ignore it.
|
|
++i;
|
|
if (i < valueString.length) {
|
|
// Take the character after the backslash.
|
|
unescapedValueString += valueString.charAt(i);
|
|
}
|
|
break;
|
|
case "\"":
|
|
// Trailing DQUOTE.
|
|
valueString = valueString.substring(i + 1);
|
|
return unescapedValueString;
|
|
default:
|
|
unescapedValueString += char;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// No trailing DQUOTE found, this was not a valid quoted-string. Consume the entire string to complete parsing.
|
|
valueString = "";
|
|
return null;
|
|
}
|
|
|
|
function consumeExtraneous() {
|
|
let result = /([,;].*)/.exec(valueString);
|
|
if (result)
|
|
valueString = result[1];
|
|
}
|
|
|
|
function getParserForParameter(paramName) {
|
|
switch (paramName) {
|
|
case "dur":
|
|
return function(paramValue, entry) {
|
|
if (paramValue !== null) {
|
|
let duration = parseFloat(paramValue);
|
|
if (!isNaN(duration)) {
|
|
entry.duration = duration;
|
|
return;
|
|
}
|
|
}
|
|
entry.duration = 0;
|
|
};
|
|
|
|
case "desc":
|
|
return function(paramValue, entry) {
|
|
entry.description = paramValue || "";
|
|
};
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
let entries = [];
|
|
let name;
|
|
while ((name = consumeToken()) !== null) {
|
|
let entry = new WI.ServerTimingEntry(name);
|
|
|
|
while (consumeDelimiter(";")) {
|
|
let paramName;
|
|
if ((paramName = consumeToken()) === null)
|
|
continue;
|
|
|
|
paramName = paramName.toLowerCase();
|
|
let parseParameter = getParserForParameter(paramName);
|
|
let paramValue = null;
|
|
if (consumeDelimiter("=")) {
|
|
// Always parse the value, even if we don't recognize the parameter name.
|
|
paramValue = consumeTokenOrQuotedString();
|
|
consumeExtraneous();
|
|
}
|
|
|
|
if (parseParameter)
|
|
parseParameter(paramValue, entry);
|
|
else
|
|
console.warn("Unknown Server-Timing parameter:", paramName, paramValue);
|
|
}
|
|
|
|
entries.push(entry);
|
|
if (!consumeDelimiter(","))
|
|
break;
|
|
}
|
|
|
|
return entries;
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get duration() { return this._duration; }
|
|
get description() { return this._description; }
|
|
|
|
set duration(duration)
|
|
{
|
|
if (this._duration !== undefined) {
|
|
console.warn("Ignoring redundant duration.");
|
|
return;
|
|
}
|
|
|
|
this._duration = duration;
|
|
}
|
|
|
|
set description(description)
|
|
{
|
|
if (this._description !== undefined) {
|
|
console.warn("Ignoring redundant description.");
|
|
return;
|
|
}
|
|
|
|
this._description = description;
|
|
}
|
|
};
|
|
|
|
/* Models/ShaderProgram.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ShaderProgram = class ShaderProgram extends WI.Object
|
|
{
|
|
constructor(target, identifier, programType, canvas, {sharesVertexFragmentShader} = {})
|
|
{
|
|
console.assert(target instanceof WI.Target, target);
|
|
console.assert(target === canvas.target, target, canvas.target);
|
|
console.assert(identifier);
|
|
console.assert(Object.values(ShaderProgram.ProgramType).includes(programType));
|
|
console.assert(canvas instanceof WI.Canvas);
|
|
console.assert(ShaderProgram.contextTypeSupportsProgramType(canvas.contextType, programType));
|
|
|
|
super();
|
|
|
|
this._target = target;
|
|
this._identifier = identifier;
|
|
this._programType = programType;
|
|
this._canvas = canvas;
|
|
|
|
this._sharesVertexFragmentShader = !!sharesVertexFragmentShader;
|
|
console.assert(!this._sharesVertexFragmentShader || (this._canvas.contextType === WI.Canvas.ContextType.WebGPU && this._programType === ShaderProgram.ProgramType.Render));
|
|
|
|
this._disabled = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static contextTypeSupportsProgramType(contextType, programType)
|
|
{
|
|
switch (contextType) {
|
|
case WI.Canvas.ContextType.WebGL:
|
|
case WI.Canvas.ContextType.OffscreenWebGL:
|
|
case WI.Canvas.ContextType.WebGL2:
|
|
case WI.Canvas.ContextType.OffscreenWebGL2:
|
|
return programType === ShaderProgram.ProgramType.Render;
|
|
|
|
case WI.Canvas.ContextType.WebGPU:
|
|
return programType === ShaderProgram.ProgramType.Compute
|
|
|| programType === ShaderProgram.ProgramType.Render;
|
|
}
|
|
|
|
console.assert();
|
|
return false;
|
|
}
|
|
|
|
static programTypeSupportsShaderType(programType, shaderType)
|
|
{
|
|
switch (programType) {
|
|
case ShaderProgram.ProgramType.Compute:
|
|
return shaderType === ShaderProgram.ShaderType.Compute;
|
|
|
|
case ShaderProgram.ProgramType.Render:
|
|
return shaderType === ShaderProgram.ShaderType.Fragment
|
|
|| shaderType === ShaderProgram.ShaderType.Vertex;
|
|
}
|
|
|
|
console.assert();
|
|
return false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get target() { return this._target; }
|
|
get identifier() { return this._identifier; }
|
|
get programType() { return this._programType; }
|
|
get canvas() { return this._canvas; }
|
|
get sharesVertexFragmentShader() { return this._sharesVertexFragmentShader; }
|
|
|
|
get displayName()
|
|
{
|
|
let format = null;
|
|
switch (this._canvas.contextType) {
|
|
case WI.Canvas.ContextType.WebGL:
|
|
case WI.Canvas.ContextType.OffscreenWebGL:
|
|
case WI.Canvas.ContextType.WebGL2:
|
|
case WI.Canvas.ContextType.OffscreenWebGL2:
|
|
format = WI.UIString("Program %d");
|
|
break;
|
|
case WI.Canvas.ContextType.WebGPU:
|
|
switch (this._programType) {
|
|
case ShaderProgram.ProgramType.Compute:
|
|
format = WI.UIString("Compute Pipeline %d");
|
|
break;
|
|
case ShaderProgram.ProgramType.Render:
|
|
format = WI.UIString("Render Pipeline %d");
|
|
break;
|
|
}
|
|
break;
|
|
}
|
|
console.assert(format);
|
|
if (!this._uniqueDisplayNumber)
|
|
this._uniqueDisplayNumber = this._canvas.nextShaderProgramDisplayNumberForProgramType(this._programType);
|
|
return format.format(this._uniqueDisplayNumber);
|
|
}
|
|
|
|
get disabled()
|
|
{
|
|
return this._disabled;
|
|
}
|
|
|
|
set disabled(disabled)
|
|
{
|
|
console.assert(this._programType === ShaderProgram.ProgramType.Render);
|
|
console.assert(this._canvas.isWebGL || this._canvas.isWebGL2);
|
|
|
|
if (this._canvas.contextType === WI.Canvas.ContextType.WebGPU)
|
|
return;
|
|
|
|
if (this._disabled === disabled)
|
|
return;
|
|
|
|
this._disabled = disabled;
|
|
|
|
this._target.CanvasAgent.setShaderProgramDisabled(this._identifier, disabled);
|
|
|
|
this.dispatchEventToListeners(ShaderProgram.Event.DisabledChanged);
|
|
}
|
|
|
|
requestShaderSource(shaderType, callback)
|
|
{
|
|
console.assert(Object.values(ShaderProgram.ShaderType).includes(shaderType));
|
|
console.assert(ShaderProgram.programTypeSupportsShaderType(this._programType, shaderType));
|
|
|
|
// COMPATIBILITY (iOS 13): `content` was renamed to `source`.
|
|
this._target.CanvasAgent.requestShaderSource(this._identifier, shaderType, (error, source) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
callback(source);
|
|
});
|
|
}
|
|
|
|
updateShader(shaderType, source)
|
|
{
|
|
console.assert(Object.values(ShaderProgram.ShaderType).includes(shaderType));
|
|
console.assert(ShaderProgram.programTypeSupportsShaderType(this._programType, shaderType));
|
|
|
|
this._target.CanvasAgent.updateShader(this._identifier, shaderType, source);
|
|
}
|
|
|
|
showHighlight()
|
|
{
|
|
console.assert(this._programType === ShaderProgram.ProgramType.Render);
|
|
console.assert(this._canvas.isWebGL || this._canvas.isWebGL2);
|
|
|
|
this._target.CanvasAgent.setShaderProgramHighlighted(this._identifier, true);
|
|
}
|
|
|
|
hideHighlight()
|
|
{
|
|
console.assert(this._programType === ShaderProgram.ProgramType.Render);
|
|
console.assert(this._canvas.isWebGL || this._canvas.isWebGL2);
|
|
|
|
this._target.CanvasAgent.setShaderProgramHighlighted(this._identifier, false);
|
|
}
|
|
};
|
|
|
|
WI.ShaderProgram.ProgramType = {
|
|
Compute: "compute",
|
|
Render: "render",
|
|
};
|
|
|
|
WI.ShaderProgram.ShaderType = {
|
|
Compute: "compute",
|
|
Fragment: "fragment",
|
|
Vertex: "vertex",
|
|
};
|
|
|
|
WI.ShaderProgram.Event = {
|
|
DisabledChanged: "shader-program-disabled-changed",
|
|
};
|
|
|
|
/* Models/SourceCodeRevision.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceCodeRevision = class SourceCodeRevision extends WI.Revision
|
|
{
|
|
constructor(sourceCode, content, base64Encoded, mimeType)
|
|
{
|
|
super();
|
|
|
|
console.assert(sourceCode instanceof WI.SourceCode);
|
|
console.assert(content === undefined || typeof content === "string");
|
|
console.assert(base64Encoded === undefined || typeof base64Encoded === "boolean");
|
|
console.assert(mimeType === undefined || typeof mimeType === "string");
|
|
|
|
this._sourceCode = sourceCode;
|
|
|
|
this._content = content || "";
|
|
this._base64Encoded = !!base64Encoded;
|
|
this._mimeType = mimeType;
|
|
this._blobContent = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceCode() { return this._sourceCode; }
|
|
get content() { return this._content; }
|
|
get base64Encoded() { return this._base64Encoded; }
|
|
get mimeType() { return this._mimeType; }
|
|
|
|
get blobContent()
|
|
{
|
|
if (!this._blobContent && this._content)
|
|
this._blobContent = WI.BlobUtilities.blobForContent(this._content, this._base64Encoded, this._mimeType);
|
|
|
|
console.assert(!this._blobContent || this._blobContent instanceof Blob);
|
|
return this._blobContent;
|
|
}
|
|
|
|
updateRevisionContent(content, {base64Encoded, mimeType, blobContent} = {})
|
|
{
|
|
console.assert(content === undefined || typeof content === "string");
|
|
this._content = content || "";
|
|
|
|
if (base64Encoded !== undefined) {
|
|
console.assert(typeof base64Encoded === "boolean");
|
|
this._base64Encoded = !!base64Encoded;
|
|
}
|
|
|
|
if (mimeType !== undefined) {
|
|
console.assert(typeof mimeType === "string");
|
|
this._mimeType = mimeType;
|
|
}
|
|
|
|
console.assert(!blobContent || blobContent instanceof Blob);
|
|
this._blobContent = blobContent !== undefined ? blobContent : null;
|
|
|
|
this._sourceCode.revisionContentDidChange(this);
|
|
}
|
|
|
|
apply()
|
|
{
|
|
this._sourceCode.currentRevision = this;
|
|
}
|
|
|
|
revert()
|
|
{
|
|
this._sourceCode.currentRevision = this._sourceCode.originalRevision;
|
|
}
|
|
|
|
copy()
|
|
{
|
|
return new WI.SourceCodeRevision(this._sourceCode, this._content, this._base64Encoded, this._mimeType);
|
|
}
|
|
};
|
|
|
|
/* Models/SourceCodeTimeline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceCodeTimeline = class SourceCodeTimeline extends WI.Timeline
|
|
{
|
|
constructor(sourceCode, sourceCodeLocation, recordType, recordEventType)
|
|
{
|
|
super();
|
|
|
|
console.assert(sourceCode);
|
|
console.assert(!sourceCodeLocation || sourceCodeLocation.sourceCode === sourceCode);
|
|
console.assert(recordType);
|
|
|
|
this._sourceCode = sourceCode;
|
|
this._sourceCodeLocation = sourceCodeLocation || null;
|
|
this._recordType = recordType;
|
|
this._recordEventType = recordEventType || null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceCode() { return this._sourceCode; }
|
|
get sourceCodeLocation() { return this._sourceCodeLocation; }
|
|
get recordType() { return this._recordType; }
|
|
get recordEventType() { return this._recordEventType; }
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie[WI.SourceCodeTimeline.SourceCodeURLCookieKey] = this._sourceCode.url ? this._sourceCode.url.hash : null;
|
|
cookie[WI.SourceCodeTimeline.SourceCodeLocationLineCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.lineNumber : null;
|
|
cookie[WI.SourceCodeTimeline.SourceCodeLocationColumnCookieKey] = this._sourceCodeLocation ? this._sourceCodeLocation.columnNumber : null;
|
|
cookie[WI.SourceCodeTimeline.RecordTypeCookieKey] = this._recordType || null;
|
|
cookie[WI.SourceCodeTimeline.RecordEventTypeCookieKey] = this._recordEventType || null;
|
|
}
|
|
};
|
|
|
|
WI.SourceCodeTimeline.TypeIdentifier = "source-code-timeline";
|
|
WI.SourceCodeTimeline.SourceCodeURLCookieKey = "source-code-timeline-source-code-url";
|
|
WI.SourceCodeTimeline.SourceCodeLocationLineCookieKey = "source-code-timeline-source-code-location-line";
|
|
WI.SourceCodeTimeline.SourceCodeLocationColumnCookieKey = "source-code-timeline-source-code-location-column";
|
|
WI.SourceCodeTimeline.SourceCodeURLCookieKey = "source-code-timeline-source-code-url";
|
|
WI.SourceCodeTimeline.RecordTypeCookieKey = "source-code-timeline-record-type";
|
|
WI.SourceCodeTimeline.RecordEventTypeCookieKey = "source-code-timeline-record-event-type";
|
|
|
|
/* Models/SourceMap.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceMap = class SourceMap
|
|
{
|
|
constructor(sourceMappingURL, payload, originalSourceCode)
|
|
{
|
|
if (!WI.SourceMap._base64Map) {
|
|
var base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
WI.SourceMap._base64Map = {};
|
|
for (var i = 0; i < base64Digits.length; ++i)
|
|
WI.SourceMap._base64Map[base64Digits.charAt(i)] = i;
|
|
}
|
|
|
|
this._originalSourceCode = originalSourceCode || null;
|
|
this._sourceMapResources = {};
|
|
this._sourceMapResourcesList = [];
|
|
|
|
this._sourceMappingURL = sourceMappingURL;
|
|
this._reverseMappingsBySourceURL = {};
|
|
this._mappings = [];
|
|
this._sources = {};
|
|
this._sourceRoot = null;
|
|
this._sourceContentByURL = {};
|
|
this._parseMappingPayload(payload);
|
|
}
|
|
|
|
// Public
|
|
|
|
get originalSourceCode()
|
|
{
|
|
return this._originalSourceCode;
|
|
}
|
|
|
|
get sourceMappingBasePathURLComponents()
|
|
{
|
|
if (this._sourceMappingURLBasePathComponents)
|
|
return this._sourceMappingURLBasePathComponents;
|
|
|
|
if (this._sourceRoot) {
|
|
var baseURLPath = absoluteURL(this._sourceRoot, this._sourceMappingURL);
|
|
console.assert(baseURLPath);
|
|
if (baseURLPath) {
|
|
var urlComponents = parseURL(baseURLPath);
|
|
if (!/\/$/.test(urlComponents.path))
|
|
urlComponents.path += "/";
|
|
this._sourceMappingURLBasePathComponents = urlComponents;
|
|
return this._sourceMappingURLBasePathComponents;
|
|
}
|
|
}
|
|
|
|
var urlComponents = parseURL(this._sourceMappingURL);
|
|
|
|
// Fallback for JavaScript debuggable named scripts that may not have a complete URL.
|
|
if (!urlComponents.path)
|
|
urlComponents.path = this._sourceMappingURL || "";
|
|
|
|
urlComponents.path = urlComponents.path.substr(0, urlComponents.path.lastIndexOf(urlComponents.lastPathComponent));
|
|
urlComponents.lastPathComponent = null;
|
|
this._sourceMappingURLBasePathComponents = urlComponents;
|
|
return this._sourceMappingURLBasePathComponents;
|
|
}
|
|
|
|
get resources()
|
|
{
|
|
return this._sourceMapResourcesList;
|
|
}
|
|
|
|
addResource(resource)
|
|
{
|
|
console.assert(!(resource.url in this._sourceMapResources));
|
|
this._sourceMapResources[resource.url] = resource;
|
|
this._sourceMapResourcesList.push(resource);
|
|
}
|
|
|
|
resourceForURL(url)
|
|
{
|
|
return this._sourceMapResources[url];
|
|
}
|
|
|
|
sources()
|
|
{
|
|
return Object.keys(this._sources);
|
|
}
|
|
|
|
sourceContent(sourceURL)
|
|
{
|
|
return this._sourceContentByURL[sourceURL];
|
|
}
|
|
|
|
_parseMappingPayload(mappingPayload)
|
|
{
|
|
if (mappingPayload.sections)
|
|
this._parseSections(mappingPayload.sections);
|
|
else
|
|
this._parseMap(mappingPayload, 0, 0);
|
|
}
|
|
|
|
_parseSections(sections)
|
|
{
|
|
for (var i = 0; i < sections.length; ++i) {
|
|
var section = sections[i];
|
|
this._parseMap(section.map, section.offset.line, section.offset.column);
|
|
}
|
|
}
|
|
|
|
findEntry(lineNumber, columnNumber)
|
|
{
|
|
var first = 0;
|
|
var count = this._mappings.length;
|
|
while (count > 1) {
|
|
var step = count >> 1;
|
|
var middle = first + step;
|
|
var mapping = this._mappings[middle];
|
|
if (lineNumber < mapping[0] || (lineNumber === mapping[0] && columnNumber < mapping[1]))
|
|
count = step;
|
|
else {
|
|
first = middle;
|
|
count -= step;
|
|
}
|
|
}
|
|
var entry = this._mappings[first];
|
|
if (!first && entry && (lineNumber < entry[0] || (lineNumber === entry[0] && columnNumber < entry[1])))
|
|
return null;
|
|
return entry;
|
|
}
|
|
|
|
findEntryReversed(sourceURL, lineNumber)
|
|
{
|
|
var mappings = this._reverseMappingsBySourceURL[sourceURL];
|
|
for ( ; lineNumber < mappings.length; ++lineNumber) {
|
|
var mapping = mappings[lineNumber];
|
|
if (mapping)
|
|
return mapping;
|
|
}
|
|
return this._mappings[0];
|
|
}
|
|
|
|
_parseMap(map, lineNumber, columnNumber)
|
|
{
|
|
var sourceIndex = 0;
|
|
var sourceLineNumber = 0;
|
|
var sourceColumnNumber = 0;
|
|
var nameIndex = 0;
|
|
|
|
var sources = [];
|
|
var originalToCanonicalURLMap = {};
|
|
for (var i = 0; i < map.sources.length; ++i) {
|
|
var originalSourceURL = map.sources[i];
|
|
var href = originalSourceURL;
|
|
if (map.sourceRoot && href.charAt(0) !== "/")
|
|
href = map.sourceRoot.replace(/\/+$/, "") + "/" + href;
|
|
var url = absoluteURL(href, this._sourceMappingURL) || href;
|
|
originalToCanonicalURLMap[originalSourceURL] = url;
|
|
sources.push(url);
|
|
this._sources[url] = true;
|
|
|
|
if (map.sourcesContent && map.sourcesContent[i])
|
|
this._sourceContentByURL[url] = map.sourcesContent[i];
|
|
}
|
|
|
|
this._sourceRoot = map.sourceRoot || null;
|
|
|
|
var stringCharIterator = new WI.SourceMap.StringCharIterator(map.mappings);
|
|
var sourceURL = sources[sourceIndex];
|
|
|
|
while (true) {
|
|
if (stringCharIterator.peek() === ",")
|
|
stringCharIterator.next();
|
|
else {
|
|
while (stringCharIterator.peek() === ";") {
|
|
lineNumber += 1;
|
|
columnNumber = 0;
|
|
stringCharIterator.next();
|
|
}
|
|
if (!stringCharIterator.hasNext())
|
|
break;
|
|
}
|
|
|
|
columnNumber += this._decodeVLQ(stringCharIterator);
|
|
if (this._isSeparator(stringCharIterator.peek())) {
|
|
this._mappings.push([lineNumber, columnNumber]);
|
|
continue;
|
|
}
|
|
|
|
var sourceIndexDelta = this._decodeVLQ(stringCharIterator);
|
|
if (sourceIndexDelta) {
|
|
sourceIndex += sourceIndexDelta;
|
|
sourceURL = sources[sourceIndex];
|
|
}
|
|
sourceLineNumber += this._decodeVLQ(stringCharIterator);
|
|
sourceColumnNumber += this._decodeVLQ(stringCharIterator);
|
|
if (!this._isSeparator(stringCharIterator.peek()))
|
|
nameIndex += this._decodeVLQ(stringCharIterator);
|
|
|
|
this._mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber]);
|
|
}
|
|
|
|
for (var i = 0; i < this._mappings.length; ++i) {
|
|
var mapping = this._mappings[i];
|
|
var url = mapping[2];
|
|
if (!url)
|
|
continue;
|
|
if (!this._reverseMappingsBySourceURL[url])
|
|
this._reverseMappingsBySourceURL[url] = [];
|
|
var reverseMappings = this._reverseMappingsBySourceURL[url];
|
|
var sourceLine = mapping[3];
|
|
if (!reverseMappings[sourceLine])
|
|
reverseMappings[sourceLine] = [mapping[0], mapping[1]];
|
|
}
|
|
}
|
|
|
|
_isSeparator(char)
|
|
{
|
|
return char === "," || char === ";";
|
|
}
|
|
|
|
_decodeVLQ(stringCharIterator)
|
|
{
|
|
// Read unsigned value.
|
|
var result = 0;
|
|
var shift = 0;
|
|
do {
|
|
var digit = WI.SourceMap._base64Map[stringCharIterator.next()];
|
|
result += (digit & WI.SourceMap.VLQ_BASE_MASK) << shift;
|
|
shift += WI.SourceMap.VLQ_BASE_SHIFT;
|
|
} while (digit & WI.SourceMap.VLQ_CONTINUATION_MASK);
|
|
|
|
// Fix the sign.
|
|
var negative = result & 1;
|
|
result >>= 1;
|
|
return negative ? -result : result;
|
|
}
|
|
};
|
|
|
|
WI.SourceMap.VLQ_BASE_SHIFT = 5;
|
|
WI.SourceMap.VLQ_BASE_MASK = (1 << 5) - 1;
|
|
WI.SourceMap.VLQ_CONTINUATION_MASK = 1 << 5;
|
|
|
|
WI.SourceMap.StringCharIterator = class StringCharIterator
|
|
{
|
|
constructor(string)
|
|
{
|
|
this._string = string;
|
|
this._position = 0;
|
|
}
|
|
|
|
next()
|
|
{
|
|
return this._string.charAt(this._position++);
|
|
}
|
|
|
|
peek()
|
|
{
|
|
return this._string.charAt(this._position);
|
|
}
|
|
|
|
hasNext()
|
|
{
|
|
return this._position < this._string.length;
|
|
}
|
|
};
|
|
|
|
/* Models/SpringTimingFunction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SpringTimingFunction = class SpringTimingFunction
|
|
{
|
|
constructor(mass, stiffness, damping, initialVelocity)
|
|
{
|
|
this.mass = Math.max(1, mass);
|
|
this.stiffness = Math.max(1, stiffness);
|
|
this.damping = Math.max(0, damping);
|
|
this.initialVelocity = initialVelocity;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromValues(values)
|
|
{
|
|
if (!values || values.length < 4)
|
|
return null;
|
|
|
|
values = values.map(Number);
|
|
if (values.includes(NaN))
|
|
return null;
|
|
|
|
return new WI.SpringTimingFunction(...values);
|
|
}
|
|
|
|
static fromString(text)
|
|
{
|
|
if (!text || !text.length)
|
|
return null;
|
|
|
|
let trimmedText = text.toLowerCase().trim();
|
|
if (!trimmedText.length)
|
|
return null;
|
|
|
|
let matches = trimmedText.match(/^spring\(([\d.]+)\s+([\d.]+)\s+([\d.]+)\s+([-\d.]+)\)$/);
|
|
if (!matches)
|
|
return null;
|
|
|
|
return WI.SpringTimingFunction.fromValues(matches.slice(1));
|
|
}
|
|
|
|
// Public
|
|
|
|
copy()
|
|
{
|
|
return new WI.SpringTimingFunction(this.mass, this.stiffness, this.damping, this.initialVelocity);
|
|
}
|
|
|
|
toString()
|
|
{
|
|
return `spring(${this.mass} ${this.stiffness} ${this.damping} ${this.initialVelocity})`;
|
|
}
|
|
|
|
solve(t)
|
|
{
|
|
let w0 = Math.sqrt(this.stiffness / this.mass);
|
|
let zeta = this.damping / (2 * Math.sqrt(this.stiffness * this.mass));
|
|
|
|
let wd = 0;
|
|
let A = 1;
|
|
let B = -this.initialVelocity + w0;
|
|
if (zeta < 1) {
|
|
// Under-damped.
|
|
wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
A = 1;
|
|
B = (zeta * w0 + -this.initialVelocity) / wd;
|
|
}
|
|
|
|
if (zeta < 1) // Under-damped
|
|
t = Math.exp(-t * zeta * w0) * (A * Math.cos(wd * t) + B * Math.sin(wd * t));
|
|
else // Critically damped (ignoring over-damped case).
|
|
t = (A + B * t) * Math.exp(-t * w0);
|
|
|
|
return 1 - t; // Map range from [1..0] to [0..1].
|
|
}
|
|
|
|
calculateDuration(epsilon)
|
|
{
|
|
epsilon = epsilon || 0.0001;
|
|
let t = 0;
|
|
let current = 0;
|
|
let minimum = Number.POSITIVE_INFINITY;
|
|
while (current >= epsilon || minimum >= epsilon) {
|
|
current = Math.abs(1 - this.solve(t)); // Undo the range mapping
|
|
if (minimum < epsilon && current >= epsilon)
|
|
minimum = Number.POSITIVE_INFINITY; // Spring reversed direction
|
|
else if (current < minimum)
|
|
minimum = current;
|
|
t += 0.1;
|
|
}
|
|
return t;
|
|
}
|
|
};
|
|
|
|
/* Models/StackTrace.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.StackTrace = class StackTrace
|
|
{
|
|
constructor(callFrames, {topCallFrameIsBoundary, truncated, parentStackTrace} = {})
|
|
{
|
|
console.assert(callFrames.every((callFrame) => callFrame instanceof WI.CallFrame), callFrames);
|
|
console.assert(!parentStackTrace || parentStackTrace instanceof WI.StackTrace, parentStackTrace);
|
|
|
|
this._callFrames = callFrames;
|
|
this._topCallFrameIsBoundary = topCallFrameIsBoundary || false;
|
|
this._truncated = truncated || false;
|
|
this._parentStackTrace = parentStackTrace || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromPayload(target, payload)
|
|
{
|
|
let result = null;
|
|
let previousStackTrace = null;
|
|
|
|
while (payload) {
|
|
let callFrames = payload.callFrames.map((x) => WI.CallFrame.fromPayload(target, x));
|
|
let stackTrace = new WI.StackTrace(callFrames, {
|
|
topCallFrameIsBoundary: payload.topCallFrameIsBoundary,
|
|
truncated: payload.truncated,
|
|
});
|
|
if (!result)
|
|
result = stackTrace;
|
|
if (previousStackTrace)
|
|
previousStackTrace._parentStackTrace = stackTrace;
|
|
|
|
previousStackTrace = stackTrace;
|
|
payload = payload.parentStackTrace;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static fromString(target, stack)
|
|
{
|
|
let callFrames = WI.StackTrace._parseStackTrace(stack);
|
|
return WI.StackTrace.fromPayload(target, {callFrames});
|
|
}
|
|
|
|
// May produce false negatives; must not produce any false positives.
|
|
// It may return false on a valid stack trace, but it will never return true on an invalid stack trace.
|
|
static isLikelyStackTrace(stack)
|
|
{
|
|
// This function runs for every logged string. It penalizes the performance.
|
|
// As most logged strings are not stack traces, exit as early as possible.
|
|
const smallestPossibleStackTraceLength = "http://a.bc/:9:1".length;
|
|
if (stack.length < smallestPossibleStackTraceLength.length * 2)
|
|
return false;
|
|
|
|
const approximateStackLengthOf50Items = 5000;
|
|
if (stack.length > approximateStackLengthOf50Items)
|
|
return false;
|
|
|
|
if (/^[^a-z$_@]/i.test(stack[0]))
|
|
return false;
|
|
|
|
if (!WI.StackTrace._likelyStackTraceRegex) {
|
|
const reasonablyLongProtocolLength = 10;
|
|
const reasonablyLongLineLength = 500;
|
|
const reasonablyLongNativeMethodLength = 120;
|
|
const stackTraceLine = `(global code|eval code|module code|\\w+)?([^:]{1,${reasonablyLongProtocolLength}}://[^:]{1,${reasonablyLongLineLength}}:\\d+:\\d+|[^@]{1,${reasonablyLongNativeMethodLength}}@\\[native code\\])`;
|
|
WI.StackTrace._likelyStackTraceRegex = new RegExp(`^${stackTraceLine}([\\n\\r]${stackTraceLine})+$`);
|
|
}
|
|
|
|
WI.StackTrace._likelyStackTraceRegex.lastIndex = 0;
|
|
return WI.StackTrace._likelyStackTraceRegex.test(stack);
|
|
}
|
|
|
|
static _parseStackTrace(stack)
|
|
{
|
|
var lines = stack.split(/\n/g);
|
|
var result = [];
|
|
|
|
for (var line of lines) {
|
|
var functionName = "";
|
|
var url = "";
|
|
var lineNumber = 0;
|
|
var columnNumber = 0;
|
|
var atIndex = line.indexOf("@");
|
|
|
|
if (atIndex !== -1) {
|
|
functionName = line.slice(0, atIndex);
|
|
({url, lineNumber, columnNumber} = WI.StackTrace._parseLocation(line.slice(atIndex + 1)));
|
|
} else if (line.includes("/"))
|
|
({url, lineNumber, columnNumber} = WI.StackTrace._parseLocation(line));
|
|
else
|
|
functionName = line;
|
|
|
|
result.push({functionName, url, lineNumber, columnNumber});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static _parseLocation(locationString)
|
|
{
|
|
var result = {url: "", lineNumber: 0, columnNumber: 0};
|
|
var locationRegEx = /(.+?)(?::(\d+)(?::(\d+))?)?$/;
|
|
var matched = locationString.match(locationRegEx);
|
|
|
|
if (!matched)
|
|
return result;
|
|
|
|
result.url = matched[1];
|
|
|
|
if (matched[2])
|
|
result.lineNumber = parseInt(matched[2]);
|
|
|
|
if (matched[3])
|
|
result.columnNumber = parseInt(matched[3]);
|
|
|
|
return result;
|
|
}
|
|
|
|
// Public
|
|
|
|
get callFrames() { return this._callFrames; }
|
|
get topCallFrameIsBoundary() { return this._topCallFrameIsBoundary; }
|
|
get truncated() { return this._truncated; }
|
|
get parentStackTrace() { return this._parentStackTrace; }
|
|
|
|
get firstNonNativeNonAnonymousNotBlackboxedCallFrame()
|
|
{
|
|
let firstNonNativeNonAnonymousCallFrame = null;
|
|
|
|
for (let frame of this._callFrames) {
|
|
if (frame.nativeCode)
|
|
continue;
|
|
|
|
if (frame.sourceCodeLocation) {
|
|
let sourceCode = frame.sourceCodeLocation.sourceCode;
|
|
if (sourceCode instanceof WI.Script && sourceCode.anonymous)
|
|
continue;
|
|
|
|
// Save the first non-native non-anonymous call frame so it can be used as a
|
|
// fallback if all remaining call frames are blackboxed.
|
|
firstNonNativeNonAnonymousCallFrame ??= frame;
|
|
}
|
|
|
|
if (frame.blackboxed)
|
|
continue;
|
|
|
|
return frame;
|
|
}
|
|
|
|
return firstNonNativeNonAnonymousCallFrame;
|
|
}
|
|
};
|
|
|
|
/* Models/StepsTimingFunction.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.StepsTimingFunction = class StepsTimingFunction
|
|
{
|
|
constructor(type, count)
|
|
{
|
|
console.assert(Object.values(WI.StepsTimingFunction.Type).includes(type), type);
|
|
console.assert(count > 0, count);
|
|
|
|
this._type = type;
|
|
this._count = count;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromString(text)
|
|
{
|
|
if (!text?.length)
|
|
return null;
|
|
|
|
let trimmedText = text.toLowerCase().replace(/\s/g, "");
|
|
if (!trimmedText.length)
|
|
return null;
|
|
|
|
let keywordValue = WI.StepsTimingFunction.keywordValues[trimmedText];
|
|
if (keywordValue)
|
|
return new WI.StepsTimingFunction(...keywordValue);
|
|
|
|
let matches = trimmedText.match(/^steps\((\d+)(?:,([a-z-]+))?\)$/);
|
|
if (!matches)
|
|
return null;
|
|
|
|
let type = matches[2] || WI.StepsTimingFunction.Type.JumpEnd;
|
|
if (Object.values(WI.StepsTimingFunction).includes(type))
|
|
return null;
|
|
|
|
let count = Number(matches[1]);
|
|
if (isNaN(count) || count <= 0)
|
|
return null;
|
|
|
|
return new WI.StepsTimingFunction(type, count);
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get count() { return this._count; }
|
|
|
|
copy()
|
|
{
|
|
return new WI.StepsTimingFunction(this._type, this._count);
|
|
}
|
|
|
|
toString()
|
|
{
|
|
if (this._type === WI.StepsTimingFunction.Type.JumpStart && this._count === 1)
|
|
return "step-start";
|
|
|
|
if (this._type === WI.StepsTimingFunction.Type.JumpEnd && this._count === 1)
|
|
return "step-end";
|
|
|
|
return `steps(${this._count}, ${this._type})`;
|
|
}
|
|
};
|
|
|
|
WI.StepsTimingFunction.Type = {
|
|
JumpStart: "jump-start",
|
|
JumpEnd: "jump-end",
|
|
JumpNone: "jump-none",
|
|
JumpBoth: "jump-both",
|
|
Start: "start",
|
|
End: "end",
|
|
};
|
|
|
|
WI.StepsTimingFunction.keywordValues = {
|
|
"step-start": [WI.StepsTimingFunction.Type.JumpStart, 1],
|
|
"step-end": [WI.StepsTimingFunction.Type.JumpEnd, 1],
|
|
};
|
|
|
|
/* Models/SymbolicBreakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SymbolicBreakpoint = class SymbolicBreakpoint extends WI.Breakpoint
|
|
{
|
|
constructor(symbol, {caseSensitive, isRegex, disabled, actions, condition, ignoreCount, autoContinue} = {})
|
|
{
|
|
console.assert(typeof symbol === "string" && symbol.trim().length, symbol);
|
|
|
|
super({disabled, condition, actions, ignoreCount, autoContinue});
|
|
|
|
this._symbol = symbol;
|
|
this._caseSensitive = caseSensitive !== undefined ? !!caseSensitive : true;
|
|
this._isRegex = isRegex !== undefined ? !!isRegex : false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static supported(target)
|
|
{
|
|
// COMPATIBILITY (iOS 16.0): Debugger.addSymbolicBreakpoint did exist yet.
|
|
return (target || InspectorBackend).hasCommand("Debugger.addSymbolicBreakpoint");
|
|
}
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.SymbolicBreakpoint(json.symbol, {
|
|
caseSensitive: json.caseSensitive,
|
|
isRegex: json.isRegex,
|
|
disabled: json.disabled,
|
|
condition: json.condition,
|
|
actions: json.actions?.map((actionJSON) => WI.BreakpointAction.fromJSON(actionJSON)) || [],
|
|
ignoreCount: json.ignoreCount,
|
|
autoContinue: json.autoContinue,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get symbol() { return this._symbol; }
|
|
get caseSensitive() { return this._caseSensitive; }
|
|
get isRegex() { return this._isRegex; }
|
|
|
|
get displayName()
|
|
{
|
|
if (this._isRegex)
|
|
return "/" + this._symbol + "/" + (!this._caseSensitive ? "i" : "");
|
|
|
|
let displayName = this._symbol;
|
|
if (!this._caseSensitive)
|
|
displayName = WI.UIString("%s (Case Insensitive)", "%s (Case Insensitive) @ Symbolic Breakpoint", "Label for case-insensitive match pattern of a symbolic breakpoint.").format(displayName);
|
|
return displayName;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
matches(symbol)
|
|
{
|
|
if (!symbol || this.disabled)
|
|
return false;
|
|
|
|
if (this._isRegex)
|
|
return (new RegExp(this._symbol, !this._caseSensitive ? "i" : "")).test(symbol);
|
|
|
|
if (!this._caseSensitive)
|
|
return symbol.toLowerCase() === this._symbol.toLowerCase();
|
|
|
|
return symbol === this._symbol;
|
|
}
|
|
|
|
equals(other)
|
|
{
|
|
console.assert(other instanceof WI.SymbolicBreakpoint, other);
|
|
|
|
return this._symbol === other.symbol
|
|
&& this._caseSensitive === other.caseSensitive
|
|
&& this._isRegex === other.isRegex;
|
|
}
|
|
|
|
remove()
|
|
{
|
|
super.remove();
|
|
|
|
WI.debuggerManager.removeSymbolicBreakpoint(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["symbolic-breakpoint-symbol"] = this._symbol;
|
|
cookie["symbolic-breakpoint-symbolic-case-sensitive"] = this._caseSensitive;
|
|
cookie["symbolic-breakpoint-symbolic-is-regex"] = this._isRegex;
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.symbol = this._symbol;
|
|
json.caseSensitive = this._caseSensitive;
|
|
json.isRegex = this._isRegex;
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.eventBreakpoints.keyPath] = this._symbol + "-" + this._caseSensitive + "-" + this._isRegex;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.SymbolicBreakpoint.ReferencePage = WI.ReferencePage.SymbolicBreakpoints;
|
|
|
|
/* Models/TextRange.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TextRange = class TextRange
|
|
{
|
|
constructor(startLineOrStartOffset, startColumnOrEndOffset, endLine, endColumn)
|
|
{
|
|
if (arguments.length === 4) {
|
|
console.assert(startLineOrStartOffset <= endLine);
|
|
console.assert(startLineOrStartOffset !== endLine || startColumnOrEndOffset <= endColumn);
|
|
|
|
this._startLine = typeof startLineOrStartOffset === "number" ? startLineOrStartOffset : NaN;
|
|
this._startColumn = typeof startColumnOrEndOffset === "number" ? startColumnOrEndOffset : NaN;
|
|
this._endLine = typeof endLine === "number" ? endLine : NaN;
|
|
this._endColumn = typeof endColumn === "number" ? endColumn : NaN;
|
|
|
|
this._startOffset = NaN;
|
|
this._endOffset = NaN;
|
|
} else if (arguments.length === 2) {
|
|
console.assert(startLineOrStartOffset <= startColumnOrEndOffset);
|
|
|
|
this._startOffset = typeof startLineOrStartOffset === "number" ? startLineOrStartOffset : NaN;
|
|
this._endOffset = typeof startColumnOrEndOffset === "number" ? startColumnOrEndOffset : NaN;
|
|
|
|
this._startLine = NaN;
|
|
this._startColumn = NaN;
|
|
this._endLine = NaN;
|
|
this._endColumn = NaN;
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromText(text)
|
|
{
|
|
let lines = text.split("\n");
|
|
return new WI.TextRange(0, 0, lines.length - 1, lines.lastValue.length);
|
|
}
|
|
|
|
// Public
|
|
|
|
get startLine() { return this._startLine; }
|
|
get startColumn() { return this._startColumn; }
|
|
get endLine() { return this._endLine; }
|
|
get endColumn() { return this._endColumn; }
|
|
get startOffset() { return this._startOffset; }
|
|
get endOffset() { return this._endOffset; }
|
|
|
|
startPosition()
|
|
{
|
|
return new WI.SourceCodePosition(this._startLine, this._startColumn);
|
|
}
|
|
|
|
endPosition()
|
|
{
|
|
return new WI.SourceCodePosition(this._endLine, this._endColumn);
|
|
}
|
|
|
|
resolveOffsets(text)
|
|
{
|
|
console.assert(typeof text === "string");
|
|
if (typeof text !== "string")
|
|
return;
|
|
|
|
console.assert(!isNaN(this._startLine));
|
|
console.assert(!isNaN(this._startColumn));
|
|
console.assert(!isNaN(this._endLine));
|
|
console.assert(!isNaN(this._endColumn));
|
|
if (isNaN(this._startLine) || isNaN(this._startColumn) || isNaN(this._endLine) || isNaN(this._endColumn))
|
|
return;
|
|
|
|
var lastNewLineOffset = 0;
|
|
for (var i = 0; i < this._startLine; ++i)
|
|
lastNewLineOffset = text.indexOf("\n", lastNewLineOffset) + 1;
|
|
|
|
this._startOffset = lastNewLineOffset + this._startColumn;
|
|
|
|
for (var i = this._startLine; i < this._endLine; ++i)
|
|
lastNewLineOffset = text.indexOf("\n", lastNewLineOffset) + 1;
|
|
|
|
this._endOffset = lastNewLineOffset + this._endColumn;
|
|
}
|
|
|
|
contains(line, column)
|
|
{
|
|
console.assert(!isNaN(this._startLine), "TextRange needs line/column data");
|
|
|
|
if (line < this._startLine || line > this._endLine)
|
|
return false;
|
|
if (line === this._startLine && column < this._startColumn)
|
|
return false;
|
|
if (line === this._endLine && column > this._endColumn)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
clone()
|
|
{
|
|
console.assert(!isNaN(this._startLine), "TextRange needs line/column data.");
|
|
return new WI.TextRange(this._startLine, this._startColumn, this._endLine, this._endColumn);
|
|
}
|
|
|
|
cloneAndModify(deltaStartLine, deltaStartColumn, deltaEndLine, deltaEndColumn)
|
|
{
|
|
console.assert(!isNaN(this._startLine), "TextRange needs line/column data.");
|
|
|
|
let startLine = this._startLine + deltaStartLine;
|
|
let startColumn = this._startColumn + deltaStartColumn;
|
|
let endLine = this._endLine + deltaEndLine;
|
|
let endColumn = this._endColumn + deltaEndColumn;
|
|
console.assert(startLine >= 0 && startColumn >= 0 && endLine >= 0 && endColumn >= 0, `Cannot have negative numbers in TextRange ${startLine}:${startColumn}...${endLine}:${endColumn}`);
|
|
|
|
return new WI.TextRange(startLine, startColumn, endLine, endColumn);
|
|
}
|
|
|
|
collapseToStart()
|
|
{
|
|
console.assert(!isNaN(this._startLine), "TextRange needs line/column data.");
|
|
return new WI.TextRange(this._startLine, this._startColumn, this._startLine, this._startColumn);
|
|
}
|
|
|
|
collapseToEnd()
|
|
{
|
|
console.assert(!isNaN(this._endLine), "TextRange needs line/column data.");
|
|
return new WI.TextRange(this._endLine, this._endColumn, this._endLine, this._endColumn);
|
|
}
|
|
|
|
relativeTo(line, column)
|
|
{
|
|
let deltaStartColumn = 0;
|
|
if (this._startLine === line)
|
|
deltaStartColumn = -column;
|
|
|
|
let deltaEndColumn = 0;
|
|
if (this._endLine === line)
|
|
deltaEndColumn = -column;
|
|
|
|
return this.cloneAndModify(-line, deltaStartColumn, -line, deltaEndColumn);
|
|
}
|
|
};
|
|
|
|
/* Models/TimelineMarker.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TimelineMarker = class TimelineMarker extends WI.Object
|
|
{
|
|
constructor(time, type, details)
|
|
{
|
|
super();
|
|
|
|
console.assert(type);
|
|
|
|
this._time = time || 0;
|
|
this._type = type;
|
|
this._details = details || null;
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
let {time, type, details} = json;
|
|
return new WI.TimelineMarker(time, type, details);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
return {
|
|
time: this._time,
|
|
type: this._type,
|
|
details: this._details || undefined,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get details() { return this._details; }
|
|
|
|
get time()
|
|
{
|
|
return this._time;
|
|
}
|
|
|
|
set time(x)
|
|
{
|
|
console.assert(typeof x === "number", "Time should be a number.");
|
|
|
|
x = x || 0;
|
|
|
|
if (this._time === x)
|
|
return;
|
|
|
|
this._time = x;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineMarker.Event.TimeChanged);
|
|
}
|
|
};
|
|
|
|
WI.TimelineMarker.Event = {
|
|
TimeChanged: "timeline-marker-time-changed"
|
|
};
|
|
|
|
WI.TimelineMarker.Type = {
|
|
CurrentTime: "current-time",
|
|
LoadEvent: "load-event",
|
|
DOMContentEvent: "dom-content-event",
|
|
TimeStamp: "timestamp",
|
|
Scanner: "scanner",
|
|
};
|
|
|
|
/* Models/TimelineRecording.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TimelineRecording = class TimelineRecording extends WI.Object
|
|
{
|
|
constructor(identifier, displayName, instruments)
|
|
{
|
|
super();
|
|
|
|
this._identifier = identifier;
|
|
this._timelines = new Map;
|
|
this._displayName = displayName;
|
|
this._capturing = false;
|
|
this._readonly = false;
|
|
this._imported = false;
|
|
this._instruments = instruments || [];
|
|
|
|
this._startTime = NaN;
|
|
this._endTime = NaN;
|
|
|
|
this._discontinuityStartTime = NaN;
|
|
this._discontinuities = null;
|
|
this._firstRecordOfTypeAfterDiscontinuity = new Set;
|
|
|
|
this._exportDataMarkers = null;
|
|
this._exportDataRecords = null;
|
|
this._exportDataMemoryPressureEvents = null;
|
|
this._exportDataSampleStackTraces = null;
|
|
this._exportDataSampleDurations = null;
|
|
|
|
this._topDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopDown);
|
|
this._bottomUpCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.BottomUp);
|
|
this._topFunctionsTopDownCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopFunctionsTopDown);
|
|
this._topFunctionsBottomUpCallingContextTree = new WI.CallingContextTree(WI.CallingContextTree.Type.TopFunctionsBottomUp);
|
|
|
|
for (let type of WI.TimelineManager.availableTimelineTypes()) {
|
|
let timeline = WI.Timeline.create(type);
|
|
this._timelines.set(type, timeline);
|
|
timeline.addEventListener(WI.Timeline.Event.TimesUpdated, this._timelineTimesUpdated, this);
|
|
}
|
|
|
|
this.reset(true);
|
|
}
|
|
|
|
// Static
|
|
|
|
static sourceCodeTimelinesSupported()
|
|
{
|
|
// FIXME: Support Network Timeline in ServiceWorker.
|
|
return WI.sharedApp.isWebDebuggable();
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static async import(identifier, json, displayName)
|
|
{
|
|
let {startTime, endTime, discontinuities, instrumentTypes, records, markers, memoryPressureEvents, sampleStackTraces, sampleDurations} = json;
|
|
let importedDisplayName = WI.UIString("Imported - %s").format(displayName);
|
|
let instruments = instrumentTypes.map((type) => WI.Instrument.createForTimelineType(type));
|
|
let recording = new WI.TimelineRecording(identifier, importedDisplayName, instruments);
|
|
|
|
recording._readonly = true;
|
|
recording._imported = true;
|
|
recording._startTime = startTime;
|
|
recording._endTime = endTime;
|
|
recording._discontinuities = discontinuities;
|
|
|
|
recording.initializeCallingContextTrees(sampleStackTraces, sampleDurations);
|
|
|
|
for (let recordJSON of records) {
|
|
let record = await WI.TimelineRecord.fromJSON(recordJSON);
|
|
if (record) {
|
|
recording.addRecord(record);
|
|
|
|
if (record instanceof WI.ScriptTimelineRecord)
|
|
record.profilePayload = recording._topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
|
|
}
|
|
}
|
|
|
|
for (let memoryPressureJSON of memoryPressureEvents) {
|
|
let memoryPressureEvent = WI.MemoryPressureEvent.fromJSON(memoryPressureJSON);
|
|
if (memoryPressureEvent)
|
|
recording.addMemoryPressureEvent(memoryPressureEvent);
|
|
}
|
|
|
|
// Add markers once we've transitioned the active recording.
|
|
setTimeout(() => {
|
|
recording.__importing = true;
|
|
|
|
for (let markerJSON of markers) {
|
|
let marker = WI.TimelineMarker.fromJSON(markerJSON);
|
|
if (marker)
|
|
recording.addEventMarker(marker);
|
|
}
|
|
|
|
recording.__importing = false;
|
|
});
|
|
|
|
return recording;
|
|
}
|
|
|
|
exportData()
|
|
{
|
|
console.assert(this.canExport(), "Attempted to export a recording which should not be exportable.");
|
|
|
|
// FIXME: Overview data (sourceCodeTimelinesMap).
|
|
// FIXME: Record hierarchy (parent / child relationship) is lost.
|
|
|
|
return {
|
|
displayName: this._displayName,
|
|
startTime: this._startTime,
|
|
endTime: this._endTime,
|
|
discontinuities: this._discontinuities,
|
|
instrumentTypes: this._instruments.map((instrument) => instrument.timelineRecordType),
|
|
records: this._exportDataRecords,
|
|
markers: this._exportDataMarkers,
|
|
memoryPressureEvents: this._exportDataMemoryPressureEvents,
|
|
sampleStackTraces: this._exportDataSampleStackTraces,
|
|
sampleDurations: this._exportDataSampleDurations,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get displayName() { return this._displayName; }
|
|
get identifier() { return this._identifier; }
|
|
get timelines() { return this._timelines; }
|
|
get instruments() { return this._instruments; }
|
|
get capturing() { return this._capturing; }
|
|
get readonly() { return this._readonly; }
|
|
get imported() { return this._imported; }
|
|
get startTime() { return this._startTime; }
|
|
get endTime() { return this._endTime; }
|
|
|
|
get topDownCallingContextTree() { return this._topDownCallingContextTree; }
|
|
get bottomUpCallingContextTree() { return this._bottomUpCallingContextTree; }
|
|
get topFunctionsTopDownCallingContextTree() { return this._topFunctionsTopDownCallingContextTree; }
|
|
get topFunctionsBottomUpCallingContextTree() { return this._topFunctionsBottomUpCallingContextTree; }
|
|
|
|
start(initiatedByBackend)
|
|
{
|
|
console.assert(!this._capturing, "Attempted to start an already started session.");
|
|
console.assert(!this._readonly, "Attempted to start a readonly session.");
|
|
|
|
this._capturing = true;
|
|
|
|
for (let instrument of this._instruments)
|
|
instrument.startInstrumentation(initiatedByBackend);
|
|
|
|
if (!isNaN(this._discontinuityStartTime)) {
|
|
for (let instrument of this._instruments)
|
|
this._firstRecordOfTypeAfterDiscontinuity.add(instrument.timelineRecordType);
|
|
}
|
|
}
|
|
|
|
stop(initiatedByBackend)
|
|
{
|
|
console.assert(this._capturing, "Attempted to stop an already stopped session.");
|
|
console.assert(!this._readonly, "Attempted to stop a readonly session.");
|
|
|
|
this._capturing = false;
|
|
|
|
for (let instrument of this._instruments)
|
|
instrument.stopInstrumentation(initiatedByBackend);
|
|
}
|
|
|
|
capturingStarted(startTime)
|
|
{
|
|
// A discontinuity occurs when the recording is stopped and resumed at
|
|
// a future time. Capturing started signals the end of the current
|
|
// discontinuity, if one exists.
|
|
if (!isNaN(this._discontinuityStartTime)) {
|
|
this._discontinuities.push({
|
|
startTime: this._discontinuityStartTime,
|
|
endTime: startTime,
|
|
});
|
|
this._discontinuityStartTime = NaN;
|
|
}
|
|
}
|
|
|
|
capturingStopped(endTime)
|
|
{
|
|
this._discontinuityStartTime = endTime;
|
|
}
|
|
|
|
saveIdentityToCookie()
|
|
{
|
|
// Do nothing. Timeline recordings are not persisted when the inspector is
|
|
// re-opened, so do not attempt to restore by identifier or display name.
|
|
}
|
|
|
|
isEmpty()
|
|
{
|
|
for (var timeline of this._timelines.values()) {
|
|
if (timeline.records.length)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
unloaded(importing)
|
|
{
|
|
console.assert(importing || !this.isEmpty(), "Shouldn't unload an empty recording; it should be reused instead.");
|
|
|
|
this._readonly = true;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.Unloaded);
|
|
}
|
|
|
|
reset(suppressEvents)
|
|
{
|
|
console.assert(!this._readonly, "Can't reset a read-only recording.");
|
|
|
|
this._sourceCodeTimelinesMap = new Map;
|
|
|
|
this._startTime = NaN;
|
|
this._endTime = NaN;
|
|
|
|
this._discontinuityStartTime = NaN;
|
|
this._discontinuities = [];
|
|
this._firstRecordOfTypeAfterDiscontinuity.clear();
|
|
|
|
this._exportDataMarkers = [];
|
|
this._exportDataRecords = [];
|
|
this._exportDataMemoryPressureEvents = [];
|
|
this._exportDataSampleStackTraces = [];
|
|
this._exportDataSampleDurations = [];
|
|
|
|
this._topDownCallingContextTree.reset();
|
|
this._bottomUpCallingContextTree.reset();
|
|
this._topFunctionsTopDownCallingContextTree.reset();
|
|
this._topFunctionsBottomUpCallingContextTree.reset();
|
|
|
|
for (var timeline of this._timelines.values())
|
|
timeline.reset(suppressEvents);
|
|
|
|
WI.RenderingFrameTimelineRecord.resetFrameIndex();
|
|
|
|
if (!suppressEvents) {
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.Reset);
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
|
|
}
|
|
}
|
|
|
|
get sourceCodeTimelines()
|
|
{
|
|
let timelines = [];
|
|
for (let timelinesForSourceCode of this._sourceCodeTimelinesMap.values())
|
|
timelines.pushAll(timelinesForSourceCode.values());
|
|
return timelines;
|
|
}
|
|
|
|
timelineForInstrument(instrument)
|
|
{
|
|
return this._timelines.get(instrument.timelineRecordType);
|
|
}
|
|
|
|
instrumentForTimeline(timeline)
|
|
{
|
|
return this._instruments.find((instrument) => instrument.timelineRecordType === timeline.type);
|
|
}
|
|
|
|
timelineForRecordType(recordType)
|
|
{
|
|
return this._timelines.get(recordType);
|
|
}
|
|
|
|
addInstrument(instrument)
|
|
{
|
|
console.assert(instrument instanceof WI.Instrument, instrument);
|
|
console.assert(!this._instruments.includes(instrument), this._instruments, instrument);
|
|
|
|
this._instruments.push(instrument);
|
|
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.InstrumentAdded, {instrument});
|
|
}
|
|
|
|
removeInstrument(instrument)
|
|
{
|
|
console.assert(instrument instanceof WI.Instrument, instrument);
|
|
console.assert(this._instruments.includes(instrument), this._instruments, instrument);
|
|
|
|
this._instruments.remove(instrument);
|
|
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.InstrumentRemoved, {instrument});
|
|
}
|
|
|
|
addEventMarker(marker)
|
|
{
|
|
this._exportDataMarkers.push(marker);
|
|
|
|
if (!this._capturing && !this.__importing)
|
|
return;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.MarkerAdded, {marker});
|
|
}
|
|
|
|
addRecord(record)
|
|
{
|
|
this._exportDataRecords.push(record);
|
|
|
|
let timeline = this._timelines.get(record.type);
|
|
console.assert(timeline, record, this._timelines);
|
|
if (!timeline)
|
|
return;
|
|
|
|
let discontinuity = null;
|
|
if (this._firstRecordOfTypeAfterDiscontinuity.take(record.type))
|
|
discontinuity = this._discontinuities.lastValue;
|
|
|
|
// Add the record to the global timeline by type.
|
|
timeline.addRecord(record, {discontinuity});
|
|
|
|
// Some records don't have source code timelines.
|
|
if (record.type === WI.TimelineRecord.Type.Network
|
|
|| record.type === WI.TimelineRecord.Type.RenderingFrame
|
|
|| record.type === WI.TimelineRecord.Type.CPU
|
|
|| record.type === WI.TimelineRecord.Type.Memory
|
|
|| record.type === WI.TimelineRecord.Type.HeapAllocations
|
|
|| record.type === WI.TimelineRecord.Type.Screenshots)
|
|
return;
|
|
|
|
if (!WI.TimelineRecording.sourceCodeTimelinesSupported())
|
|
return;
|
|
|
|
// Add the record to the source code timelines.
|
|
let sourceCode = null;
|
|
if (record.sourceCodeLocation)
|
|
sourceCode = record.sourceCodeLocation.sourceCode;
|
|
else if (record.type === WI.TimelineRecord.Type.Media) {
|
|
if (record.domNode && record.domNode.frame)
|
|
sourceCode = record.domNode.frame.mainResource;
|
|
}
|
|
if (!sourceCode)
|
|
sourceCode = WI.networkManager.mainFrame.provisionalMainResource || WI.networkManager.mainFrame.mainResource;
|
|
|
|
var sourceCodeTimelines = this._sourceCodeTimelinesMap.get(sourceCode);
|
|
if (!sourceCodeTimelines) {
|
|
sourceCodeTimelines = new Map;
|
|
this._sourceCodeTimelinesMap.set(sourceCode, sourceCodeTimelines);
|
|
}
|
|
|
|
var newTimeline = false;
|
|
var key = this._keyForRecord(record);
|
|
var sourceCodeTimeline = sourceCodeTimelines.get(key);
|
|
if (!sourceCodeTimeline) {
|
|
sourceCodeTimeline = new WI.SourceCodeTimeline(sourceCode, record.sourceCodeLocation, record.type, record.eventType);
|
|
sourceCodeTimelines.set(key, sourceCodeTimeline);
|
|
newTimeline = true;
|
|
}
|
|
|
|
sourceCodeTimeline.addRecord(record);
|
|
|
|
if (newTimeline)
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.SourceCodeTimelineAdded, {sourceCodeTimeline});
|
|
}
|
|
|
|
addMemoryPressureEvent(memoryPressureEvent)
|
|
{
|
|
this._exportDataMemoryPressureEvents.push(memoryPressureEvent);
|
|
|
|
let memoryTimeline = this._timelines.get(WI.TimelineRecord.Type.Memory);
|
|
console.assert(memoryTimeline, this._timelines);
|
|
if (!memoryTimeline)
|
|
return;
|
|
|
|
memoryTimeline.addMemoryPressureEvent(memoryPressureEvent);
|
|
}
|
|
|
|
discontinuitiesInTimeRange(startTime, endTime)
|
|
{
|
|
return this._discontinuities.filter((item) => item.startTime <= endTime && item.endTime >= startTime);
|
|
}
|
|
|
|
addScriptInstrumentForProgrammaticCapture()
|
|
{
|
|
for (let instrument of this._instruments) {
|
|
if (instrument instanceof WI.ScriptInstrument)
|
|
return;
|
|
}
|
|
|
|
this.addInstrument(new WI.ScriptInstrument);
|
|
|
|
let instrumentTypes = this._instruments.map((instrument) => instrument.timelineRecordType);
|
|
WI.timelineManager.enabledTimelineTypes = instrumentTypes;
|
|
}
|
|
|
|
computeElapsedTime(timestamp)
|
|
{
|
|
if (!timestamp || isNaN(timestamp))
|
|
return NaN;
|
|
return timestamp;
|
|
}
|
|
|
|
initializeTimeBoundsIfNecessary(timestamp)
|
|
{
|
|
if (isNaN(this._startTime)) {
|
|
console.assert(isNaN(this._endTime));
|
|
|
|
this._startTime = timestamp;
|
|
this._endTime = timestamp;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
|
|
}
|
|
}
|
|
|
|
initializeCallingContextTrees(stackTraces, sampleDurations)
|
|
{
|
|
this._exportDataSampleStackTraces.pushAll(stackTraces);
|
|
this._exportDataSampleDurations.pushAll(sampleDurations);
|
|
|
|
for (let i = 0; i < stackTraces.length; i++) {
|
|
this._topDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
|
|
this._bottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
|
|
this._topFunctionsTopDownCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
|
|
this._topFunctionsBottomUpCallingContextTree.updateTreeWithStackTrace(stackTraces[i], sampleDurations[i]);
|
|
}
|
|
}
|
|
|
|
get exportMode()
|
|
{
|
|
return WI.FileUtilities.SaveMode.SingleFile;
|
|
}
|
|
|
|
canExport()
|
|
{
|
|
if (!WI.FileUtilities.canSave(this.exportMode))
|
|
return false;
|
|
|
|
if (this._capturing)
|
|
return false;
|
|
|
|
if (isNaN(this._startTime))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
// Testing
|
|
|
|
get markersForTesting() { return this._exportDataMarkers; }
|
|
|
|
// Private
|
|
|
|
_keyForRecord(record)
|
|
{
|
|
var key = record.type;
|
|
if (record instanceof WI.ScriptTimelineRecord || record instanceof WI.LayoutTimelineRecord)
|
|
key += ":" + record.eventType;
|
|
if (record instanceof WI.ScriptTimelineRecord && record.eventType === WI.ScriptTimelineRecord.EventType.EventDispatched)
|
|
key += ":" + record.details;
|
|
if (record instanceof WI.MediaTimelineRecord) {
|
|
key += ":" + record.eventType;
|
|
if (record.eventType === WI.MediaTimelineRecord.EventType.DOMEvent) {
|
|
if (record.domEvent && record.domEvent.eventName)
|
|
key += ":" + record.domEvent.eventName;
|
|
} else if (record.eventType === WI.MediaTimelineRecord.EventType.PowerEfficientPlaybackStateChanged)
|
|
key += ":" + (record.isPowerEfficient ? "enabled" : "disabled");
|
|
}
|
|
if (record.sourceCodeLocation)
|
|
key += ":" + record.sourceCodeLocation.lineNumber + ":" + record.sourceCodeLocation.columnNumber;
|
|
return key;
|
|
}
|
|
|
|
_timelineTimesUpdated(event)
|
|
{
|
|
var timeline = event.target;
|
|
var changed = false;
|
|
|
|
if (isNaN(this._startTime) || timeline.startTime < this._startTime) {
|
|
this._startTime = timeline.startTime;
|
|
changed = true;
|
|
}
|
|
|
|
if (isNaN(this._endTime) || this._endTime < timeline.endTime) {
|
|
this._endTime = timeline.endTime;
|
|
changed = true;
|
|
}
|
|
|
|
if (changed)
|
|
this.dispatchEventToListeners(WI.TimelineRecording.Event.TimesUpdated);
|
|
}
|
|
};
|
|
|
|
WI.TimelineRecording.Event = {
|
|
Reset: "timeline-recording-reset",
|
|
Unloaded: "timeline-recording-unloaded",
|
|
SourceCodeTimelineAdded: "timeline-recording-source-code-timeline-added",
|
|
InstrumentAdded: "timeline-recording-instrument-added",
|
|
InstrumentRemoved: "timeline-recording-instrument-removed",
|
|
TimesUpdated: "timeline-recording-times-updated",
|
|
MarkerAdded: "timeline-recording-marker-added",
|
|
};
|
|
|
|
WI.TimelineRecording.SerializationVersion = 1;
|
|
|
|
/* Models/URLBreakpoint.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.URLBreakpoint = class URLBreakpoint extends WI.Breakpoint
|
|
{
|
|
constructor(type, url, {disabled, actions, condition, ignoreCount, autoContinue} = {})
|
|
{
|
|
console.assert(Object.values(WI.URLBreakpoint.Type).includes(type), type);
|
|
console.assert(typeof url === "string", url);
|
|
|
|
super({disabled, actions, condition, ignoreCount, autoContinue});
|
|
|
|
this._type = type;
|
|
this._url = url;
|
|
}
|
|
|
|
// Static
|
|
|
|
static get supportsEditing()
|
|
{
|
|
// COMPATIBILITY (iOS 14): DOMDebugger.setURLBreakpoint did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setURLBreakpoint", "options");
|
|
}
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.URLBreakpoint(json.type, json.url, {
|
|
disabled: json.disabled,
|
|
condition: json.condition,
|
|
actions: json.actions?.map((actionJSON) => WI.BreakpointAction.fromJSON(actionJSON)) || [],
|
|
ignoreCount: json.ignoreCount,
|
|
autoContinue: json.autoContinue,
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get type() { return this._type; }
|
|
get url() { return this._url; }
|
|
|
|
get displayName()
|
|
{
|
|
if (this === WI.domDebuggerManager.allRequestsBreakpoint)
|
|
return WI.repeatedUIString.allRequests();
|
|
|
|
switch (this._type) {
|
|
case WI.URLBreakpoint.Type.Text:
|
|
return doubleQuotedString(this._url);
|
|
|
|
case WI.URLBreakpoint.Type.RegularExpression:
|
|
return "/" + this._url + "/";
|
|
}
|
|
|
|
console.assert();
|
|
return WI.UIString("URL");
|
|
}
|
|
|
|
get special()
|
|
{
|
|
return this === WI.domDebuggerManager.allRequestsBreakpoint || super.special;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
return WI.URLBreakpoint.supportsEditing || super.editable;
|
|
}
|
|
|
|
remove()
|
|
{
|
|
super.remove();
|
|
|
|
WI.domDebuggerManager.removeURLBreakpoint(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["url-breakpoint-type"] = this._type;
|
|
cookie["url-breakpoint-url"] = this._url;
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.type = this._type;
|
|
json.url = this._url;
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json[WI.objectStores.urlBreakpoints.keyPath] = this._type + ":" + this._url;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.URLBreakpoint.Type = {
|
|
Text: "text",
|
|
RegularExpression: "regex",
|
|
};
|
|
|
|
WI.URLBreakpoint.ReferencePage = WI.ReferencePage.URLBreakpoints;
|
|
|
|
/* Models/WebInspectorExtension.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020-2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WebInspectorExtension = class WebInspectorExtension
|
|
{
|
|
constructor(extensionID, extensionBundleIdentifier, displayName)
|
|
{
|
|
console.assert(typeof extensionID === "string", extensionID);
|
|
console.assert(typeof extensionBundleIdentifier === "string", extensionBundleIdentifier);
|
|
console.assert(typeof displayName === "string", displayName);
|
|
|
|
this._extensionID = extensionID;
|
|
this._extensionBundleIdentifier = extensionBundleIdentifier;
|
|
this._displayName = displayName;
|
|
}
|
|
|
|
// Public
|
|
|
|
get extensionID() { return this._extensionID; }
|
|
get extensionBundleIdentifier() { return this._extensionBundleIdentifier; }
|
|
get displayName() { return this._displayName; }
|
|
};
|
|
|
|
// Note: these values are synonymous with the values of enum class Inspector::ExtensionError.
|
|
WI.WebInspectorExtension.ErrorCode = {
|
|
ContextDestroyed: "ContextDestroyed",
|
|
InternalError: "InternalError",
|
|
InvalidRequest: "InvalidRequest",
|
|
RegistrationFailed: "RegistrationFailed",
|
|
NotImplemented: "NotImplemented",
|
|
};
|
|
|
|
/* Models/WebSocketResource.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WebSocketResource = class WebSocketResource extends WI.Resource
|
|
{
|
|
constructor(url, {loaderIdentifier, requestIdentifier, requestHeaders, timestamp, walltime, requestSentTimestamp} = {})
|
|
{
|
|
super(url, {
|
|
type: WI.Resource.Type.WebSocket,
|
|
loaderIdentifier,
|
|
requestIdentifier,
|
|
requestMethod: "GET",
|
|
requestHeaders,
|
|
requestSentTimestamp,
|
|
});
|
|
|
|
this._timestamp = timestamp;
|
|
this._walltime = walltime;
|
|
this._readyState = WI.WebSocketResource.ReadyState.Connecting;
|
|
this._frames = [];
|
|
}
|
|
|
|
// Public
|
|
|
|
get frames() { return this._frames; }
|
|
get walltime() { return this._walltime; }
|
|
|
|
get readyState()
|
|
{
|
|
return this._readyState;
|
|
}
|
|
|
|
set readyState(state)
|
|
{
|
|
if (state === this._readyState)
|
|
return;
|
|
|
|
let previousState = this._readyState;
|
|
this._readyState = state;
|
|
|
|
this.dispatchEventToListeners(WI.WebSocketResource.Event.ReadyStateChanged, {previousState, state});
|
|
}
|
|
|
|
addFrame(data, payloadLength, isOutgoing, opcode, timestamp, elapsedTime)
|
|
{
|
|
let frameData;
|
|
|
|
// Binary data is never shown in the UI, don't clog memory with it.
|
|
if (opcode === WI.WebSocketResource.OpCodes.BinaryFrame)
|
|
frameData = null;
|
|
else
|
|
frameData = data;
|
|
|
|
let frame = {data: frameData, isOutgoing, opcode, walltime: this._walltimeForWebSocketTimestamp(timestamp)};
|
|
this._frames.push(frame);
|
|
if (InspectorFrontendHost.isUnderTest())
|
|
frame.dataForTest = data;
|
|
|
|
this.increaseSize(payloadLength, elapsedTime);
|
|
|
|
this.dispatchEventToListeners(WI.WebSocketResource.Event.FrameAdded, frame);
|
|
}
|
|
|
|
// Protected
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
console.assert(false, "A WebSocket's content was requested. WebSockets do not have content so the request is nonsensical.");
|
|
|
|
return super.requestContentFromBackend();
|
|
}
|
|
|
|
// Private
|
|
|
|
_walltimeForWebSocketTimestamp(timestamp)
|
|
{
|
|
return this._walltime + (timestamp - this._timestamp);
|
|
}
|
|
};
|
|
|
|
WI.WebSocketResource.Event = {
|
|
FrameAdded: "web-socket-frame-added",
|
|
ReadyStateChanged: "web-socket-resource-ready-state-changed",
|
|
};
|
|
|
|
WI.WebSocketResource.ReadyState = {
|
|
Closed: Symbol("closed"),
|
|
Connecting: Symbol("connecting"),
|
|
Open: Symbol("open"),
|
|
};
|
|
|
|
WI.WebSocketResource.OpCodes = {
|
|
ContinuationFrame: 0,
|
|
TextFrame: 1,
|
|
BinaryFrame: 2,
|
|
ConnectionCloseFrame: 8,
|
|
PingFrame: 9,
|
|
PongFrame: 10,
|
|
};
|
|
|
|
/* Models/WrappedPromise.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WrappedPromise = class WrappedPromise
|
|
{
|
|
constructor(work)
|
|
{
|
|
this._settled = false;
|
|
this._promise = new Promise((resolve, reject) => {
|
|
this._resolveCallback = resolve;
|
|
this._rejectCallback = reject;
|
|
|
|
// Allow work to resolve or reject the promise by shimming our
|
|
// internal callbacks. This ensures that this._settled gets set properly.
|
|
if (work && typeof work === "function")
|
|
return work(this.resolve.bind(this), this.reject.bind(this));
|
|
});
|
|
}
|
|
|
|
// Public
|
|
|
|
get settled()
|
|
{
|
|
return this._settled;
|
|
}
|
|
|
|
get promise()
|
|
{
|
|
return this._promise;
|
|
}
|
|
|
|
resolve(value)
|
|
{
|
|
if (this._settled)
|
|
throw new Error("Promise is already settled, cannot call resolve().");
|
|
|
|
this._settled = true;
|
|
this._resolveCallback(value);
|
|
}
|
|
|
|
reject(value)
|
|
{
|
|
if (this._settled)
|
|
throw new Error("Promise is already settled, cannot call reject().");
|
|
|
|
this._settled = true;
|
|
this._rejectCallback(value);
|
|
}
|
|
};
|
|
|
|
/* Models/LocalResource.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017-2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NOEVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// LocalResource is a complete resource constructed by the frontend. It
|
|
// does not need to be tied to an object in the backend. The local resource
|
|
// does not need to be complete at creation and can get its content later.
|
|
//
|
|
// Construction values try to mimic protocol inputs to WI.Resource:
|
|
//
|
|
// request: { url, method, headers, timestamp, walltime, finishedTimestamp, data }
|
|
// response: { mimeType, headers, statusCode, statusText, failureReasonText, content, base64Encoded }
|
|
// metrics: { responseSource, protocol, priority, remoteAddress, connectionIdentifier, sizes }
|
|
// timing: { startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd }
|
|
|
|
WI.LocalResource = class LocalResource extends WI.Resource
|
|
{
|
|
constructor({request, response, metrics, timing, mappedFilePath})
|
|
{
|
|
console.assert(request);
|
|
console.assert(typeof request.url === "string");
|
|
console.assert(response);
|
|
|
|
metrics = metrics || {};
|
|
timing = timing || {};
|
|
|
|
super(request.url, {
|
|
mimeType: response.mimeType || (response.headers || {}).valueForCaseInsensitiveKey("Content-Type") || null,
|
|
requestMethod: request.method,
|
|
requestHeaders: request.headers,
|
|
requestData: request.data,
|
|
requestSentTimestamp: request.timestamp,
|
|
requestSentWalltime: request.walltime,
|
|
});
|
|
|
|
// NOTE: This directly overwrites WI.Resource properties.
|
|
this._finishedOrFailedTimestamp = request.finishedTimestamp || NaN;
|
|
this._statusCode = response.statusCode || NaN;
|
|
this._statusText = response.statusText || null;
|
|
this._responseHeaders = response.headers || {};
|
|
this._failureReasonText = response.failureReasonText || null;
|
|
this._timingData = new WI.ResourceTimingData(this, timing);
|
|
|
|
this._responseSource = metrics.responseSource || WI.Resource.ResponseSource.Unknown;
|
|
this._protocol = metrics.protocol || null;
|
|
this._priority = metrics.priority || WI.Resource.NetworkPriority.Unknown;
|
|
this._remoteAddress = metrics.remoteAddress || null;
|
|
this._connectionIdentifier = metrics.connectionIdentifier || null;
|
|
this._requestHeadersTransferSize = !isNaN(metrics.requestHeaderBytesSent) ? metrics.requestHeaderBytesSent : NaN;
|
|
this._requestBodyTransferSize = !isNaN(metrics.requestBodyBytesSent) ? metrics.requestBodyBytesSent : NaN;
|
|
this._responseHeadersTransferSize = !isNaN(metrics.responseHeaderBytesReceived) ? metrics.responseHeaderBytesReceived : NaN;
|
|
this._responseBodyTransferSize = !isNaN(metrics.responseBodyBytesReceived) ? metrics.responseBodyBytesReceived : NaN;
|
|
this._responseBodySize = !isNaN(metrics.responseBodyDecodedSize) ? metrics.responseBodyDecodedSize : NaN;
|
|
this._isProxyConnection = !!metrics.isProxyConnection;
|
|
|
|
// Set by `WI.LocalResourceOverride`.
|
|
this._localResourceOverride = null;
|
|
|
|
// Finalize WI.Resource.
|
|
this._finished = true;
|
|
this._failed = false; // FIXME: How should we denote a failure? Assume from status code / failure reason?
|
|
this._cached = false; // FIXME: How should we denote cached? Assume from response source?
|
|
|
|
// Finalize WI.SourceCode.
|
|
let content = response.content || "";
|
|
let base64Encoded = response.base64Encoded || false;
|
|
this._originalRevision = new WI.SourceCodeRevision(this, content, base64Encoded, this._mimeType);
|
|
this._currentRevision = this._originalRevision;
|
|
|
|
this._mappedFilePath = mappedFilePath || null;
|
|
}
|
|
|
|
// Static
|
|
|
|
static canMapToFile()
|
|
{
|
|
return InspectorFrontendHost.canLoad();
|
|
}
|
|
|
|
static resetPathsThatFailedToLoadFromFileSystem()
|
|
{
|
|
WI.LocalResource._pathsThatFailedToLoadFromFileSystem.clear();
|
|
}
|
|
|
|
static headersArrayToHeadersObject(headers)
|
|
{
|
|
let result = {};
|
|
if (headers) {
|
|
for (let {name, value} of headers)
|
|
result[name] = value;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static fromHAREntry(entry, archiveStartWalltime)
|
|
{
|
|
// FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info
|
|
|
|
let {request, response, startedDateTime, timings} = entry;
|
|
let requestSentWalltime = WI.HARBuilder.dateFromHARDate(startedDateTime) / 1000;
|
|
let requestSentTimestamp = requestSentWalltime - archiveStartWalltime;
|
|
let finishedTimestamp = NaN;
|
|
|
|
let timing = {
|
|
startTime: NaN,
|
|
domainLookupStart: NaN,
|
|
domainLookupEnd: NaN,
|
|
connectStart: NaN,
|
|
connectEnd: NaN,
|
|
secureConnectionStart: NaN,
|
|
requestStart: NaN,
|
|
responseStart: NaN,
|
|
responseEnd: NaN,
|
|
};
|
|
|
|
if (!isNaN(requestSentWalltime) && !isNaN(archiveStartWalltime)) {
|
|
let hasBlocked = timings.blocked !== -1;
|
|
let hasDNS = timings.dns !== -1;
|
|
let hasConnect = timings.connect !== -1;
|
|
let hasSecureConnect = timings.ssl !== -1;
|
|
|
|
// FIXME: <https://webkit.org/b/195692> Web Inspector: ResourceTimingData should allow a startTime of 0
|
|
timing.startTime = requestSentTimestamp || Number.EPSILON;
|
|
timing.fetchStart = timing.startTime;
|
|
|
|
let accumulation = timing.startTime;
|
|
|
|
if (hasBlocked)
|
|
accumulation += (timings.blocked / 1000);
|
|
|
|
if (hasDNS) {
|
|
timing.domainLookupStart = accumulation;
|
|
accumulation += (timings.dns / 1000);
|
|
timing.domainLookupEnd = accumulation;
|
|
}
|
|
|
|
if (hasConnect) {
|
|
timing.connectStart = accumulation;
|
|
accumulation += (timings.connect / 1000);
|
|
timing.connectEnd = accumulation;
|
|
|
|
if (hasSecureConnect)
|
|
timing.secureConnectionStart = timing.connectEnd - (timings.ssl / 1000);
|
|
}
|
|
|
|
accumulation += (timings.send / 1000);
|
|
timing.requestStart = accumulation;
|
|
|
|
accumulation += (timings.wait / 1000);
|
|
timing.responseStart = accumulation;
|
|
|
|
accumulation += (timings.receive / 1000);
|
|
timing.responseEnd = accumulation;
|
|
|
|
finishedTimestamp = timing.responseEnd;
|
|
}
|
|
|
|
let serverAddress = entry.serverIPAddress || null;
|
|
if (serverAddress && typeof entry._serverPort === "number")
|
|
serverAddress += ":" + entry._serverPort;
|
|
|
|
return new WI.LocalResource({
|
|
request: {
|
|
url: request.url,
|
|
method: request.method,
|
|
headers: LocalResource.headersArrayToHeadersObject(request.headers),
|
|
timestamp: requestSentTimestamp,
|
|
walltime: requestSentWalltime,
|
|
finishedTimestamp: finishedTimestamp,
|
|
data: request.postData ? request.postData.text : null,
|
|
},
|
|
response: {
|
|
headers: LocalResource.headersArrayToHeadersObject(response.headers),
|
|
mimeType: response.content.mimeType,
|
|
statusCode: response.status,
|
|
statusText: response.statusText,
|
|
failureReasonText: response._error || null,
|
|
content: response.content.text,
|
|
base64Encoded: response.content.encoding === "base64",
|
|
},
|
|
metrics: {
|
|
responseSource: WI.HARBuilder.responseSourceFromHARFetchType(entry._fetchType),
|
|
protocol: WI.HARBuilder.protocolFromHARProtocol(response.httpVersion),
|
|
priority: WI.HARBuilder.networkPriorityFromHARPriority(entry._priority),
|
|
remoteAddress: serverAddress,
|
|
connectionIdentifier: entry.connection ? parseInt(entry.connection) : null,
|
|
requestHeaderBytesSent: request.headersSize >= 0 ? request.headersSize : NaN,
|
|
requestBodyBytesSent: request.bodySize >= 0 ? request.bodySize : NaN,
|
|
responseHeaderBytesReceived: response.headersSize >= 0 ? response.headersSize : NaN,
|
|
responseBodyBytesReceived: response.bodySize >= 0 ? response.bodySize : NaN,
|
|
responseBodyDecodedSize: response.content.size || NaN,
|
|
},
|
|
timing,
|
|
});
|
|
}
|
|
|
|
// Import / Export
|
|
|
|
static fromJSON(json)
|
|
{
|
|
return new WI.LocalResource(json);
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
return {
|
|
request: {
|
|
url: this.url,
|
|
method: this.requestMethod,
|
|
headers: this.requestHeaders,
|
|
data: this.requestData,
|
|
},
|
|
response: {
|
|
headers: this.responseHeaders,
|
|
mimeType: this.mimeType,
|
|
statusCode: this.statusCode,
|
|
statusText: this.statusText,
|
|
content: this.currentRevision.content,
|
|
base64Encoded: this.currentRevision.base64Encoded,
|
|
},
|
|
mappedFilePath: this._mappedFilePath,
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get localResourceOverride() { return this._localResourceOverride; }
|
|
|
|
get mappedFilePath()
|
|
{
|
|
return this._mappedFilePath;
|
|
}
|
|
|
|
set mappedFilePath(mappedFilePath)
|
|
{
|
|
console.assert(WI.LocalResource.canMapToFile());
|
|
console.assert(mappedFilePath);
|
|
|
|
if (mappedFilePath === this._mappedFilePath)
|
|
return;
|
|
|
|
this._mappedFilePath = mappedFilePath;
|
|
|
|
const forceUpdate = true;
|
|
this._updateContentFromFileSystem(forceUpdate).then(() => {
|
|
this.dispatchEventToListeners(WI.LocalResource.Event.MappedFilePathChanged);
|
|
});
|
|
}
|
|
|
|
get isMappedToDirectory()
|
|
{
|
|
return this._mappedFilePath?.endsWith("/");
|
|
}
|
|
|
|
async requestContentFromMappedDirectory(subpath)
|
|
{
|
|
return this._loadFromFileSystem({subpath});
|
|
}
|
|
|
|
// Protected
|
|
|
|
async requestContent()
|
|
{
|
|
await this._updateContentFromFileSystem();
|
|
|
|
return super.requestContent();
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
return Promise.resolve({
|
|
content: this._originalRevision.content,
|
|
base64Encoded: this._originalRevision.base64Encoded,
|
|
});
|
|
}
|
|
|
|
handleCurrentRevisionContentChange()
|
|
{
|
|
if (this._mimeType !== this.currentRevision.mimeType) {
|
|
let oldMIMEType = this._mimeType;
|
|
this._mimeType = this.currentRevision.mimeType;
|
|
this.dispatchEventToListeners(WI.Resource.Event.MIMETypeDidChange, {oldMIMEType});
|
|
}
|
|
}
|
|
|
|
// Private
|
|
|
|
async _loadFromFileSystem({subpath} = {})
|
|
{
|
|
let path = this._mappedFilePath;
|
|
if (!path)
|
|
return null;
|
|
|
|
if (this.isMappedToDirectory) {
|
|
if (!subpath)
|
|
return null;
|
|
|
|
path += subpath;
|
|
}
|
|
|
|
let content = null;
|
|
try {
|
|
content = await InspectorFrontendHost.load(path);
|
|
} catch { }
|
|
|
|
if (typeof content === "string")
|
|
WI.LocalResource._pathsThatFailedToLoadFromFileSystem.delete(path);
|
|
else if (!WI.LocalResource._pathsThatFailedToLoadFromFileSystem.has(path)) {
|
|
WI.LocalResource._pathsThatFailedToLoadFromFileSystem.add(path);
|
|
|
|
let message = WI.UIString("Local Override: could not load \u201C%s\u201D").format(path);
|
|
|
|
if (window.InspectorTest)
|
|
console.warn(message);
|
|
else {
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
async _updateContentFromFileSystem(forceUpdate)
|
|
{
|
|
let content = await this._loadFromFileSystem();
|
|
if (typeof content !== "string")
|
|
return;
|
|
|
|
if (!forceUpdate && content === this.currentRevision.content)
|
|
return;
|
|
|
|
this.editableRevision.updateRevisionContent(content);
|
|
}
|
|
};
|
|
|
|
WI.LocalResource._pathsThatFailedToLoadFromFileSystem = new Set;
|
|
|
|
WI.LocalResource.Event = {
|
|
MappedFilePathChanged: "local-resource-mapped-file-path-changed",
|
|
};
|
|
|
|
/* Models/SourceMapResource.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SourceMapResource = class SourceMapResource extends WI.Resource
|
|
{
|
|
constructor(url, sourceMap)
|
|
{
|
|
super(url);
|
|
|
|
console.assert(url);
|
|
console.assert(sourceMap);
|
|
|
|
this._sourceMap = sourceMap;
|
|
|
|
var inheritedMIMEType = this._sourceMap.originalSourceCode instanceof WI.Resource ? this._sourceMap.originalSourceCode.syntheticMIMEType : null;
|
|
|
|
let fileExtension = WI.fileExtensionForURL(url) || "";
|
|
|
|
// React serves JSX resources with "js" extension.
|
|
let fileExtensionMIMEType = fileExtension === "js" ? "text/jsx" : WI.mimeTypeForFileExtension(fileExtension, true);
|
|
|
|
// FIXME: This is a layering violation. It should use a helper function on the
|
|
// Resource base-class to set _mimeType and _type.
|
|
this._mimeType = fileExtensionMIMEType || inheritedMIMEType || "text/javascript";
|
|
this._type = WI.Resource.typeFromMIMEType(this._mimeType);
|
|
|
|
// Mark the resource as loaded so it does not show a spinner in the sidebar.
|
|
// We will really load the resource the first time content is requested.
|
|
this.markAsFinished();
|
|
}
|
|
|
|
// Public
|
|
|
|
get sourceMap() { return this._sourceMap; }
|
|
|
|
get sourceMapDisplaySubpath()
|
|
{
|
|
var sourceMappingBasePathURLComponents = this._sourceMap.sourceMappingBasePathURLComponents;
|
|
var resourceURLComponents = this.urlComponents;
|
|
|
|
// Fallback for JavaScript debuggable named scripts that may not have a complete URL.
|
|
if (!resourceURLComponents.path)
|
|
resourceURLComponents.path = this.url;
|
|
|
|
// Different schemes / hosts. Return the host + path of this resource.
|
|
if (resourceURLComponents.scheme !== sourceMappingBasePathURLComponents.scheme || resourceURLComponents.host !== sourceMappingBasePathURLComponents.host) {
|
|
let subpath = "";
|
|
if (resourceURLComponents.host) {
|
|
subpath += resourceURLComponents.host;
|
|
if (resourceURLComponents.port)
|
|
subpath += ":" + resourceURLComponents.port;
|
|
subpath += resourceURLComponents.path;
|
|
} else {
|
|
// Remove the leading "/" so there isn't an empty folder.
|
|
subpath += resourceURLComponents.path.substring(1);
|
|
}
|
|
return subpath;
|
|
}
|
|
|
|
// Same host, but not a subpath of the base. This implies a ".." in the relative path.
|
|
if (!resourceURLComponents.path.startsWith(sourceMappingBasePathURLComponents.path))
|
|
return relativePath(resourceURLComponents.path, sourceMappingBasePathURLComponents.path);
|
|
|
|
// Same host. Just a subpath of the base.
|
|
return resourceURLComponents.path.substring(sourceMappingBasePathURLComponents.path.length, resourceURLComponents.length);
|
|
}
|
|
|
|
get supportsScriptBlackboxing()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
requestContentFromBackend()
|
|
{
|
|
// Revert the markAsFinished that was done in the constructor.
|
|
this.revertMarkAsFinished();
|
|
|
|
var inlineContent = this._sourceMap.sourceContent(this.url);
|
|
if (inlineContent) {
|
|
// Force inline content to be asynchronous to match the expected load pattern.
|
|
// FIXME: We don't know the MIME-type for inline content. Guess by analyzing the content?
|
|
// Returns a promise.
|
|
return Promise.resolve().then(sourceMapResourceLoaded.bind(this, {content: inlineContent, mimeType: this.mimeType, statusCode: 200}));
|
|
}
|
|
|
|
function sourceMapResourceNotAvailable(error, content, mimeType, statusCode)
|
|
{
|
|
this.markAsFailed();
|
|
return Promise.resolve({
|
|
error: WI.UIString("An error occurred trying to load the resource."),
|
|
content,
|
|
mimeType,
|
|
statusCode
|
|
});
|
|
}
|
|
|
|
function sourceMapResourceLoadError(error)
|
|
{
|
|
console.error(error || "There was an unknown error calling Network.loadResource.");
|
|
this.markAsFailed();
|
|
return Promise.resolve({error: WI.UIString("An error occurred trying to load the resource.")});
|
|
}
|
|
|
|
function sourceMapResourceLoaded(parameters)
|
|
{
|
|
var {error, content, mimeType, statusCode} = parameters;
|
|
|
|
var base64encoded = false;
|
|
|
|
if (statusCode >= 400 || error)
|
|
return sourceMapResourceNotAvailable(error, content, mimeType, statusCode);
|
|
|
|
// FIXME: Add support for picking the best MIME-type. Right now the file extension is the best bet.
|
|
// The constructor set MIME-type based on the file extension and we ignore mimeType here.
|
|
|
|
this.markAsFinished();
|
|
|
|
return Promise.resolve({
|
|
content,
|
|
mimeType,
|
|
base64encoded,
|
|
statusCode
|
|
});
|
|
}
|
|
|
|
if (!this._target.hasCommand("Network.loadResource"))
|
|
return sourceMapResourceLoadError.call(this);
|
|
|
|
var frameIdentifier = null;
|
|
if (this._sourceMap.originalSourceCode instanceof WI.Resource && this._sourceMap.originalSourceCode.parentFrame)
|
|
frameIdentifier = this._sourceMap.originalSourceCode.parentFrame.id;
|
|
|
|
if (!frameIdentifier)
|
|
frameIdentifier = WI.networkManager.mainFrame ? WI.networkManager.mainFrame.id : "";
|
|
|
|
return this._target.NetworkAgent.loadResource(frameIdentifier, this.url).then(sourceMapResourceLoaded.bind(this)).catch(sourceMapResourceLoadError.bind(this));
|
|
}
|
|
|
|
createSourceCodeLocation(lineNumber, columnNumber)
|
|
{
|
|
// SourceCodeLocations are always constructed with raw resources and raw locations. Lookup the raw location.
|
|
var entry = this._sourceMap.findEntryReversed(this.url, lineNumber);
|
|
var rawLineNumber = entry[0];
|
|
var rawColumnNumber = entry[1];
|
|
|
|
// If the raw location is an inline script we need to include that offset.
|
|
var originalSourceCode = this._sourceMap.originalSourceCode;
|
|
if (originalSourceCode instanceof WI.Script) {
|
|
if (rawLineNumber === 0)
|
|
rawColumnNumber += originalSourceCode.range.startColumn;
|
|
rawLineNumber += originalSourceCode.range.startLine;
|
|
}
|
|
|
|
// Create the SourceCodeLocation and since we already know the the mapped location set it directly.
|
|
var location = originalSourceCode.createSourceCodeLocation(rawLineNumber, rawColumnNumber);
|
|
location._setMappedLocation(this, lineNumber, columnNumber);
|
|
return location;
|
|
}
|
|
|
|
createSourceCodeTextRange(textRange)
|
|
{
|
|
// SourceCodeTextRanges are always constructed with raw resources and raw locations.
|
|
// However, we can provide the most accurate mapped locations in construction.
|
|
var startSourceCodeLocation = this.createSourceCodeLocation(textRange.startLine, textRange.startColumn);
|
|
var endSourceCodeLocation = this.createSourceCodeLocation(textRange.endLine, textRange.endColumn);
|
|
return new WI.SourceCodeTextRange(this._sourceMap.originalSourceCode, startSourceCodeLocation, endSourceCodeLocation);
|
|
}
|
|
};
|
|
|
|
/* Models/AuditTestBase.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestBase = class AuditTestBase extends WI.Object
|
|
{
|
|
constructor(name, {description, supports, setup, disabled} = {})
|
|
{
|
|
console.assert(typeof name === "string", name);
|
|
console.assert(!description || typeof description === "string", description);
|
|
console.assert(supports === undefined || typeof supports === "number", supports);
|
|
console.assert(!setup || typeof setup === "string", setup);
|
|
console.assert(disabled === undefined || typeof disabled === "boolean", disabled);
|
|
|
|
super();
|
|
|
|
// This class should not be instantiated directly. Create a concrete subclass instead.
|
|
console.assert(this.constructor !== WI.AuditTestBase && this instanceof WI.AuditTestBase, this);
|
|
|
|
this._name = name;
|
|
this._description = description || "";
|
|
this._supports = supports ?? NaN;
|
|
this._setup = setup || "";
|
|
|
|
this.determineIfSupported({warn: true});
|
|
|
|
this._runningState = disabled ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
|
|
this._result = null;
|
|
|
|
this._parent = null;
|
|
|
|
this._default = false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get runningState() { return this._runningState; }
|
|
get result() { return this._result; }
|
|
get supported() { return this._supported; }
|
|
|
|
get name()
|
|
{
|
|
return this._name;
|
|
}
|
|
|
|
set name(name)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(WI.auditManager.editing);
|
|
console.assert(name && typeof name === "string", name);
|
|
|
|
if (name === this._name)
|
|
return;
|
|
|
|
let oldName = this._name;
|
|
this._name = name;
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.NameChanged, {oldName});
|
|
}
|
|
|
|
get description()
|
|
{
|
|
return this._description;
|
|
}
|
|
|
|
set description(description)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(WI.auditManager.editing);
|
|
console.assert(typeof description === "string", description);
|
|
|
|
if (description === this._description)
|
|
return;
|
|
|
|
this._description = description;
|
|
}
|
|
|
|
get supports()
|
|
{
|
|
return this._supports;
|
|
}
|
|
|
|
set supports(supports)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(WI.auditManager.editing);
|
|
console.assert(typeof supports === "number", supports);
|
|
|
|
if (supports === this._supports)
|
|
return;
|
|
|
|
this._supports = supports;
|
|
this.determineIfSupported();
|
|
}
|
|
|
|
get setup()
|
|
{
|
|
return this._setup;
|
|
}
|
|
|
|
set setup(setup)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(typeof setup === "string", setup);
|
|
|
|
if (setup === this._setup)
|
|
return;
|
|
|
|
this._setup = setup;
|
|
|
|
this.clearResult();
|
|
}
|
|
|
|
get disabled()
|
|
{
|
|
return this._runningState === WI.AuditManager.RunningState.Disabled;
|
|
}
|
|
|
|
set disabled(disabled)
|
|
{
|
|
this.updateDisabled(disabled);
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
return !this._default;
|
|
}
|
|
|
|
get default()
|
|
{
|
|
return this._default;
|
|
}
|
|
|
|
markAsDefault()
|
|
{
|
|
console.assert(!this._default);
|
|
this._default = true;
|
|
}
|
|
|
|
get topLevelTest()
|
|
{
|
|
let test = this;
|
|
while (test._parent)
|
|
test = test._parent;
|
|
return test;
|
|
}
|
|
|
|
async runSetup()
|
|
{
|
|
console.assert(this.topLevelTest === this);
|
|
|
|
if (!this._setup)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
let agentCommandFunction = null;
|
|
let agentCommandArguments = {};
|
|
if (target.hasDomain("Audit")) {
|
|
agentCommandFunction = target.AuditAgent.run;
|
|
agentCommandArguments.test = this._setup;
|
|
} else {
|
|
agentCommandFunction = target.RuntimeAgent.evaluate;
|
|
agentCommandArguments.expression = `(function() { "use strict"; return eval(\`(${this._setup.replace(/`/g, "\\`")})\`)(); })()`;
|
|
agentCommandArguments.objectGroup = AuditTestBase.ObjectGroup;
|
|
agentCommandArguments.doNotPauseOnExceptionsAndMuteConsole = true;
|
|
}
|
|
|
|
try {
|
|
let response = await agentCommandFunction.invoke(agentCommandArguments);
|
|
|
|
if (response.result.type === "object" && response.result.className === "Promise") {
|
|
if (WI.RuntimeManager.supportsAwaitPromise())
|
|
response = await target.RuntimeAgent.awaitPromise(response.result.objectId);
|
|
else {
|
|
response = null;
|
|
WI.AuditManager.synthesizeError(WI.UIString("Async audits are not supported."));
|
|
}
|
|
}
|
|
|
|
if (response) {
|
|
let remoteObject = WI.RemoteObject.fromPayload(response.result, WI.mainTarget);
|
|
if (response.wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error"))
|
|
WI.AuditManager.synthesizeError(remoteObject.description);
|
|
}
|
|
} catch (error) {
|
|
WI.AuditManager.synthesizeError(error.message);
|
|
}
|
|
}
|
|
|
|
async start()
|
|
{
|
|
// Called from WI.AuditManager.
|
|
|
|
if (!this._supported || this.disabled)
|
|
return;
|
|
|
|
console.assert(WI.auditManager.runningState === WI.AuditManager.RunningState.Active);
|
|
|
|
console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
|
|
if (this._runningState !== WI.AuditManager.RunningState.Inactive)
|
|
return;
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Active;
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Scheduled);
|
|
|
|
await this.run();
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Inactive;
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Completed);
|
|
}
|
|
|
|
stop()
|
|
{
|
|
// Called from WI.AuditManager.
|
|
// Overridden by sub-classes if needed.
|
|
|
|
if (!this._supported || this.disabled)
|
|
return;
|
|
|
|
console.assert(WI.auditManager.runningState === WI.AuditManager.RunningState.Stopping);
|
|
|
|
if (this._runningState !== WI.AuditManager.RunningState.Active)
|
|
return;
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Stopping;
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Stopping);
|
|
}
|
|
|
|
clearResult(options = {})
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
if (!this._result)
|
|
return false;
|
|
|
|
this._result = null;
|
|
|
|
if (!options.suppressResultChangedEvent)
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.ResultChanged);
|
|
|
|
return true;
|
|
}
|
|
|
|
async clone()
|
|
{
|
|
console.assert(WI.auditManager.editing);
|
|
|
|
return this.constructor.fromPayload(this.toJSON());
|
|
}
|
|
|
|
remove()
|
|
{
|
|
console.assert(WI.auditManager.editing);
|
|
|
|
if (!this._parent || this._default) {
|
|
WI.auditManager.removeTest(this);
|
|
return;
|
|
}
|
|
|
|
console.assert(this.editable);
|
|
console.assert(this._parent instanceof WI.AuditTestGroup);
|
|
this._parent.removeTest(this);
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
let path = [];
|
|
let test = this;
|
|
while (test) {
|
|
path.push(test.name);
|
|
test = test._parent;
|
|
}
|
|
path.reverse();
|
|
|
|
cookie["audit-path"] = path.join(",");
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
let json = {
|
|
type: this.constructor.TypeIdentifier,
|
|
name: this._name,
|
|
};
|
|
if (this._description)
|
|
json.description = this._description;
|
|
if (!isNaN(this._supports))
|
|
json.supports = Number.isFinite(this._supports) ? this._supports : WI.AuditTestBase.Version + 1;
|
|
if (this._setup)
|
|
json.setup = this._setup;
|
|
if (key === WI.ObjectStore.toJSONSymbol)
|
|
json.disabled = this.disabled;
|
|
return json;
|
|
}
|
|
|
|
// Protected
|
|
|
|
async run()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
determineIfSupported(options = {})
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
let supportedBefore = this._supported;
|
|
|
|
if (this._supports > WI.AuditTestBase.Version) {
|
|
this.updateSupported(false, options);
|
|
|
|
if (options.warn && supportedBefore !== this._supported && Number.isFinite(this._supports))
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 is too new to run in this Web Inspector").format(this.name));
|
|
} else if (InspectorBackend.hasDomain("Audit") && this._supports > InspectorBackend.getVersion("Audit")) {
|
|
this.updateSupported(false, options);
|
|
|
|
if (options.warn && supportedBefore !== this._supported && Number.isFinite(this._supports))
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 is too new to run in the inspected page").format(this.name));
|
|
} else
|
|
this.updateSupported(true, options);
|
|
|
|
return this._supported;
|
|
}
|
|
|
|
updateSupported(supported, options = {})
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
if (supported === this._supported)
|
|
return;
|
|
|
|
this._supported = supported;
|
|
|
|
if (!options.silent)
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.SupportedChanged);
|
|
|
|
if (!this._supported)
|
|
this.clearResult();
|
|
}
|
|
|
|
updateDisabled(disabled, options = {})
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
console.assert(this._runningState === WI.AuditManager.RunningState.Disabled || this._runningState === WI.AuditManager.RunningState.Inactive);
|
|
if (this._runningState !== WI.AuditManager.RunningState.Disabled && this._runningState !== WI.AuditManager.RunningState.Inactive)
|
|
return;
|
|
|
|
let runningState = disabled ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
|
|
if (runningState === this._runningState)
|
|
return;
|
|
|
|
this._runningState = runningState;
|
|
|
|
if (!options.silent)
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.DisabledChanged);
|
|
|
|
if (this.disabled)
|
|
this.clearResult();
|
|
}
|
|
|
|
updateResult(result)
|
|
{
|
|
// Overridden by sub-classes if needed.
|
|
|
|
console.assert(result instanceof WI.AuditTestResultBase, result);
|
|
|
|
this._result = result;
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.ResultChanged);
|
|
}
|
|
};
|
|
|
|
// Keep this in sync with Inspector::Protocol::Audit::VERSION.
|
|
WI.AuditTestBase.Version = 4;
|
|
|
|
WI.AuditTestBase.ObjectGroup = "audit";
|
|
|
|
WI.AuditTestBase.Event = {
|
|
Completed: "audit-test-base-completed",
|
|
DisabledChanged: "audit-test-base-disabled-changed",
|
|
NameChanged: "audit-test-base-name-changed",
|
|
Progress: "audit-test-base-progress",
|
|
ResultChanged: "audit-test-base-result-changed",
|
|
Scheduled: "audit-test-base-scheduled",
|
|
Stopping: "audit-test-base-stopping",
|
|
SupportedChanged: "audit-test-base-supported-changed",
|
|
TestChanged: "audit-test-base-test-changed",
|
|
};
|
|
|
|
/* Models/AuditTestCase.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestCase = class AuditTestCase extends WI.AuditTestBase
|
|
{
|
|
constructor(name, test, options = {})
|
|
{
|
|
console.assert(typeof test === "string", test);
|
|
|
|
super(name, options);
|
|
|
|
this._test = test;
|
|
}
|
|
|
|
// Static
|
|
|
|
static async fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (payload.type !== WI.AuditTestCase.TypeIdentifier)
|
|
return null;
|
|
|
|
if (typeof payload.name !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("name")));
|
|
return null;
|
|
}
|
|
|
|
if (typeof payload.test !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("test")));
|
|
return null;
|
|
}
|
|
|
|
let options = {};
|
|
|
|
if (typeof payload.description === "string")
|
|
options.description = payload.description;
|
|
else if ("description" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("description")));
|
|
|
|
if (typeof payload.supports === "number")
|
|
options.supports = payload.supports;
|
|
else if ("supports" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-number \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("supports")));
|
|
|
|
if (typeof payload.setup === "string")
|
|
options.setup = payload.setup;
|
|
else if ("setup" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("setup")));
|
|
|
|
if (typeof payload.disabled === "boolean")
|
|
options.disabled = payload.disabled;
|
|
|
|
return new WI.AuditTestCase(payload.name, payload.test, options);
|
|
}
|
|
|
|
// Public
|
|
|
|
get test()
|
|
{
|
|
return this._test;
|
|
}
|
|
|
|
set test(test)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(typeof test === "string", test);
|
|
|
|
if (test === this._test)
|
|
return;
|
|
|
|
this._test = test;
|
|
|
|
this.clearResult();
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.TestChanged);
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.test = this._test;
|
|
return json;
|
|
}
|
|
|
|
// Protected
|
|
|
|
async run()
|
|
{
|
|
const levelStrings = Object.values(WI.AuditTestCaseResult.Level);
|
|
let level = null;
|
|
let data = {};
|
|
let metadata = {
|
|
url: WI.networkManager.mainFrame.url,
|
|
startTimestamp: null,
|
|
endTimestamp: null,
|
|
};
|
|
let resolvedDOMNodes = null;
|
|
|
|
function setLevel(newLevel) {
|
|
let newLevelIndex = levelStrings.indexOf(newLevel);
|
|
if (newLevelIndex < 0) {
|
|
addError(WI.UIString("Return string must be one of %s").format(JSON.stringify(levelStrings)));
|
|
return;
|
|
}
|
|
|
|
if (newLevelIndex <= levelStrings.indexOf(level))
|
|
return;
|
|
|
|
level = newLevel;
|
|
}
|
|
|
|
function addError(value) {
|
|
setLevel(WI.AuditTestCaseResult.Level.Error);
|
|
|
|
if (!data.errors)
|
|
data.errors = [];
|
|
|
|
data.errors.push(value);
|
|
}
|
|
|
|
async function parseResponse(response) {
|
|
let remoteObject = WI.RemoteObject.fromPayload(response.result, WI.mainTarget);
|
|
if (response.wasThrown || (remoteObject.type === "object" && remoteObject.subtype === "error")) {
|
|
addError(remoteObject.description);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "boolean") {
|
|
setLevel(remoteObject.value ? WI.AuditTestCaseResult.Level.Pass : WI.AuditTestCaseResult.Level.Fail);
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type === "string") {
|
|
setLevel(remoteObject.value.trim().toLowerCase());
|
|
return;
|
|
}
|
|
|
|
if (remoteObject.type !== "object" || remoteObject.subtype) {
|
|
addError(WI.UIString("Return value is not an object, string, or boolean"));
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
ownProperties: true,
|
|
};
|
|
|
|
function checkResultProperty(key, value, type, subtype) {
|
|
function addErrorForValueType(valueType) {
|
|
let errorString = null;
|
|
if (valueType === "object" || valueType === "array")
|
|
errorString = WI.UIString("\u0022%s\u0022 must be an %s");
|
|
else
|
|
errorString = WI.UIString("\u0022%s\u0022 must be a %s");
|
|
addError(errorString.format(key, valueType));
|
|
}
|
|
|
|
if (value.subtype !== subtype) {
|
|
addErrorForValueType(subtype);
|
|
return null;
|
|
}
|
|
|
|
if (value.type !== type) {
|
|
addErrorForValueType(type);
|
|
return null;
|
|
}
|
|
|
|
if (type === "boolean" || type === "string")
|
|
return value.value;
|
|
|
|
return value;
|
|
}
|
|
|
|
async function resultArrayForEach(key, value, callback) {
|
|
let array = checkResultProperty(key, value, "object", "array");
|
|
if (!array)
|
|
return;
|
|
|
|
let arrayProperties = await new Promise((resolve, reject) => array.getPropertyDescriptors(resolve, options));
|
|
for (let i = 0; i < array.size; ++i) {
|
|
let arrayPropertyForIndex = arrayProperties.find((arrayProperty) => arrayProperty.name === String(i));
|
|
if (arrayPropertyForIndex)
|
|
await callback(arrayPropertyForIndex);
|
|
}
|
|
}
|
|
|
|
let properties = await new Promise((resolve, reject) => remoteObject.getPropertyDescriptors(resolve, options));
|
|
for (let property of properties) {
|
|
let key = property.name;
|
|
if (key === "__proto__")
|
|
continue;
|
|
|
|
let value = property.value;
|
|
|
|
switch (key) {
|
|
case "level": {
|
|
let levelString = checkResultProperty(key, value, "string");
|
|
if (levelString)
|
|
setLevel(levelString.trim().toLowerCase());
|
|
break;
|
|
}
|
|
|
|
case "pass":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Pass);
|
|
break;
|
|
|
|
case "warn":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Warn);
|
|
break;
|
|
|
|
case "fail":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Fail);
|
|
break;
|
|
|
|
case "error":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Error);
|
|
break;
|
|
|
|
case "unsupported":
|
|
if (checkResultProperty(key, value, "boolean"))
|
|
setLevel(WI.AuditTestCaseResult.Level.Unsupported);
|
|
break;
|
|
|
|
case "domNodes":
|
|
await resultArrayForEach(key, value, async (item) => {
|
|
if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "node") {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be valid DOM nodes").format(WI.unlocalizedString("domNodes")));
|
|
return;
|
|
}
|
|
|
|
let domNodeId = await new Promise((resolve, reject) => item.value.pushNodeToFrontend(resolve));
|
|
let domNode = WI.domManager.nodeForId(domNodeId);
|
|
if (!domNode)
|
|
return;
|
|
|
|
if (!data.domNodes)
|
|
data.domNodes = [];
|
|
data.domNodes.push(WI.cssPath(domNode, {full: true}));
|
|
|
|
if (!resolvedDOMNodes)
|
|
resolvedDOMNodes = [];
|
|
resolvedDOMNodes.push(domNode);
|
|
});
|
|
break;
|
|
|
|
case "domAttributes":
|
|
await resultArrayForEach(key, value, (item) => {
|
|
if (!item || !item.value || item.value.type !== "string" || !item.value.value.length) {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be non-empty strings").format(WI.unlocalizedString("domAttributes")));
|
|
return;
|
|
}
|
|
|
|
if (!data.domAttributes)
|
|
data.domAttributes = [];
|
|
data.domAttributes.push(item.value.value);
|
|
});
|
|
break;
|
|
|
|
case "errors":
|
|
await resultArrayForEach(key, value, (item) => {
|
|
if (!item || !item.value || item.value.type !== "object" || item.value.subtype !== "error") {
|
|
addError(WI.UIString("All items in \u0022%s\u0022 must be error objects").format(WI.unlocalizedString("errors")));
|
|
return;
|
|
}
|
|
|
|
addError(item.value.description);
|
|
});
|
|
break;
|
|
|
|
default:
|
|
if (value.objectId) {
|
|
try {
|
|
function inspectedPage_stringify() {
|
|
return JSON.stringify(this);
|
|
}
|
|
let stringifiedValue = await value.callFunction(inspectedPage_stringify);
|
|
data[key] = JSON.parse(stringifiedValue.value);
|
|
} catch {
|
|
addError(WI.UIString("\u0022%s\u0022 is not JSON serializable").format(key));
|
|
}
|
|
} else
|
|
data[key] = value.value;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
let agentCommandFunction = null;
|
|
let agentCommandArguments = {};
|
|
if (target.hasDomain("Audit")) {
|
|
agentCommandFunction = target.AuditAgent.run;
|
|
agentCommandArguments.test = this._test;
|
|
} else {
|
|
agentCommandFunction = target.RuntimeAgent.evaluate;
|
|
agentCommandArguments.expression = `(function() { "use strict"; return eval(\`(${this._test.replace(/`/g, "\\`")})\`)(); })()`;
|
|
agentCommandArguments.objectGroup = WI.AuditTestCase.ObjectGroup;
|
|
agentCommandArguments.doNotPauseOnExceptionsAndMuteConsole = true;
|
|
}
|
|
|
|
try {
|
|
metadata.startTimestamp = new Date;
|
|
let response = await agentCommandFunction.invoke(agentCommandArguments);
|
|
metadata.endTimestamp = new Date;
|
|
|
|
if (response.result.type === "object" && response.result.className === "Promise") {
|
|
if (WI.RuntimeManager.supportsAwaitPromise()) {
|
|
metadata.asyncTimestamp = metadata.endTimestamp;
|
|
response = await target.RuntimeAgent.awaitPromise(response.result.objectId);
|
|
metadata.endTimestamp = new Date;
|
|
} else {
|
|
response = null;
|
|
addError(WI.UIString("Async audits are not supported."));
|
|
setLevel(WI.AuditTestCaseResult.Level.Unsupported);
|
|
}
|
|
}
|
|
|
|
if (response)
|
|
await parseResponse(response);
|
|
} catch (error) {
|
|
metadata.endTimestamp = new Date;
|
|
addError(error.message);
|
|
}
|
|
|
|
if (!level)
|
|
addError(WI.UIString("Missing result level"));
|
|
|
|
let options = {
|
|
description: this.description,
|
|
metadata,
|
|
};
|
|
if (!isEmptyObject(data))
|
|
options.data = data;
|
|
if (resolvedDOMNodes)
|
|
options.resolvedDOMNodes = resolvedDOMNodes;
|
|
this.updateResult(new WI.AuditTestCaseResult(this.name, level, options));
|
|
}
|
|
};
|
|
|
|
WI.AuditTestCase.TypeIdentifier = "test-case";
|
|
|
|
/* Models/AuditTestGroup.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestGroup = class AuditTestGroup extends WI.AuditTestBase
|
|
{
|
|
constructor(name, tests, options = {})
|
|
{
|
|
console.assert(Array.isArray(tests), tests);
|
|
|
|
// Set disabled once `_tests` is set so that it propagates.
|
|
let disabled = options.disabled;
|
|
options.disabled = false;
|
|
|
|
super(name, options);
|
|
|
|
this._tests = [];
|
|
for (let test of tests)
|
|
this.addTest(test);
|
|
|
|
if (disabled)
|
|
this.updateDisabled(true);
|
|
}
|
|
|
|
// Static
|
|
|
|
static async fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (payload.type !== WI.AuditTestGroup.TypeIdentifier)
|
|
return null;
|
|
|
|
if (typeof payload.name !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("name")));
|
|
return null;
|
|
}
|
|
|
|
if (!Array.isArray(payload.tests)) {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-array \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("tests")));
|
|
return null;
|
|
}
|
|
|
|
let tests = await Promise.all(payload.tests.map(async (test) => {
|
|
let testCase = await WI.AuditTestCase.fromPayload(test);
|
|
if (testCase)
|
|
return testCase;
|
|
|
|
let testGroup = await WI.AuditTestGroup.fromPayload(test);
|
|
if (testGroup)
|
|
return testGroup;
|
|
|
|
return null;
|
|
}));
|
|
tests = tests.filter((test) => !!test);
|
|
if (!tests.length)
|
|
return null;
|
|
|
|
let options = {};
|
|
|
|
if (typeof payload.description === "string")
|
|
options.description = payload.description;
|
|
else if ("description" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("description")));
|
|
|
|
if (typeof payload.supports === "number")
|
|
options.supports = payload.supports;
|
|
else if ("supports" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-number \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("supports")));
|
|
|
|
if (typeof payload.setup === "string")
|
|
options.setup = payload.setup;
|
|
else if ("setup" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("setup")));
|
|
|
|
if (typeof payload.disabled === "boolean")
|
|
options.disabled = payload.disabled;
|
|
|
|
return new WI.AuditTestGroup(payload.name, tests, options);
|
|
}
|
|
|
|
// Public
|
|
|
|
get tests() { return this._tests; }
|
|
|
|
addTest(test)
|
|
{
|
|
console.assert(test instanceof WI.AuditTestBase, test);
|
|
console.assert(!this._tests.includes(test), test);
|
|
console.assert(!test._parent, test);
|
|
|
|
this._tests.push(test);
|
|
test._parent = this;
|
|
|
|
test.addEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCompleted, this);
|
|
test.addEventListener(WI.AuditTestBase.Event.DisabledChanged, this._handleTestDisabledChanged, this);
|
|
test.addEventListener(WI.AuditTestBase.Event.Progress, this._handleTestProgress, this);
|
|
if (this.editable) {
|
|
test.addEventListener(WI.AuditTestBase.Event.SupportedChanged, this._handleTestSupportedChanged, this);
|
|
test.addEventListener(WI.AuditTestBase.Event.TestChanged, this._handleTestChanged, this);
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestGroup.Event.TestAdded, {test});
|
|
|
|
this.determineIfSupported();
|
|
|
|
if (this._checkDisabled(test))
|
|
test.updateDisabled(true, {silent: true});
|
|
}
|
|
|
|
removeTest(test)
|
|
{
|
|
console.assert(this.editable);
|
|
console.assert(WI.auditManager.editing);
|
|
console.assert(test instanceof WI.AuditTestBase, test);
|
|
console.assert(test.editable, test);
|
|
console.assert(this._tests.includes(test), test);
|
|
console.assert(test._parent === this, test);
|
|
|
|
test.removeEventListener(WI.AuditTestBase.Event.Completed, this._handleTestCompleted, this);
|
|
test.removeEventListener(WI.AuditTestBase.Event.DisabledChanged, this._handleTestDisabledChanged, this);
|
|
test.removeEventListener(WI.AuditTestBase.Event.Progress, this._handleTestProgress, this);
|
|
test.removeEventListener(WI.AuditTestBase.Event.SupportedChanged, this._handleTestSupportedChanged, this);
|
|
test.removeEventListener(WI.AuditTestBase.Event.TestChanged, this._handleTestChanged, this);
|
|
|
|
this._tests.remove(test);
|
|
test._parent = null;
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestGroup.Event.TestRemoved, {test});
|
|
|
|
this.determineIfSupported();
|
|
|
|
this._checkDisabled();
|
|
}
|
|
|
|
stop()
|
|
{
|
|
// Called from WI.AuditManager.
|
|
|
|
for (let test of this._tests)
|
|
test.stop();
|
|
|
|
super.stop();
|
|
}
|
|
|
|
clearResult(options = {})
|
|
{
|
|
let cleared = !!this.result;
|
|
|
|
if (!options.excludeTests && this._tests) {
|
|
for (let test of this._tests) {
|
|
if (test.clearResult(options))
|
|
cleared = true;
|
|
}
|
|
}
|
|
|
|
return super.clearResult({
|
|
...options,
|
|
suppressResultChangedEvent: !cleared,
|
|
});
|
|
}
|
|
|
|
toJSON(key)
|
|
{
|
|
let json = super.toJSON(key);
|
|
json.tests = this._tests.map((testCase) => testCase.toJSON(key));
|
|
return json;
|
|
}
|
|
|
|
// Protected
|
|
|
|
async run()
|
|
{
|
|
let count = this._tests.length;
|
|
for (let index = 0; index < count && this._runningState === WI.AuditManager.RunningState.Active; ++index) {
|
|
let test = this._tests[index];
|
|
if (test.disabled || !test.supported)
|
|
continue;
|
|
|
|
await test.start();
|
|
|
|
if (test instanceof WI.AuditTestCase)
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Progress, {index, count});
|
|
}
|
|
|
|
this.updateResult();
|
|
}
|
|
|
|
determineIfSupported(options = {})
|
|
{
|
|
if (this._tests) {
|
|
for (let test of this._tests)
|
|
test.determineIfSupported({...options, warn: false, silent: true});
|
|
}
|
|
|
|
return super.determineIfSupported(options);
|
|
}
|
|
|
|
updateSupported(supported, options = {})
|
|
{
|
|
if (this._tests && (!supported || this._tests.every((test) => !test.supported))) {
|
|
supported = false;
|
|
|
|
for (let test of this._tests)
|
|
test.updateSupported(supported, {silent: true});
|
|
}
|
|
|
|
super.updateSupported(supported, options);
|
|
}
|
|
|
|
updateDisabled(disabled, options = {})
|
|
{
|
|
if (!options.excludeTests && this._tests) {
|
|
for (let test of this._tests)
|
|
test.updateDisabled(disabled, options);
|
|
}
|
|
|
|
super.updateDisabled(disabled, options);
|
|
}
|
|
|
|
updateResult()
|
|
{
|
|
let results = this._tests.map((test) => test.result).filter((result) => !!result);
|
|
if (!results.length)
|
|
return;
|
|
|
|
super.updateResult(new WI.AuditTestGroupResult(this.name, results, {
|
|
description: this.description,
|
|
}));
|
|
}
|
|
|
|
// Private
|
|
|
|
_checkDisabled(test)
|
|
{
|
|
let testDisabled = !test || !test.supported || test.disabled;
|
|
let enabledTestCount = this._tests.filter((existing) => existing.supported && !existing.disabled).length;
|
|
|
|
if (testDisabled && !enabledTestCount)
|
|
this.updateDisabled(true);
|
|
else if (!testDisabled && enabledTestCount === 1)
|
|
this.updateDisabled(false, {excludeTests: true});
|
|
else {
|
|
// Don't change `disabled`, as we're currently in an "indeterminate" state.
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.DisabledChanged);
|
|
}
|
|
|
|
return this.disabled;
|
|
}
|
|
|
|
_handleTestCompleted(event)
|
|
{
|
|
if (this._runningState === WI.AuditManager.RunningState.Active)
|
|
return;
|
|
|
|
this.updateResult();
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Completed);
|
|
}
|
|
|
|
_handleTestDisabledChanged(event)
|
|
{
|
|
this._checkDisabled(event.target);
|
|
}
|
|
|
|
_handleTestProgress(event)
|
|
{
|
|
if (this._runningState !== WI.AuditManager.RunningState.Active)
|
|
return;
|
|
|
|
let walk = (tests) => {
|
|
let count = 0;
|
|
for (let test of tests) {
|
|
if (test.disabled || !test.supported)
|
|
continue;
|
|
|
|
if (test instanceof WI.AuditTestCase)
|
|
++count;
|
|
else if (test instanceof WI.AuditTestGroup)
|
|
count += walk(test.tests);
|
|
}
|
|
return count;
|
|
};
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.Progress, {
|
|
index: event.data.index + walk(this._tests.slice(0, this._tests.indexOf(event.target))),
|
|
count: walk(this._tests),
|
|
});
|
|
}
|
|
|
|
_handleTestSupportedChanged(event)
|
|
{
|
|
this.determineIfSupported();
|
|
}
|
|
|
|
_handleTestChanged(event)
|
|
{
|
|
console.assert(WI.auditManager.editing);
|
|
|
|
this.clearResult({excludeTests: true});
|
|
|
|
this.dispatchEventToListeners(WI.AuditTestBase.Event.TestChanged);
|
|
}
|
|
};
|
|
|
|
WI.AuditTestGroup.TypeIdentifier = "test-group";
|
|
|
|
WI.AuditTestGroup.Event = {
|
|
TestAdded: "audit-test-group-test-added",
|
|
TestRemoved: "audit-test-group-test-removed",
|
|
};
|
|
|
|
/* Models/AuditTestResultBase.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestResultBase = class AuditTestResultBase
|
|
{
|
|
constructor(name, {description} = {})
|
|
{
|
|
console.assert(typeof name === "string");
|
|
console.assert(!description || typeof description === "string");
|
|
|
|
this._name = name;
|
|
this._description = description || null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
get description() { return this._description; }
|
|
|
|
get result()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
get didPass()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get didWarn()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get didFail()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get didError()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get unsupported()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
get disabled()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
get editable()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
saveIdentityToCookie(cookie)
|
|
{
|
|
cookie["audit-" + this.constructor.TypeIdentifier + "-name"] = this._name;
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = {
|
|
type: this.constructor.TypeIdentifier,
|
|
name: this._name,
|
|
};
|
|
if (this._description)
|
|
json.description = this._description;
|
|
return json;
|
|
}
|
|
};
|
|
|
|
/* Models/AuditTestCaseResult.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestCaseResult = class AuditTestCaseResult extends WI.AuditTestResultBase
|
|
{
|
|
constructor(name, level, {description, data, metadata, resolvedDOMNodes} = {})
|
|
{
|
|
console.assert(Object.values(WI.AuditTestCaseResult.Level).includes(level));
|
|
console.assert(!data || typeof data === "object");
|
|
console.assert(!metadata || typeof metadata === "object");
|
|
|
|
super(name, {description});
|
|
|
|
this._level = level;
|
|
this._data = data || {};
|
|
this._metadata = metadata || {};
|
|
|
|
// This array is a mirror of `this._data.domNodes` where each item is a `WI.DOMNode`.
|
|
this._resolvedDOMNodes = resolvedDOMNodes || [];
|
|
}
|
|
|
|
// Static
|
|
|
|
static async fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (payload.type !== WI.AuditTestCaseResult.TypeIdentifier)
|
|
return null;
|
|
|
|
if (typeof payload.name !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("name")));
|
|
return null;
|
|
}
|
|
|
|
if (!Object.values(WI.AuditTestCaseResult.Level).includes(payload.level)) {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has an invalid \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("level")));
|
|
return null;
|
|
}
|
|
|
|
if (typeof payload.data !== "object" || payload.data === null) {
|
|
if ("data" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("data")));
|
|
payload.data = {};
|
|
} else {
|
|
function checkArray(key) {
|
|
if (!(key in payload.data))
|
|
return;
|
|
|
|
if (!Array.isArray(payload.data[key])) {
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-array \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("data.%s").format(key)));
|
|
payload.data[key] = [];
|
|
}
|
|
|
|
payload.data[key] = payload.data[key].filter((item) => typeof item === "string");
|
|
}
|
|
checkArray("domNodes");
|
|
checkArray("domAttributes");
|
|
checkArray("errors");
|
|
}
|
|
|
|
if (typeof payload.metadata !== "object" || payload.metadata === null) {
|
|
if ("metadata" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("metadata")));
|
|
|
|
payload.metadata = {};
|
|
} else {
|
|
if (typeof payload.metadata.startTimestamp === "string")
|
|
payload.metadata.startTimestamp = new Date(payload.metadata.startTimestamp);
|
|
else {
|
|
if ("startTimestamp" in payload.metadata)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("metadata.startTimestamp")));
|
|
|
|
payload.metadata.startTimestamp = null;
|
|
}
|
|
|
|
if (typeof payload.metadata.asyncTimestamp === "string")
|
|
payload.metadata.asyncTimestamp = new Date(payload.metadata.asyncTimestamp);
|
|
else {
|
|
if ("asyncTimestamp" in payload.metadata)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("metadata.asyncTimestamp")));
|
|
|
|
payload.metadata.asyncTimestamp = null;
|
|
}
|
|
|
|
if (typeof payload.metadata.endTimestamp === "string")
|
|
payload.metadata.endTimestamp = new Date(payload.metadata.endTimestamp);
|
|
else {
|
|
if ("endTimestamp" in payload.metadata)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("metadata.endTimestamp")));
|
|
|
|
payload.metadata.endTimestamp = null;
|
|
}
|
|
|
|
if (typeof payload.metadata.url !== "string") {
|
|
if ("url" in payload.metadata)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-object \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("metadata.url")));
|
|
|
|
payload.metadata.url = null;
|
|
}
|
|
}
|
|
|
|
let options = {};
|
|
|
|
if (typeof payload.description === "string")
|
|
options.description = payload.description;
|
|
else if ("description" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("description")));
|
|
|
|
if (!isEmptyObject(payload.data)) {
|
|
options.data = {};
|
|
for (let key in payload.data) {
|
|
if (key === "domNodes" || key === "domAttributes" || key === "errors") {
|
|
if (!payload.data[key].length)
|
|
continue;
|
|
}
|
|
|
|
if (key === "domNodes") {
|
|
if (InspectorBackend.hasDomain("DOM") && (!payload.metadata.url || payload.metadata.url === WI.networkManager.mainFrame.url)) {
|
|
let documentNode = await new Promise((resolve) => WI.domManager.requestDocument(resolve));
|
|
options.resolvedDOMNodes = await Promise.all(payload.data.domNodes.map(async (domNodeString) => {
|
|
let nodeId = 0;
|
|
try {
|
|
nodeId = await documentNode.querySelector(domNodeString);
|
|
} catch { }
|
|
return WI.domManager.nodeForId(nodeId);
|
|
}));
|
|
}
|
|
}
|
|
|
|
options.data[key] = payload.data[key];
|
|
}
|
|
}
|
|
|
|
if (!isEmptyObject(payload.metadata)) {
|
|
options.metadata = {};
|
|
if (payload.metadata.startTimestamp && !isNaN(payload.metadata.startTimestamp))
|
|
options.metadata.startTimestamp = payload.metadata.startTimestamp;
|
|
if (payload.metadata.asyncTimestamp && !isNaN(payload.metadata.asyncTimestamp))
|
|
options.metadata.asyncTimestamp = payload.metadata.asyncTimestamp;
|
|
if (payload.metadata.endTimestamp && !isNaN(payload.metadata.endTimestamp))
|
|
options.metadata.endTimestamp = payload.metadata.endTimestamp;
|
|
if (payload.metadata.url)
|
|
options.metadata.url = payload.metadata.url;
|
|
}
|
|
|
|
return new WI.AuditTestCaseResult(payload.name, payload.level, options);
|
|
}
|
|
|
|
// Public
|
|
|
|
get level() { return this._level; }
|
|
get data() { return this._data; }
|
|
get metadata() { return this._metadata; }
|
|
get resolvedDOMNodes() { return this._resolvedDOMNodes; }
|
|
|
|
get result()
|
|
{
|
|
return this;
|
|
}
|
|
|
|
get didPass()
|
|
{
|
|
return this._level === WI.AuditTestCaseResult.Level.Pass;
|
|
}
|
|
|
|
get didWarn()
|
|
{
|
|
return this._level === WI.AuditTestCaseResult.Level.Warn;
|
|
}
|
|
|
|
get didFail()
|
|
{
|
|
return this._level === WI.AuditTestCaseResult.Level.Fail;
|
|
}
|
|
|
|
get didError()
|
|
{
|
|
return this._level === WI.AuditTestCaseResult.Level.Error;
|
|
}
|
|
|
|
get unsupported()
|
|
{
|
|
return this._level === WI.AuditTestCaseResult.Level.Unsupported;
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = super.toJSON();
|
|
json.level = this._level;
|
|
|
|
let data = {};
|
|
for (let key in this._data) {
|
|
if (key === "domNodes" || key === "domAttributes" || key === "errors") {
|
|
if (!this._data[key].length)
|
|
continue;
|
|
}
|
|
|
|
data[key] = this._data[key];
|
|
}
|
|
if (!isEmptyObject(data))
|
|
json.data = data;
|
|
|
|
let metadata = {};
|
|
if (this._metadata.startTimestamp && !isNaN(this._metadata.startTimestamp))
|
|
metadata.startTimestamp = this._metadata.startTimestamp;
|
|
if (this._metadata.asyncTimestamp && !isNaN(this._metadata.asyncTimestamp))
|
|
metadata.asyncTimestamp = this._metadata.asyncTimestamp;
|
|
if (this._metadata.endTimestamp && !isNaN(this._metadata.endTimestamp))
|
|
metadata.endTimestamp = this._metadata.endTimestamp;
|
|
if (this._metadata.url)
|
|
metadata.url = this._metadata.url;
|
|
if (!isEmptyObject(metadata))
|
|
json.metadata = metadata;
|
|
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.AuditTestCaseResult.TypeIdentifier = "test-case-result";
|
|
|
|
// Keep this ordered by precedence.
|
|
WI.AuditTestCaseResult.Level = {
|
|
Pass: "pass",
|
|
Warn: "warn",
|
|
Fail: "fail",
|
|
Error: "error",
|
|
Unsupported: "unsupported",
|
|
};
|
|
|
|
/* Models/AuditTestGroupResult.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditTestGroupResult = class AuditTestGroupResult extends WI.AuditTestResultBase
|
|
{
|
|
constructor(name, results, {description} = {})
|
|
{
|
|
console.assert(Array.isArray(results));
|
|
|
|
super(name, {description});
|
|
|
|
this._results = results;
|
|
}
|
|
|
|
// Static
|
|
|
|
static async fromPayload(payload)
|
|
{
|
|
if (typeof payload !== "object" || payload === null)
|
|
return null;
|
|
|
|
if (payload.type !== WI.AuditTestGroupResult.TypeIdentifier)
|
|
return null;
|
|
|
|
if (typeof payload.name !== "string") {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("name")));
|
|
return null;
|
|
}
|
|
|
|
if (!Array.isArray(payload.results)) {
|
|
WI.AuditManager.synthesizeError(WI.UIString("\u0022%s\u0022 has a non-array \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("results")));
|
|
return null;
|
|
}
|
|
|
|
let results = await Promise.all(payload.results.map(async (test) => {
|
|
let testCaseResult = await WI.AuditTestCaseResult.fromPayload(test);
|
|
if (testCaseResult)
|
|
return testCaseResult;
|
|
|
|
let testGroupResult = await WI.AuditTestGroupResult.fromPayload(test);
|
|
if (testGroupResult)
|
|
return testGroupResult;
|
|
|
|
return null;
|
|
}));
|
|
results = results.filter((result) => !!result);
|
|
if (!results.length)
|
|
return null;
|
|
|
|
let options = {};
|
|
|
|
if (typeof payload.description === "string")
|
|
options.description = payload.description;
|
|
else if ("description" in payload)
|
|
WI.AuditManager.synthesizeWarning(WI.UIString("\u0022%s\u0022 has a non-string \u0022%s\u0022 value").format(payload.name, WI.unlocalizedString("description")));
|
|
|
|
return new WI.AuditTestGroupResult(payload.name, results, options);
|
|
}
|
|
|
|
// Public
|
|
|
|
get results() { return this._results; }
|
|
|
|
get levelCounts()
|
|
{
|
|
let counts = {};
|
|
for (let level of Object.values(WI.AuditTestCaseResult.Level))
|
|
counts[level] = 0;
|
|
|
|
for (let result of this._results) {
|
|
if (result instanceof WI.AuditTestCaseResult)
|
|
++counts[result.level];
|
|
else if (result instanceof WI.AuditTestGroupResult) {
|
|
for (let [level, count] of Object.entries(result.levelCounts))
|
|
counts[level] += count;
|
|
}
|
|
}
|
|
|
|
return counts;
|
|
}
|
|
|
|
get didPass()
|
|
{
|
|
return this._results.some((result) => result.didPass);
|
|
}
|
|
|
|
get didWarn()
|
|
{
|
|
return this._results.some((result) => result.didWarn);
|
|
}
|
|
|
|
get didFail()
|
|
{
|
|
return this._results.some((result) => result.didFail);
|
|
}
|
|
|
|
get didError()
|
|
{
|
|
return this._results.some((result) => result.didError);
|
|
}
|
|
|
|
get unsupported()
|
|
{
|
|
return this._results.some((result) => result.unsupported);
|
|
}
|
|
|
|
toJSON()
|
|
{
|
|
let json = super.toJSON();
|
|
json.results = this._results.map((result) => result.toJSON());
|
|
return json;
|
|
}
|
|
};
|
|
|
|
WI.AuditTestGroupResult.TypeIdentifier = "test-group-result";
|
|
|
|
/* Proxies/FormatterWorkerProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FormatterWorkerProxy = class FormatterWorkerProxy
|
|
{
|
|
constructor()
|
|
{
|
|
this._formatterWorker = new Worker("Workers/Formatter/FormatterWorker.js");
|
|
this._formatterWorker.addEventListener("message", this._handleMessage.bind(this));
|
|
|
|
this._nextCallId = 1;
|
|
this._callbacks = new Map;
|
|
}
|
|
|
|
// Static
|
|
|
|
static singleton()
|
|
{
|
|
if (!FormatterWorkerProxy.instance)
|
|
FormatterWorkerProxy.instance = new FormatterWorkerProxy;
|
|
return FormatterWorkerProxy.instance;
|
|
}
|
|
|
|
// Actions
|
|
|
|
formatJavaScript(sourceText, isModule, indentString, includeSourceMapData)
|
|
{
|
|
this.performAction("formatJavaScript", ...arguments);
|
|
}
|
|
|
|
formatCSS(sourceText, indentString, includeSourceMapData)
|
|
{
|
|
this.performAction("formatCSS", ...arguments);
|
|
}
|
|
|
|
formatHTML(sourceText, indentString, includeSourceMapData)
|
|
{
|
|
this.performAction("formatHTML", ...arguments);
|
|
}
|
|
|
|
formatXML(sourceText, indentString, includeSourceMapData)
|
|
{
|
|
this.performAction("formatXML", ...arguments);
|
|
}
|
|
|
|
// Public
|
|
|
|
performAction(actionName)
|
|
{
|
|
let callId = this._nextCallId++;
|
|
let callback = arguments[arguments.length - 1];
|
|
let actionArguments = Array.prototype.slice.call(arguments, 1, arguments.length - 1);
|
|
|
|
console.assert(typeof actionName === "string", "performAction should always have an actionName");
|
|
console.assert(typeof callback === "function", "performAction should always have a callback");
|
|
|
|
this._callbacks.set(callId, callback);
|
|
this._postMessage({callId, actionName, actionArguments});
|
|
}
|
|
|
|
// Private
|
|
|
|
_postMessage()
|
|
{
|
|
this._formatterWorker.postMessage(...arguments);
|
|
}
|
|
|
|
_handleMessage(event)
|
|
{
|
|
let data = event.data;
|
|
|
|
// Action response.
|
|
if (data.callId) {
|
|
let callback = this._callbacks.get(data.callId);
|
|
this._callbacks.delete(data.callId);
|
|
callback(data.result);
|
|
return;
|
|
}
|
|
|
|
console.error("Unexpected FormatterWorker message", data);
|
|
}
|
|
};
|
|
|
|
/* Proxies/HeapSnapshotDiffProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapSnapshotDiffProxy = class HeapSnapshotDiffProxy extends WI.Object
|
|
{
|
|
constructor(snapshotDiffObjectId, snapshot1, snapshot2, totalSize, totalObjectCount, categories)
|
|
{
|
|
super();
|
|
|
|
this._proxyObjectId = snapshotDiffObjectId;
|
|
|
|
console.assert(snapshot1 instanceof WI.HeapSnapshotProxy);
|
|
console.assert(snapshot2 instanceof WI.HeapSnapshotProxy);
|
|
|
|
this._snapshot1 = snapshot1;
|
|
this._snapshot2 = snapshot2;
|
|
this._totalSize = totalSize;
|
|
this._totalObjectCount = totalObjectCount;
|
|
this._categories = Map.fromObject(categories);
|
|
}
|
|
|
|
// Static
|
|
|
|
static deserialize(objectId, serializedSnapshotDiff)
|
|
{
|
|
let {snapshot1: serializedSnapshot1, snapshot2: serializedSnapshot2, totalSize, totalObjectCount, categories} = serializedSnapshotDiff;
|
|
// FIXME: The objectId for these snapshots is the snapshotDiff's objectId. Currently these
|
|
// snapshots are only used for static data so the proxing doesn't matter. However,
|
|
// should we serialize the objectId with the snapshot so we have the right objectId?
|
|
let snapshot1 = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot1);
|
|
let snapshot2 = WI.HeapSnapshotProxy.deserialize(objectId, serializedSnapshot2);
|
|
return new WI.HeapSnapshotDiffProxy(objectId, snapshot1, snapshot2, totalSize, totalObjectCount, categories);
|
|
}
|
|
|
|
// Public
|
|
|
|
get snapshot1() { return this._snapshot1; }
|
|
get snapshot2() { return this._snapshot2; }
|
|
get totalSize() { return this._totalSize; }
|
|
get totalObjectCount() { return this._totalObjectCount; }
|
|
get categories() { return this._categories; }
|
|
get invalid() { return this._snapshot1.invalid || this._snapshot2.invalid; }
|
|
|
|
updateForCollectionEvent(event)
|
|
{
|
|
console.assert(!this.invalid);
|
|
if (!event.data.affectedSnapshots.includes(this._snapshot2._identifier))
|
|
return;
|
|
|
|
this.update(() => {
|
|
this.dispatchEventToListeners(WI.HeapSnapshotProxy.Event.CollectedNodes, event.data);
|
|
});
|
|
}
|
|
|
|
allocationBucketCounts(bucketSizes, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "allocationBucketCounts", bucketSizes, callback);
|
|
}
|
|
|
|
instancesWithClassName(className, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "instancesWithClassName", className, (serializedNodes) => {
|
|
callback(serializedNodes.map(WI.HeapSnapshotNodeProxy.deserialize.bind(null, this._proxyObjectId)));
|
|
});
|
|
}
|
|
|
|
update(callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "update", ({liveSize, categories}) => {
|
|
this._categories = Map.fromObject(categories);
|
|
callback();
|
|
});
|
|
}
|
|
|
|
nodeWithIdentifier(nodeIdentifier, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "nodeWithIdentifier", nodeIdentifier, (serializedNode) => {
|
|
callback(WI.HeapSnapshotNodeProxy.deserialize(this._proxyObjectId, serializedNode));
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Proxies/HeapSnapshotEdgeProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// Directed edge between two HeapSnapshotNodes 'from' and 'to'.
|
|
|
|
WI.HeapSnapshotEdgeProxy = class HeapSnapshotEdgeProxy
|
|
{
|
|
constructor(objectId, fromIdentifier, toIdentifier, type, data)
|
|
{
|
|
this._proxyObjectId = objectId;
|
|
|
|
console.assert(type in WI.HeapSnapshotEdgeProxy.EdgeType);
|
|
|
|
this.fromIdentifier = fromIdentifier;
|
|
this.toIdentifier = toIdentifier;
|
|
this.type = type;
|
|
this.data = data;
|
|
|
|
this.from = null;
|
|
this.to = null;
|
|
}
|
|
|
|
isPrivateSymbol()
|
|
{
|
|
if (WI.settings.engineeringShowPrivateSymbolsInHeapSnapshot.value)
|
|
return false;
|
|
|
|
return typeof this.data === "string" && this.data.startsWith("PrivateSymbol");
|
|
}
|
|
|
|
// Static
|
|
|
|
static deserialize(objectId, serializedEdge)
|
|
{
|
|
let {from, to, type, data} = serializedEdge;
|
|
return new WI.HeapSnapshotEdgeProxy(objectId, from, to, type, data);
|
|
}
|
|
};
|
|
|
|
WI.HeapSnapshotEdgeProxy.EdgeType = {
|
|
Internal: "Internal", // No data.
|
|
Property: "Property", // data is string property name.
|
|
Index: "Index", // data is numeric index.
|
|
Variable: "Variable", // data is string variable name.
|
|
};
|
|
|
|
/* Proxies/HeapSnapshotNodeProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapSnapshotNodeProxy = class HeapSnapshotNodeProxy
|
|
{
|
|
constructor(snapshotObjectId, {id, className, size, retainedSize, internal, isObjectType, gcRoot, dead, dominatorNodeIdentifier, hasChildren})
|
|
{
|
|
this._proxyObjectId = snapshotObjectId;
|
|
|
|
this.id = id;
|
|
this.className = className;
|
|
this.size = size;
|
|
this.retainedSize = retainedSize;
|
|
this.internal = internal;
|
|
this.isObjectType = isObjectType;
|
|
this.gcRoot = gcRoot;
|
|
this.dead = dead;
|
|
this.dominatorNodeIdentifier = dominatorNodeIdentifier;
|
|
this.hasChildren = hasChildren;
|
|
}
|
|
|
|
// Static
|
|
|
|
static deserialize(objectId, serializedNode)
|
|
{
|
|
return new WI.HeapSnapshotNodeProxy(objectId, serializedNode);
|
|
}
|
|
|
|
// Proxied
|
|
|
|
shortestGCRootPath(callback)
|
|
{
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "shortestGCRootPath", this.id, (serializedPath) => {
|
|
let isNode = false;
|
|
let path = serializedPath.map((component) => {
|
|
isNode = !isNode;
|
|
if (isNode)
|
|
return WI.HeapSnapshotNodeProxy.deserialize(this._proxyObjectId, component);
|
|
return WI.HeapSnapshotEdgeProxy.deserialize(this._proxyObjectId, component);
|
|
});
|
|
|
|
for (let i = 1; i < path.length; i += 2) {
|
|
console.assert(path[i] instanceof WI.HeapSnapshotEdgeProxy);
|
|
let edge = path[i];
|
|
edge.from = path[i - 1];
|
|
edge.to = path[i + 1];
|
|
}
|
|
|
|
callback(path);
|
|
});
|
|
}
|
|
|
|
dominatedNodes(callback)
|
|
{
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "dominatedNodes", this.id, (serializedNodes) => {
|
|
callback(serializedNodes.map(WI.HeapSnapshotNodeProxy.deserialize.bind(null, this._proxyObjectId)));
|
|
});
|
|
}
|
|
|
|
retainedNodes(callback)
|
|
{
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "retainedNodes", this.id, ({retainedNodes: serializedNodes, edges: serializedEdges}) => {
|
|
let deserializedNodes = serializedNodes.map(WI.HeapSnapshotNodeProxy.deserialize.bind(null, this._proxyObjectId));
|
|
let deserializedEdges = serializedEdges.map(WI.HeapSnapshotEdgeProxy.deserialize.bind(null, this._proxyObjectId));
|
|
callback(deserializedNodes, deserializedEdges);
|
|
});
|
|
}
|
|
|
|
retainers(callback)
|
|
{
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "retainers", this.id, ({retainers: serializedNodes, edges: serializedEdges}) => {
|
|
let deserializedNodes = serializedNodes.map(WI.HeapSnapshotNodeProxy.deserialize.bind(null, this._proxyObjectId));
|
|
let deserializedEdges = serializedEdges.map(WI.HeapSnapshotEdgeProxy.deserialize.bind(null, this._proxyObjectId));
|
|
callback(deserializedNodes, deserializedEdges);
|
|
});
|
|
}
|
|
};
|
|
|
|
/* Proxies/HeapSnapshotProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapSnapshotProxy = class HeapSnapshotProxy extends WI.Object
|
|
{
|
|
constructor(snapshotObjectId, identifier, title, totalSize, totalObjectCount, liveSize, categories, imported)
|
|
{
|
|
super();
|
|
|
|
this._proxyObjectId = snapshotObjectId;
|
|
|
|
this._identifier = identifier;
|
|
this._title = title;
|
|
this._totalSize = totalSize;
|
|
this._totalObjectCount = totalObjectCount;
|
|
this._liveSize = liveSize;
|
|
this._categories = Map.fromObject(categories);
|
|
this._imported = imported;
|
|
this._snapshotStringData = null;
|
|
|
|
console.assert(!this.invalid);
|
|
|
|
if (!WI.HeapSnapshotProxy.ValidSnapshotProxies)
|
|
WI.HeapSnapshotProxy.ValidSnapshotProxies = [];
|
|
WI.HeapSnapshotProxy.ValidSnapshotProxies.push(this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static deserialize(objectId, serializedSnapshot)
|
|
{
|
|
let {identifier, title, totalSize, totalObjectCount, liveSize, categories, imported} = serializedSnapshot;
|
|
return new WI.HeapSnapshotProxy(objectId, identifier, title, totalSize, totalObjectCount, liveSize, categories, imported);
|
|
}
|
|
|
|
static invalidateSnapshotProxies()
|
|
{
|
|
if (!WI.HeapSnapshotProxy.ValidSnapshotProxies)
|
|
return;
|
|
|
|
for (let snapshotProxy of WI.HeapSnapshotProxy.ValidSnapshotProxies)
|
|
snapshotProxy._invalidate();
|
|
|
|
WI.HeapSnapshotProxy.ValidSnapshotProxies = null;
|
|
}
|
|
|
|
// Public
|
|
|
|
get proxyObjectId() { return this._proxyObjectId; }
|
|
get identifier() { return this._identifier; }
|
|
get title() { return this._title; }
|
|
get totalSize() { return this._totalSize; }
|
|
get totalObjectCount() { return this._totalObjectCount; }
|
|
get liveSize() { return this._liveSize; }
|
|
get categories() { return this._categories; }
|
|
get imported() { return this._imported; }
|
|
get invalid() { return this._proxyObjectId === 0; }
|
|
|
|
get snapshotStringData()
|
|
{
|
|
return this._snapshotStringData;
|
|
}
|
|
|
|
set snapshotStringData(data)
|
|
{
|
|
this._snapshotStringData = data;
|
|
}
|
|
|
|
updateForCollectionEvent(event)
|
|
{
|
|
console.assert(!this.invalid);
|
|
if (!event.data.affectedSnapshots.includes(this._identifier))
|
|
return;
|
|
|
|
this.update(() => {
|
|
this.dispatchEventToListeners(WI.HeapSnapshotProxy.Event.CollectedNodes, event.data);
|
|
});
|
|
}
|
|
|
|
allocationBucketCounts(bucketSizes, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "allocationBucketCounts", bucketSizes, callback);
|
|
}
|
|
|
|
instancesWithClassName(className, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "instancesWithClassName", className, (serializedNodes) => {
|
|
callback(serializedNodes.map(WI.HeapSnapshotNodeProxy.deserialize.bind(null, this._proxyObjectId)));
|
|
});
|
|
}
|
|
|
|
update(callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "update", ({liveSize, categories}) => {
|
|
this._liveSize = liveSize;
|
|
this._categories = Map.fromObject(categories);
|
|
callback();
|
|
});
|
|
}
|
|
|
|
nodeWithIdentifier(nodeIdentifier, callback)
|
|
{
|
|
console.assert(!this.invalid);
|
|
WI.HeapSnapshotWorkerProxy.singleton().callMethod(this._proxyObjectId, "nodeWithIdentifier", nodeIdentifier, (serializedNode) => {
|
|
callback(WI.HeapSnapshotNodeProxy.deserialize(this._proxyObjectId, serializedNode));
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
_invalidate()
|
|
{
|
|
this._proxyObjectId = 0;
|
|
this._liveSize = 0;
|
|
|
|
this.dispatchEventToListeners(WI.HeapSnapshotProxy.Event.Invalidated);
|
|
}
|
|
};
|
|
|
|
WI.HeapSnapshotProxy.Event = {
|
|
CollectedNodes: "heap-snapshot-proxy-collected-nodes",
|
|
Invalidated: "heap-snapshot-proxy-invalidated",
|
|
};
|
|
|
|
/* Proxies/HeapSnapshotWorkerProxy.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.HeapSnapshotWorkerProxy = class HeapSnapshotWorkerProxy extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._heapSnapshotWorker = new Worker("Workers/HeapSnapshot/HeapSnapshotWorker.js");
|
|
this._heapSnapshotWorker.addEventListener("message", this._handleMessage.bind(this));
|
|
|
|
this._nextCallId = 1;
|
|
this._callbacks = new Map;
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static singleton()
|
|
{
|
|
if (!HeapSnapshotWorkerProxy.instance)
|
|
HeapSnapshotWorkerProxy.instance = new HeapSnapshotWorkerProxy;
|
|
return HeapSnapshotWorkerProxy.instance;
|
|
}
|
|
|
|
// Actions
|
|
|
|
clearSnapshots(callback)
|
|
{
|
|
this.performAction("clearSnapshots", callback);
|
|
}
|
|
|
|
createSnapshot(snapshotStringData, callback)
|
|
{
|
|
this.performAction("createSnapshot", ...arguments);
|
|
}
|
|
|
|
createSnapshotDiff(objectId1, objectId2, callback)
|
|
{
|
|
this.performAction("createSnapshotDiff", ...arguments);
|
|
}
|
|
|
|
createImportedSnapshot(snapshotStringData, title, callback)
|
|
{
|
|
const imported = true;
|
|
this.performAction("createSnapshot", snapshotStringData, title, imported, callback);
|
|
}
|
|
|
|
// Public
|
|
|
|
performAction(actionName)
|
|
{
|
|
let callId = this._nextCallId++;
|
|
let callback = arguments[arguments.length - 1];
|
|
let actionArguments = Array.prototype.slice.call(arguments, 1, arguments.length - 1);
|
|
|
|
console.assert(typeof actionName === "string", "performAction should always have an actionName");
|
|
console.assert(typeof callback === "function", "performAction should always have a callback");
|
|
|
|
this._callbacks.set(callId, callback);
|
|
this._postMessage({callId, actionName, actionArguments});
|
|
}
|
|
|
|
callMethod(objectId, methodName)
|
|
{
|
|
let callId = this._nextCallId++;
|
|
let callback = arguments[arguments.length - 1];
|
|
let methodArguments = Array.prototype.slice.call(arguments, 2, arguments.length - 1);
|
|
|
|
console.assert(typeof objectId === "number", "callMethod should always have an objectId");
|
|
console.assert(typeof methodName === "string", "callMethod should always have a methodName");
|
|
console.assert(typeof callback === "function", "callMethod should always have a callback");
|
|
|
|
this._callbacks.set(callId, callback);
|
|
this._postMessage({callId, objectId, methodName, methodArguments});
|
|
}
|
|
|
|
// Private
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
this.clearSnapshots(() => {
|
|
WI.HeapSnapshotProxy.invalidateSnapshotProxies();
|
|
});
|
|
}
|
|
|
|
_postMessage()
|
|
{
|
|
this._heapSnapshotWorker.postMessage(...arguments);
|
|
}
|
|
|
|
_handleMessage(event)
|
|
{
|
|
let data = event.data;
|
|
|
|
// Error.
|
|
if (data.error) {
|
|
console.assert(data.callId);
|
|
this._callbacks.delete(data.callId);
|
|
return;
|
|
}
|
|
|
|
// Event.
|
|
if (data.eventName) {
|
|
this.dispatchEventToListeners(data.eventName, data.eventData);
|
|
return;
|
|
}
|
|
|
|
// Action or Method Response.
|
|
if (data.callId) {
|
|
let callback = this._callbacks.get(data.callId);
|
|
this._callbacks.delete(data.callId);
|
|
callback(data.result);
|
|
return;
|
|
}
|
|
|
|
console.error("Unexpected HeapSnapshotWorker message", data);
|
|
}
|
|
};
|
|
|
|
WI.HeapSnapshotWorkerProxy.Event = {
|
|
Collection: "heap-snapshot-collection",
|
|
};
|
|
|
|
/* Controllers/QueryController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.QueryController = class QueryController
|
|
{
|
|
// Public
|
|
|
|
executeQuery(query)
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
findQueryMatches(query, searchString, specialCharacterIndices)
|
|
{
|
|
if (query.length > searchString.length)
|
|
return [];
|
|
|
|
let matches = [];
|
|
let queryIndex = 0;
|
|
let searchIndex = 0;
|
|
let specialIndex = 0;
|
|
let deadBranches = (new Array(query.length)).fill(Infinity);
|
|
let type = WI.QueryMatch.Type.Special;
|
|
|
|
function pushMatch(index) {
|
|
matches.push(new WI.QueryMatch(type, index, queryIndex));
|
|
searchIndex = index + 1;
|
|
queryIndex++;
|
|
}
|
|
|
|
function matchNextSpecialCharacter() {
|
|
if (specialIndex >= specialCharacterIndices.length)
|
|
return false;
|
|
|
|
let originalSpecialIndex = specialIndex;
|
|
while (specialIndex < specialCharacterIndices.length) {
|
|
// Normal character matching can move past special characters,
|
|
// so advance the special character index if it's before the
|
|
// current search string position.
|
|
let index = specialCharacterIndices[specialIndex++];
|
|
if (index < searchIndex)
|
|
continue;
|
|
|
|
if (query[queryIndex] === searchString[index]) {
|
|
pushMatch(index);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
specialIndex = originalSpecialIndex;
|
|
return false;
|
|
}
|
|
|
|
function backtrack() {
|
|
while (matches.length) {
|
|
queryIndex--;
|
|
|
|
let lastMatch = matches.pop();
|
|
if (lastMatch.type !== WI.QueryMatch.Type.Special)
|
|
continue;
|
|
|
|
deadBranches[lastMatch.queryIndex] = lastMatch.index;
|
|
searchIndex = matches.lastValue ? matches.lastValue.index + 1 : 0;
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
while (queryIndex < query.length && searchIndex <= searchString.length) {
|
|
if (type === WI.QueryMatch.Type.Special && !matchNextSpecialCharacter())
|
|
type = WI.QueryMatch.Type.Normal;
|
|
|
|
if (type === WI.QueryMatch.Type.Normal) {
|
|
let index = searchString.indexOf(query[queryIndex], searchIndex);
|
|
if (index >= 0 && index < deadBranches[queryIndex]) {
|
|
pushMatch(index);
|
|
type = WI.QueryMatch.Type.Special;
|
|
} else if (!backtrack())
|
|
return [];
|
|
}
|
|
}
|
|
|
|
if (queryIndex < query.length)
|
|
return [];
|
|
|
|
return matches;
|
|
}
|
|
|
|
// Protected
|
|
|
|
findSpecialCharacterIndices(string, separators)
|
|
{
|
|
if (!string.length)
|
|
return [];
|
|
|
|
// Special characters include the following:
|
|
// - The first character.
|
|
// - Uppercase characters that follow a lowercase letter.
|
|
// - Separators and the first character following the separator.
|
|
|
|
let indices = [0];
|
|
|
|
for (let i = 1; i < string.length; ++i) {
|
|
let character = string[i];
|
|
let isSpecial = false;
|
|
|
|
if (separators.includes(character))
|
|
isSpecial = true;
|
|
else {
|
|
let previousCharacter = string[i - 1];
|
|
if (separators.includes(previousCharacter))
|
|
isSpecial = true;
|
|
else if (character.isUpperCase() && previousCharacter.isLowerCase())
|
|
isSpecial = true;
|
|
}
|
|
|
|
if (isSpecial)
|
|
indices.push(i);
|
|
}
|
|
|
|
return indices;
|
|
}
|
|
};
|
|
|
|
/* Controllers/AnimationManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: AnimationManager lacks advanced multi-target support. (Animations per-target)
|
|
|
|
WI.AnimationManager = class AnimationManager
|
|
{
|
|
constructor()
|
|
{
|
|
this._enabled = false;
|
|
this._animationCollection = new WI.AnimationCollection;
|
|
this._animationIdMap = new Map;
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleMainResourceDidChange, this);
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Animation"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Animation");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 13.1): Animation.enable did not exist yet.
|
|
if (target.hasCommand("Animation.enable"))
|
|
target.AnimationAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
get animationCollection() { return this._animationCollection; }
|
|
|
|
get supported()
|
|
{
|
|
// COMPATIBILITY (iOS 13.1): Animation.enable did not exist yet.
|
|
return InspectorBackend.hasCommand("Animation.enable");
|
|
}
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13.1): Animation.disable did not exist yet.
|
|
if (target.hasCommand("Animation.disable"))
|
|
target.AnimationAgent.disable();
|
|
}
|
|
|
|
this._animationCollection.clear();
|
|
this._animationIdMap.clear();
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
// AnimationObserver
|
|
|
|
animationCreated(animationPayload)
|
|
{
|
|
console.assert(!this._animationIdMap.has(animationPayload.animationId), `Animation already exists with id ${animationPayload.animationId}.`);
|
|
|
|
let animation = WI.Animation.fromPayload(animationPayload);
|
|
this._animationCollection.add(animation);
|
|
this._animationIdMap.set(animation.animationId, animation);
|
|
}
|
|
|
|
nameChanged(animationId, name)
|
|
{
|
|
let animation = this._animationIdMap.get(animationId);
|
|
console.assert(animation);
|
|
if (!animation)
|
|
return;
|
|
|
|
animation.nameChanged(name);
|
|
}
|
|
|
|
effectChanged(animationId, effect)
|
|
{
|
|
let animation = this._animationIdMap.get(animationId);
|
|
console.assert(animation);
|
|
if (!animation)
|
|
return;
|
|
|
|
animation.effectChanged(effect);
|
|
}
|
|
|
|
targetChanged(animationId, effect)
|
|
{
|
|
let animation = this._animationIdMap.get(animationId);
|
|
console.assert(animation);
|
|
if (!animation)
|
|
return;
|
|
|
|
animation.targetChanged(effect);
|
|
}
|
|
|
|
animationDestroyed(animationId)
|
|
{
|
|
let animation = this._animationIdMap.take(animationId);
|
|
console.assert(animation);
|
|
if (!animation)
|
|
return;
|
|
|
|
this._animationCollection.remove(animation);
|
|
}
|
|
|
|
// Private
|
|
|
|
_handleMainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
WI.Animation.resetUniqueDisplayNameNumbers();
|
|
|
|
this._animationCollection.clear();
|
|
this._animationIdMap.clear();
|
|
}
|
|
};
|
|
|
|
/* Controllers/AuditManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.AuditManager = class AuditManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._tests = [];
|
|
this._results = [];
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Inactive;
|
|
this._runningTests = [];
|
|
|
|
this._disabledDefaultTestsSetting = new WI.Setting("audit-disabled-default-tests", []);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static synthesizeWarning(message)
|
|
{
|
|
message = WI.UIString("Audit Warning: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.warn(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Warning, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
static synthesizeError(message)
|
|
{
|
|
message = WI.UIString("Audit Error: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.error(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
// Public
|
|
|
|
get tests() { return this._tests; }
|
|
get results() { return this._results; }
|
|
get runningState() { return this._runningState; }
|
|
|
|
get editing()
|
|
{
|
|
return this._runningState === WI.AuditManager.RunningState.Disabled;
|
|
}
|
|
|
|
set editing(editing)
|
|
{
|
|
console.assert(this._runningState === WI.AuditManager.RunningState.Disabled || this._runningState === WI.AuditManager.RunningState.Inactive);
|
|
if (this._runningState !== WI.AuditManager.RunningState.Disabled && this._runningState !== WI.AuditManager.RunningState.Inactive)
|
|
return;
|
|
|
|
let runningState = editing ? WI.AuditManager.RunningState.Disabled : WI.AuditManager.RunningState.Inactive;
|
|
console.assert(runningState !== this._runningState);
|
|
if (runningState === this._runningState)
|
|
return;
|
|
|
|
this._runningState = runningState;
|
|
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.EditingChanged);
|
|
|
|
if (!this.editing) {
|
|
WI.objectStores.audits.clear();
|
|
|
|
let disabledDefaultTests = [];
|
|
let saveDisabledDefaultTest = (test) => {
|
|
if (test.supported && test.disabled)
|
|
disabledDefaultTests.push(test.name);
|
|
|
|
if (test instanceof WI.AuditTestGroup) {
|
|
for (let child of test.tests)
|
|
saveDisabledDefaultTest(child);
|
|
}
|
|
};
|
|
|
|
for (let test of this._tests) {
|
|
if (test.default)
|
|
saveDisabledDefaultTest(test);
|
|
else
|
|
WI.objectStores.audits.putObject(test);
|
|
}
|
|
|
|
this._disabledDefaultTestsSetting.value = disabledDefaultTests;
|
|
}
|
|
}
|
|
|
|
async start(tests)
|
|
{
|
|
console.assert(this._runningState === WI.AuditManager.RunningState.Inactive);
|
|
if (this._runningState !== WI.AuditManager.RunningState.Inactive)
|
|
return null;
|
|
|
|
if (tests && tests.length)
|
|
tests = tests.filter((test) => typeof test === "object" && test instanceof WI.AuditTestBase);
|
|
else
|
|
tests = this._tests;
|
|
|
|
console.assert(tests.length);
|
|
if (!tests.length)
|
|
return null;
|
|
|
|
let mainResource = WI.networkManager.mainFrame.mainResource;
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Active;
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.RunningStateChanged);
|
|
|
|
this._runningTests = tests;
|
|
for (let test of this._runningTests)
|
|
test.clearResult();
|
|
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.TestScheduled);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
await Promise.chain(this._runningTests.map((test) => async () => {
|
|
if (this._runningState !== WI.AuditManager.RunningState.Active)
|
|
return;
|
|
|
|
if (target.hasDomain("Audit"))
|
|
await target.AuditAgent.setup();
|
|
|
|
let topLevelTest = test.topLevelTest;
|
|
console.assert(topLevelTest || window.InspectorTest, "No matching top-level test found", test);
|
|
if (topLevelTest)
|
|
await topLevelTest.runSetup();
|
|
|
|
await test.start();
|
|
|
|
if (target.hasDomain("Audit"))
|
|
await target.AuditAgent.teardown();
|
|
}));
|
|
|
|
let result = this._runningTests.map((test) => test.result).filter((result) => !!result);
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Inactive;
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.RunningStateChanged);
|
|
|
|
this._runningTests = [];
|
|
|
|
this._addResult(result);
|
|
|
|
if (mainResource !== WI.networkManager.mainFrame.mainResource) {
|
|
// Navigated while tests were running.
|
|
for (let test of this._tests)
|
|
test.clearResult();
|
|
}
|
|
|
|
return this._results.lastValue === result ? result : null;
|
|
}
|
|
|
|
stop()
|
|
{
|
|
console.assert(this._runningState === WI.AuditManager.RunningState.Active);
|
|
if (this._runningState !== WI.AuditManager.RunningState.Active)
|
|
return;
|
|
|
|
this._runningState = WI.AuditManager.RunningState.Stopping;
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.RunningStateChanged);
|
|
|
|
for (let test of this._runningTests)
|
|
test.stop();
|
|
}
|
|
|
|
async processJSON({json, error})
|
|
{
|
|
if (error) {
|
|
WI.AuditManager.synthesizeError(error);
|
|
return;
|
|
}
|
|
|
|
if (typeof json !== "object" || json === null) {
|
|
WI.AuditManager.synthesizeError(WI.UIString("invalid JSON"));
|
|
return;
|
|
}
|
|
|
|
if (json.type !== WI.AuditTestCase.TypeIdentifier && json.type !== WI.AuditTestGroup.TypeIdentifier
|
|
&& json.type !== WI.AuditTestCaseResult.TypeIdentifier && json.type !== WI.AuditTestGroupResult.TypeIdentifier) {
|
|
WI.AuditManager.synthesizeError(WI.UIString("unknown %s \u0022%s\u0022").format(WI.unlocalizedString("type"), json.type));
|
|
return;
|
|
}
|
|
|
|
let object = await WI.AuditTestGroup.fromPayload(json) || await WI.AuditTestCase.fromPayload(json) || await WI.AuditTestGroupResult.fromPayload(json) || await WI.AuditTestCaseResult.fromPayload(json);
|
|
if (!object)
|
|
return;
|
|
|
|
if (object instanceof WI.AuditTestBase)
|
|
this.addTest(object, {save: true});
|
|
else if (object instanceof WI.AuditTestResultBase)
|
|
this._addResult(object);
|
|
|
|
WI.showRepresentedObject(object);
|
|
}
|
|
|
|
export(saveMode, object)
|
|
{
|
|
console.assert(object instanceof WI.AuditTestCase || object instanceof WI.AuditTestGroup || object instanceof WI.AuditTestCaseResult || object instanceof WI.AuditTestGroupResult, object);
|
|
|
|
function dataForObject(object) {
|
|
return {
|
|
displayType: object instanceof WI.AuditTestResultBase ? WI.UIString("Result") : WI.UIString("Audit"),
|
|
content: JSON.stringify(object),
|
|
suggestedName: object.name + (object instanceof WI.AuditTestResultBase ? ".result" : ".audit"),
|
|
};
|
|
}
|
|
|
|
let data = [dataForObject(object)];
|
|
|
|
if (saveMode === WI.FileUtilities.SaveMode.FileVariants && object instanceof WI.AuditTestBase && object.result)
|
|
data.push(dataForObject(object.result));
|
|
|
|
WI.FileUtilities.save(saveMode, data);
|
|
}
|
|
|
|
loadStoredTests()
|
|
{
|
|
if (this._tests.length)
|
|
return;
|
|
|
|
this._addDefaultTests();
|
|
|
|
WI.objectStores.audits.getAll().then(async (tests) => {
|
|
for (let payload of tests) {
|
|
let test = await WI.AuditTestGroup.fromPayload(payload) || await WI.AuditTestCase.fromPayload(payload);
|
|
if (!test)
|
|
continue;
|
|
|
|
const key = null;
|
|
WI.objectStores.audits.associateObject(test, key, payload);
|
|
|
|
this.addTest(test);
|
|
}
|
|
});
|
|
}
|
|
|
|
addTest(test, {save} = {})
|
|
{
|
|
console.assert(test instanceof WI.AuditTestBase, test);
|
|
console.assert(!this._tests.includes(test), test);
|
|
|
|
this._tests.push(test);
|
|
|
|
if (save)
|
|
WI.objectStores.audits.putObject(test);
|
|
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.TestAdded, {test});
|
|
}
|
|
|
|
removeTest(test)
|
|
{
|
|
console.assert(this.editing);
|
|
console.assert(test instanceof WI.AuditTestBase, test);
|
|
console.assert(this._tests.includes(test) || test.default, test);
|
|
|
|
if (test.default) {
|
|
test.clearResult();
|
|
|
|
if (test.disabled) {
|
|
InspectorFrontendHost.beep();
|
|
return;
|
|
}
|
|
|
|
test.disabled = true;
|
|
|
|
let disabledTests = this._disabledDefaultTestsSetting.value.slice();
|
|
disabledTests.push(test.name);
|
|
this._disabledDefaultTestsSetting.value = disabledTests;
|
|
|
|
return;
|
|
}
|
|
|
|
console.assert(test.editable, test);
|
|
|
|
this._tests.remove(test);
|
|
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.TestRemoved, {test});
|
|
|
|
WI.objectStores.audits.deleteObject(test);
|
|
}
|
|
|
|
// Private
|
|
|
|
_addResult(result)
|
|
{
|
|
if (!result || (Array.isArray(result) && !result.length))
|
|
return;
|
|
|
|
this._results.push(result);
|
|
|
|
this.dispatchEventToListeners(WI.AuditManager.Event.TestCompleted, {
|
|
result,
|
|
index: this._results.length - 1,
|
|
});
|
|
}
|
|
|
|
_handleFrameMainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
if (this._runningState === WI.AuditManager.RunningState.Active)
|
|
this.stop();
|
|
else {
|
|
for (let test of this._tests)
|
|
test.clearResult();
|
|
}
|
|
}
|
|
|
|
_addDefaultTests()
|
|
{
|
|
console.assert(WI.DefaultAudits, "Default audits not loaded.");
|
|
if (!WI.DefaultAudits)
|
|
return;
|
|
|
|
const defaultTests = [
|
|
new WI.AuditTestGroup(WI.UIString("Demo Audit"), [
|
|
new WI.AuditTestGroup(WI.UIString("Result Levels"), [
|
|
new WI.AuditTestCase("level-pass", WI.DefaultAudits.levelPass.toString(), {description: WI.UIString("This is what the result of a passing test with no data looks like.")}),
|
|
new WI.AuditTestCase("level-warn", WI.DefaultAudits.levelWarn.toString(), {description: WI.UIString("This is what the result of a warning test with no data looks like.")}),
|
|
new WI.AuditTestCase("level-fail", WI.DefaultAudits.levelFail.toString(), {description: WI.UIString("This is what the result of a failing test with no data looks like.")}),
|
|
new WI.AuditTestCase("level-error", WI.DefaultAudits.levelError.toString(), {description: WI.UIString("This is what the result of a test that threw an error with no data looks like.")}),
|
|
new WI.AuditTestCase("level-unsupported", WI.DefaultAudits.levelUnsupported.toString(), {description: WI.UIString("This is what the result of an unsupported test with no data looks like.")}),
|
|
], {description: WI.UIString("These are all of the different test result levels.")}),
|
|
new WI.AuditTestGroup(WI.UIString("Result Data"), [
|
|
new WI.AuditTestCase("data-domNodes", WI.DefaultAudits.dataDOMNodes.toString(), {description: WI.UIString("This is an example of how result DOM nodes are shown. It will pass with the <body> element.")}),
|
|
new WI.AuditTestCase("data-domAttributes", WI.DefaultAudits.dataDOMAttributes.toString(), {description: WI.UIString("This is an example of how result DOM attributes are highlighted on any returned DOM nodes. It will pass with all elements with an id attribute.")}),
|
|
new WI.AuditTestCase("data-errors", WI.DefaultAudits.dataErrors.toString(), {description: WI.UIString("This is an example of how errors are shown. The error was thrown manually, but execution errors will appear in the same way.")}),
|
|
new WI.AuditTestCase("data-custom", WI.DefaultAudits.dataCustom.toString(), {description: WI.UIString("This is an example of how custom result data is shown."), supports: 3}),
|
|
], {description: WI.UIString("These are example tests that demonstrate all of the different types of data that can be returned with the test result.")}),
|
|
new WI.AuditTestGroup(WI.UIString("Specially Exposed Data"), [
|
|
new WI.AuditTestGroup(WI.UIString("Accessibility"), [
|
|
new WI.AuditTestCase("getElementsByComputedRole", WI.DefaultAudits.getElementsByComputedRole.toString(), {description: WI.UIString("This is an example test that uses %s to find elements with a computed role of \u201Clink\u201D.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getElementsByComputedRole")), supports: 1}),
|
|
new WI.AuditTestCase("getActiveDescendant", WI.DefaultAudits.getActiveDescendant.toString(), {description: WI.UIString("This is an example test that uses %s to find any element that meets criteria for active descendant (\u201C%s\u201D) of the <body> element, if it exists.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getActiveDescendant"), WI.unlocalizedString("aria-activedescendant")), supports: 1}),
|
|
new WI.AuditTestCase("getChildNodes", WI.DefaultAudits.getChildNodes.toString(), {description: WI.UIString("This is an example test that uses %s to find child nodes of the <body> element in the accessibility tree.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getChildNodes")), supports: 1}),
|
|
new WI.AuditTestCase("getComputedProperties", WI.DefaultAudits.getComputedProperties.toString(), {description: WI.UIString("This is an example test that uses %s to find a variety of accessibility information about the <body> element.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getComputedProperties")), supports: 3}),
|
|
new WI.AuditTestCase("getControlledNodes", WI.DefaultAudits.getControlledNodes.toString(), {description: WI.UIString("This is an example test that uses %s to find all nodes controlled (\u201C%s\u201D) by the <body> element, if any exist.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getControlledNodes"), WI.unlocalizedString("aria-controls")), supports: 1}),
|
|
new WI.AuditTestCase("getFlowedNodes", WI.DefaultAudits.getFlowedNodes.toString(), {description: WI.UIString("This is an example test that uses %s to find all nodes flowed to (\u201C%s\u201D) from the <body> element, if any exist.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getFlowedNodes"), WI.unlocalizedString("aria-flowto")), supports: 1}),
|
|
new WI.AuditTestCase("getMouseEventNode", WI.DefaultAudits.getMouseEventNode.toString(), {description: WI.UIString("This is an example test that uses %s to find the node that would handle mouse events for the <body> element, if applicable.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getMouseEventNode")), supports: 1}),
|
|
new WI.AuditTestCase("getOwnedNodes", WI.DefaultAudits.getOwnedNodes.toString(), {description: WI.UIString("This is an example test that uses %s to find all nodes owned (\u201C%s\u201D) by the <body> element, if any exist.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getOwnedNodes"), WI.unlocalizedString("aria-owns")), supports: 1}),
|
|
new WI.AuditTestCase("getParentNode", WI.DefaultAudits.getParentNode.toString(), {description: WI.UIString("This is an example test that uses %s to find the parent node of the <body> element in the accessibility tree.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getParentNode")), supports: 1}),
|
|
new WI.AuditTestCase("getSelectedChildNodes", WI.DefaultAudits.getSelectedChildNodes.toString(), {description: WI.UIString("This is an example test that uses %s to find all child nodes that are selected (\u201C%s\u201D) of the <body> element in the accessibility tree.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getSelectedChildNodes"), WI.unlocalizedString("aria-selected")), supports: 1}),
|
|
], {description: WI.UIString("These are example tests that demonstrate how to use %s to get information about the accessibility tree.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility")), supports: 1}),
|
|
new WI.AuditTestGroup(WI.UIString("DOM"), [
|
|
new WI.AuditTestCase("hasEventListeners", WI.DefaultAudits.hasEventListeners.toString(), {description: WI.UIString("This is an example test that uses %s to find data indicating whether the <body> element has any event listeners.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.hasEventListeners")), supports: 3}),
|
|
new WI.AuditTestCase("hasEventListeners-click", WI.DefaultAudits.hasEventListenersClick.toString(), {description: WI.UIString("This is an example test that uses %s to find data indicating whether the <body> element has any click event listeners.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.hasEventListenersClick")), supports: 3}),
|
|
], {description: WI.UIString("These are example tests that demonstrate how to use %s to get information about DOM nodes.").format(WI.unlocalizedString("WebInspectorAudit.DOM")), supports: 1}),
|
|
new WI.AuditTestGroup(WI.UIString("Resources"), [
|
|
new WI.AuditTestCase("getResources", WI.DefaultAudits.getResources.toString(), {description: WI.UIString("This is an example test that uses %s to find basic information about each resource.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getResources")), supports: 3}),
|
|
new WI.AuditTestCase("getResourceContent", WI.DefaultAudits.getResourceContent.toString(), {description: WI.UIString("This is an example test that uses %s to find the contents of the main resource.").format(WI.unlocalizedString("WebInspectorAudit.Accessibility.getResourceContent")), supports: 3}),
|
|
], {description: WI.UIString("These are example tests that demonstrate how to use %s to get information about loaded resources.").format(WI.unlocalizedString("WebInspectorAudit.Resources")), supports: 2}),
|
|
], {description: WI.UIString("These are example tests that demonstrate how to use %s to access information not normally available to JavaScript.").format(WI.unlocalizedString("WebInspectorAudit")), supports: 1}),
|
|
new WI.AuditTestCase("unsupported", WI.DefaultAudits.unsupported.toString(), {description: WI.UIString("This is an example of a test that will not run because it is unsupported."), supports: Infinity}),
|
|
], {description: WI.UIString("These are example tests that demonstrate the functionality and structure of audits.")}),
|
|
new WI.AuditTestGroup(WI.UIString("Accessibility"), [
|
|
new WI.AuditTestCase("testMenuRoleForRequiredChildren", WI.DefaultAudits.testMenuRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D and \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("menu"), WI.unlocalizedString("menubar")), supports: 1}),
|
|
new WI.AuditTestCase("testGridRoleForRequiredChildren", WI.DefaultAudits.testGridRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("grid")), supports: 1}),
|
|
new WI.AuditTestCase("testForAriaLabelledBySpelling", WI.DefaultAudits.testForAriaLabelledBySpelling.toString(), {description: WI.UIString("Ensure that \u201C%s\u201D is spelled correctly.").format(WI.unlocalizedString("aria-labelledby")), supports: 1}),
|
|
new WI.AuditTestCase("testForMultipleBanners", WI.DefaultAudits.testForMultipleBanners.toString(), {description: WI.UIString("Ensure that only one banner is used on the page."), supports: 1}),
|
|
new WI.AuditTestCase("testForLinkLabels", WI.DefaultAudits.testForLinkLabels.toString(), {description: WI.UIString("Ensure that links have accessible labels for assistive technology."), supports: 1}),
|
|
new WI.AuditTestCase("testRowGroupRoleForRequiredChildren", WI.DefaultAudits.testRowGroupRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("rowgroup")), supports: 1}),
|
|
new WI.AuditTestCase("testTableRoleForRequiredChildren", WI.DefaultAudits.testTableRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("table")), supports: 1}),
|
|
new WI.AuditTestCase("testForMultipleLiveRegions", WI.DefaultAudits.testForMultipleLiveRegions.toString(), {description: WI.UIString("Ensure that only one live region is used on the page."), supports: 1}),
|
|
new WI.AuditTestCase("testListBoxRoleForRequiredChildren", WI.DefaultAudits.testListBoxRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("listbox")), supports: 1}),
|
|
new WI.AuditTestCase("testImageLabels", WI.DefaultAudits.testImageLabels.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have accessible labels for assistive technology.").format(WI.unlocalizedString("img")), supports: 1}),
|
|
new WI.AuditTestCase("testForAriaHiddenFalse", WI.DefaultAudits.testForAriaHiddenFalse.toString(), {description: WI.UIString("Ensure aria-hidden=\u0022%s\u0022 is not used.").format(WI.unlocalizedString("false")), supports: 1}),
|
|
new WI.AuditTestCase("testTreeRoleForRequiredChildren", WI.DefaultAudits.testTreeRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("tree")), supports: 1}),
|
|
new WI.AuditTestCase("testRadioGroupRoleForRequiredChildren", WI.DefaultAudits.testRadioGroupRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("radiogroup")), supports: 1}),
|
|
new WI.AuditTestCase("testFeedRoleForRequiredChildren", WI.DefaultAudits.testFeedRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("feed")), supports: 1}),
|
|
new WI.AuditTestCase("testTabListRoleForRequiredChildren", WI.DefaultAudits.testTabListRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("tablist")), supports: 1}),
|
|
new WI.AuditTestCase("testButtonLabels", WI.DefaultAudits.testButtonLabels.toString(), {description: WI.UIString("Ensure that buttons have accessible labels for assistive technology."), supports: 1}),
|
|
new WI.AuditTestCase("testRowRoleForRequiredChildren", WI.DefaultAudits.testRowRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("row")), supports: 1}),
|
|
new WI.AuditTestCase("testListRoleForRequiredChildren", WI.DefaultAudits.testListRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("list")), supports: 1}),
|
|
new WI.AuditTestCase("testComboBoxRoleForRequiredChildren", WI.DefaultAudits.testComboBoxRoleForRequiredChildren.toString(), {description: WI.UIString("Ensure that elements of role \u201C%s\u201D have required owned elements in accordance with WAI-ARIA.").format(WI.unlocalizedString("combobox")), supports: 1}),
|
|
new WI.AuditTestCase("testForMultipleMainContentSections", WI.DefaultAudits.testForMultipleMainContentSections.toString(), {description: WI.UIString("Ensure that only one main content section is used on the page."), supports: 1}),
|
|
new WI.AuditTestCase("testDialogsForLabels", WI.DefaultAudits.testDialogsForLabels.toString(), {description: WI.UIString("Ensure that dialogs have accessible labels for assistive technology."), supports: 1}),
|
|
new WI.AuditTestCase("testForInvalidAriaHiddenValue", WI.DefaultAudits.testForInvalidAriaHiddenValue.toString(), {description: WI.UIString("Ensure that values for \u201C%s\u201D are valid.").format(WI.unlocalizedString("aria-hidden")), supports: 1})
|
|
], {description: WI.UIString("Diagnoses common accessibility problems affecting screen readers and other assistive technology.")}),
|
|
];
|
|
|
|
let checkDisabledDefaultTest = (test) => {
|
|
test.markAsDefault();
|
|
|
|
if (this._disabledDefaultTestsSetting.value.includes(test.name))
|
|
test.disabled = true;
|
|
|
|
if (test instanceof WI.AuditTestGroup) {
|
|
for (let child of test.tests)
|
|
checkDisabledDefaultTest(child);
|
|
}
|
|
};
|
|
|
|
for (let test of defaultTests) {
|
|
checkDisabledDefaultTest(test);
|
|
|
|
this.addTest(test);
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.AuditManager.RunningState = {
|
|
Disabled: "disabled",
|
|
Inactive: "inactive",
|
|
Active: "active",
|
|
Stopping: "stopping",
|
|
};
|
|
|
|
WI.AuditManager.Event = {
|
|
EditingChanged: "audit-manager-editing-changed",
|
|
RunningStateChanged: "audit-manager-running-state-changed",
|
|
TestAdded: "audit-manager-test-added",
|
|
TestCompleted: "audit-manager-test-completed",
|
|
TestRemoved: "audit-manager-test-removed",
|
|
TestScheduled: "audit-manager-test-scheduled",
|
|
};
|
|
|
|
/* Controllers/ApplicationCacheManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: ApplicationCacheManager lacks advanced multi-target support. (ApplciationCache objects per-target)
|
|
|
|
WI.ApplicationCacheManager = class ApplicationCacheManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
this._reset();
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["ApplicationCache"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "ApplicationCache");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("ApplicationCache")) {
|
|
target.ApplicationCacheAgent.enable();
|
|
target.ApplicationCacheAgent.getFramesWithManifests(this._framesWithManifestsLoaded.bind(this));
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get online() { return this._online; }
|
|
|
|
get applicationCacheObjects()
|
|
{
|
|
var applicationCacheObjects = [];
|
|
for (var id in this._applicationCacheObjects)
|
|
applicationCacheObjects.push(this._applicationCacheObjects[id]);
|
|
return applicationCacheObjects;
|
|
}
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
this._reset();
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
this._enabled = false;
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13): ApplicationCache.disable did not exist yet.
|
|
if (target.hasCommand("ApplicationCache.disable"))
|
|
target.ApplicationCacheAgent.disable();
|
|
}
|
|
|
|
WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.removeEventListener(WI.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this);
|
|
|
|
this._reset();
|
|
}
|
|
|
|
requestApplicationCache(frame, callback)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
function callbackWrapper(error, applicationCache)
|
|
{
|
|
if (error) {
|
|
callback(null);
|
|
return;
|
|
}
|
|
|
|
callback(applicationCache);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.ApplicationCacheAgent.getApplicationCacheForFrame(frame.id, callbackWrapper);
|
|
}
|
|
|
|
// ApplicationCacheObserver
|
|
|
|
networkStateUpdated(isNowOnline)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
this._online = isNowOnline;
|
|
|
|
this.dispatchEventToListeners(WI.ApplicationCacheManager.Event.NetworkStateUpdated, {online: this._online});
|
|
}
|
|
|
|
applicationCacheStatusUpdated(frameId, manifestURL, status)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let frame = WI.networkManager.frameForIdentifier(frameId);
|
|
if (!frame)
|
|
return;
|
|
|
|
this._frameManifestUpdated(frame, manifestURL, status);
|
|
}
|
|
|
|
// Private
|
|
|
|
_reset()
|
|
{
|
|
this._online = true;
|
|
this._applicationCacheObjects = {};
|
|
|
|
this.dispatchEventToListeners(WI.ApplicationCacheManager.Event.Cleared);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (event.target.isMainFrame()) {
|
|
this._reset();
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (target.hasDomain("ApplicationCache"))
|
|
target.ApplicationCacheAgent.getManifestForFrame(event.target.id, this._manifestForFrameLoaded.bind(this, event.target.id));
|
|
}
|
|
|
|
_childFrameWasRemoved(event)
|
|
{
|
|
this._frameManifestRemoved(event.data.childFrame);
|
|
}
|
|
|
|
_manifestForFrameLoaded(frameId, error, manifestURL)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
var frame = WI.networkManager.frameForIdentifier(frameId);
|
|
if (!frame)
|
|
return;
|
|
|
|
// A frame can go away between `ApplicationCache.getManifestForFrame` being called and the
|
|
// response being received.
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
if (!manifestURL)
|
|
this._frameManifestRemoved(frame);
|
|
}
|
|
|
|
_framesWithManifestsLoaded(error, framesWithManifests)
|
|
{
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
for (var i = 0; i < framesWithManifests.length; ++i) {
|
|
var frame = WI.networkManager.frameForIdentifier(framesWithManifests[i].frameId);
|
|
if (!frame)
|
|
continue;
|
|
|
|
this._frameManifestUpdated(frame, framesWithManifests[i].manifestURL, framesWithManifests[i].status);
|
|
}
|
|
}
|
|
|
|
_frameManifestUpdated(frame, manifestURL, status)
|
|
{
|
|
if (status === WI.ApplicationCacheManager.Status.Uncached) {
|
|
this._frameManifestRemoved(frame);
|
|
return;
|
|
}
|
|
|
|
if (!manifestURL)
|
|
return;
|
|
|
|
var manifestFrame = this._applicationCacheObjects[frame.id];
|
|
if (manifestFrame && manifestURL !== manifestFrame.manifest.manifestURL)
|
|
this._frameManifestRemoved(frame);
|
|
|
|
var oldStatus = manifestFrame ? manifestFrame.status : -1;
|
|
var statusChanged = manifestFrame && status !== oldStatus;
|
|
if (manifestFrame)
|
|
manifestFrame.status = status;
|
|
|
|
if (!this._applicationCacheObjects[frame.id]) {
|
|
var cacheManifest = new WI.ApplicationCacheManifest(manifestURL);
|
|
this._applicationCacheObjects[frame.id] = new WI.ApplicationCacheFrame(frame, cacheManifest, status);
|
|
|
|
this.dispatchEventToListeners(WI.ApplicationCacheManager.Event.FrameManifestAdded, {frameManifest: this._applicationCacheObjects[frame.id]});
|
|
}
|
|
|
|
if (statusChanged)
|
|
this.dispatchEventToListeners(WI.ApplicationCacheManager.Event.FrameManifestStatusChanged, {frameManifest: this._applicationCacheObjects[frame.id]});
|
|
}
|
|
|
|
_frameManifestRemoved(frame)
|
|
{
|
|
if (!this._applicationCacheObjects[frame.id])
|
|
return;
|
|
|
|
delete this._applicationCacheObjects[frame.id];
|
|
|
|
this.dispatchEventToListeners(WI.ApplicationCacheManager.Event.FrameManifestRemoved, {frame});
|
|
}
|
|
};
|
|
|
|
WI.ApplicationCacheManager.Event = {
|
|
Cleared: "application-cache-manager-cleared",
|
|
FrameManifestAdded: "application-cache-manager-frame-manifest-added",
|
|
FrameManifestRemoved: "application-cache-manager-frame-manifest-removed",
|
|
FrameManifestStatusChanged: "application-cache-manager-frame-manifest-status-changed",
|
|
NetworkStateUpdated: "application-cache-manager-network-state-updated"
|
|
};
|
|
|
|
WI.ApplicationCacheManager.Status = {
|
|
Uncached: 0,
|
|
Idle: 1,
|
|
Checking: 2,
|
|
Downloading: 3,
|
|
UpdateReady: 4,
|
|
Obsolete: 5
|
|
};
|
|
|
|
/* Controllers/BreakpointLogMessageLexer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BreakpointLogMessageLexer = class BreakpointLogMessageLexer extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._stateFunctions = {
|
|
[WI.BreakpointLogMessageLexer.State.Expression]: this._expression,
|
|
[WI.BreakpointLogMessageLexer.State.PlainText]: this._plainText,
|
|
[WI.BreakpointLogMessageLexer.State.PossiblePlaceholder]: this._possiblePlaceholder,
|
|
[WI.BreakpointLogMessageLexer.State.RegExpOrStringLiteral]: this._regExpOrStringLiteral,
|
|
};
|
|
|
|
this.reset();
|
|
}
|
|
|
|
// Public
|
|
|
|
tokenize(input)
|
|
{
|
|
this.reset();
|
|
this._input = input;
|
|
|
|
while (this._index < this._input.length) {
|
|
let stateFunction = this._stateFunctions[this._states.lastValue];
|
|
console.assert(stateFunction);
|
|
if (!stateFunction) {
|
|
this.reset();
|
|
return null;
|
|
}
|
|
|
|
stateFunction.call(this);
|
|
}
|
|
|
|
// Needed for trailing plain text.
|
|
this._finishPlainText();
|
|
|
|
return this._tokens;
|
|
}
|
|
|
|
reset()
|
|
{
|
|
this._input = "";
|
|
this._buffer = "";
|
|
|
|
this._index = 0;
|
|
this._states = [WI.BreakpointLogMessageLexer.State.PlainText];
|
|
this._literalStartCharacter = "";
|
|
this._curlyBraceDepth = 0;
|
|
this._tokens = [];
|
|
}
|
|
|
|
// Private
|
|
|
|
_finishPlainText()
|
|
{
|
|
this._appendToken(WI.BreakpointLogMessageLexer.TokenType.PlainText);
|
|
}
|
|
|
|
_finishExpression()
|
|
{
|
|
this._appendToken(WI.BreakpointLogMessageLexer.TokenType.Expression);
|
|
}
|
|
|
|
_appendToken(type)
|
|
{
|
|
if (!this._buffer)
|
|
return;
|
|
|
|
this._tokens.push({type, data: this._buffer});
|
|
this._buffer = "";
|
|
}
|
|
|
|
_consume()
|
|
{
|
|
console.assert(this._index < this._input.length);
|
|
|
|
let character = this._peek();
|
|
this._index++;
|
|
return character;
|
|
}
|
|
|
|
_peek()
|
|
{
|
|
return this._input[this._index] || null;
|
|
}
|
|
|
|
// States
|
|
|
|
_expression()
|
|
{
|
|
let character = this._consume();
|
|
|
|
if (character === "}") {
|
|
if (this._curlyBraceDepth === 0) {
|
|
this._finishExpression();
|
|
|
|
console.assert(this._states.lastValue === WI.BreakpointLogMessageLexer.State.Expression);
|
|
this._states.pop();
|
|
return;
|
|
}
|
|
|
|
this._curlyBraceDepth--;
|
|
}
|
|
|
|
this._buffer += character;
|
|
|
|
if (character === "/" || character === "\"" || character === "'") {
|
|
this._literalStartCharacter = character;
|
|
this._states.push(WI.BreakpointLogMessageLexer.State.RegExpOrStringLiteral);
|
|
} else if (character === "{")
|
|
this._curlyBraceDepth++;
|
|
}
|
|
|
|
_plainText()
|
|
{
|
|
let character = this._peek();
|
|
|
|
if (character === "$")
|
|
this._states.push(WI.BreakpointLogMessageLexer.State.PossiblePlaceholder);
|
|
else {
|
|
this._buffer += character;
|
|
this._consume();
|
|
}
|
|
}
|
|
|
|
_possiblePlaceholder()
|
|
{
|
|
let character = this._consume();
|
|
console.assert(character === "$");
|
|
let nextCharacter = this._peek();
|
|
|
|
console.assert(this._states.lastValue === WI.BreakpointLogMessageLexer.State.PossiblePlaceholder);
|
|
this._states.pop();
|
|
|
|
if (nextCharacter === "{") {
|
|
this._finishPlainText();
|
|
this._consume();
|
|
this._states.push(WI.BreakpointLogMessageLexer.State.Expression);
|
|
} else
|
|
this._buffer += character;
|
|
}
|
|
|
|
_regExpOrStringLiteral()
|
|
{
|
|
let character = this._consume();
|
|
this._buffer += character;
|
|
|
|
if (character === "\\") {
|
|
if (this._peek() !== null)
|
|
this._buffer += this._consume();
|
|
return;
|
|
}
|
|
|
|
if (character === this._literalStartCharacter) {
|
|
console.assert(this._states.lastValue === WI.BreakpointLogMessageLexer.State.RegExpOrStringLiteral);
|
|
this._states.pop();
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.BreakpointLogMessageLexer.State = {
|
|
Expression: Symbol("expression"),
|
|
PlainText: Symbol("plain-text"),
|
|
PossiblePlaceholder: Symbol("possible-placeholder"),
|
|
RegExpOrStringLiteral: Symbol("regexp-or-string-literal"),
|
|
};
|
|
|
|
WI.BreakpointLogMessageLexer.TokenType = {
|
|
PlainText: "token-type-plain-text",
|
|
Expression: "token-type-expression",
|
|
};
|
|
|
|
/* Controllers/BrowserManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.BrowserManager = class BrowserManager
|
|
{
|
|
constructor()
|
|
{
|
|
this._enabled = false;
|
|
this._extensionNameIdentifierMap = new Map;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 13.4): Browser did not exist yet.
|
|
if (target.hasDomain("Browser"))
|
|
target.BrowserAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
for (let target of WI.targetManager.allTargets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
for (let target of WI.targetManager.allTargets) {
|
|
// COMPATIBILITY (iOS 13.4): Browser did not exist yet.
|
|
if (target.hasDomain("Browser"))
|
|
target.BrowserAgent.disable();
|
|
}
|
|
|
|
this._extensionNameIdentifierMap.clear();
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
isExtensionScheme(scheme)
|
|
{
|
|
return scheme && scheme.endsWith("-extension");
|
|
}
|
|
|
|
extensionNameForId(extensionId)
|
|
{
|
|
return this._extensionNameIdentifierMap.get(extensionId) || null;
|
|
}
|
|
|
|
extensionNameForURL(url)
|
|
{
|
|
console.assert(this.isExtensionScheme(parseURL(url).scheme));
|
|
|
|
let match = url.match(/^[a-z\-]*extension:\/\/([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})\//);
|
|
if (!match)
|
|
return null;
|
|
|
|
return this.extensionNameForId(match[1]);
|
|
}
|
|
|
|
extensionNameForExecutionContext(context)
|
|
{
|
|
console.assert(context instanceof WI.ExecutionContext);
|
|
console.assert(context.type === WI.ExecutionContext.Type.User);
|
|
|
|
let match = context.name.match(/^[A-Za-z]*ExtensionWorld-([0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12})$/);
|
|
if (!match)
|
|
return null;
|
|
|
|
return this.extensionNameForId(match[1]);
|
|
}
|
|
|
|
// BrowserObserver
|
|
|
|
extensionsEnabled(extensions)
|
|
{
|
|
for (let {extensionId, name} of extensions) {
|
|
console.assert(!this._extensionNameIdentifierMap.has(extensionId), `Extension already exists with id '${extensionId}'.`);
|
|
|
|
this._extensionNameIdentifierMap.set(extensionId, name);
|
|
}
|
|
}
|
|
|
|
extensionsDisabled(extensionIds)
|
|
{
|
|
for (let extensionId of extensionIds) {
|
|
let name = this._extensionNameIdentifierMap.take(extensionId);
|
|
console.assert(name, `Missing extension with id '${extensionId}'.`);
|
|
}
|
|
}
|
|
};
|
|
|
|
/* Controllers/CSSManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: CSSManager lacks advanced multi-target support. (Stylesheets per-target)
|
|
|
|
WI.CSSManager = class CSSManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceAdded, this);
|
|
WI.Resource.addEventListener(WI.SourceCode.Event.ContentDidChange, this._resourceContentDidChange, this);
|
|
WI.Resource.addEventListener(WI.Resource.Event.TypeDidChange, this._resourceTypeDidChange, this);
|
|
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.AttributeModified, this._nodeAttributesDidChange, this);
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.AttributeRemoved, this._nodeAttributesDidChange, this);
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.EnabledPseudoClassesChanged, this._nodePseudoClassesDidChange, this);
|
|
|
|
this._colorFormatSetting = new WI.Setting("default-color-format", WI.Color.Format.Original);
|
|
|
|
this._styleSheetIdentifierMap = new Map;
|
|
this._styleSheetFrameURLMap = new Map;
|
|
this._nodeStylesMap = {};
|
|
this._modifiedStyles = new Map;
|
|
this._defaultUserPreferences = new Map;
|
|
this._overriddenUserPreferences = new Map;
|
|
this._propertyNameCompletions = null;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (target.hasDomain("CSS"))
|
|
target.CSSAgent.enable();
|
|
}
|
|
|
|
initializeCSSPropertyNameCompletions(target)
|
|
{
|
|
console.assert(target.hasDomain("CSS"));
|
|
|
|
if (this._propertyNameCompletions)
|
|
return;
|
|
|
|
target.CSSAgent.getSupportedCSSProperties((error, cssProperties) => {
|
|
if (error)
|
|
return;
|
|
|
|
this._propertyNameCompletions = new WI.CSSPropertyNameCompletions(cssProperties);
|
|
|
|
WI.CSSKeywordCompletions.addCustomCompletions(cssProperties);
|
|
|
|
// CodeMirror is not included by tests so we shouldn't assume it always exists.
|
|
// If it isn't available we skip MIME type associations.
|
|
if (!window.CodeMirror)
|
|
return;
|
|
|
|
let propertyNamesForCodeMirror = {};
|
|
let valueKeywordsForCodeMirror = {"inherit": true, "initial": true, "unset": true, "revert": true, "revert-layer": true, "var": true, "env": true};
|
|
let colorKeywordsForCodeMirror = {};
|
|
|
|
function nameForCodeMirror(name) {
|
|
// CodeMirror parses the vendor prefix separate from the property or keyword name,
|
|
// so we need to strip vendor prefixes from our names. Also strip function parenthesis.
|
|
return name.replace(/^-[^-]+-/, "").replace(/\(\)$/, "").toLowerCase();
|
|
}
|
|
|
|
for (let property of cssProperties) {
|
|
// Properties can also be value keywords, like when used in a transition.
|
|
// So we add them to both lists.
|
|
let codeMirrorPropertyName = nameForCodeMirror(property.name);
|
|
propertyNamesForCodeMirror[codeMirrorPropertyName] = true;
|
|
valueKeywordsForCodeMirror[codeMirrorPropertyName] = true;
|
|
}
|
|
|
|
for (let propertyName in WI.CSSKeywordCompletions._propertyKeywordMap) {
|
|
let keywords = WI.CSSKeywordCompletions._propertyKeywordMap[propertyName];
|
|
for (let keyword of keywords) {
|
|
// Skip numbers, like the ones defined for font-weight.
|
|
if (keyword === WI.CSSKeywordCompletions.AllPropertyNamesPlaceholder || !isNaN(Number(keyword)))
|
|
continue;
|
|
valueKeywordsForCodeMirror[nameForCodeMirror(keyword)] = true;
|
|
}
|
|
}
|
|
|
|
for (let color of WI.CSSKeywordCompletions._colors)
|
|
colorKeywordsForCodeMirror[nameForCodeMirror(color)] = true;
|
|
|
|
// TODO: Remove these keywords once they are built-in codemirror or once we get values from WebKit itself.
|
|
valueKeywordsForCodeMirror["conic-gradient"] = true;
|
|
valueKeywordsForCodeMirror["repeating-conic-gradient"] = true;
|
|
|
|
function updateCodeMirrorCSSMode(mimeType) {
|
|
let modeSpec = CodeMirror.resolveMode(mimeType);
|
|
|
|
console.assert(modeSpec.propertyKeywords);
|
|
console.assert(modeSpec.valueKeywords);
|
|
console.assert(modeSpec.colorKeywords);
|
|
|
|
modeSpec.propertyKeywords = propertyNamesForCodeMirror;
|
|
modeSpec.valueKeywords = valueKeywordsForCodeMirror;
|
|
modeSpec.colorKeywords = colorKeywordsForCodeMirror;
|
|
|
|
CodeMirror.defineMIME(mimeType, modeSpec);
|
|
}
|
|
|
|
updateCodeMirrorCSSMode("text/css");
|
|
updateCodeMirrorCSSMode("text/x-scss");
|
|
});
|
|
|
|
if (target.hasCommand("CSS.getSupportedSystemFontFamilyNames")) {
|
|
target.CSSAgent.getSupportedSystemFontFamilyNames((error, fontFamilyNames) =>{
|
|
if (error)
|
|
return;
|
|
|
|
WI.CSSKeywordCompletions.addPropertyCompletionValues("font-family", fontFamilyNames);
|
|
WI.CSSKeywordCompletions.addPropertyCompletionValues("font", fontFamilyNames);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsInspectorStyleSheet()
|
|
{
|
|
return InspectorBackend.hasCommand("CSS.createStyleSheet");
|
|
}
|
|
|
|
static protocolStyleSheetOriginToEnum(origin)
|
|
{
|
|
switch (origin) {
|
|
case InspectorBackend.Enum.CSS.StyleSheetOrigin.User:
|
|
return WI.CSSStyleSheet.Type.User;
|
|
|
|
case InspectorBackend.Enum.CSS.StyleSheetOrigin.UserAgent:
|
|
return WI.CSSStyleSheet.Type.UserAgent;
|
|
|
|
case InspectorBackend.Enum.CSS.StyleSheetOrigin.Inspector:
|
|
return WI.CSSStyleSheet.Type.Inspector;
|
|
}
|
|
|
|
// COMPATIBILITY (iOS 14): CSS.StyleSheetOrigin.Regular was replaced with CSS.StyleSheetOrigin.Author.
|
|
console.assert(!InspectorBackend.Enum.CSS.StyleSheetOrigin.Author || origin === InspectorBackend.Enum.CSS.StyleSheetOrigin.Author);
|
|
console.assert(!InspectorBackend.Enum.CSS.StyleSheetOrigin.Regular || origin === InspectorBackend.Enum.CSS.StyleSheetOrigin.Regular);
|
|
return WI.CSSStyleSheet.Type.Author;
|
|
}
|
|
|
|
static protocolGroupingTypeToEnum(type)
|
|
{
|
|
// COMPATIBILITY (iOS 13): CSS.Grouping did not exist yet.
|
|
if (!InspectorBackend.Enum.CSS.Grouping) {
|
|
switch (type) {
|
|
case "mediaRule":
|
|
return WI.CSSGrouping.Type.MediaRule;
|
|
case "importRule":
|
|
return WI.CSSGrouping.Type.MediaImportRule;
|
|
case "linkedSheet":
|
|
return WI.CSSGrouping.Type.MediaLinkNode;
|
|
case "inlineSheet":
|
|
return WI.CSSGrouping.Type.MediaStyleNode;
|
|
}
|
|
}
|
|
return type;
|
|
}
|
|
|
|
static displayNameForPseudoId(pseudoId)
|
|
{
|
|
// Compatibility (iOS 12.2): CSS.PseudoId did not exist.
|
|
if (!InspectorBackend.Enum.CSS.PseudoId) {
|
|
switch (pseudoId) {
|
|
case 1: // PseudoId.FirstLine
|
|
return WI.unlocalizedString("::first-line");
|
|
case 2: // PseudoId.FirstLetter
|
|
return WI.unlocalizedString("::first-letter");
|
|
case 3: // PseudoId.Marker
|
|
return WI.unlocalizedString("::marker");
|
|
case 4: // PseudoId.Before
|
|
return WI.unlocalizedString("::before");
|
|
case 5: // PseudoId.After
|
|
return WI.unlocalizedString("::after");
|
|
case 6: // PseudoId.Selection
|
|
return WI.unlocalizedString("::-webkit-selection");
|
|
case 7: // PseudoId.Scrollbar
|
|
return WI.unlocalizedString("::-webkit-scrollbar");
|
|
case 8: // PseudoId.ScrollbarThumb
|
|
return WI.unlocalizedString("::-webkit-scrollbar-thumb");
|
|
case 9: // PseudoId.ScrollbarButton
|
|
return WI.unlocalizedString("::-webkit-scrollbar-button");
|
|
case 10: // PseudoId.ScrollbarTrack
|
|
return WI.unlocalizedString("::-webkit-scrollbar-track");
|
|
case 11: // PseudoId.ScrollbarTrackPiece
|
|
return WI.unlocalizedString("::-webkit-scrollbar-track-piece");
|
|
case 12: // PseudoId.ScrollbarCorner
|
|
return WI.unlocalizedString("::-webkit-scrollbar-corner");
|
|
case 13: // PseudoId.Resizer
|
|
return WI.unlocalizedString("::-webkit-resizer");
|
|
|
|
default:
|
|
console.error("Unknown pseudo id", pseudoId);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
switch (pseudoId) {
|
|
case CSSManager.PseudoSelectorNames.FirstLine:
|
|
return WI.unlocalizedString("::first-line");
|
|
case CSSManager.PseudoSelectorNames.FirstLetter:
|
|
return WI.unlocalizedString("::first-letter");
|
|
case CSSManager.PseudoSelectorNames.Highlight:
|
|
return WI.unlocalizedString("::highlight");
|
|
case CSSManager.PseudoSelectorNames.GrammarError:
|
|
return WI.unlocalizedString("::grammar-error");
|
|
case CSSManager.PseudoSelectorNames.Marker:
|
|
return WI.unlocalizedString("::marker");
|
|
case CSSManager.PseudoSelectorNames.Before:
|
|
return WI.unlocalizedString("::before");
|
|
case CSSManager.PseudoSelectorNames.After:
|
|
return WI.unlocalizedString("::after");
|
|
case CSSManager.PseudoSelectorNames.Selection:
|
|
return WI.unlocalizedString("::selection");
|
|
case CSSManager.PseudoSelectorNames.Backdrop:
|
|
return WI.unlocalizedString("::backdrop");
|
|
case CSSManager.PseudoSelectorNames.SpellingError:
|
|
return WI.unlocalizedString("::spelling-error");
|
|
case CSSManager.PseudoSelectorNames.ViewTransition:
|
|
return WI.unlocalizedString("::view-transition");
|
|
case CSSManager.PseudoSelectorNames.ViewTransitionGroup:
|
|
return WI.unlocalizedString("::view-transition-group");
|
|
case CSSManager.PseudoSelectorNames.ViewTransitionImagePair:
|
|
return WI.unlocalizedString("::view-transition-image-pair");
|
|
case CSSManager.PseudoSelectorNames.ViewTransitionNew:
|
|
return WI.unlocalizedString("::view-transition-new");
|
|
case CSSManager.PseudoSelectorNames.ViewTransitionOld:
|
|
return WI.unlocalizedString("::view-transition-old");
|
|
// COMPATIBILITY (iOS 17.0): PseudoId unprefixed aliases for prefixed protocol names.
|
|
case CSSManager.PseudoSelectorNames.WebKitResizer:
|
|
case "resizer":
|
|
return WI.unlocalizedString("::-webkit-resizer");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbar:
|
|
case "scrollbar":
|
|
return WI.unlocalizedString("::-webkit-scrollbar");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbarThumb:
|
|
case "scrollbar-thumb":
|
|
return WI.unlocalizedString("::-webkit-scrollbar-thumb");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbarButton:
|
|
case "scrollbar-button":
|
|
return WI.unlocalizedString("::-webkit-scrollbar-button");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbarTrack:
|
|
case "scrollbar-track":
|
|
return WI.unlocalizedString("::-webkit-scrollbar-track");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbarTrackPiece:
|
|
case "scrollbar-track-piece":
|
|
return WI.unlocalizedString("::-webkit-scrollbar-track-piece");
|
|
case CSSManager.PseudoSelectorNames.WebKitScrollbarCorner:
|
|
case "scrollbar-corner":
|
|
return WI.unlocalizedString("::-webkit-scrollbar-corner");
|
|
|
|
default:
|
|
console.error("Unknown pseudo id", pseudoId);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
static displayNameForForceablePseudoClass(pseudoClass)
|
|
{
|
|
switch (pseudoClass) {
|
|
case WI.CSSManager.ForceablePseudoClass.Active:
|
|
return WI.unlocalizedString(":active");
|
|
case WI.CSSManager.ForceablePseudoClass.Focus:
|
|
return WI.unlocalizedString(":focus");
|
|
case WI.CSSManager.ForceablePseudoClass.FocusVisible:
|
|
return WI.unlocalizedString(":focus-visible");
|
|
case WI.CSSManager.ForceablePseudoClass.FocusWithin:
|
|
return WI.unlocalizedString(":focus-within");
|
|
case WI.CSSManager.ForceablePseudoClass.Hover:
|
|
return WI.unlocalizedString(":hover");
|
|
case WI.CSSManager.ForceablePseudoClass.Target:
|
|
return WI.unlocalizedString(":target");
|
|
case WI.CSSManager.ForceablePseudoClass.Visited:
|
|
return WI.unlocalizedString(":visited");
|
|
}
|
|
|
|
console.assert(false, "Unknown pseudo class", pseudoClass);
|
|
return "";
|
|
}
|
|
|
|
// Public
|
|
|
|
get propertyNameCompletions() { return this._propertyNameCompletions; }
|
|
|
|
get overriddenUserPreferences() { return this._overriddenUserPreferences; }
|
|
|
|
get defaultUserPreferences() { return this._defaultUserPreferences; }
|
|
|
|
get overriddenUserPreferences() { return this._overriddenUserPreferences; }
|
|
|
|
get preferredColorFormat()
|
|
{
|
|
return this._colorFormatSetting.value;
|
|
}
|
|
|
|
get styleSheets()
|
|
{
|
|
return Array.from(this._styleSheetIdentifierMap.values());
|
|
}
|
|
|
|
get supportsOverrideUserPreference()
|
|
{
|
|
return InspectorBackend.hasCommand("Page.overrideUserPreference") && this._defaultUserPreferences.size;
|
|
}
|
|
|
|
get supportsOverrideColorScheme()
|
|
{
|
|
// A backend for a platform that does not support color schemes will not dispatch an initial event (Page.defaultAppearanceDidChange or Page.defaultUserPreferencesDidChange)
|
|
// with the default value for the color scheme preference which gets stored on the frontend.
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `PrefersColorScheme` value for `Page.UserPreferenceName` did not exist yet.
|
|
return this._defaultUserPreferences.has(InspectorBackend.Enum.Page.UserPreferenceName?.PrefersColorScheme) || this._defaultUserPreferences.has(WI.CSSManager.ForcedAppearancePreference);
|
|
}
|
|
|
|
// COMPATIBILITY (macOS 13, iOS 16.0): `Page.setForcedAppearance()` was removed in favor of `Page.overrideUserPreference()`
|
|
setForcedAppearance(name)
|
|
{
|
|
let commandArguments = {};
|
|
|
|
switch (name) {
|
|
case WI.CSSManager.Appearance.Light:
|
|
commandArguments.appearance = InspectorBackend.Enum.Page.Appearance.Light;
|
|
break;
|
|
|
|
case WI.CSSManager.Appearance.Dark:
|
|
commandArguments.appearance = InspectorBackend.Enum.Page.Appearance.Dark;
|
|
break;
|
|
|
|
case null:
|
|
// COMPATIBILITY (iOS 14): the `appearance`` parameter of `Page.setForcedAppearance` was not optional.
|
|
// Since support can't be tested directly, check for the `options`` parameter of `DOMDebugger.setDOMBreakpoint` (iOS 14.0+).
|
|
// FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed.
|
|
if (!InspectorBackend.hasCommand("DOMDebugger.setDOMBreakpoint", "options"))
|
|
commandArguments.appearance = "";
|
|
break;
|
|
|
|
default:
|
|
console.assert(false, "Unknown appearance", name);
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
return target.PageAgent.setForcedAppearance.invoke(commandArguments);
|
|
}
|
|
|
|
set layoutContextTypeChangedMode(layoutContextTypeChangedMode)
|
|
{
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 14.5): CSS.setLayoutContextTypeChangedMode did not exist.
|
|
if (target.hasCommand("CSS.setLayoutContextTypeChangedMode"))
|
|
target.CSSAgent.setLayoutContextTypeChangedMode(layoutContextTypeChangedMode);
|
|
}
|
|
}
|
|
|
|
canForcePseudoClass(pseudoClass)
|
|
{
|
|
if (!InspectorBackend.hasCommand("CSS.forcePseudoState"))
|
|
return false;
|
|
|
|
if (!pseudoClass)
|
|
return true;
|
|
|
|
switch (pseudoClass) {
|
|
case WI.CSSManager.ForceablePseudoClass.Active:
|
|
case WI.CSSManager.ForceablePseudoClass.Focus:
|
|
case WI.CSSManager.ForceablePseudoClass.Hover:
|
|
case WI.CSSManager.ForceablePseudoClass.Visited:
|
|
return true;
|
|
|
|
case WI.CSSManager.ForceablePseudoClass.FocusVisible:
|
|
case WI.CSSManager.ForceablePseudoClass.FocusWithin:
|
|
case WI.CSSManager.ForceablePseudoClass.Target:
|
|
// COMPATIBILITY (macOS 12.3, iOS 15.4): CSS.ForceablePseudoClass did not exist yet.
|
|
return !!InspectorBackend.Enum.CSS.ForceablePseudoClass;
|
|
}
|
|
|
|
console.assert(false, "Unknown pseudo class", pseudoClass);
|
|
return false;
|
|
}
|
|
|
|
overrideUserPreference(preference, value)
|
|
{
|
|
let promises = [];
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `Page.overrideUserPreference()` did not exist yet.
|
|
if (target.hasCommand("Page.overrideUserPreference") && InspectorBackend.Enum.Page.UserPreferenceName[preference])
|
|
promises.push(target.PageAgent.overrideUserPreference(preference, value));
|
|
|
|
// COMPATIBILITY (macOS 13, iOS 16.0): `Page.setForcedAppearance()` was removed in favor of `Page.overrideUserPreference()`
|
|
if (preference === WI.CSSManager.ForcedAppearancePreference && target.hasCommand("Page.setForcedAppearance"))
|
|
promises.push(this.setForcedAppearance(value || null));
|
|
}
|
|
|
|
if (value)
|
|
this._overriddenUserPreferences.set(preference, value);
|
|
else
|
|
this._overriddenUserPreferences.delete(preference);
|
|
|
|
Promise.allSettled(promises).then(() => {
|
|
this.mediaQueryResultChanged();
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.OverriddenUserPreferencesDidChange);
|
|
})
|
|
}
|
|
|
|
propertyNameHasOtherVendorPrefix(name)
|
|
{
|
|
if (!name || name.length < 4 || name.charAt(0) !== "-")
|
|
return false;
|
|
|
|
var match = name.match(/^(?:-moz-|-ms-|-o-|-epub-)/);
|
|
if (!match)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
propertyValueHasOtherVendorKeyword(value)
|
|
{
|
|
var match = value.match(/(?:-moz-|-ms-|-o-|-epub-)[-\w]+/);
|
|
if (!match)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
canonicalNameForPropertyName(name)
|
|
{
|
|
if (!name || name.length < 8 || name.charAt(0) !== "-")
|
|
return name;
|
|
|
|
// Keep in sync with prefix list from Source/WebInspectorUI/Scripts/update-inspector-css-documentation
|
|
var match = name.match(/^(?:-webkit-|-khtml-|-apple-)(.+)/);
|
|
if (!match)
|
|
return name;
|
|
|
|
return match[1];
|
|
}
|
|
|
|
styleSheetForIdentifier(id)
|
|
{
|
|
let styleSheet = this._styleSheetIdentifierMap.get(id);
|
|
if (styleSheet)
|
|
return styleSheet;
|
|
|
|
styleSheet = new WI.CSSStyleSheet(id);
|
|
this._styleSheetIdentifierMap.set(id, styleSheet);
|
|
return styleSheet;
|
|
}
|
|
|
|
stylesForNode(node)
|
|
{
|
|
if (node.id in this._nodeStylesMap)
|
|
return this._nodeStylesMap[node.id];
|
|
|
|
var styles = new WI.DOMNodeStyles(node);
|
|
this._nodeStylesMap[node.id] = styles;
|
|
return styles;
|
|
}
|
|
|
|
inspectorStyleSheetsForFrame(frame)
|
|
{
|
|
return this.styleSheets.filter((styleSheet) => styleSheet.isInspectorStyleSheet() && styleSheet.parentFrame === frame);
|
|
}
|
|
|
|
preferredInspectorStyleSheetForFrame(frame, callback)
|
|
{
|
|
var inspectorStyleSheets = this.inspectorStyleSheetsForFrame(frame);
|
|
for (let styleSheet of inspectorStyleSheets) {
|
|
if (styleSheet[WI.CSSManager.PreferredInspectorStyleSheetSymbol]) {
|
|
callback(styleSheet);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CSSAgent.createStyleSheet(frame.id, function(error, styleSheetId) {
|
|
if (error || !styleSheetId) {
|
|
WI.reportInternalError(error || styleSheetId);
|
|
return;
|
|
}
|
|
|
|
const url = null;
|
|
let styleSheet = WI.cssManager.styleSheetForIdentifier(styleSheetId);
|
|
styleSheet.updateInfo(url, frame, styleSheet.origin, styleSheet.isInlineStyleTag(), styleSheet.startLineNumber, styleSheet.startColumnNumber);
|
|
styleSheet[WI.CSSManager.PreferredInspectorStyleSheetSymbol] = true;
|
|
callback(styleSheet);
|
|
});
|
|
}
|
|
|
|
mediaTypeChanged()
|
|
{
|
|
// Act the same as if media queries changed.
|
|
this.mediaQueryResultChanged();
|
|
}
|
|
|
|
get modifiedStyles()
|
|
{
|
|
return Array.from(this._modifiedStyles.values());
|
|
}
|
|
|
|
addModifiedStyle(style)
|
|
{
|
|
this._modifiedStyles.set(style.stringId, style);
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.ModifiedStylesChanged);
|
|
}
|
|
|
|
getModifiedStyle(style)
|
|
{
|
|
return this._modifiedStyles.get(style.stringId);
|
|
}
|
|
|
|
removeModifiedStyle(style)
|
|
{
|
|
this._modifiedStyles.delete(style.stringId);
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.ModifiedStylesChanged);
|
|
}
|
|
|
|
// PageObserver
|
|
|
|
// COMPATIBILITY (macOS 13, iOS 16.0): `Page.defaultAppearanceDidChange` was removed in favor of `Page.defaultUserPreferencesDidChange`
|
|
defaultAppearanceDidChange(protocolName)
|
|
{
|
|
let appearance = null;
|
|
|
|
switch (protocolName) {
|
|
case InspectorBackend.Enum.Page.Appearance.Light:
|
|
appearance = WI.CSSManager.Appearance.Light;
|
|
break;
|
|
|
|
case InspectorBackend.Enum.Page.Appearance.Dark:
|
|
appearance = WI.CSSManager.Appearance.Dark;
|
|
break;
|
|
|
|
default:
|
|
console.error("Unknown default appearance name:", protocolName);
|
|
break;
|
|
}
|
|
|
|
this.mediaQueryResultChanged();
|
|
|
|
this._defaultUserPreferences.set(WI.CSSManager.ForcedAppearancePreference, appearance);
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.DefaultUserPreferencesDidChange);
|
|
}
|
|
|
|
defaultUserPreferencesDidChange(userPreferences)
|
|
{
|
|
this._defaultUserPreferences.clear();
|
|
|
|
for (let userPreference of userPreferences)
|
|
this._defaultUserPreferences.set(userPreference.name, userPreference.value)
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.DefaultUserPreferencesDidChange);
|
|
}
|
|
|
|
// CSSObserver
|
|
|
|
mediaQueryResultChanged()
|
|
{
|
|
for (var key in this._nodeStylesMap)
|
|
this._nodeStylesMap[key].mediaQueryResultDidChange();
|
|
}
|
|
|
|
styleSheetChanged(styleSheetIdentifier)
|
|
{
|
|
var styleSheet = this.styleSheetForIdentifier(styleSheetIdentifier);
|
|
console.assert(styleSheet);
|
|
|
|
// Do not observe inline styles
|
|
if (styleSheet.isInlineStyleAttributeStyleSheet())
|
|
return;
|
|
|
|
if (!styleSheet.noteContentDidChange())
|
|
return;
|
|
|
|
this._updateResourceContent(styleSheet);
|
|
}
|
|
|
|
styleSheetAdded(styleSheetInfo)
|
|
{
|
|
console.assert(!this._styleSheetIdentifierMap.has(styleSheetInfo.styleSheetId), "Attempted to add a CSSStyleSheet but identifier was already in use");
|
|
let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId);
|
|
let parentFrame = WI.networkManager.frameForIdentifier(styleSheetInfo.frameId);
|
|
let origin = WI.CSSManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin);
|
|
styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, styleSheetInfo.isInline, styleSheetInfo.startLine, styleSheetInfo.startColumn);
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.StyleSheetAdded, {styleSheet});
|
|
}
|
|
|
|
styleSheetRemoved(styleSheetIdentifier)
|
|
{
|
|
let styleSheet = this._styleSheetIdentifierMap.get(styleSheetIdentifier);
|
|
console.assert(styleSheet, "Attempted to remove a CSSStyleSheet that was not tracked");
|
|
if (!styleSheet)
|
|
return;
|
|
|
|
this._styleSheetIdentifierMap.delete(styleSheetIdentifier);
|
|
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.StyleSheetRemoved, {styleSheet});
|
|
}
|
|
|
|
// Private
|
|
|
|
_nodePseudoClassesDidChange(event)
|
|
{
|
|
var node = event.target;
|
|
|
|
for (var key in this._nodeStylesMap) {
|
|
var nodeStyles = this._nodeStylesMap[key];
|
|
if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
|
|
continue;
|
|
nodeStyles.pseudoClassesDidChange(node);
|
|
}
|
|
}
|
|
|
|
_nodeAttributesDidChange(event)
|
|
{
|
|
var node = event.target;
|
|
|
|
for (var key in this._nodeStylesMap) {
|
|
var nodeStyles = this._nodeStylesMap[key];
|
|
if (nodeStyles.node !== node && !nodeStyles.node.isDescendant(node))
|
|
continue;
|
|
nodeStyles.attributeDidChange(node, event.data.name);
|
|
}
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
// Clear our maps when the main frame navigates.
|
|
|
|
this._styleSheetIdentifierMap.clear();
|
|
this._styleSheetFrameURLMap.clear();
|
|
this._modifiedStyles.clear();
|
|
|
|
// COMPATIBILITY (macOS 14.0, iOS 17.0): the `PrefersColorScheme` override used to be cleared on main frame navigation
|
|
// Since support can't be tested directly, check for the `reason` parameter of `Console.messagesCleared` as that change shipped in the same release.
|
|
// FIXME: Use explicit version checking once <https://webkit.org/b/148680> is fixed.
|
|
if (!InspectorBackend.hasEvent("Console.messagesCleared", "reason")) {
|
|
this._overriddenUserPreferences.delete(InspectorBackend.Enum.Page.UserPreferenceName.PrefersColorScheme);
|
|
this.dispatchEventToListeners(WI.CSSManager.Event.OverriddenUserPreferencesDidChange);
|
|
}
|
|
|
|
this._nodeStylesMap = {};
|
|
}
|
|
|
|
_resourceAdded(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
var resource = event.data.resource;
|
|
console.assert(resource);
|
|
|
|
if (resource.type !== WI.Resource.Type.StyleSheet)
|
|
return;
|
|
|
|
this._clearStyleSheetsForResource(resource);
|
|
}
|
|
|
|
_resourceTypeDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Resource);
|
|
|
|
var resource = event.target;
|
|
if (resource.type !== WI.Resource.Type.StyleSheet)
|
|
return;
|
|
|
|
this._clearStyleSheetsForResource(resource);
|
|
}
|
|
|
|
_clearStyleSheetsForResource(resource)
|
|
{
|
|
// Clear known stylesheets for this URL and frame. This will cause the style sheets to
|
|
// be updated next time _fetchInfoForAllStyleSheets is called.
|
|
this._styleSheetIdentifierMap.delete(this._frameURLMapKey(resource.parentFrame, resource.url));
|
|
}
|
|
|
|
_frameURLMapKey(frame, url)
|
|
{
|
|
return frame.id + ":" + url;
|
|
}
|
|
|
|
_lookupStyleSheetForResource(resource, callback)
|
|
{
|
|
this._lookupStyleSheet(resource.parentFrame, resource.url, callback);
|
|
}
|
|
|
|
_lookupStyleSheet(frame, url, callback)
|
|
{
|
|
console.assert(frame instanceof WI.Frame);
|
|
|
|
let key = this._frameURLMapKey(frame, url);
|
|
|
|
function styleSheetsFetched()
|
|
{
|
|
callback(this._styleSheetFrameURLMap.get(key) || null);
|
|
}
|
|
|
|
let styleSheet = this._styleSheetFrameURLMap.get(key) || null;
|
|
if (styleSheet)
|
|
callback(styleSheet);
|
|
else
|
|
this._fetchInfoForAllStyleSheets(styleSheetsFetched.bind(this));
|
|
}
|
|
|
|
_fetchInfoForAllStyleSheets(callback)
|
|
{
|
|
console.assert(typeof callback === "function");
|
|
|
|
function processStyleSheets(error, styleSheets)
|
|
{
|
|
this._styleSheetFrameURLMap.clear();
|
|
|
|
if (error) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
for (let styleSheetInfo of styleSheets) {
|
|
let parentFrame = WI.networkManager.frameForIdentifier(styleSheetInfo.frameId);
|
|
let origin = WI.CSSManager.protocolStyleSheetOriginToEnum(styleSheetInfo.origin);
|
|
|
|
let styleSheet = this.styleSheetForIdentifier(styleSheetInfo.styleSheetId);
|
|
styleSheet.updateInfo(styleSheetInfo.sourceURL, parentFrame, origin, styleSheetInfo.isInline, styleSheetInfo.startLine, styleSheetInfo.startColumn);
|
|
|
|
let key = this._frameURLMapKey(parentFrame, styleSheetInfo.sourceURL);
|
|
this._styleSheetFrameURLMap.set(key, styleSheet);
|
|
}
|
|
|
|
callback();
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.CSSAgent.getAllStyleSheets(processStyleSheets.bind(this));
|
|
}
|
|
|
|
_resourceContentDidChange(event)
|
|
{
|
|
var resource = event.target;
|
|
if (resource === this._ignoreResourceContentDidChangeEventForResource)
|
|
return;
|
|
|
|
// Ignore changes to resource overrides, those are not live on the page.
|
|
if (resource.localResourceOverride)
|
|
return;
|
|
|
|
// Ignore if it isn't a CSS style sheet.
|
|
if (resource.type !== WI.Resource.Type.StyleSheet || resource.syntheticMIMEType !== "text/css")
|
|
return;
|
|
|
|
function applyStyleSheetChanges()
|
|
{
|
|
function styleSheetFound(styleSheet)
|
|
{
|
|
resource.__pendingChangeTimeout.cancel();
|
|
|
|
console.assert(styleSheet);
|
|
if (!styleSheet)
|
|
return;
|
|
|
|
// To prevent updating a TextEditor's content while the user is typing in it we want to
|
|
// ignore the next _updateResourceContent call.
|
|
resource.__ignoreNextUpdateResourceContent = true;
|
|
|
|
let revision = styleSheet.editableRevision;
|
|
revision.updateRevisionContent(resource.content);
|
|
}
|
|
|
|
this._lookupStyleSheetForResource(resource, styleSheetFound.bind(this));
|
|
}
|
|
|
|
if (!resource.__pendingChangeTimeout)
|
|
resource.__pendingChangeTimeout = new Throttler(applyStyleSheetChanges.bind(this), 100);
|
|
resource.__pendingChangeTimeout.fire();
|
|
}
|
|
|
|
_updateResourceContent(styleSheet)
|
|
{
|
|
console.assert(styleSheet);
|
|
|
|
function fetchedStyleSheetContent(parameters)
|
|
{
|
|
styleSheet.__pendingChangeTimeout.cancel();
|
|
|
|
let representedObject = parameters.sourceCode;
|
|
|
|
console.assert(representedObject.url);
|
|
if (!representedObject.url)
|
|
return;
|
|
|
|
if (!styleSheet.isInspectorStyleSheet()) {
|
|
// Only try to update stylesheet resources. Other resources, like documents, can contain
|
|
// multiple stylesheets and we don't have the source ranges to update those.
|
|
representedObject = representedObject.parentFrame.resourcesForURL(representedObject.url).find((resource) => resource.type === WI.Resource.Type.StyleSheet);
|
|
if (!representedObject)
|
|
return;
|
|
}
|
|
|
|
if (representedObject.__ignoreNextUpdateResourceContent) {
|
|
representedObject.__ignoreNextUpdateResourceContent = false;
|
|
return;
|
|
}
|
|
|
|
this._ignoreResourceContentDidChangeEventForResource = representedObject;
|
|
|
|
let revision = representedObject.editableRevision;
|
|
if (styleSheet.isInspectorStyleSheet()) {
|
|
revision.updateRevisionContent(representedObject.content);
|
|
styleSheet.dispatchEventToListeners(WI.SourceCode.Event.ContentDidChange);
|
|
} else
|
|
revision.updateRevisionContent(parameters.content);
|
|
|
|
this._ignoreResourceContentDidChangeEventForResource = null;
|
|
}
|
|
|
|
function styleSheetReady()
|
|
{
|
|
styleSheet.requestContent().then(fetchedStyleSheetContent.bind(this));
|
|
}
|
|
|
|
function applyStyleSheetChanges()
|
|
{
|
|
if (styleSheet.url)
|
|
styleSheetReady.call(this);
|
|
else
|
|
this._fetchInfoForAllStyleSheets(styleSheetReady.bind(this));
|
|
}
|
|
|
|
if (!styleSheet.__pendingChangeTimeout)
|
|
styleSheet.__pendingChangeTimeout = new Throttler(applyStyleSheetChanges.bind(this), 100);
|
|
styleSheet.__pendingChangeTimeout.fire();
|
|
}
|
|
};
|
|
|
|
WI.CSSManager.Event = {
|
|
StyleSheetAdded: "css-manager-style-sheet-added",
|
|
StyleSheetRemoved: "css-manager-style-sheet-removed",
|
|
ModifiedStylesChanged: "css-manager-modified-styles-changed",
|
|
DefaultUserPreferencesDidChange: "css-manager-default-user-preferences-did-change",
|
|
OverriddenUserPreferencesDidChange: "css-manager-overriden-user-preferences-did-change",
|
|
};
|
|
|
|
WI.CSSManager.UserPreferenceDefaultValue = "System";
|
|
|
|
// COMPATIBILITY (macOS 13, iOS 16.0): `Page.setForcedAppearance()` was removed in favor of `Page.overrideUserPreference()`
|
|
WI.CSSManager.ForcedAppearancePreference = "ForcedAppearancePreference";
|
|
WI.CSSManager.Appearance = {
|
|
Light: "Light",
|
|
Dark: "Dark",
|
|
};
|
|
|
|
WI.CSSManager.PseudoSelectorNames = {
|
|
After: "after",
|
|
Before: "before",
|
|
Backdrop: "backdrop",
|
|
FirstLetter: "first-letter",
|
|
FirstLine: "first-line",
|
|
Highlight: "highlight",
|
|
GrammarError: "grammar-error",
|
|
Marker: "marker",
|
|
Selection: "selection",
|
|
SpellingError: "spelling-error",
|
|
ViewTransition: "view-transition",
|
|
ViewTransitionGroup: "view-transition-group",
|
|
ViewTransitionImagePair: "view-transition-image-pair",
|
|
ViewTransitionNew: "view-transition-new",
|
|
ViewTransitionOld: "view-transition-old",
|
|
WebKitResizer: "-webkit-resizer",
|
|
WebKitScrollbar: "-webkit-scrollbar",
|
|
WebKitScrollbarButton: "-webkit-scrollbar-button",
|
|
WebKitScrollbarCorner: "-webkit-scrollbar-corner",
|
|
WebKitScrollbarThumb: "-webkit-scrollbar-thumb",
|
|
WebKitScrollbarTrack: "-webkit-scrollbar-track",
|
|
WebKitScrollbarTrackPiece: "-webkit-scrollbar-track-piece",
|
|
};
|
|
|
|
WI.CSSManager.LayoutContextTypeChangedMode = {
|
|
Observed: "observed",
|
|
All: "all",
|
|
};
|
|
|
|
WI.CSSManager.PseudoElementNames = ["before", "after"];
|
|
|
|
WI.CSSManager.ForceablePseudoClass = {
|
|
Active: "active",
|
|
Focus: "focus",
|
|
FocusVisible: "focus-visible",
|
|
FocusWithin: "focus-within",
|
|
Hover: "hover",
|
|
Target: "target",
|
|
Visited: "visited",
|
|
};
|
|
|
|
WI.CSSManager.PreferredInspectorStyleSheetSymbol = Symbol("css-manager-preferred-inspector-style-sheet");
|
|
|
|
/* Controllers/CSSQueryController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CSSQueryController = class CSSQueryController extends WI.QueryController
|
|
{
|
|
constructor(values)
|
|
{
|
|
console.assert(Array.isArray(values), values);
|
|
|
|
super();
|
|
|
|
this._values = values || [];
|
|
this._cachedSpecialCharacterIndicesForValueMap = new Map;
|
|
}
|
|
|
|
// Public
|
|
|
|
addValues(values)
|
|
{
|
|
console.assert(Array.isArray(values), values);
|
|
if (!values.length)
|
|
return;
|
|
|
|
this._values.pushAll(values);
|
|
}
|
|
|
|
reset()
|
|
{
|
|
this._values = [];
|
|
this._cachedSpecialCharacterIndicesForValueMap.clear();
|
|
}
|
|
|
|
executeQuery(query)
|
|
{
|
|
if (!query || !this._values.length)
|
|
return [];
|
|
|
|
query = query.toLowerCase();
|
|
|
|
let results = [];
|
|
|
|
for (let value of this._values) {
|
|
if (!this._cachedSpecialCharacterIndicesForValueMap.has(value))
|
|
this._cachedSpecialCharacterIndicesForValueMap.set(value, this._findSpecialCharacterIndicesInPropertyName(value));
|
|
|
|
let matches = this.findQueryMatches(query, value.toLowerCase(), this._cachedSpecialCharacterIndicesForValueMap.get(value));
|
|
if (matches.length)
|
|
results.push(new WI.QueryResult(value, matches));
|
|
}
|
|
|
|
return results.sort((a, b) => {
|
|
if (a.rank === b.rank)
|
|
return WI.CSSProperty.sortPreferringNonPrefixed(a.value, b.value);
|
|
return b.rank - a.rank;
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
_findSpecialCharacterIndicesInPropertyName(propertyName)
|
|
{
|
|
return this.findSpecialCharacterIndices(propertyName, "-_");
|
|
}
|
|
};
|
|
|
|
/* Controllers/CanvasManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.CanvasManager = class CanvasManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
this._canvasCollection = new WI.CanvasCollection;
|
|
this._canvasForIdentifierForTargetMap = new Map;
|
|
this._shaderProgramForIdentifierForTargetMap = new Map;
|
|
this._savedRecordings = new Set;
|
|
|
|
WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._handleTargetRemoved, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Canvas"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Canvas");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("Canvas")) {
|
|
target.CanvasAgent.enable();
|
|
|
|
// COMPATIBILITY (iOS 12): Canvas.setRecordingAutoCaptureFrameCount did not exist yet.
|
|
if (target.hasCommand("Canvas.setRecordingAutoCaptureFrameCount") && WI.settings.canvasRecordingAutoCaptureEnabled.value && WI.settings.canvasRecordingAutoCaptureFrameCount.value)
|
|
target.CanvasAgent.setRecordingAutoCaptureFrameCount(WI.settings.canvasRecordingAutoCaptureFrameCount.value);
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsRecordingAutoCapture()
|
|
{
|
|
return InspectorBackend.hasCommand("Canvas.setRecordingAutoCaptureFrameCount");
|
|
}
|
|
|
|
// Public
|
|
|
|
get canvasCollection() { return this._canvasCollection; }
|
|
get savedRecordings() { return this._savedRecordings; }
|
|
|
|
async processJSON({filename, json, error})
|
|
{
|
|
if (error) {
|
|
WI.Recording.synthesizeError(error);
|
|
return;
|
|
}
|
|
|
|
if (typeof json !== "object" || json === null) {
|
|
WI.Recording.synthesizeError(WI.UIString("invalid JSON"));
|
|
return;
|
|
}
|
|
|
|
let recording = WI.Recording.fromPayload(json);
|
|
if (!recording)
|
|
return;
|
|
|
|
let extensionStart = filename.lastIndexOf(".");
|
|
if (extensionStart !== -1)
|
|
filename = filename.substring(0, extensionStart);
|
|
recording.createDisplayName(filename);
|
|
|
|
this._savedRecordings.add(recording);
|
|
|
|
this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingSaved, {recording, imported: true, initiatedByUser: true});
|
|
}
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("Canvas"))
|
|
target.CanvasAgent.disable();
|
|
}
|
|
|
|
this._canvasCollection.clear();
|
|
this._canvasForIdentifierForTargetMap.clear();
|
|
this._shaderProgramForIdentifierForTargetMap.clear();
|
|
this._savedRecordings.clear();
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
setRecordingAutoCaptureFrameCount(enabled, count)
|
|
{
|
|
console.assert(!isNaN(count) && count >= 0);
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 12): Canvas.setRecordingAutoCaptureFrameCount did not exist yet.
|
|
if (target.hasCommand("Canvas.setRecordingAutoCaptureFrameCount"))
|
|
target.CanvasAgent.setRecordingAutoCaptureFrameCount(enabled ? count : 0);
|
|
}
|
|
|
|
WI.settings.canvasRecordingAutoCaptureEnabled.value = enabled && count;
|
|
WI.settings.canvasRecordingAutoCaptureFrameCount.value = count;
|
|
}
|
|
|
|
// CanvasObserver
|
|
|
|
canvasAdded(target, canvasPayload)
|
|
{
|
|
let canvas = WI.Canvas.fromPayload(target, canvasPayload);
|
|
|
|
let canvasForIdentifierMap = this._canvasForIdentifierForTargetMap.getOrInitialize(target, () => new Map);
|
|
console.assert(!canvasForIdentifierMap.has(canvas.identifier), `Canvas already exists with id ${canvas.identifier}.`);
|
|
canvasForIdentifierMap.set(canvas.identifier, canvas);
|
|
|
|
this._canvasCollection.add(canvas);
|
|
}
|
|
|
|
canvasRemoved(target, canvasIdentifier)
|
|
{
|
|
let canvasForIdentifierMap = this._canvasForIdentifierForTargetMap.get(target);
|
|
console.assert(canvasForIdentifierMap);
|
|
if (!canvasForIdentifierMap)
|
|
return;
|
|
|
|
let canvas = canvasForIdentifierMap.take(canvasIdentifier);
|
|
console.assert(canvas);
|
|
if (!canvas)
|
|
return;
|
|
|
|
this._saveRecordings(canvas);
|
|
|
|
this._canvasCollection.remove(canvas);
|
|
|
|
let shaderProgramForIdentifierMap = this._shaderProgramForIdentifierForTargetMap.get(target);
|
|
if (shaderProgramForIdentifierMap) {
|
|
for (let program of canvas.shaderProgramCollection)
|
|
shaderProgramForIdentifierMap.delete(program.identifier);
|
|
}
|
|
|
|
canvas.shaderProgramCollection.clear();
|
|
}
|
|
|
|
canvasSizeChanged(target, canvasIdentifier, width, height)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.sizeChanged(new WI.Size(width, height));
|
|
}
|
|
|
|
canvasMemoryChanged(target, canvasIdentifier, memoryCost)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.memoryChanged(memoryCost);
|
|
}
|
|
|
|
clientNodesChanged(target, canvasIdentifier)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.clientNodesChanged();
|
|
}
|
|
|
|
recordingStarted(target, canvasIdentifier, initiator)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.recordingStarted(initiator);
|
|
}
|
|
|
|
recordingProgress(target, canvasIdentifier, framesPayload, bufferUsed)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.recordingProgress(framesPayload, bufferUsed);
|
|
}
|
|
|
|
recordingFinished(target, canvasIdentifier, recordingPayload)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.recordingFinished(recordingPayload);
|
|
}
|
|
|
|
extensionEnabled(target, canvasIdentifier, extension)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, canvasIdentifier);
|
|
if (!canvas)
|
|
return;
|
|
|
|
canvas.enableExtension(extension);
|
|
}
|
|
|
|
programCreated(target, shaderProgramPayload)
|
|
{
|
|
let canvas = this._canvasForIdentifier(target, shaderProgramPayload.canvasId);
|
|
if (!canvas)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 13.0): `Canvas.ShaderProgram.programType` did not exist yet.
|
|
let programType = shaderProgramPayload.programType;
|
|
if (!programType)
|
|
programType = WI.ShaderProgram.ProgramType.Render;
|
|
|
|
let options = {};
|
|
|
|
// COMPATIBILITY (iOS 13.0): `Canvas.ShaderProgram.sharesVertexFragmentShader` did not exist yet.
|
|
if (shaderProgramPayload.sharesVertexFragmentShader)
|
|
options.sharesVertexFragmentShader = true;
|
|
|
|
let program = new WI.ShaderProgram(target, shaderProgramPayload.programId, programType, canvas, options);
|
|
|
|
let shaderProgramForIdentifierMap = this._shaderProgramForIdentifierForTargetMap.getOrInitialize(target, () => new Map);
|
|
console.assert(!shaderProgramForIdentifierMap.has(program.identifier), `ShaderProgram already exists with id ${program.identifier}.`);
|
|
shaderProgramForIdentifierMap.set(program.identifier, program);
|
|
|
|
canvas.shaderProgramCollection.add(program);
|
|
}
|
|
|
|
programDeleted(target, programIdentifier)
|
|
{
|
|
let shaderProgramForIdentifierMap = this._shaderProgramForIdentifierForTargetMap.get(target);
|
|
console.assert(shaderProgramForIdentifierMap);
|
|
if (!shaderProgramForIdentifierMap)
|
|
return;
|
|
|
|
let program = shaderProgramForIdentifierMap.take(programIdentifier);
|
|
console.assert(program);
|
|
if (!program)
|
|
return;
|
|
|
|
program.canvas.shaderProgramCollection.remove(program);
|
|
}
|
|
|
|
// Private
|
|
|
|
_canvasForIdentifier(target, canvasIdentifier)
|
|
{
|
|
let canvasForIdentifierMap = this._canvasForIdentifierForTargetMap.get(target);
|
|
console.assert(canvasForIdentifierMap);
|
|
if (!canvasForIdentifierMap)
|
|
return null;
|
|
|
|
let canvas = canvasForIdentifierMap.get(canvasIdentifier);
|
|
console.assert(canvas);
|
|
if (!canvas)
|
|
return null;
|
|
|
|
return canvas;
|
|
}
|
|
|
|
_saveRecordings(canvas)
|
|
{
|
|
for (let recording of canvas.recordingCollection) {
|
|
recording.source = null;
|
|
recording.createDisplayName(recording.displayName);
|
|
this._savedRecordings.add(recording);
|
|
this.dispatchEventToListeners(WI.CanvasManager.Event.RecordingSaved, {recording});
|
|
}
|
|
}
|
|
|
|
_handleTargetRemoved(event)
|
|
{
|
|
let {target} = event.data;
|
|
|
|
this._canvasForIdentifierForTargetMap.delete(target);
|
|
this._shaderProgramForIdentifierForTargetMap.delete(target);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
WI.Canvas.resetUniqueDisplayNameNumbers();
|
|
|
|
for (let canvasForIdentifierMap of this._canvasForIdentifierForTargetMap.values()) {
|
|
for (let canvas of canvasForIdentifierMap.values())
|
|
this._saveRecordings(canvas);
|
|
}
|
|
|
|
this._canvasCollection.clear();
|
|
this._canvasForIdentifierForTargetMap.clear();
|
|
this._shaderProgramForIdentifierForTargetMap.clear();
|
|
}
|
|
|
|
// Testing
|
|
|
|
get shaderPrograms()
|
|
{
|
|
let programs = [];
|
|
for (let shaderProgramForIdentifierMap of this._shaderProgramForIdentifierForTargetMap.values())
|
|
programs.pushAll(shaderProgramForIdentifierMap.values());
|
|
return programs;
|
|
}
|
|
};
|
|
|
|
WI.CanvasManager.Event = {
|
|
RecordingSaved: "canvas-manager-recording-saved",
|
|
};
|
|
|
|
/* Controllers/ConsoleManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2015 Tobias Reiss <tobi+webkit@basecode.de>
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ConsoleManager = class ConsoleManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._warningCount = 0;
|
|
this._errorCount = 0;
|
|
this._issues = [];
|
|
|
|
this._lastMessageLevel = null;
|
|
this._clearMessagesRequested = false;
|
|
this._isNewPageOrReload = false;
|
|
this._remoteObjectsToRelease = null;
|
|
|
|
this._customLoggingChannels = [];
|
|
|
|
this._snippets = new Set;
|
|
this._restoringSnippets = false;
|
|
|
|
this._failedSourceMapConsoleMessages = new Set;
|
|
|
|
WI.ConsoleSnippet.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleSnippetContentChanged, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
let serializedSnippets = await WI.objectStores.consoleSnippets.getAll();
|
|
|
|
this._restoringSnippets = true;
|
|
for (let serializedSnippet of serializedSnippets) {
|
|
let snippet = WI.ConsoleSnippet.fromJSON(serializedSnippet);
|
|
|
|
const key = null;
|
|
WI.objectStores.consoleSnippets.associateObject(snippet, key, serializedSnippet);
|
|
|
|
this.addSnippet(snippet);
|
|
}
|
|
this._restoringSnippets = false;
|
|
})());
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsLogChannels()
|
|
{
|
|
return InspectorBackend.hasCommand("Console.getLoggingChannels");
|
|
}
|
|
|
|
static issueMatchSourceCode(issue, sourceCode)
|
|
{
|
|
if (sourceCode instanceof WI.SourceMapResource)
|
|
return issue.sourceCodeLocation && issue.sourceCodeLocation.displaySourceCode === sourceCode;
|
|
if (sourceCode instanceof WI.Resource)
|
|
return issue.url === sourceCode.url && (!issue.sourceCodeLocation || issue.sourceCodeLocation.sourceCode === sourceCode);
|
|
if (sourceCode instanceof WI.Script)
|
|
return issue.sourceCodeLocation && issue.sourceCodeLocation.sourceCode === sourceCode;
|
|
return false;
|
|
}
|
|
|
|
// Public
|
|
|
|
get warningCount() { return this._warningCount; }
|
|
get errorCount() { return this._errorCount; }
|
|
get snippets() { return this._snippets; }
|
|
get customLoggingChannels() { return this._customLoggingChannels; }
|
|
get failedSourceMapConsoleMessages() { return this._failedSourceMapConsoleMessages; }
|
|
|
|
issuesForSourceCode(sourceCode)
|
|
{
|
|
var issues = [];
|
|
|
|
for (var i = 0; i < this._issues.length; ++i) {
|
|
var issue = this._issues[i];
|
|
if (WI.ConsoleManager.issueMatchSourceCode(issue, sourceCode))
|
|
issues.push(issue);
|
|
}
|
|
|
|
return issues;
|
|
}
|
|
|
|
releaseRemoteObjectWithConsoleClear(remoteObject)
|
|
{
|
|
if (!this._remoteObjectsToRelease)
|
|
this._remoteObjectsToRelease = new Set;
|
|
this._remoteObjectsToRelease.add(remoteObject);
|
|
}
|
|
|
|
addSnippet(snippet)
|
|
{
|
|
console.assert(snippet instanceof WI.ConsoleSnippet, snippet);
|
|
console.assert(!this._snippets.has(snippet), snippet);
|
|
console.assert(!this._snippets.some((existingSnippet) => snippet.contentIdentifier === existingSnippet.contentIdentifier), snippet);
|
|
|
|
this._snippets.add(snippet);
|
|
|
|
if (!this._restoringSnippets)
|
|
WI.objectStores.consoleSnippets.putObject(snippet);
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetAdded, {snippet});
|
|
}
|
|
|
|
removeSnippet(snippet)
|
|
{
|
|
console.assert(snippet instanceof WI.ConsoleSnippet, snippet);
|
|
console.assert(this._snippets.has(snippet), snippet);
|
|
|
|
this._snippets.delete(snippet);
|
|
|
|
if (!this._restoringSnippets)
|
|
WI.objectStores.consoleSnippets.deleteObject(snippet);
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.SnippetRemoved, {snippet});
|
|
}
|
|
|
|
// ConsoleObserver
|
|
|
|
messageWasAdded(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, requestId, timestamp)
|
|
{
|
|
// FIXME: Get a request from request ID.
|
|
|
|
if (parameters)
|
|
parameters = parameters.map((x) => WI.RemoteObject.fromPayload(x, target));
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `stackTrace` was an array of `Console.CallFrame`.
|
|
if (Array.isArray(stackTrace))
|
|
stackTrace = {callFrames: stackTrace};
|
|
if (stackTrace)
|
|
stackTrace = WI.StackTrace.fromPayload(target, stackTrace);
|
|
|
|
const request = null;
|
|
let message = new WI.ConsoleMessage(target, source, level, text, type, url, line, column, repeatCount, parameters, stackTrace, request, timestamp);
|
|
|
|
this._incrementMessageLevelCount(message.level, message.repeatCount);
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.MessageAdded, {message});
|
|
|
|
if (message.level === WI.ConsoleMessage.MessageLevel.Warning || message.level === WI.ConsoleMessage.MessageLevel.Error) {
|
|
let issue = new WI.IssueMessage(message);
|
|
this._issues.push(issue);
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.IssueAdded, {issue});
|
|
|
|
this._collectFailedSourceMapConsoleMessage(message);
|
|
}
|
|
}
|
|
|
|
messagesCleared(reason)
|
|
{
|
|
if (this._remoteObjectsToRelease) {
|
|
for (let remoteObject of this._remoteObjectsToRelease)
|
|
remoteObject.release();
|
|
this._remoteObjectsToRelease = null;
|
|
}
|
|
|
|
WI.ConsoleCommandResultMessage.clearMaximumSavedResultIndex();
|
|
|
|
// COMPATIBILITY (iOS 16.4, macOS 13.3): `Console.ClearReason` did not exist.
|
|
if (!reason) {
|
|
if (this._clearMessagesRequested) {
|
|
// Frontend requested "clear console" and Backend successfully completed the request.
|
|
this._clearMessagesRequested = false;
|
|
this._clearMessages();
|
|
return;
|
|
}
|
|
|
|
// Received an unrequested clear console event.
|
|
// This could be for a navigation or other reasons (like console.clear()).
|
|
// If this was a reload, we may not want to dispatch WI.ConsoleManager.Event.Cleared.
|
|
// To detect if this is a reload we wait a turn and check if there was a main resource change reload.
|
|
setTimeout(this._delayedMessagesCleared.bind(this), 0);
|
|
|
|
return;
|
|
}
|
|
|
|
switch (reason) {
|
|
case WI.ConsoleManager.ClearReason.ConsoleAPI:
|
|
this._clearMessages();
|
|
return;
|
|
|
|
case WI.ConsoleManager.ClearReason.MainFrameNavigation:
|
|
console.assert(this._isNewPageOrReload);
|
|
this._isNewPageOrReload = false;
|
|
|
|
if (WI.settings.clearLogOnNavigate.value)
|
|
this._clearMessages();
|
|
|
|
return;
|
|
}
|
|
|
|
console.assert(false, "not reached");
|
|
}
|
|
|
|
messageRepeatCountUpdated(count, timestamp)
|
|
{
|
|
this._incrementMessageLevelCount(this._lastMessageLevel, 1);
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.PreviousMessageRepeatCountUpdated, {count, timestamp});
|
|
}
|
|
|
|
requestClearMessages()
|
|
{
|
|
this._clearMessagesRequested = true;
|
|
|
|
for (let target of WI.targets)
|
|
target.ConsoleAgent.clearMessages();
|
|
}
|
|
|
|
initializeLogChannels(target)
|
|
{
|
|
console.assert(target.hasDomain("Console"));
|
|
|
|
if (!WI.ConsoleManager.supportsLogChannels())
|
|
return;
|
|
|
|
if (this._customLoggingChannels.length)
|
|
return;
|
|
|
|
target.ConsoleAgent.getLoggingChannels((error, channels) => {
|
|
if (error)
|
|
return;
|
|
|
|
this._customLoggingChannels = channels.map(WI.LoggingChannel.fromPayload);
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
_incrementMessageLevelCount(level, count)
|
|
{
|
|
switch (level) {
|
|
case WI.ConsoleMessage.MessageLevel.Warning:
|
|
this._warningCount += count;
|
|
break;
|
|
case WI.ConsoleMessage.MessageLevel.Error:
|
|
this._errorCount += count;
|
|
break;
|
|
}
|
|
|
|
this._lastMessageLevel = level;
|
|
}
|
|
|
|
_clearMessages()
|
|
{
|
|
this._warningCount = 0;
|
|
this._errorCount = 0;
|
|
this._issues = [];
|
|
|
|
this._lastMessageLevel = null;
|
|
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.Cleared);
|
|
}
|
|
|
|
_collectFailedSourceMapConsoleMessage(message)
|
|
{
|
|
if (!WI.settings.experimentalGroupSourceMapErrors.value)
|
|
return;
|
|
|
|
if (message.source !== WI.ConsoleMessage.MessageSource.Network && message.level !== WI.ConsoleMessage.MessageLevel.Error)
|
|
return;
|
|
|
|
if (WI.networkManager.isSourceMapURL(message.url))
|
|
this._failedSourceMapConsoleMessages.add(message);
|
|
}
|
|
|
|
_delayedMessagesCleared()
|
|
{
|
|
if (this._isNewPageOrReload) {
|
|
this._isNewPageOrReload = false;
|
|
|
|
if (!WI.settings.clearLogOnNavigate.value)
|
|
return;
|
|
}
|
|
|
|
this._clearMessages();
|
|
}
|
|
|
|
_handleSnippetContentChanged(event)
|
|
{
|
|
let snippet = event.target;
|
|
|
|
console.assert(this._snippets.has(snippet), snippet);
|
|
|
|
WI.objectStores.consoleSnippets.putObject(snippet);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
this._isNewPageOrReload = true;
|
|
|
|
let timestamp = Date.now();
|
|
let wasReloaded = event.data.oldMainResource && event.data.oldMainResource.url === event.target.mainResource.url;
|
|
this.dispatchEventToListeners(WI.ConsoleManager.Event.SessionStarted, {timestamp, wasReloaded});
|
|
|
|
WI.ConsoleCommandResultMessage.clearMaximumSavedResultIndex();
|
|
}
|
|
};
|
|
|
|
WI.ConsoleManager.Event = {
|
|
SessionStarted: "console-manager-session-was-started",
|
|
Cleared: "console-manager-cleared",
|
|
MessageAdded: "console-manager-message-added",
|
|
IssueAdded: "console-manager-issue-added",
|
|
PreviousMessageRepeatCountUpdated: "console-manager-previous-message-repeat-count-updated",
|
|
SnippetAdded: "console-manager-snippet-added",
|
|
SnippetRemoved: "console-manager-snippet-removed",
|
|
};
|
|
|
|
WI.ConsoleManager.ClearReason = {
|
|
ConsoleAPI: "console-api",
|
|
MainFrameNavigation: "main-frame-navigation",
|
|
}
|
|
|
|
/* Controllers/DOMDebuggerManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DOMDebuggerManager = class DOMDebuggerManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._domBreakpointURLMap = new Multimap;
|
|
this._domBreakpointFrameIdentifierMap = new Map;
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = false;
|
|
|
|
this._listenerBreakpoints = [];
|
|
this._allAnimationFramesBreakpoint = null;
|
|
this._allIntervalsBreakpoint = null;
|
|
this._allListenersBreakpoint = null;
|
|
this._allTimeoutsBreakpoint = null;
|
|
|
|
this._urlBreakpoints = [];
|
|
this._allRequestsBreakpoint = null;
|
|
|
|
WI.DOMBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._handleDOMBreakpointDisabledStateChanged, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._handleDOMBreakpointEditablePropertyChanged, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._handleDOMBreakpointEditablePropertyChanged, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._handleDOMBreakpointEditablePropertyChanged, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleDOMBreakpointActionsChanged, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.DOMBreakpoint.Event.DOMNodeWillChange, this._handleDOMBreakpointDOMNodeWillChange, this);
|
|
WI.DOMBreakpoint.addEventListener(WI.DOMBreakpoint.Event.DOMNodeDidChange, this._handleDOMBreakpointDOMNodeDidChange, this);
|
|
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._handleEventBreakpointDisabledStateChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleEventBreakpointActionsChanged, this);
|
|
|
|
WI.URLBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._handleURLBreakpointDisabledStateChanged, this);
|
|
WI.URLBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._handleURLBreakpointEditablePropertyChanged, this);
|
|
WI.URLBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._handleURLBreakpointEditablePropertyChanged, this);
|
|
WI.URLBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._handleURLBreakpointEditablePropertyChanged, this);
|
|
WI.URLBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleURLBreakpointActionsChanged, this);
|
|
|
|
WI.domManager.addEventListener(WI.DOMManager.Event.NodeRemoved, this._nodeRemoved, this);
|
|
WI.domManager.addEventListener(WI.DOMManager.Event.NodeInserted, this._nodeInserted, this);
|
|
|
|
WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, this._mainFrameDidChange, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.ChildFrameWasRemoved, this._childFrameWasRemoved, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
|
|
let loadBreakpoints = (constructor, objectStore, oldSettings, callback) => {
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
for (let key of oldSettings) {
|
|
let existingSerializedBreakpoints = WI.Setting.migrateValue(key);
|
|
if (existingSerializedBreakpoints) {
|
|
for (let existingSerializedBreakpoint of existingSerializedBreakpoints)
|
|
await objectStore.putObject(constructor.fromJSON(existingSerializedBreakpoint));
|
|
}
|
|
}
|
|
|
|
let serializedBreakpoints = await objectStore.getAll();
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let serializedBreakpoint of serializedBreakpoints) {
|
|
let breakpoint = constructor.fromJSON(serializedBreakpoint);
|
|
|
|
const key = null;
|
|
objectStore.associateObject(breakpoint, key, serializedBreakpoint);
|
|
|
|
callback(breakpoint);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
})());
|
|
};
|
|
|
|
function loadLegacySpecialBreakpoint(shownSettingsKey, enabledSettingsKey, callback) {
|
|
if (!WI.Setting.migrateValue(shownSettingsKey))
|
|
return;
|
|
|
|
return callback({
|
|
disabled: !WI.Setting.migrateValue(enabledSettingsKey),
|
|
});
|
|
}
|
|
|
|
loadBreakpoints(WI.DOMBreakpoint, WI.objectStores.domBreakpoints, ["dom-breakpoints"], (breakpoint) => {
|
|
this.addDOMBreakpoint(breakpoint);
|
|
});
|
|
|
|
if (DOMDebuggerManager.supportsEventBreakpoints() || DOMDebuggerManager.supportsEventListenerBreakpoints()) {
|
|
loadBreakpoints(WI.EventBreakpoint, WI.objectStores.eventBreakpoints, ["event-breakpoints"], (breakpoint) => {
|
|
this.addEventBreakpoint(breakpoint);
|
|
});
|
|
|
|
this._allAnimationFramesBreakpoint ??= loadLegacySpecialBreakpoint("show-all-animation-frames-breakpoint", "break-on-all-animation-frames", (options) => new WI.EventBreakpoint(WI.EventBreakpoint.Type.AnimationFrame, options));
|
|
this._allIntervalsBreakpoint ??= loadLegacySpecialBreakpoint("show-all-inteverals-breakpoint", "break-on-all-intervals", (options) => new WI.EventBreakpoint(WI.EventBreakpoint.Type.Interval, options));
|
|
this._allListenersBreakpoint ??= loadLegacySpecialBreakpoint("show-all-listeners-breakpoint", "break-on-all-listeners", (options) => new WI.EventBreakpoint(WI.EventBreakpoint.Type.Listener, options));
|
|
this._allTimeoutsBreakpoint ??= loadLegacySpecialBreakpoint("show-all-timeouts-breakpoint", "break-on-all-timeouts", (options) => new WI.EventBreakpoint(WI.EventBreakpoint.Type.Timeout, options));
|
|
}
|
|
|
|
if (DOMDebuggerManager.supportsURLBreakpoints() || DOMDebuggerManager.supportsXHRBreakpoints()) {
|
|
loadBreakpoints(WI.URLBreakpoint, WI.objectStores.urlBreakpoints, ["xhr-breakpoints", "url-breakpoints"], (breakpoint) => {
|
|
this.addURLBreakpoint(breakpoint);
|
|
});
|
|
|
|
this._allRequestsBreakpoint ??= loadLegacySpecialBreakpoint("show-all-requests-breakpoint", "break-on-all-requests", (options) => new WI.URLBreakpoint(WI.URLBreakpoint.Type.Text, "", options));
|
|
}
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (target.hasDomain("DOMDebugger")) {
|
|
this._restoringBreakpoints = true;
|
|
|
|
if (target === WI.assumingMainTarget() && target.mainResource)
|
|
this._speculativelyResolveDOMBreakpointsForURL(target.mainResource.url);
|
|
|
|
if (this._allAnimationFramesBreakpoint && !this._allAnimationFramesBreakpoint.disabled)
|
|
this._setEventBreakpoint(this._allAnimationFramesBreakpoint, target);
|
|
|
|
if (this._allIntervalsBreakpoint && !this._allIntervalsBreakpoint.disabled)
|
|
this._setEventBreakpoint(this._allIntervalsBreakpoint, target);
|
|
|
|
if (this._allListenersBreakpoint && !this._allListenersBreakpoint.disabled)
|
|
this._setEventBreakpoint(this._allListenersBreakpoint, target);
|
|
|
|
if (this._allTimeoutsBreakpoint && !this._allTimeoutsBreakpoint.disabled)
|
|
this._setEventBreakpoint(this._allTimeoutsBreakpoint, target);
|
|
|
|
if (this._allRequestsBreakpoint)
|
|
this._setURLBreakpoint(this._allRequestsBreakpoint, target);
|
|
|
|
for (let breakpoint of this._listenerBreakpoints) {
|
|
if (!breakpoint.disabled)
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
for (let breakpoint of this._urlBreakpoints) {
|
|
if (!breakpoint.disabled)
|
|
this._setURLBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsEventBreakpoints()
|
|
{
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.setEventBreakpoint and DOMDebugger.removeEventBreakpoint did not exist yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setEventBreakpoint")
|
|
&& InspectorBackend.hasCommand("DOMDebugger.removeEventBreakpoint");
|
|
}
|
|
|
|
static supportsEventListenerBreakpoints()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): Replaced by DOMDebugger.setEventBreakpoint and DOMDebugger.removeEventBreakpoint.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setEventListenerBreakpoint")
|
|
&& InspectorBackend.hasCommand("DOMDebugger.removeEventListenerBreakpoint");
|
|
}
|
|
|
|
static supportsURLBreakpoints()
|
|
{
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.setURLBreakpoint and DOMDebugger.removeURLBreakpoint did not exist yet.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setURLBreakpoint")
|
|
&& InspectorBackend.hasCommand("DOMDebugger.removeURLBreakpoint");
|
|
}
|
|
|
|
static supportsXHRBreakpoints()
|
|
{
|
|
// COMPATIBILITY (iOS 13): Replaced by DOMDebugger.setURLBreakpoint and DOMDebugger.removeURLBreakpoint.
|
|
return InspectorBackend.hasCommand("DOMDebugger.setXHRBreakpoint")
|
|
&& InspectorBackend.hasCommand("DOMDebugger.removeXHRBreakpoint");
|
|
}
|
|
|
|
static supportsAllListenersBreakpoint()
|
|
{
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.EventBreakpointType.Interval and DOMDebugger.EventBreakpointType.Timeout did not exist yet.
|
|
return DOMDebuggerManager.supportsEventBreakpoints()
|
|
&& InspectorBackend.Enum.DOMDebugger.EventBreakpointType.Interval
|
|
&& InspectorBackend.Enum.DOMDebugger.EventBreakpointType.Timeout;
|
|
}
|
|
|
|
// Public
|
|
|
|
get supported()
|
|
{
|
|
return InspectorBackend.hasDomain("DOMDebugger");
|
|
}
|
|
|
|
get allAnimationFramesBreakpoint() { return this._allAnimationFramesBreakpoint; }
|
|
get allIntervalsBreakpoint() { return this._allIntervalsBreakpoint; }
|
|
get allListenersBreakpoint() { return this._allListenersBreakpoint; }
|
|
get allTimeoutsBreakpoint() { return this._allTimeoutsBreakpoint; }
|
|
get allRequestsBreakpoint() { return this._allRequestsBreakpoint; }
|
|
|
|
get domBreakpoints()
|
|
{
|
|
let mainFrame = WI.networkManager.mainFrame;
|
|
if (!mainFrame)
|
|
return [];
|
|
|
|
let resolvedBreakpoints = [];
|
|
let frames = [mainFrame];
|
|
while (frames.length) {
|
|
let frame = frames.shift();
|
|
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(frame);
|
|
if (domBreakpointNodeIdentifierMap)
|
|
resolvedBreakpoints.pushAll(domBreakpointNodeIdentifierMap.values());
|
|
|
|
frames.pushAll(frame.childFrameCollection);
|
|
}
|
|
|
|
return resolvedBreakpoints;
|
|
}
|
|
|
|
get listenerBreakpoints() { return this._listenerBreakpoints; }
|
|
get urlBreakpoints() { return this._urlBreakpoints; }
|
|
|
|
domBreakpointsForNode(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode);
|
|
|
|
if (!node || !node.frame)
|
|
return [];
|
|
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(node.frame);
|
|
if (!domBreakpointNodeIdentifierMap)
|
|
return [];
|
|
|
|
let breakpoints = domBreakpointNodeIdentifierMap.get(node);
|
|
return breakpoints ? Array.from(breakpoints) : [];
|
|
}
|
|
|
|
domBreakpointsInSubtree(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode);
|
|
|
|
let breakpoints = [];
|
|
|
|
if (node.children) {
|
|
let children = Array.from(node.children);
|
|
while (children.length) {
|
|
let child = children.pop();
|
|
if (child.children)
|
|
children.pushAll(child.children);
|
|
breakpoints.pushAll(this.domBreakpointsForNode(child));
|
|
}
|
|
}
|
|
|
|
return breakpoints;
|
|
}
|
|
|
|
addDOMBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.DOMBreakpoint, breakpoint);
|
|
console.assert(breakpoint.url, breakpoint);
|
|
if (!breakpoint || !breakpoint.url)
|
|
return;
|
|
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
|
|
this._domBreakpointURLMap.add(breakpoint.url, breakpoint);
|
|
|
|
if (breakpoint.domNode) {
|
|
this._resolveDOMBreakpoint(breakpoint, breakpoint.domNode);
|
|
|
|
if (!breakpoint.disabled) {
|
|
// We should get the target associated with the nodeIdentifier of this breakpoint.
|
|
let target = WI.assumingMainTarget();
|
|
if (target)
|
|
this._setDOMBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.DOMBreakpointAdded, {breakpoint});
|
|
|
|
WI.debuggerManager.addProbesForBreakpoint(breakpoint);
|
|
} else
|
|
this._speculativelyResolveDOMBreakpoint(breakpoint);
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.domBreakpoints.putObject(breakpoint);
|
|
}
|
|
|
|
removeDOMBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.DOMBreakpoint, breakpoint);
|
|
console.assert(breakpoint.url, breakpoint);
|
|
if (!breakpoint || !breakpoint.url)
|
|
return;
|
|
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
this._domBreakpointURLMap.delete(breakpoint.url);
|
|
|
|
if (breakpoint.domNode) {
|
|
if (breakpoint.domNode.frame) {
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(breakpoint.domNode.frame);
|
|
domBreakpointNodeIdentifierMap.delete(breakpoint.domNode, breakpoint);
|
|
if (!domBreakpointNodeIdentifierMap.size)
|
|
this._domBreakpointFrameIdentifierMap.delete(breakpoint.domNode.frame);
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.DOMBreakpointRemoved, {breakpoint});
|
|
|
|
breakpoint.domNode = null;
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.domBreakpoints.deleteObject(breakpoint);
|
|
}
|
|
|
|
removeDOMBreakpointsForNode(node)
|
|
{
|
|
this.domBreakpointsForNode(node).forEach(this.removeDOMBreakpoint, this);
|
|
}
|
|
|
|
listenerBreakpointsForEventName(eventName)
|
|
{
|
|
if (DOMDebuggerManager.supportsAllListenersBreakpoint() && this._allListenersBreakpoint && !this._allListenersBreakpoint.disabled)
|
|
return this._allListenersBreakpoint;
|
|
|
|
// Order event breakpoints based on how closely they match the given symbol. As an example,
|
|
// a regular expression is likely going to match more events than a case-insensitive string.
|
|
const rankFunctions = [
|
|
(breakpoint) => breakpoint.caseSensitive && !breakpoint.isRegex, // exact match
|
|
(breakpoint) => !breakpoint.caseSensitive && !breakpoint.isRegex, // case-insensitive
|
|
(breakpoint) => breakpoint.caseSensitive && breakpoint.isRegex, // case-sensitive regex
|
|
(breakpoint) => !breakpoint.caseSensitive && breakpoint.isRegex, // case-insensitive regex
|
|
];
|
|
return this._listenerBreakpoints
|
|
.filter((breakpoint) => breakpoint.matches(eventName))
|
|
.sort((a, b) => {
|
|
let aRank = rankFunctions.findIndex((rankFunction) => rankFunction(a));
|
|
let bRank = rankFunctions.findIndex((rankFunction) => rankFunction(b));
|
|
return aRank - bRank;
|
|
});
|
|
}
|
|
|
|
addEventBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.EventBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return false;
|
|
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
|
|
switch (breakpoint.type) {
|
|
case WI.EventBreakpoint.Type.AnimationFrame:
|
|
console.assert(!this._allAnimationFramesBreakpoint, this._allAnimationFramesBreakpoint, breakpoint);
|
|
this._allAnimationFramesBreakpoint = breakpoint;
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Interval:
|
|
console.assert(!this._allIntervalsBreakpoint, this._allIntervalsBreakpoint, breakpoint);
|
|
this._allIntervalsBreakpoint = breakpoint;
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Listener:
|
|
if (breakpoint.eventName) {
|
|
if (this._listenerBreakpoints.some((existing) => existing.equals(breakpoint)))
|
|
return false;
|
|
|
|
this._listenerBreakpoints.push(breakpoint);
|
|
} else {
|
|
console.assert(!this._allListenersBreakpoint, this._allListenersBreakpoint, breakpoint);
|
|
this._allListenersBreakpoint = breakpoint;
|
|
}
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Timeout:
|
|
console.assert(!this._allTimeoutsBreakpoint, this._allTimeoutsBreakpoint, breakpoint);
|
|
this._allTimeoutsBreakpoint = breakpoint;
|
|
break;
|
|
}
|
|
|
|
WI.debuggerManager.addProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointAdded, {breakpoint});
|
|
|
|
if (!breakpoint.disabled) {
|
|
for (let target of WI.targets)
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.eventBreakpoints.putObject(breakpoint);
|
|
|
|
return true;
|
|
}
|
|
|
|
removeEventBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.EventBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
switch (breakpoint.type) {
|
|
case WI.EventBreakpoint.Type.AnimationFrame:
|
|
console.assert(this._allAnimationFramesBreakpoint, this._allAnimationFramesBreakpoint);
|
|
this._allAnimationFramesBreakpoint = null;
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Interval:
|
|
console.assert(this._allIntervalsBreakpoint, this._allIntervalsBreakpoint);
|
|
this._allIntervalsBreakpoint = null;
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Listener:
|
|
if (breakpoint.eventName) {
|
|
console.assert(this._listenerBreakpoints.includes(breakpoint), breakpoint);
|
|
if (!this._listenerBreakpoints.includes(breakpoint))
|
|
return;
|
|
|
|
this._listenerBreakpoints.remove(breakpoint);
|
|
} else {
|
|
console.assert(this._allListenersBreakpoint, this._allListenersBreakpoint);
|
|
this._allListenersBreakpoint = null;
|
|
}
|
|
break;
|
|
|
|
case WI.EventBreakpoint.Type.Timeout:
|
|
console.assert(this._allTimeoutsBreakpoint, this._allTimeoutsBreakpoint);
|
|
this._allTimeoutsBreakpoint = null;
|
|
break;
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.eventBreakpoints.deleteObject(breakpoint);
|
|
|
|
WI.debuggerManager.removeProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
|
|
}
|
|
|
|
urlBreakpointForURL(url)
|
|
{
|
|
return this._urlBreakpoints.find((breakpoint) => breakpoint.url === url) || null;
|
|
}
|
|
|
|
urlBreakpointsMatchingURL(url)
|
|
{
|
|
return this._urlBreakpoints
|
|
.filter((urlBreakpoint) => {
|
|
switch (urlBreakpoint.type) {
|
|
case WI.URLBreakpoint.Type.Text:
|
|
return urlBreakpoint.url.toLowerCase() === url.toLowerCase();
|
|
|
|
case WI.URLBreakpoint.Type.RegularExpression:
|
|
return (new RegExp(urlBreakpoint.url, "i")).test(url);
|
|
}
|
|
|
|
return false;
|
|
})
|
|
.sort((a, b) => {
|
|
// Order URL breakpoints based on how closely they match the given URL.
|
|
const typeRankings = [
|
|
WI.URLBreakpoint.Type.Text,
|
|
WI.URLBreakpoint.Type.RegularExpression,
|
|
];
|
|
return typeRankings.indexOf(a.type) - typeRankings.indexOf(b.type);
|
|
});
|
|
}
|
|
|
|
addURLBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.URLBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return false;
|
|
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
if (breakpoint.url) {
|
|
if (this._urlBreakpoints.some((entry) => entry.type === breakpoint.type && entry.url === breakpoint.url))
|
|
return false;
|
|
|
|
this._urlBreakpoints.push(breakpoint);
|
|
} else {
|
|
console.assert(!this._allRequestsBreakpoint, this._allRequestsBreakpoint, breakpoint);
|
|
this._allRequestsBreakpoint = breakpoint;
|
|
}
|
|
|
|
WI.debuggerManager.addProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.URLBreakpointAdded, {breakpoint});
|
|
|
|
if (!breakpoint.disabled) {
|
|
for (let target of WI.targets)
|
|
this._setURLBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.urlBreakpoints.putObject(breakpoint);
|
|
|
|
return true;
|
|
}
|
|
|
|
removeURLBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.URLBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
if (breakpoint.url) {
|
|
console.assert(this._urlBreakpoints.includes(breakpoint), breakpoint);
|
|
if (!this._urlBreakpoints.includes(breakpoint))
|
|
return;
|
|
|
|
this._urlBreakpoints.remove(breakpoint);
|
|
} else {
|
|
console.assert(this._allRequestsBreakpoint, this._allRequestsBreakpoint);
|
|
this._allRequestsBreakpoint = null;
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.urlBreakpoints.deleteObject(breakpoint);
|
|
|
|
WI.debuggerManager.removeProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DOMDebuggerManager.Event.URLBreakpointRemoved, {breakpoint});
|
|
}
|
|
|
|
// Private
|
|
|
|
_detachDOMBreakpointsForFrame(frame)
|
|
{
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(frame);
|
|
if (domBreakpointNodeIdentifierMap) {
|
|
this._domBreakpointFrameIdentifierMap.delete(frame);
|
|
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = true;
|
|
for (let breakpoint of domBreakpointNodeIdentifierMap.values())
|
|
breakpoint.domNode = null;
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = false;
|
|
}
|
|
|
|
for (let childFrame of frame.childFrameCollection)
|
|
this._detachDOMBreakpointsForFrame(childFrame);
|
|
}
|
|
|
|
_speculativelyResolveDOMBreakpointsForURL(url)
|
|
{
|
|
let domBreakpoints = this._domBreakpointURLMap.get(url);
|
|
if (!domBreakpoints)
|
|
return;
|
|
|
|
for (let breakpoint of domBreakpoints)
|
|
this._speculativelyResolveDOMBreakpoint(breakpoint);
|
|
}
|
|
|
|
_speculativelyResolveDOMBreakpoint(breakpoint)
|
|
{
|
|
if (breakpoint.domNode)
|
|
return;
|
|
|
|
WI.domManager.pushNodeByPathToFrontend(breakpoint.path, (nodeIdentifier) => {
|
|
if (!nodeIdentifier)
|
|
return;
|
|
|
|
if (breakpoint.domNode) {
|
|
// This breakpoint may have been resolved by a node being inserted before this
|
|
// callback is invoked. If so, the `nodeIdentifier` should match, so don't try
|
|
// to resolve it again as it would've already been resolved.
|
|
console.assert(breakpoint.domNode.id === nodeIdentifier);
|
|
return;
|
|
}
|
|
|
|
this._restoringBreakpoints = true;
|
|
this._resolveDOMBreakpoint(breakpoint, WI.domManager.nodeForId(nodeIdentifier));
|
|
this._restoringBreakpoints = false;
|
|
});
|
|
}
|
|
|
|
_resolveDOMBreakpoint(breakpoint, node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode, node);
|
|
|
|
if (!node.frame)
|
|
return;
|
|
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(node.frame);
|
|
if (!domBreakpointNodeIdentifierMap) {
|
|
domBreakpointNodeIdentifierMap = new Multimap;
|
|
this._domBreakpointFrameIdentifierMap.set(node.frame, domBreakpointNodeIdentifierMap);
|
|
}
|
|
|
|
domBreakpointNodeIdentifierMap.add(node, breakpoint);
|
|
|
|
breakpoint.domNode = node;
|
|
}
|
|
|
|
_setDOMBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(!breakpoint.disabled, breakpoint);
|
|
console.assert(breakpoint.domNode instanceof WI.DOMNode, breakpoint);
|
|
console.assert(target.type !== WI.TargetType.Worker, "Worker targets do not support DOM breakpoints", target);
|
|
|
|
if (!this._restoringBreakpoints && !WI.debuggerManager.breakpointsDisabledTemporarily)
|
|
WI.debuggerManager.breakpointsEnabled = true;
|
|
|
|
target.DOMDebuggerAgent.setDOMBreakpoint.invoke({
|
|
nodeId: breakpoint.domNode.id,
|
|
type: breakpoint.type,
|
|
options: breakpoint.optionsToProtocol(),
|
|
});
|
|
}
|
|
|
|
_removeDOMBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(breakpoint.domNode instanceof WI.DOMNode, breakpoint);
|
|
console.assert(target.type !== WI.TargetType.Worker, "Worker targets do not support DOM breakpoints", target);
|
|
|
|
target.DOMDebuggerAgent.removeDOMBreakpoint(breakpoint.domNode.id, breakpoint.type);
|
|
}
|
|
|
|
_commandArgumentsForEventBreakpoint(breakpoint)
|
|
{
|
|
let commandArguments = {};
|
|
|
|
switch (breakpoint) {
|
|
case this._allAnimationFramesBreakpoint:
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.AnimationFrame;
|
|
if (!DOMDebuggerManager.supportsAllListenersBreakpoint())
|
|
commandArguments.eventName = "requestAnimationFrame";
|
|
break;
|
|
|
|
case this._allIntervalsBreakpoint:
|
|
if (DOMDebuggerManager.supportsAllListenersBreakpoint())
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.Interval;
|
|
else {
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.Timer;
|
|
commandArguments.eventName = "setInterval";
|
|
}
|
|
break;
|
|
|
|
case this._allListenersBreakpoint:
|
|
if (!DOMDebuggerManager.supportsAllListenersBreakpoint())
|
|
return;
|
|
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.Listener;
|
|
break;
|
|
|
|
case this._allTimeoutsBreakpoint:
|
|
if (DOMDebuggerManager.supportsAllListenersBreakpoint())
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.Timeout;
|
|
else {
|
|
commandArguments.breakpointType = WI.EventBreakpoint.Type.Timer;
|
|
commandArguments.eventName = "setTimeout";
|
|
}
|
|
break;
|
|
|
|
default:
|
|
console.assert(breakpoint.type === WI.EventBreakpoint.Type.Listener, breakpoint.type);
|
|
console.assert(breakpoint.eventName, breakpoint.eventName);
|
|
commandArguments.breakpointType = breakpoint.type;
|
|
commandArguments.eventName = breakpoint.eventName;
|
|
commandArguments.caseSensitive = breakpoint.caseSensitive;
|
|
commandArguments.isRegex = breakpoint.isRegex;
|
|
break;
|
|
}
|
|
|
|
return commandArguments;
|
|
}
|
|
|
|
_setEventBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(!breakpoint.disabled, breakpoint);
|
|
|
|
// Worker targets do not support `requestAnimationFrame` breakpoints.
|
|
if (breakpoint === this._allAnimationFramesBreakpoint && target.type === WI.TargetType.Worker)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 12.0): DOMDebugger.setEventListenerBreakpoint was replaced by DOMDebugger.setEventBreakpoint.
|
|
if (!target.hasCommand("DOMDebugger.setEventBreakpoint")) {
|
|
console.assert(breakpoint.type === WI.EventBreakpoint.Type.Listener);
|
|
|
|
if (!this._restoringBreakpoints && !WI.debuggerManager.breakpointsDisabledTemporarily)
|
|
WI.debuggerManager.breakpointsEnabled = true;
|
|
|
|
target.DOMDebuggerAgent.setEventListenerBreakpoint(breakpoint.eventName);
|
|
return;
|
|
}
|
|
|
|
let commandArguments = this._commandArgumentsForEventBreakpoint(breakpoint);
|
|
|
|
if (!this._restoringBreakpoints && !WI.debuggerManager.breakpointsDisabledTemporarily)
|
|
WI.debuggerManager.breakpointsEnabled = true;
|
|
|
|
commandArguments.options = breakpoint.optionsToProtocol();
|
|
|
|
target.DOMDebuggerAgent.setEventBreakpoint.invoke(commandArguments);
|
|
}
|
|
|
|
_removeEventBreakpoint(breakpoint, target)
|
|
{
|
|
// Worker targets do not support `requestAnimationFrame` breakpoints.
|
|
if (breakpoint === this._allAnimationFramesBreakpoint && target.type === WI.TargetType.Worker)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 12.0): DOMDebugger.removeEventListenerBreakpoint was replaced by DOMDebugger.removeEventBreakpoint.
|
|
if (!target.hasCommand("DOMDebugger.removeEventBreakpoint")) {
|
|
console.assert(breakpoint.type === WI.EventBreakpoint.Type.Listener);
|
|
target.DOMDebuggerAgent.removeEventListenerBreakpoint(breakpoint.eventName);
|
|
return;
|
|
}
|
|
|
|
let commandArguments = this._commandArgumentsForEventBreakpoint(breakpoint);
|
|
|
|
target.DOMDebuggerAgent.removeEventBreakpoint.invoke(commandArguments);
|
|
}
|
|
|
|
_setURLBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(!breakpoint.disabled, breakpoint);
|
|
|
|
if (!this._restoringBreakpoints && !WI.debuggerManager.breakpointsDisabledTemporarily)
|
|
WI.debuggerManager.breakpointsEnabled = true;
|
|
|
|
// COMPATIBILITY (iOS 12.2): DOMDebugger.setXHRBreakpoint was replaced by DOMDebugger.setURLBreakpoint.
|
|
if (!target.hasCommand("DOMDebugger.setURLBreakpoint")) {
|
|
let isRegex = breakpoint.type === WI.URLBreakpoint.Type.RegularExpression;
|
|
target.DOMDebuggerAgent.setXHRBreakpoint(breakpoint.url, isRegex);
|
|
return;
|
|
}
|
|
|
|
|
|
target.DOMDebuggerAgent.setURLBreakpoint.invoke({
|
|
url: breakpoint.url,
|
|
isRegex: breakpoint.type === WI.URLBreakpoint.Type.RegularExpression,
|
|
options: breakpoint.optionsToProtocol(),
|
|
});
|
|
}
|
|
|
|
_removeURLBreakpoint(breakpoint, target)
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): DOMDebugger.removeXHRBreakpoint was replaced by DOMDebugger.removeURLBreakpoint.
|
|
if (!target.hasCommand("DOMDebugger.removeURLBreakpoint")) {
|
|
target.DOMDebuggerAgent.removeXHRBreakpoint(breakpoint.url);
|
|
return;
|
|
}
|
|
|
|
target.DOMDebuggerAgent.removeURLBreakpoint.invoke({
|
|
url: breakpoint.url,
|
|
isRegex: breakpoint.type === WI.URLBreakpoint.Type.RegularExpression,
|
|
});
|
|
}
|
|
|
|
_handleDOMBreakpointDisabledStateChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.domBreakpoints.putObject(breakpoint);
|
|
|
|
if (!breakpoint.domNode)
|
|
return;
|
|
|
|
// We should get the target associated with the nodeIdentifier of this breakpoint.
|
|
let target = WI.assumingMainTarget();
|
|
if (target) {
|
|
if (breakpoint.disabled)
|
|
this._removeDOMBreakpoint(breakpoint, target);
|
|
else
|
|
this._setDOMBreakpoint(breakpoint, target);
|
|
}
|
|
}
|
|
|
|
_handleDOMBreakpointEditablePropertyChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.domBreakpoints.putObject(breakpoint);
|
|
|
|
if (!breakpoint.domNode)
|
|
return;
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
this._restoringBreakpoints = true;
|
|
// We should get the target associated with the nodeIdentifier of this breakpoint.
|
|
let target = WI.assumingMainTarget();
|
|
if (target) {
|
|
// Clear the old breakpoint from the backend before setting the new one.
|
|
this._removeDOMBreakpoint(breakpoint, target);
|
|
this._setDOMBreakpoint(breakpoint, target);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
|
|
_handleDOMBreakpointActionsChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
this._handleDOMBreakpointEditablePropertyChanged(event);
|
|
|
|
if (!breakpoint.domNode)
|
|
return;
|
|
|
|
WI.debuggerManager.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_handleDOMBreakpointDOMNodeWillChange(event)
|
|
{
|
|
if (this._clearingDOMBreakpointsForRemovedDOMNode)
|
|
return;
|
|
|
|
let breakpoint = event.target;
|
|
|
|
if (!breakpoint.domNode)
|
|
return;
|
|
|
|
if (!breakpoint.disabled) {
|
|
// We should get the target associated with the nodeIdentifier of this breakpoint.
|
|
let target = WI.assumingMainTarget();
|
|
if (target)
|
|
this._removeDOMBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
WI.debuggerManager.removeProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_handleDOMBreakpointDOMNodeDidChange(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (!breakpoint.domNode)
|
|
return;
|
|
|
|
if (!breakpoint.disabled) {
|
|
// We should get the target associated with the nodeIdentifier of this breakpoint.
|
|
let target = WI.assumingMainTarget();
|
|
if (target)
|
|
this._setDOMBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
WI.debuggerManager.addProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_handleEventBreakpointDisabledStateChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Specific event listener breakpoints are handled by `DOMManager`.
|
|
if (breakpoint.eventListener)
|
|
return;
|
|
|
|
for (let target of WI.targets) {
|
|
if (breakpoint.disabled)
|
|
this._removeEventBreakpoint(breakpoint, target);
|
|
else
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.eventBreakpoints.putObject(breakpoint);
|
|
}
|
|
|
|
_handleEventBreakpointEditablePropertyChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Specific event listener breakpoints are handled by `DOMManager`.
|
|
if (breakpoint.eventListener)
|
|
return;
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.eventBreakpoints.putObject(breakpoint);
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let target of WI.targets) {
|
|
// Clear the old breakpoint from the backend before setting the new one.
|
|
this._removeEventBreakpoint(breakpoint, target);
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
|
|
_handleEventBreakpointActionsChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Specific event listener breakpoints are handled by `DOMManager`.
|
|
if (breakpoint.eventListener)
|
|
return;
|
|
|
|
this._handleEventBreakpointEditablePropertyChanged(event);
|
|
|
|
WI.debuggerManager.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_handleURLBreakpointDisabledStateChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
for (let target of WI.targets) {
|
|
if (breakpoint.disabled)
|
|
this._removeURLBreakpoint(breakpoint, target);
|
|
else
|
|
this._setURLBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.urlBreakpoints.putObject(breakpoint);
|
|
}
|
|
|
|
_handleURLBreakpointEditablePropertyChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.urlBreakpoints.putObject(breakpoint);
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let target of WI.targets) {
|
|
// Clear the old breakpoint from the backend before setting the new one.
|
|
this._removeURLBreakpoint(breakpoint, target)
|
|
this._setURLBreakpoint(breakpoint, target);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
|
|
_handleURLBreakpointActionsChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
this._handleURLBreakpointEditablePropertyChanged(event);
|
|
|
|
WI.debuggerManager.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_childFrameWasRemoved(event)
|
|
{
|
|
let frame = event.data.childFrame;
|
|
this._detachDOMBreakpointsForFrame(frame);
|
|
}
|
|
|
|
_mainFrameDidChange(event)
|
|
{
|
|
this._speculativelyResolveDOMBreakpointsForURL(WI.networkManager.mainFrame.url);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
let frame = event.target;
|
|
if (frame.isMainFrame()) {
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = true;
|
|
for (let breakpoint of this._domBreakpointURLMap.values())
|
|
breakpoint.domNode = null;
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = false;
|
|
|
|
this._domBreakpointFrameIdentifierMap.clear();
|
|
} else
|
|
this._detachDOMBreakpointsForFrame(frame);
|
|
|
|
this._speculativelyResolveDOMBreakpointsForURL(frame.url);
|
|
}
|
|
|
|
_nodeInserted(event)
|
|
{
|
|
let node = event.data.node;
|
|
if (node.nodeType() !== Node.ELEMENT_NODE || !node.frame)
|
|
return;
|
|
|
|
let url = node.frame.url;
|
|
let breakpoints = this._domBreakpointURLMap.get(url);
|
|
if (!breakpoints)
|
|
return;
|
|
|
|
let resolvableBreakpoints = [];
|
|
for (let breakpoint of breakpoints) {
|
|
if (!breakpoint.domNode)
|
|
resolvableBreakpoints.push(breakpoint);
|
|
}
|
|
if (!resolvableBreakpoints.length)
|
|
return;
|
|
|
|
// This is not very expensive because `WI.DOMNode` children are lazily populated, so it's
|
|
// unlikely that there will be a deep subtree to walk.
|
|
let stack = [node];
|
|
while (stack.length) {
|
|
let child = stack.pop();
|
|
let path = child.path();
|
|
|
|
for (let i = resolvableBreakpoints.length - 1; i >= 0; --i) {
|
|
if (resolvableBreakpoints[i].path === path) {
|
|
this._restoringBreakpoints = true;
|
|
this._resolveDOMBreakpoint(resolvableBreakpoints[i], child);
|
|
this._restoringBreakpoints = false;
|
|
|
|
resolvableBreakpoints.splice(i, 1);
|
|
}
|
|
}
|
|
if (!resolvableBreakpoints.length)
|
|
break;
|
|
|
|
if (child.children?.length)
|
|
stack.pushAll(child.children);
|
|
}
|
|
}
|
|
|
|
_nodeRemoved(event)
|
|
{
|
|
let node = event.data.node;
|
|
if (node.nodeType() !== Node.ELEMENT_NODE || !node.frame)
|
|
return;
|
|
|
|
let domBreakpointNodeIdentifierMap = this._domBreakpointFrameIdentifierMap.get(node.frame);
|
|
if (!domBreakpointNodeIdentifierMap)
|
|
return;
|
|
|
|
for (let [breakpointOwner, breakpoints] of domBreakpointNodeIdentifierMap.sets()) {
|
|
if (breakpointOwner == node || node.isAncestor(breakpointOwner)) {
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = true;
|
|
for (let breakpoint of breakpoints)
|
|
breakpoint.domNode = null;
|
|
this._clearingDOMBreakpointsForRemovedDOMNode = false;
|
|
|
|
domBreakpointNodeIdentifierMap.delete(breakpointOwner);
|
|
if (!domBreakpointNodeIdentifierMap.size) {
|
|
this._domBreakpointFrameIdentifierMap.delete(node.frame);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.DOMDebuggerManager.Event = {
|
|
DOMBreakpointAdded: "dom-debugger-manager-dom-breakpoint-added",
|
|
DOMBreakpointRemoved: "dom-debugger-manager-dom-breakpoint-removed",
|
|
EventBreakpointAdded: "dom-debugger-manager-event-breakpoint-added",
|
|
EventBreakpointRemoved: "dom-debugger-manager-event-breakpoint-removed",
|
|
URLBreakpointAdded: "dom-debugger-manager-url-breakpoint-added",
|
|
URLBreakpointRemoved: "dom-debugger-manager-url-breakpoint-removed",
|
|
};
|
|
|
|
/* Controllers/DOMManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2009, 2010 Google Inc. All rights reserved.
|
|
* Copyright (C) 2009 Joseph Pecoraro
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions are
|
|
* met:
|
|
*
|
|
* * Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* * Redistributions in binary form must reproduce the above
|
|
* copyright notice, this list of conditions and the following disclaimer
|
|
* in the documentation and/or other materials provided with the
|
|
* distribution.
|
|
* * Neither the name of Google Inc. nor the names of its
|
|
* contributors may be used to endorse or promote products derived from
|
|
* this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
|
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
|
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
|
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
|
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
|
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
|
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
|
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: DOMManager lacks advanced multi-target support. (DOMNodes per-target)
|
|
|
|
WI.DOMManager = class DOMManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._idToDOMNode = {};
|
|
this._document = null;
|
|
this._documentPromise = null;
|
|
this._attributeLoadNodeIds = {};
|
|
this._restoreSelectedNodeIsAllowed = true;
|
|
this._loadNodeAttributesTimeout = 0;
|
|
this._inspectedNode = null;
|
|
|
|
this._breakpointsForEventListeners = new Map;
|
|
|
|
this._hasRequestedDocument = false;
|
|
this._pendingDocumentRequestCallbacks = null;
|
|
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._handleEventBreakpointDisabledStateChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._handleEventBreakpointEditablePropertyChanged, this);
|
|
WI.EventBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleEventBreakpointActionsChanged, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
// FIXME: This should be improved when adding better DOM multi-target support since it is really per-target.
|
|
// This currently uses a setTimeout since it doesn't need to happen immediately, and DOMManager uses the
|
|
// global DOMAgent to request the document, so we want to make sure we've transitioned the global agents
|
|
// to this target if necessary.
|
|
if (target.hasDomain("DOM")) {
|
|
setTimeout(() => {
|
|
this.ensureDocument();
|
|
});
|
|
|
|
if (WI.engineeringSettingsAllowed()) {
|
|
if (DOMManager.supportsEditingUserAgentShadowTrees({target}))
|
|
target.DOMAgent.setAllowEditingUserAgentShadowTrees(WI.settings.engineeringAllowEditingUserAgentShadowTrees.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
transitionPageTarget()
|
|
{
|
|
this._documentUpdated();
|
|
}
|
|
|
|
// Static
|
|
|
|
static buildHighlightConfigs(mode)
|
|
{
|
|
mode = mode || "all";
|
|
|
|
let commandArguments = {
|
|
highlightConfig: {showInfo: mode === "all"},
|
|
};
|
|
|
|
if (mode === "all" || mode === "content")
|
|
commandArguments.highlightConfig.contentColor = {r: 111, g: 168, b: 220, a: 0.66};
|
|
|
|
if (mode === "all" || mode === "padding")
|
|
commandArguments.highlightConfig.paddingColor = {r: 147, g: 196, b: 125, a: 0.66};
|
|
|
|
if (mode === "all" || mode === "border")
|
|
commandArguments.highlightConfig.borderColor = {r: 255, g: 229, b: 153, a: 0.66};
|
|
|
|
if (mode === "all" || mode === "margin")
|
|
commandArguments.highlightConfig.marginColor = {r: 246, g: 178, b: 107, a: 0.66};
|
|
|
|
if (WI.settings.showGridOverlayDuringElementSelection.value) {
|
|
commandArguments.gridOverlayConfig = {
|
|
gridColor: WI.DOMNode.defaultLayoutOverlayColor.toProtocol(),
|
|
showLineNames: WI.settings.gridOverlayShowLineNames.value,
|
|
showLineNumbers: WI.settings.gridOverlayShowLineNumbers.value,
|
|
showExtendedGridLines: WI.settings.gridOverlayShowExtendedGridLines.value,
|
|
showTrackSizes: WI.settings.gridOverlayShowTrackSizes.value,
|
|
showAreaNames: WI.settings.gridOverlayShowAreaNames.value,
|
|
};
|
|
}
|
|
|
|
if (WI.settings.showFlexOverlayDuringElementSelection.value) {
|
|
commandArguments.flexOverlayConfig = {
|
|
flexColor: WI.DOMNode.defaultLayoutOverlayColor.toProtocol(),
|
|
showOrderNumbers: WI.settings.flexOverlayShowOrderNumbers.value,
|
|
};
|
|
}
|
|
|
|
if (WI.settings.showRulersDuringElementSelection.value)
|
|
commandArguments.showRulers = true;
|
|
|
|
return commandArguments;
|
|
}
|
|
|
|
static wrapClientCallback(callback)
|
|
{
|
|
if (!callback)
|
|
return null;
|
|
|
|
return function(error, result) {
|
|
if (error)
|
|
console.error("Error during DOMAgent operation: " + error);
|
|
callback(error ? null : result);
|
|
};
|
|
}
|
|
|
|
static supportsEventListenerBreakpoints()
|
|
{
|
|
return InspectorBackend.hasCommand("DOM.setBreakpointForEventListener")
|
|
&& InspectorBackend.hasCommand("DOM.removeBreakpointForEventListener");
|
|
}
|
|
|
|
static supportsEventListenerBreakpointConfiguration()
|
|
{
|
|
// COMPATIBILITY (iOS 14): DOM.setBreakpointForEventListener did not have an "options" parameter yet.
|
|
return InspectorBackend.hasCommand("DOM.setBreakpointForEventListener", "options");
|
|
}
|
|
|
|
static supportsEditingUserAgentShadowTrees({frontendOnly, target} = {})
|
|
{
|
|
target = target || InspectorBackend;
|
|
return WI.settings.engineeringAllowEditingUserAgentShadowTrees.value
|
|
&& (frontendOnly || target.hasCommand("DOM.setAllowEditingUserAgentShadowTrees"));
|
|
|
|
}
|
|
|
|
// Public
|
|
|
|
get inspectedNode() { return this._inspectedNode; }
|
|
|
|
get eventListenerBreakpoints()
|
|
{
|
|
return Array.from(this._breakpointsForEventListeners.values());
|
|
}
|
|
|
|
*attachedNodes({filter} = {})
|
|
{
|
|
if (!this._document)
|
|
return;
|
|
|
|
filter ??= (node) => true;
|
|
|
|
// Traverse the node tree in the same order items would appear if the entire tree were expanded in order to
|
|
// provide a predictable order for the results.
|
|
let currentBranch = [this._document];
|
|
while (currentBranch.length) {
|
|
let currentNode = currentBranch.at(-1);
|
|
|
|
if (filter(currentNode))
|
|
yield currentNode;
|
|
|
|
// The `::before` pseudo element is the first child of any node.
|
|
let beforePseudoElement = currentNode.beforePseudoElement();
|
|
if (beforePseudoElement && filter(beforePseudoElement))
|
|
yield beforePseudoElement;
|
|
|
|
let firstChild = currentNode.children?.[0];
|
|
if (firstChild) {
|
|
currentBranch.push(firstChild);
|
|
continue;
|
|
}
|
|
|
|
while (currentBranch.length) {
|
|
let parent = currentBranch.pop();
|
|
|
|
// The `::after` pseudo element is the last child of any node.
|
|
let parentAfterPseudoElement = parent.afterPseudoElement();
|
|
if (parentAfterPseudoElement && filter(parentAfterPseudoElement))
|
|
yield parentAfterPseudoElement;
|
|
|
|
if (parent.nextSibling) {
|
|
currentBranch.push(parent.nextSibling);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
requestDocument(callback)
|
|
{
|
|
if (typeof callback !== "function")
|
|
return this._requestDocumentWithPromise();
|
|
|
|
this._requestDocumentWithCallback(callback);
|
|
}
|
|
|
|
ensureDocument()
|
|
{
|
|
this.requestDocument(function(){});
|
|
}
|
|
|
|
pushNodeToFrontend(objectId, callback)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
this._dispatchWhenDocumentAvailable((callbackWrapper) => {
|
|
target.DOMAgent.requestNode(objectId, callbackWrapper);
|
|
}, callback);
|
|
}
|
|
|
|
pushNodeByPathToFrontend(path, callback)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
this._dispatchWhenDocumentAvailable((callbackWrapper) => {
|
|
target.DOMAgent.pushNodeByPathToFrontend(path, callbackWrapper);
|
|
}, callback);
|
|
}
|
|
|
|
// DOMObserver
|
|
|
|
willDestroyDOMNode(nodeId)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
node.markDestroyed();
|
|
delete this._idToDOMNode[nodeId];
|
|
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node});
|
|
}
|
|
|
|
didAddEventListener(nodeId)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
|
|
}
|
|
|
|
willRemoveEventListener(nodeId)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node.dispatchEventToListeners(WI.DOMNode.Event.EventListenersChanged);
|
|
}
|
|
|
|
didFireEvent(nodeId, eventName, timestamp, data)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node.didFireEvent(eventName, timestamp, data);
|
|
}
|
|
|
|
powerEfficientPlaybackStateChanged(nodeId, timestamp, isPowerEfficient)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node.powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient);
|
|
}
|
|
|
|
// CSSObserver
|
|
|
|
nodeLayoutFlagsChanged(nodeId, layoutFlags)
|
|
{
|
|
let domNode = this._idToDOMNode[nodeId];
|
|
console.assert(domNode instanceof WI.DOMNode, domNode, nodeId);
|
|
if (!domNode)
|
|
return;
|
|
|
|
domNode.layoutFlags = layoutFlags;
|
|
}
|
|
|
|
// Private
|
|
|
|
_dispatchWhenDocumentAvailable(func, callback)
|
|
{
|
|
var callbackWrapper = DOMManager.wrapClientCallback(callback);
|
|
|
|
function onDocumentAvailable()
|
|
{
|
|
if (this._document)
|
|
func(callbackWrapper);
|
|
else {
|
|
if (callbackWrapper)
|
|
callbackWrapper("No document");
|
|
}
|
|
}
|
|
this.requestDocument(onDocumentAvailable.bind(this));
|
|
}
|
|
|
|
_requestDocumentWithPromise()
|
|
{
|
|
if (this._documentPromise)
|
|
return this._documentPromise.promise;
|
|
|
|
this._documentPromise = new WI.WrappedPromise;
|
|
if (this._document)
|
|
this._documentPromise.resolve(this._document);
|
|
else {
|
|
this._requestDocumentWithCallback((doc) => {
|
|
this._documentPromise.resolve(doc);
|
|
});
|
|
}
|
|
|
|
return this._documentPromise.promise;
|
|
}
|
|
|
|
_requestDocumentWithCallback(callback)
|
|
{
|
|
if (this._document) {
|
|
callback(this._document);
|
|
return;
|
|
}
|
|
|
|
if (this._pendingDocumentRequestCallbacks)
|
|
this._pendingDocumentRequestCallbacks.push(callback);
|
|
else
|
|
this._pendingDocumentRequestCallbacks = [callback];
|
|
|
|
if (this._hasRequestedDocument)
|
|
return;
|
|
|
|
if (!WI.pageTarget)
|
|
return;
|
|
|
|
if (!WI.pageTarget.hasDomain("DOM"))
|
|
return;
|
|
|
|
this._hasRequestedDocument = true;
|
|
|
|
WI.pageTarget.DOMAgent.getDocument((error, root) => {
|
|
if (!error)
|
|
this._setDocument(root);
|
|
|
|
for (let callback of this._pendingDocumentRequestCallbacks)
|
|
callback(this._document);
|
|
|
|
this._pendingDocumentRequestCallbacks = null;
|
|
});
|
|
}
|
|
|
|
_attributeModified(nodeId, name, value)
|
|
{
|
|
var node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node._setAttribute(name, value);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name});
|
|
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name});
|
|
}
|
|
|
|
_attributeRemoved(nodeId, name)
|
|
{
|
|
var node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node._removeAttribute(name);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeRemoved, {node, name});
|
|
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeRemoved, {name});
|
|
}
|
|
|
|
_inlineStyleInvalidated(nodeIds)
|
|
{
|
|
for (var nodeId of nodeIds)
|
|
this._attributeLoadNodeIds[nodeId] = true;
|
|
if (this._loadNodeAttributesTimeout)
|
|
return;
|
|
this._loadNodeAttributesTimeout = setTimeout(this._loadNodeAttributes.bind(this), 0);
|
|
}
|
|
|
|
_loadNodeAttributes()
|
|
{
|
|
function callback(nodeId, error, attributes)
|
|
{
|
|
if (error) {
|
|
console.error("Error during DOMAgent operation: " + error);
|
|
return;
|
|
}
|
|
var node = this._idToDOMNode[nodeId];
|
|
if (node) {
|
|
node._setAttributesPayload(attributes);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.AttributeModified, {node, name: "style"});
|
|
node.dispatchEventToListeners(WI.DOMNode.Event.AttributeModified, {name: "style"});
|
|
}
|
|
}
|
|
|
|
this._loadNodeAttributesTimeout = 0;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
for (var nodeId in this._attributeLoadNodeIds) {
|
|
if (!(nodeId in this._idToDOMNode))
|
|
continue;
|
|
var nodeIdAsNumber = parseInt(nodeId);
|
|
target.DOMAgent.getAttributes(nodeIdAsNumber, callback.bind(this, nodeIdAsNumber));
|
|
}
|
|
this._attributeLoadNodeIds = {};
|
|
}
|
|
|
|
_characterDataModified(nodeId, newValue)
|
|
{
|
|
var node = this._idToDOMNode[nodeId];
|
|
node._nodeValue = newValue;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.CharacterDataModified, {node});
|
|
}
|
|
|
|
nodeForId(nodeId)
|
|
{
|
|
return this._idToDOMNode[nodeId] || null;
|
|
}
|
|
|
|
_documentUpdated()
|
|
{
|
|
this._setDocument(null);
|
|
}
|
|
|
|
_setDocument(payload)
|
|
{
|
|
for (let node of Object.values(this._idToDOMNode))
|
|
node.markDestroyed();
|
|
|
|
this._idToDOMNode = {};
|
|
|
|
for (let breakpoint of this._breakpointsForEventListeners.values())
|
|
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
|
|
this._breakpointsForEventListeners.clear();
|
|
|
|
let newDocument = null;
|
|
if (payload && "nodeId" in payload)
|
|
newDocument = new WI.DOMNode(this, null, false, payload);
|
|
|
|
if (this._document === newDocument)
|
|
return;
|
|
|
|
this._document = newDocument;
|
|
|
|
// Force the promise to be recreated so that it resolves to the new document.
|
|
this._documentPromise = null;
|
|
|
|
if (!this._document)
|
|
this._hasRequestedDocument = false;
|
|
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.DocumentUpdated, {document: this._document});
|
|
}
|
|
|
|
_setDetachedRoot(payload)
|
|
{
|
|
new WI.DOMNode(this, null, false, payload);
|
|
}
|
|
|
|
_setChildNodes(parentId, payloads)
|
|
{
|
|
if (!parentId && payloads.length) {
|
|
this._setDetachedRoot(payloads[0]);
|
|
return;
|
|
}
|
|
|
|
var parent = this._idToDOMNode[parentId];
|
|
|
|
if (parent.children) {
|
|
for (let node of parent.children)
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node, parent});
|
|
}
|
|
|
|
parent._setChildrenPayload(payloads);
|
|
|
|
for (let node of parent.children)
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
|
|
}
|
|
|
|
_childNodeCountUpdated(nodeId, newValue)
|
|
{
|
|
var node = this._idToDOMNode[nodeId];
|
|
node.childNodeCount = newValue;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.ChildNodeCountUpdated, node);
|
|
}
|
|
|
|
_childNodeInserted(parentId, prevId, payload)
|
|
{
|
|
var parent = this._idToDOMNode[parentId];
|
|
var prev = this._idToDOMNode[prevId];
|
|
var node = parent._insertChild(prev, payload);
|
|
this._idToDOMNode[node.id] = node;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
|
|
}
|
|
|
|
_childNodeRemoved(parentId, nodeId)
|
|
{
|
|
var parent = this._idToDOMNode[parentId];
|
|
var node = this._idToDOMNode[nodeId];
|
|
parent._removeChild(node);
|
|
this._unbind(node);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node, parent});
|
|
}
|
|
|
|
_customElementStateChanged(elementId, newState)
|
|
{
|
|
const node = this._idToDOMNode[elementId];
|
|
node._customElementState = newState;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.CustomElementStateChanged, {node});
|
|
}
|
|
|
|
_pseudoElementAdded(parentId, pseudoElement)
|
|
{
|
|
var parent = this._idToDOMNode[parentId];
|
|
if (!parent)
|
|
return;
|
|
|
|
var node = new WI.DOMNode(this, parent.ownerDocument, false, pseudoElement);
|
|
node.parentNode = parent;
|
|
this._idToDOMNode[node.id] = node;
|
|
console.assert(!parent.pseudoElements().get(node.pseudoType()));
|
|
parent.pseudoElements().set(node.pseudoType(), node);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeInserted, {node, parent});
|
|
}
|
|
|
|
_pseudoElementRemoved(parentId, pseudoElementId)
|
|
{
|
|
var pseudoElement = this._idToDOMNode[pseudoElementId];
|
|
if (!pseudoElement)
|
|
return;
|
|
|
|
var parent = pseudoElement.parentNode;
|
|
console.assert(parent);
|
|
console.assert(parent.id === parentId);
|
|
if (!parent)
|
|
return;
|
|
|
|
parent._removeChild(pseudoElement);
|
|
this._unbind(pseudoElement);
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.NodeRemoved, {node: pseudoElement, parent});
|
|
}
|
|
|
|
_unbind(node)
|
|
{
|
|
node.markDestroyed();
|
|
|
|
delete this._idToDOMNode[node.id];
|
|
|
|
for (let i = 0; node.children && i < node.children.length; ++i)
|
|
this._unbind(node.children[i]);
|
|
|
|
let templateContent = node.templateContent();
|
|
if (templateContent)
|
|
this._unbind(templateContent);
|
|
|
|
for (let pseudoElement of node.pseudoElements().values())
|
|
this._unbind(pseudoElement);
|
|
|
|
// FIXME: Handle shadow roots.
|
|
}
|
|
|
|
get restoreSelectedNodeIsAllowed()
|
|
{
|
|
return this._restoreSelectedNodeIsAllowed;
|
|
}
|
|
|
|
inspectElement(nodeId, options = {})
|
|
{
|
|
var node = this._idToDOMNode[nodeId];
|
|
if (!node || !node.ownerDocument)
|
|
return;
|
|
|
|
// This code path is hit by "Reveal in DOM Tree" and clicking element links/console widgets.
|
|
// Unless overridden by callers, assume that this is navigation is initiated by a Inspect mode.
|
|
let initiatorHint = options.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Inspect;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.DOMNodeWasInspected, {node, initiatorHint});
|
|
|
|
this._inspectModeEnabled = false;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
|
|
}
|
|
|
|
inspectNodeObject(remoteObject)
|
|
{
|
|
this._restoreSelectedNodeIsAllowed = false;
|
|
|
|
function nodeAvailable(nodeId)
|
|
{
|
|
remoteObject.release();
|
|
|
|
console.assert(nodeId);
|
|
if (!nodeId)
|
|
return;
|
|
|
|
this.inspectElement(nodeId);
|
|
|
|
// Re-resolve the node in the console's object group when adding to the console.
|
|
let domNode = this.nodeForId(nodeId);
|
|
WI.RemoteObject.resolveNode(domNode, WI.RuntimeManager.ConsoleObjectGroup).then((remoteObject) => {
|
|
WI.consoleLogViewController.appendImmediateExecutionWithResult(WI.UIString("Selected Element"), remoteObject, {addSpecialUserLogClass: true});
|
|
});
|
|
}
|
|
|
|
remoteObject.pushNodeToFrontend(nodeAvailable.bind(this));
|
|
}
|
|
|
|
highlightDOMNodeList(nodes, mode)
|
|
{
|
|
if (this._hideDOMNodeHighlightTimeout) {
|
|
clearTimeout(this._hideDOMNodeHighlightTimeout);
|
|
this._hideDOMNodeHighlightTimeout = undefined;
|
|
}
|
|
|
|
let nodeIds = [];
|
|
for (let node of nodes) {
|
|
console.assert(node instanceof WI.DOMNode, node);
|
|
console.assert(!node.destroyed, node);
|
|
if (node.destroyed)
|
|
continue;
|
|
nodeIds.push(node.id);
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.highlightNodeList.invoke({
|
|
nodeIds,
|
|
...WI.DOMManager.buildHighlightConfigs(mode),
|
|
});
|
|
}
|
|
|
|
highlightSelector(selectorString, frameId, mode)
|
|
{
|
|
if (this._hideDOMNodeHighlightTimeout) {
|
|
clearTimeout(this._hideDOMNodeHighlightTimeout);
|
|
this._hideDOMNodeHighlightTimeout = undefined;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.highlightSelector.invoke({
|
|
selectorString,
|
|
frameId,
|
|
...WI.DOMManager.buildHighlightConfigs(mode),
|
|
});
|
|
}
|
|
|
|
highlightRect(rect, usePageCoordinates)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.highlightRect.invoke({
|
|
x: rect.x,
|
|
y: rect.y,
|
|
width: rect.width,
|
|
height: rect.height,
|
|
color: {r: 111, g: 168, b: 220, a: 0.66},
|
|
outlineColor: {r: 255, g: 229, b: 153, a: 0.66},
|
|
usePageCoordinates
|
|
});
|
|
}
|
|
|
|
hideDOMNodeHighlight()
|
|
{
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("DOM.hideHighlight"))
|
|
target.DOMAgent.hideHighlight();
|
|
}
|
|
}
|
|
|
|
highlightDOMNodeForTwoSeconds(nodeId)
|
|
{
|
|
let node = this._idToDOMNode[nodeId];
|
|
if (!node)
|
|
return;
|
|
|
|
node.highlight();
|
|
|
|
this._hideDOMNodeHighlightTimeout = setTimeout(this.hideDOMNodeHighlight.bind(this), 2000);
|
|
}
|
|
|
|
get inspectModeEnabled()
|
|
{
|
|
return this._inspectModeEnabled;
|
|
}
|
|
|
|
set inspectModeEnabled(enabled)
|
|
{
|
|
if (enabled === this._inspectModeEnabled)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setInspectModeEnabled.invoke({
|
|
enabled,
|
|
...WI.DOMManager.buildHighlightConfigs(),
|
|
}, (error) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._inspectModeEnabled = enabled;
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.InspectModeStateChanged);
|
|
});
|
|
}
|
|
|
|
setInspectedNode(node)
|
|
{
|
|
console.assert(node instanceof WI.DOMNode);
|
|
if (node === this._inspectedNode)
|
|
return;
|
|
|
|
console.assert(!node.destroyed, node);
|
|
if (node.destroyed)
|
|
return;
|
|
|
|
let callback = (error) => {
|
|
console.assert(!error, error);
|
|
if (error)
|
|
return;
|
|
|
|
let lastInspectedNode = this._inspectedNode;
|
|
this._inspectedNode = node;
|
|
|
|
this.dispatchEventToListeners(WI.DOMManager.Event.InspectedNodeChanged, {lastInspectedNode});
|
|
};
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setInspectedNode(node.id, callback);
|
|
}
|
|
|
|
getSupportedEventNames(callback)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("DOM.getSupportedEventNames"))
|
|
return Promise.resolve(new Set);
|
|
|
|
if (!this._getSupportedEventNamesPromise) {
|
|
this._getSupportedEventNamesPromise = target.DOMAgent.getSupportedEventNames()
|
|
.then(({eventNames}) => new Set(eventNames));
|
|
}
|
|
|
|
return this._getSupportedEventNamesPromise;
|
|
}
|
|
|
|
setEventListenerDisabled(eventListener, disabled)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
target.DOMAgent.setEventListenerDisabled(eventListener.eventListenerId, disabled);
|
|
}
|
|
|
|
setBreakpointForEventListener(eventListener)
|
|
{
|
|
let breakpoint = this._breakpointsForEventListeners.get(eventListener.eventListenerId);
|
|
if (breakpoint) {
|
|
console.assert(breakpoint.disabled);
|
|
breakpoint.disabled = false;
|
|
return;
|
|
}
|
|
|
|
breakpoint = new WI.EventBreakpoint(WI.EventBreakpoint.Type.Listener, {eventName: eventListener.type, eventListener});
|
|
console.assert(!breakpoint.disabled);
|
|
|
|
this._breakpointsForEventListeners.set(eventListener.eventListenerId, breakpoint);
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("DOM"))
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
WI.debuggerManager.addProbesForBreakpoint(breakpoint);
|
|
|
|
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointAdded, {breakpoint});
|
|
}
|
|
|
|
removeBreakpointForEventListener(eventListener)
|
|
{
|
|
let breakpoint = this._breakpointsForEventListeners.take(eventListener.eventListenerId);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
WI.debuggerManager.removeProbesForBreakpoint(breakpoint);
|
|
|
|
WI.domDebuggerManager.dispatchEventToListeners(WI.DOMDebuggerManager.Event.EventBreakpointRemoved, {breakpoint});
|
|
}
|
|
|
|
removeEventListenerBreakpointsForNode(domNode)
|
|
{
|
|
for (let breakpoint of Array.from(this._breakpointsForEventListeners.values())) {
|
|
let eventListener = breakpoint.eventListener;
|
|
if (eventListener.nodeId === domNode.id)
|
|
this.removeBreakpointForEventListener(eventListener);
|
|
}
|
|
}
|
|
|
|
breakpointForEventListenerId(eventListenerId)
|
|
{
|
|
return this._breakpointsForEventListeners.get(eventListenerId) || null;
|
|
}
|
|
|
|
// Private
|
|
|
|
_setEventBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(!breakpoint.disabled, breakpoint);
|
|
|
|
let eventListener = breakpoint.eventListener;
|
|
console.assert(eventListener);
|
|
|
|
if (!WI.debuggerManager.breakpointsDisabledTemporarily)
|
|
WI.debuggerManager.breakpointsEnabled = true;
|
|
|
|
target.DOMAgent.setBreakpointForEventListener.invoke({
|
|
eventListenerId: eventListener.eventListenerId,
|
|
options: breakpoint.optionsToProtocol(),
|
|
});
|
|
}
|
|
|
|
_removeEventBreakpoint(breakpoint, target)
|
|
{
|
|
let eventListener = breakpoint.eventListener;
|
|
console.assert(eventListener);
|
|
|
|
target.DOMAgent.removeBreakpointForEventListener(eventListener.eventListenerId);
|
|
}
|
|
|
|
_handleEventBreakpointDisabledStateChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Non-specific event listener breakpoints are handled by `DOMDebuggerManager`.
|
|
if (!breakpoint.eventListener)
|
|
return;
|
|
|
|
for (let target of WI.targets) {
|
|
if (!target.hasDomain("DOM"))
|
|
continue;
|
|
|
|
if (breakpoint.disabled)
|
|
this._removeEventBreakpoint(breakpoint, target);
|
|
else
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
}
|
|
|
|
_handleEventBreakpointEditablePropertyChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Non-specific event listener breakpoints are handled by `DOMDebuggerManager`.
|
|
if (!breakpoint.eventListener)
|
|
return;
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
for (let target of WI.targets) {
|
|
// Clear the old breakpoint from the backend before setting the new one.
|
|
this._removeEventBreakpoint(breakpoint, target);
|
|
this._setEventBreakpoint(breakpoint, target);
|
|
}
|
|
}
|
|
|
|
_handleEventBreakpointActionsChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
// Non-specific event listener breakpoints are handled by `DOMDebuggerManager`.
|
|
if (!breakpoint.eventListener)
|
|
return;
|
|
|
|
this._handleEventBreakpointEditablePropertyChanged(event);
|
|
|
|
WI.debuggerManager.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
this._restoreSelectedNodeIsAllowed = true;
|
|
|
|
this.ensureDocument();
|
|
|
|
WI.DOMNode.resetDefaultLayoutOverlayConfiguration();
|
|
}
|
|
};
|
|
|
|
WI.DOMManager.Event = {
|
|
AttributeModified: "dom-manager-attribute-modified",
|
|
AttributeRemoved: "dom-manager-attribute-removed",
|
|
CharacterDataModified: "dom-manager-character-data-modified",
|
|
NodeInserted: "dom-manager-node-inserted",
|
|
NodeRemoved: "dom-manager-node-removed",
|
|
CustomElementStateChanged: "dom-manager-custom-element-state-changed",
|
|
DocumentUpdated: "dom-manager-document-updated",
|
|
ChildNodeCountUpdated: "dom-manager-child-node-count-updated",
|
|
DOMNodeWasInspected: "dom-manager-dom-node-was-inspected",
|
|
InspectModeStateChanged: "dom-manager-inspect-mode-state-changed",
|
|
InspectedNodeChanged: "dom-manager-inspected-node-changed",
|
|
};
|
|
|
|
/* Controllers/DOMStorageManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: DOMStorageManager lacks advanced multi-target support. (DOMStorage per-target)
|
|
|
|
WI.DOMStorageManager = class DOMStorageManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
this._reset();
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["DOMStorage"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "DOMStorage");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("DOMStorage"))
|
|
target.DOMStorageAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
get domStorageObjects() { return this._domStorageObjects; }
|
|
|
|
get cookieStorageObjects()
|
|
{
|
|
var cookieStorageObjects = [];
|
|
for (var host in this._cookieStorageObjects)
|
|
cookieStorageObjects.push(this._cookieStorageObjects[host]);
|
|
return cookieStorageObjects;
|
|
}
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
this._reset();
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.SecurityOriginDidChange, this._securityOriginDidChange, this);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
this._enabled = false;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("DOMStorage"))
|
|
target.DOMStorageAgent.disable();
|
|
}
|
|
|
|
WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.removeEventListener(WI.Frame.Event.SecurityOriginDidChange, this._securityOriginDidChange, this);
|
|
|
|
this._reset();
|
|
}
|
|
|
|
// DOMStorageObserver
|
|
|
|
itemsCleared(storageId)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let domStorage = this._domStorageForIdentifier(storageId);
|
|
if (domStorage)
|
|
domStorage.itemsCleared(storageId);
|
|
}
|
|
|
|
itemRemoved(storageId, key)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let domStorage = this._domStorageForIdentifier(storageId);
|
|
if (domStorage)
|
|
domStorage.itemRemoved(key);
|
|
}
|
|
|
|
itemAdded(storageId, key, value)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let domStorage = this._domStorageForIdentifier(storageId);
|
|
if (domStorage)
|
|
domStorage.itemAdded(key, value);
|
|
}
|
|
|
|
itemUpdated(storageId, key, oldValue, newValue)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let domStorage = this._domStorageForIdentifier(storageId);
|
|
if (domStorage)
|
|
domStorage.itemUpdated(key, oldValue, newValue);
|
|
}
|
|
|
|
// InspectorObserver
|
|
|
|
inspectDOMStorage(id)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
var domStorage = this._domStorageForIdentifier(id);
|
|
console.assert(domStorage);
|
|
if (!domStorage)
|
|
return;
|
|
this.dispatchEventToListeners(WI.DOMStorageManager.Event.DOMStorageObjectWasInspected, {domStorage});
|
|
}
|
|
|
|
// Private
|
|
|
|
_reset()
|
|
{
|
|
this._domStorageObjects = [];
|
|
this._cookieStorageObjects = {};
|
|
|
|
this.dispatchEventToListeners(DOMStorageManager.Event.Cleared);
|
|
|
|
let mainFrame = WI.networkManager.mainFrame;
|
|
if (mainFrame) {
|
|
this._addDOMStorageIfNeeded(mainFrame);
|
|
this._addCookieStorageIfNeeded(mainFrame);
|
|
}
|
|
}
|
|
|
|
_domStorageForIdentifier(id)
|
|
{
|
|
for (var storageObject of this._domStorageObjects) {
|
|
// The id is an object, so we need to compare the properties using Object.shallowEqual.
|
|
if (Object.shallowEqual(storageObject.id, id))
|
|
return storageObject;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_addDOMStorageIfNeeded(frame)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (!InspectorBackend.hasDomain("DOMStorage"))
|
|
return;
|
|
|
|
// Don't show storage if we don't have a security origin (about:blank).
|
|
if (!frame.securityOrigin || frame.securityOrigin === "://")
|
|
return;
|
|
|
|
// FIXME: Consider passing the other parts of the origin along.
|
|
|
|
let addDOMStorage = (isLocalStorage) => {
|
|
let identifier = {securityOrigin: frame.securityOrigin, isLocalStorage};
|
|
if (this._domStorageForIdentifier(identifier))
|
|
return;
|
|
|
|
let domStorage = new WI.DOMStorageObject(identifier, frame.mainResource.urlComponents.host, identifier.isLocalStorage);
|
|
this._domStorageObjects.push(domStorage);
|
|
this.dispatchEventToListeners(DOMStorageManager.Event.DOMStorageObjectWasAdded, {domStorage});
|
|
};
|
|
addDOMStorage(true);
|
|
addDOMStorage(false);
|
|
}
|
|
|
|
_addCookieStorageIfNeeded(frame)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (!InspectorBackend.hasCommand("Page.getCookies"))
|
|
return;
|
|
|
|
// Add the host of the frame that changed the main resource to the list of hosts there could be cookies for.
|
|
let host = parseURL(frame.url).host;
|
|
if (!host)
|
|
return;
|
|
|
|
if (this._cookieStorageObjects[host])
|
|
return;
|
|
|
|
this._cookieStorageObjects[host] = new WI.CookieStorageObject(host);
|
|
this.dispatchEventToListeners(WI.DOMStorageManager.Event.CookieStorageObjectWasAdded, {cookieStorage: this._cookieStorageObjects[host]});
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (event.target.isMainFrame()) {
|
|
this._reset();
|
|
return;
|
|
}
|
|
|
|
this._addCookieStorageIfNeeded(event.target);
|
|
}
|
|
|
|
_securityOriginDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
this._addDOMStorageIfNeeded(event.target);
|
|
}
|
|
};
|
|
|
|
WI.DOMStorageManager.Event = {
|
|
CookieStorageObjectWasAdded: "dom-storage-manager-cookie-storage-object-was-added",
|
|
DOMStorageObjectWasAdded: "dom-storage-manager-dom-storage-object-was-added",
|
|
DOMStorageObjectWasInspected: "dom-storage-manager-dom-storage-object-was-inspected",
|
|
Cleared: "dom-storage-manager-cleared",
|
|
};
|
|
|
|
/* Controllers/DatabaseManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: DatabaseManager lacks advanced multi-target support. (DataBase per-target)
|
|
|
|
WI.DatabaseManager = class DatabaseManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
this._reset();
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Database"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Database");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("Database"))
|
|
target.DatabaseAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
get databases() { return this._databaseObjects; }
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
this._reset();
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
this._enabled = false;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("Database"))
|
|
target.DatabaseAgent.disable();
|
|
}
|
|
|
|
WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
|
|
this._reset();
|
|
}
|
|
|
|
// DatabaseObserver
|
|
|
|
databaseWasAdded(id, host, name, version)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
var database = new WI.DatabaseObject(id, host, name, version);
|
|
|
|
this._databaseObjects.push(database);
|
|
this.dispatchEventToListeners(WI.DatabaseManager.Event.DatabaseWasAdded, {database});
|
|
}
|
|
|
|
// InspectorObserver
|
|
|
|
inspectDatabase(id)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
var database = this._databaseForIdentifier(id);
|
|
console.assert(database);
|
|
if (!database)
|
|
return;
|
|
this.dispatchEventToListeners(WI.DatabaseManager.Event.DatabaseWasInspected, {database});
|
|
}
|
|
|
|
// Private
|
|
|
|
_reset()
|
|
{
|
|
this._databaseObjects = [];
|
|
|
|
this.dispatchEventToListeners(WI.DatabaseManager.Event.Cleared);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (event.target.isMainFrame())
|
|
this._reset();
|
|
}
|
|
|
|
_databaseForIdentifier(id)
|
|
{
|
|
for (var i = 0; i < this._databaseObjects.length; ++i) {
|
|
if (this._databaseObjects[i].id === id)
|
|
return this._databaseObjects[i];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
};
|
|
|
|
WI.DatabaseManager.Event = {
|
|
DatabaseWasAdded: "database-manager-database-was-added",
|
|
DatabaseWasInspected: "database-manager-database-was-inspected",
|
|
Cleared: "database-manager-cleared",
|
|
};
|
|
|
|
/* Controllers/DebuggerManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DebuggerManager = class DebuggerManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._breakpointDisabledStateDidChange, this);
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._breakpointEditablePropertyDidChange, this);
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._breakpointEditablePropertyDidChange, this);
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._breakpointEditablePropertyDidChange, this);
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleBreakpointActionsDidChange, this);
|
|
WI.JavaScriptBreakpoint.addEventListener(WI.JavaScriptBreakpoint.Event.DisplayLocationDidChange, this._breakpointDisplayLocationDidChange, this);
|
|
|
|
WI.SymbolicBreakpoint.addEventListener(WI.Breakpoint.Event.DisabledStateDidChange, this._handleSymbolicBreakpointDisabledStateChanged, this);
|
|
WI.SymbolicBreakpoint.addEventListener(WI.Breakpoint.Event.ConditionDidChange, this._handleSymbolicBreakpointEditablePropertyChanged, this);
|
|
WI.SymbolicBreakpoint.addEventListener(WI.Breakpoint.Event.IgnoreCountDidChange, this._handleSymbolicBreakpointEditablePropertyChanged, this);
|
|
WI.SymbolicBreakpoint.addEventListener(WI.Breakpoint.Event.AutoContinueDidChange, this._handleSymbolicBreakpointEditablePropertyChanged, this);
|
|
WI.SymbolicBreakpoint.addEventListener(WI.Breakpoint.Event.ActionsDidChange, this._handleSymbolicBreakpointActionsChanged, this);
|
|
|
|
WI.timelineManager.addEventListener(WI.TimelineManager.Event.CapturingStateChanged, this._handleTimelineCapturingStateChanged, this);
|
|
|
|
WI.auditManager.addEventListener(WI.AuditManager.Event.TestScheduled, this._handleAuditManagerTestScheduled, this);
|
|
WI.auditManager.addEventListener(WI.AuditManager.Event.TestCompleted, this._handleAuditManagerTestCompleted, this);
|
|
|
|
WI.targetManager.addEventListener(WI.TargetManager.Event.TargetRemoved, this._targetRemoved, this);
|
|
|
|
WI.settings.blackboxBreakpointEvaluations.addEventListener(WI.Setting.Event.Changed, this._handleBlackboxBreakpointEvaluationsChange, this);
|
|
|
|
if (WI.engineeringSettingsAllowed()) {
|
|
WI.settings.engineeringShowInternalScripts.addEventListener(WI.Setting.Event.Changed, this._handleEngineeringShowInternalScriptsSettingChanged, this);
|
|
WI.settings.engineeringPauseForInternalScripts.addEventListener(WI.Setting.Event.Changed, this._handleEngineeringPauseForInternalScriptsSettingChanged, this);
|
|
}
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
|
|
this._breakpointsEnabledSetting = new WI.Setting("breakpoints-enabled", true);
|
|
this._asyncStackTraceDepthSetting = new WI.Setting("async-stack-trace-depth", 200);
|
|
|
|
this._debuggerStatementsBreakpointSetting = new WI.Setting("debugger-statements-breakpoint", {});
|
|
this._debuggerStatementsBreakpoint = null;
|
|
|
|
this._allExceptionsBreakpointSetting = new WI.Setting("all-exceptions-breakpoint", {disabled: true});
|
|
this._allExceptionsBreakpoint = null;
|
|
|
|
this._uncaughtExceptionsBreakpointSetting = new WI.Setting("uncaught-exceptions-breakpoint", {disabled: true});
|
|
this._uncaughtExceptionsBreakpoint = null;
|
|
|
|
this._assertionFailuresBreakpointSetting = new WI.Setting("assertion-failures-breakpoint", null);
|
|
if (WI.Setting.isFirstLaunch)
|
|
this._assertionFailuresBreakpointSetting.value = {disabled: true};
|
|
this._assertionFailuresBreakpoint = null;
|
|
|
|
this._allMicrotasksBreakpointSetting = new WI.Setting("all-microtasks-breakpoint", null);
|
|
this._allMicrotasksBreakpoint = null;
|
|
|
|
this._breakpoints = [];
|
|
this._breakpointContentIdentifierMap = new Multimap;
|
|
this._breakpointScriptIdentifierMap = new Multimap;
|
|
this._breakpointIdMap = new Map;
|
|
|
|
this._symbolicBreakpoints = [];
|
|
|
|
this._nextBreakpointActionIdentifier = 1;
|
|
|
|
this._blackboxedURLsSetting = new WI.Setting("debugger-blackboxed-urls", []);
|
|
this._blackboxedPatternsSetting = new WI.Setting("debugger-blackboxed-patterns", []);
|
|
this._blackboxedPatternDataMap = new Map;
|
|
this._blackboxedCallFrameGroupsToAutoExpand = [];
|
|
|
|
this._activeCallFrame = null;
|
|
|
|
this._internalWebKitScripts = [];
|
|
this._targetDebuggerDataMap = new Map;
|
|
|
|
// Used to detect deleted probe actions.
|
|
this._knownProbeIdentifiersForBreakpoint = new Map;
|
|
|
|
// Main lookup tables for probes and probe sets.
|
|
this._probesByIdentifier = new Map;
|
|
this._probeSetsByBreakpoint = new Map;
|
|
|
|
// Restore the correct breakpoints enabled setting if Web Inspector had
|
|
// previously been left in a state where breakpoints were temporarily disabled.
|
|
this._temporarilyDisabledBreakpointsRestoreSetting = new WI.Setting("temporarily-disabled-breakpoints-restore", null);
|
|
if (this._temporarilyDisabledBreakpointsRestoreSetting.value !== null) {
|
|
this._breakpointsEnabledSetting.value = this._temporarilyDisabledBreakpointsRestoreSetting.value;
|
|
this._temporarilyDisabledBreakpointsRestoreSetting.value = null;
|
|
}
|
|
this._temporarilyDisableBreakpointsRequestCount = 0;
|
|
|
|
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
|
|
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
let existingSerializedBreakpoints = WI.Setting.migrateValue("breakpoints");
|
|
if (existingSerializedBreakpoints) {
|
|
for (let existingSerializedBreakpoint of existingSerializedBreakpoints)
|
|
await WI.objectStores.breakpoints.putObject(WI.JavaScriptBreakpoint.fromJSON(existingSerializedBreakpoint));
|
|
}
|
|
|
|
let serializedBreakpoints = await WI.objectStores.breakpoints.getAll();
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let serializedBreakpoint of serializedBreakpoints) {
|
|
let breakpoint = WI.JavaScriptBreakpoint.fromJSON(serializedBreakpoint);
|
|
|
|
const key = null;
|
|
WI.objectStores.breakpoints.associateObject(breakpoint, key, serializedBreakpoint);
|
|
|
|
this.addBreakpoint(breakpoint);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
})());
|
|
|
|
if (WI.SymbolicBreakpoint.supported()) {
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
let serializedSymbolicBreakpoints = await WI.objectStores.symbolicBreakpoints.getAll();
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let serializedSymbolicBreakpoint of serializedSymbolicBreakpoints) {
|
|
let symbolicBreakpoint = WI.SymbolicBreakpoint.fromJSON(serializedSymbolicBreakpoint);
|
|
|
|
const key = null;
|
|
WI.objectStores.symbolicBreakpoints.associateObject(symbolicBreakpoint, key, serializedSymbolicBreakpoint);
|
|
|
|
this.addSymbolicBreakpoint(symbolicBreakpoint);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
})());
|
|
}
|
|
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
// Wait one microtask so that `WI.debuggerManager` can be initialized.
|
|
await new Promise((resolve, reject) => queueMicrotask(resolve));
|
|
|
|
let loadSpecialBreakpoint = (setting, enabledSettingsKey, shownSettingsKey) => {
|
|
let serializedBreakpoint = setting.value;
|
|
|
|
if (!serializedBreakpoint && (!shownSettingsKey || WI.Setting.migrateValue(shownSettingsKey))) {
|
|
serializedBreakpoint = setting.value = {};
|
|
setting.save();
|
|
}
|
|
|
|
if (WI.Setting.migrateValue(enabledSettingsKey)) {
|
|
if (!serializedBreakpoint)
|
|
serializedBreakpoint = setting.value = {};
|
|
serializedBreakpoint.disabled = false;
|
|
setting.save();
|
|
}
|
|
|
|
if (!serializedBreakpoint)
|
|
return null;
|
|
|
|
return this._createSpecialBreakpoint(serializedBreakpoint);
|
|
};
|
|
|
|
this._restoringBreakpoints = true;
|
|
|
|
if (WI.JavaScriptBreakpoint.supportsDebuggerStatements()) {
|
|
this._debuggerStatementsBreakpoint = loadSpecialBreakpoint(this._debuggerStatementsBreakpointSetting, "break-on-debugger-statements");
|
|
if (this._debuggerStatementsBreakpoint)
|
|
this.addBreakpoint(this._debuggerStatementsBreakpoint);
|
|
}
|
|
|
|
this._allExceptionsBreakpoint = loadSpecialBreakpoint(this._allExceptionsBreakpointSetting, "break-on-all-exceptions");
|
|
if (this._allExceptionsBreakpoint)
|
|
this.addBreakpoint(this._allExceptionsBreakpoint);
|
|
|
|
this._uncaughtExceptionsBreakpoint = loadSpecialBreakpoint(this._uncaughtExceptionsBreakpointSetting, "break-on-uncaught-exceptions");
|
|
if (this._uncaughtExceptionsBreakpoint)
|
|
this.addBreakpoint(this._uncaughtExceptionsBreakpoint);
|
|
|
|
this._assertionFailuresBreakpoint = loadSpecialBreakpoint(this._assertionFailuresBreakpointSetting, "break-on-assertion-failures", "show-assertion-failures-breakpoint");
|
|
if (this._assertionFailuresBreakpoint)
|
|
this.addBreakpoint(this._assertionFailuresBreakpoint);
|
|
|
|
if (WI.JavaScriptBreakpoint.supportsMicrotasks()) {
|
|
this._allMicrotasksBreakpoint = loadSpecialBreakpoint(this._allMicrotasksBreakpointSetting, "break-on-all-microtasks", "show-all-microtasks-breakpoint");
|
|
if (this._allMicrotasksBreakpoint)
|
|
this.addBreakpoint(this._allMicrotasksBreakpoint);
|
|
}
|
|
|
|
this._restoringBreakpoints = false;
|
|
})());
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
let targetData = this.dataForTarget(target);
|
|
|
|
// Initialize global state.
|
|
target.DebuggerAgent.enable();
|
|
target.DebuggerAgent.setBreakpointsActive(this._breakpointsEnabledSetting.value);
|
|
|
|
if (WI.SymbolicBreakpoint.supported(target)) {
|
|
for (let breakpoint of this._symbolicBreakpoints) {
|
|
if (!breakpoint.disabled)
|
|
this._setSymbolicBreakpoint(breakpoint, target);
|
|
}
|
|
}
|
|
|
|
if (this._debuggerStatementsBreakpoint)
|
|
this._updateSpecialBreakpoint(this._debuggerStatementsBreakpoint, target);
|
|
|
|
this._setPauseOnExceptions(target);
|
|
|
|
if (this._assertionFailuresBreakpoint)
|
|
this._updateSpecialBreakpoint(this._assertionFailuresBreakpoint, target);
|
|
|
|
if (this._allMicrotasksBreakpoint)
|
|
this._updateSpecialBreakpoint(this._allMicrotasksBreakpoint, target);
|
|
|
|
target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
|
|
|
|
// COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
|
|
if (target.hasCommand("Debugger.setShouldBlackboxURL")) {
|
|
const shouldBlackbox = true;
|
|
|
|
{
|
|
const caseSensitive = true;
|
|
for (let url of this._blackboxedURLsSetting.value)
|
|
target.DebuggerAgent.setShouldBlackboxURL(url, shouldBlackbox, caseSensitive);
|
|
}
|
|
|
|
{
|
|
const isRegex = true;
|
|
for (let data of this._blackboxedPatternsSetting.value) {
|
|
this._blackboxedPatternDataMap.set(new RegExp(data.url, !data.caseSensitive ? "i" : ""), data);
|
|
target.DebuggerAgent.setShouldBlackboxURL(data.url, shouldBlackbox, data.caseSensitive, isRegex);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._setBlackboxBreakpointEvaluations(target);
|
|
|
|
if (WI.engineeringSettingsAllowed()) {
|
|
// COMPATIBILITY (iOS 12): DebuggerAgent.setPauseForInternalScripts did not exist yet.
|
|
if (target.hasCommand("Debugger.setPauseForInternalScripts"))
|
|
target.DebuggerAgent.setPauseForInternalScripts(WI.settings.engineeringPauseForInternalScripts.value);
|
|
}
|
|
|
|
if (this.paused)
|
|
targetData.pauseIfNeeded();
|
|
|
|
// Initialize breakpoints.
|
|
this._restoringBreakpoints = true;
|
|
for (let breakpoint of this._breakpoints) {
|
|
if (breakpoint.disabled)
|
|
continue;
|
|
if (!breakpoint.contentIdentifier)
|
|
continue;
|
|
this._setBreakpoint(breakpoint, target);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsBlackboxingScripts()
|
|
{
|
|
// COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
|
|
return InspectorBackend.hasCommand("Debugger.setShouldBlackboxURL");
|
|
}
|
|
|
|
static supportsBlackboxingBreakpointEvaluations()
|
|
{
|
|
// COMPATIBILITY (macOS 12.3, iOS 15.4): Debugger.setBlackboxBreakpointEvaluations did not exist yet.
|
|
return InspectorBackend.hasCommand("Debugger.setBlackboxBreakpointEvaluations");
|
|
}
|
|
|
|
static pauseReasonFromPayload(payload)
|
|
{
|
|
switch (payload) {
|
|
case InspectorBackend.Enum.Debugger.PausedReason.AnimationFrame:
|
|
return WI.DebuggerManager.PauseReason.AnimationFrame;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Assert:
|
|
return WI.DebuggerManager.PauseReason.Assertion;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.BlackboxedScript:
|
|
return WI.DebuggerManager.PauseReason.BlackboxedScript;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Breakpoint:
|
|
return WI.DebuggerManager.PauseReason.Breakpoint;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.CSPViolation:
|
|
return WI.DebuggerManager.PauseReason.CSPViolation;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.DOM:
|
|
return WI.DebuggerManager.PauseReason.DOM;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.DebuggerStatement:
|
|
return WI.DebuggerManager.PauseReason.DebuggerStatement;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.EventListener:
|
|
return WI.DebuggerManager.PauseReason.EventListener;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Exception:
|
|
return WI.DebuggerManager.PauseReason.Exception;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.FunctionCall:
|
|
return WI.DebuggerManager.PauseReason.FunctionCall;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Interval:
|
|
return WI.DebuggerManager.PauseReason.Interval;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Listener:
|
|
return WI.DebuggerManager.PauseReason.Listener;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Microtask:
|
|
return WI.DebuggerManager.PauseReason.Microtask;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.PauseOnNextStatement:
|
|
return WI.DebuggerManager.PauseReason.PauseOnNextStatement;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Timeout:
|
|
return WI.DebuggerManager.PauseReason.Timeout;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Timer:
|
|
return WI.DebuggerManager.PauseReason.Timer;
|
|
case InspectorBackend.Enum.Debugger.PausedReason.URL:
|
|
case InspectorBackend.Enum.Debugger.PausedReason.Fetch: // COMPATIBILITY (macOS 13.0, iOS 16.0): Debugger.paused.reason.Fetch was replaced by Debugger.paused.reason.URL
|
|
case InspectorBackend.Enum.Debugger.PausedReason.XHR: // COMPATIBILITY (macOS 13.0, iOS 16.0): Debugger.paused.reason.XHR was replaced by Debugger.paused.reason.URL
|
|
return WI.DebuggerManager.PauseReason.URL;
|
|
default:
|
|
return WI.DebuggerManager.PauseReason.Other;
|
|
}
|
|
}
|
|
|
|
// Public
|
|
|
|
get paused()
|
|
{
|
|
for (let [target, targetData] of this._targetDebuggerDataMap) {
|
|
if (targetData.paused)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
get activeCallFrame()
|
|
{
|
|
return this._activeCallFrame;
|
|
}
|
|
|
|
set activeCallFrame(callFrame)
|
|
{
|
|
if (callFrame === this._activeCallFrame)
|
|
return;
|
|
|
|
this._activeCallFrame = callFrame || null;
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
|
|
}
|
|
|
|
dataForTarget(target)
|
|
{
|
|
let targetData = this._targetDebuggerDataMap.get(target);
|
|
if (targetData)
|
|
return targetData;
|
|
|
|
targetData = new WI.DebuggerData(target);
|
|
this._targetDebuggerDataMap.set(target, targetData);
|
|
return targetData;
|
|
}
|
|
|
|
get debuggerStatementsBreakpoint() { return this._debuggerStatementsBreakpoint; }
|
|
get allExceptionsBreakpoint() { return this._allExceptionsBreakpoint; }
|
|
get uncaughtExceptionsBreakpoint() { return this._uncaughtExceptionsBreakpoint; }
|
|
get assertionFailuresBreakpoint() { return this._assertionFailuresBreakpoint; }
|
|
get allMicrotasksBreakpoint() { return this._allMicrotasksBreakpoint; }
|
|
get breakpoints() { return this._breakpoints; }
|
|
|
|
createAssertionFailuresBreakpoint(options = {})
|
|
{
|
|
console.assert(!this._assertionFailuresBreakpoint);
|
|
|
|
this._assertionFailuresBreakpoint = this._createSpecialBreakpoint(options);
|
|
this.addBreakpoint(this._assertionFailuresBreakpoint);
|
|
}
|
|
|
|
createAllMicrotasksBreakpoint(options = {})
|
|
{
|
|
console.assert(!this._allMicrotasksBreakpoint);
|
|
|
|
this._allMicrotasksBreakpoint = this._createSpecialBreakpoint(options);
|
|
this.addBreakpoint(this._allMicrotasksBreakpoint);
|
|
}
|
|
|
|
breakpointForIdentifier(id)
|
|
{
|
|
return this._breakpointIdMap.get(id) || null;
|
|
}
|
|
|
|
breakpointsForSourceCode(sourceCode)
|
|
{
|
|
console.assert(sourceCode instanceof WI.Resource || sourceCode instanceof WI.Script);
|
|
|
|
if (sourceCode instanceof WI.SourceMapResource)
|
|
return Array.from(this.breakpointsForSourceCode(sourceCode.sourceMap.originalSourceCode)).filter((breakpoint) => breakpoint.sourceCodeLocation.displaySourceCode === sourceCode);
|
|
|
|
let contentIdentifierBreakpoints = this._breakpointContentIdentifierMap.get(sourceCode.contentIdentifier);
|
|
if (contentIdentifierBreakpoints) {
|
|
this._associateBreakpointsWithSourceCode(contentIdentifierBreakpoints, sourceCode);
|
|
return contentIdentifierBreakpoints;
|
|
}
|
|
|
|
if (sourceCode instanceof WI.Script) {
|
|
let scriptIdentifierBreakpoints = this._breakpointScriptIdentifierMap.get(sourceCode.id);
|
|
if (scriptIdentifierBreakpoints) {
|
|
this._associateBreakpointsWithSourceCode(scriptIdentifierBreakpoints, sourceCode);
|
|
return scriptIdentifierBreakpoints;
|
|
}
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
breakpointsForSourceCodeLocation(sourceCodeLocation)
|
|
{
|
|
console.assert(sourceCodeLocation instanceof WI.SourceCodeLocation, sourceCodeLocation);
|
|
|
|
return this.breakpointsForSourceCode(sourceCodeLocation.sourceCode)
|
|
.filter((breakpoint) => breakpoint.hasResolvedLocation(sourceCodeLocation));
|
|
}
|
|
|
|
breakpointForSourceCodeLocation(sourceCodeLocation)
|
|
{
|
|
console.assert(sourceCodeLocation instanceof WI.SourceCodeLocation);
|
|
|
|
for (let breakpoint of this.breakpointsForSourceCode(sourceCodeLocation.sourceCode)) {
|
|
if (breakpoint.sourceCodeLocation.isEqual(sourceCodeLocation))
|
|
return breakpoint;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
symbolicBreakpointsForSymbol(symbol)
|
|
{
|
|
console.assert(WI.SymbolicBreakpoint.supported());
|
|
|
|
// Order symbolic breakpoints based on how closely they match the given symbol. As an example,
|
|
// a regular expression is likely going to match more symbols than a case-insensitive string.
|
|
const rankFunctions = [
|
|
(breakpoint) => breakpoint.caseSensitive && !breakpoint.isRegex, // exact match
|
|
(breakpoint) => !breakpoint.caseSensitive && !breakpoint.isRegex, // case-insensitive
|
|
(breakpoint) => breakpoint.caseSensitive && breakpoint.isRegex, // case-sensitive regex
|
|
(breakpoint) => !breakpoint.caseSensitive && breakpoint.isRegex, // case-insensitive regex
|
|
];
|
|
return this._symbolicBreakpoints
|
|
.filter((breakpoint) => breakpoint.matches(symbol))
|
|
.sort((a, b) => {
|
|
let aRank = rankFunctions.findIndex((rankFunction) => rankFunction(a));
|
|
let bRank = rankFunctions.findIndex((rankFunction) => rankFunction(b));
|
|
return aRank - bRank;
|
|
});
|
|
}
|
|
|
|
get breakpointsEnabled()
|
|
{
|
|
return this._breakpointsEnabledSetting.value;
|
|
}
|
|
|
|
set breakpointsEnabled(enabled)
|
|
{
|
|
if (this._breakpointsEnabledSetting.value === enabled)
|
|
return;
|
|
|
|
console.assert(!(enabled && this.breakpointsDisabledTemporarily), "Should not enable breakpoints when we are temporarily disabling breakpoints.");
|
|
if (enabled && this.breakpointsDisabledTemporarily)
|
|
return;
|
|
|
|
this._breakpointsEnabledSetting.value = enabled;
|
|
|
|
for (let target of WI.targets) {
|
|
target.DebuggerAgent.setBreakpointsActive(enabled);
|
|
this._setPauseOnExceptions(target);
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointsEnabledDidChange);
|
|
}
|
|
|
|
get breakpointsDisabledTemporarily()
|
|
{
|
|
return this._temporarilyDisabledBreakpointsRestoreSetting.value !== null;
|
|
}
|
|
|
|
scriptForIdentifier(id, target)
|
|
{
|
|
console.assert(target instanceof WI.Target);
|
|
return this.dataForTarget(target).scriptForIdentifier(id);
|
|
}
|
|
|
|
scriptsForURL(url, target)
|
|
{
|
|
// FIXME: This may not be safe. A Resource's URL may differ from a Script's URL.
|
|
console.assert(target instanceof WI.Target);
|
|
return this.dataForTarget(target).scriptsForURL(url);
|
|
}
|
|
|
|
get searchableScripts()
|
|
{
|
|
return this.knownNonResourceScripts.filter((script) => !!script.contentIdentifier);
|
|
}
|
|
|
|
get knownNonResourceScripts()
|
|
{
|
|
let knownScripts = [];
|
|
|
|
for (let targetData of this._targetDebuggerDataMap.values()) {
|
|
for (let script of targetData.scripts) {
|
|
if (script.resource)
|
|
continue;
|
|
if (!WI.settings.debugShowConsoleEvaluations.value && isWebInspectorConsoleEvaluationScript(script.sourceURL))
|
|
continue;
|
|
if (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(script.sourceURL))
|
|
continue;
|
|
knownScripts.push(script);
|
|
}
|
|
}
|
|
|
|
return knownScripts;
|
|
}
|
|
|
|
blackboxDataForSourceCode(sourceCode)
|
|
{
|
|
for (let regex of this._blackboxedPatternDataMap.keys()) {
|
|
if (regex.test(sourceCode.contentIdentifier))
|
|
return {type: DebuggerManager.BlackboxType.Pattern, regex};
|
|
}
|
|
|
|
if (this._blackboxedURLsSetting.value.includes(sourceCode.contentIdentifier))
|
|
return {type: DebuggerManager.BlackboxType.URL};
|
|
|
|
return null;
|
|
}
|
|
|
|
get blackboxPatterns()
|
|
{
|
|
return Array.from(this._blackboxedPatternDataMap.keys());
|
|
}
|
|
|
|
setShouldBlackboxScript(sourceCode, shouldBlackbox)
|
|
{
|
|
console.assert(DebuggerManager.supportsBlackboxingScripts());
|
|
console.assert(sourceCode instanceof WI.SourceCode);
|
|
console.assert(sourceCode.contentIdentifier);
|
|
console.assert(!isWebKitInjectedScript(sourceCode.contentIdentifier));
|
|
console.assert(shouldBlackbox !== ((this.blackboxDataForSourceCode(sourceCode) || {}).type === DebuggerManager.BlackboxType.URL));
|
|
|
|
this._blackboxedURLsSetting.value.toggleIncludes(sourceCode.contentIdentifier, shouldBlackbox);
|
|
this._blackboxedURLsSetting.save();
|
|
|
|
const caseSensitive = true;
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
|
|
if (target.hasCommand("Debugger.setShouldBlackboxURL"))
|
|
target.DebuggerAgent.setShouldBlackboxURL(sourceCode.contentIdentifier, !!shouldBlackbox, caseSensitive);
|
|
}
|
|
|
|
this.dispatchEventToListeners(DebuggerManager.Event.BlackboxChanged);
|
|
}
|
|
|
|
setShouldBlackboxPattern(regex, shouldBlackbox)
|
|
{
|
|
console.assert(DebuggerManager.supportsBlackboxingScripts());
|
|
console.assert(regex instanceof RegExp);
|
|
|
|
if (shouldBlackbox) {
|
|
console.assert(!this._blackboxedPatternDataMap.has(regex));
|
|
|
|
let data = {
|
|
url: regex.source,
|
|
caseSensitive: !regex.ignoreCase,
|
|
};
|
|
this._blackboxedPatternDataMap.set(regex, data);
|
|
this._blackboxedPatternsSetting.value.push(data);
|
|
} else {
|
|
console.assert(this._blackboxedPatternDataMap.has(regex));
|
|
this._blackboxedPatternsSetting.value.remove(this._blackboxedPatternDataMap.take(regex));
|
|
}
|
|
|
|
this._blackboxedPatternsSetting.save();
|
|
|
|
const isRegex = true;
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13): Debugger.setShouldBlackboxURL did not exist yet.
|
|
if (target.hasCommand("Debugger.setShouldBlackboxURL"))
|
|
target.DebuggerAgent.setShouldBlackboxURL(regex.source, !!shouldBlackbox, !regex.ignoreCase, isRegex);
|
|
}
|
|
|
|
this.dispatchEventToListeners(DebuggerManager.Event.BlackboxChanged);
|
|
}
|
|
|
|
rememberBlackboxedCallFrameGroupToAutoExpand(blackboxedCallFrameGroup)
|
|
{
|
|
console.assert(!this.shouldAutoExpandBlackboxedCallFrameGroup(blackboxedCallFrameGroup), blackboxedCallFrameGroup);
|
|
|
|
this._blackboxedCallFrameGroupsToAutoExpand.push(blackboxedCallFrameGroup);
|
|
}
|
|
|
|
shouldAutoExpandBlackboxedCallFrameGroup(blackboxedCallFrameGroup)
|
|
{
|
|
console.assert(Array.isArray(blackboxedCallFrameGroup) && blackboxedCallFrameGroup.length && blackboxedCallFrameGroup.every((callFrame) => callFrame instanceof WI.CallFrame && callFrame.blackboxed), blackboxedCallFrameGroup);
|
|
|
|
return this._blackboxedCallFrameGroupsToAutoExpand.some((blackboxedCallFrameGroupToAutoExpand) => {
|
|
if (blackboxedCallFrameGroupToAutoExpand.length !== blackboxedCallFrameGroup.length)
|
|
return false;
|
|
|
|
return blackboxedCallFrameGroupToAutoExpand.every((item, i) => item.isEqual(blackboxedCallFrameGroup[i]));
|
|
});
|
|
}
|
|
|
|
get asyncStackTraceDepth()
|
|
{
|
|
return this._asyncStackTraceDepthSetting.value;
|
|
}
|
|
|
|
set asyncStackTraceDepth(x)
|
|
{
|
|
if (this._asyncStackTraceDepthSetting.value === x)
|
|
return;
|
|
|
|
this._asyncStackTraceDepthSetting.value = x;
|
|
|
|
for (let target of WI.targets)
|
|
target.DebuggerAgent.setAsyncStackTraceDepth(this._asyncStackTraceDepthSetting.value);
|
|
}
|
|
|
|
get probeSets()
|
|
{
|
|
return [...this._probeSetsByBreakpoint.values()];
|
|
}
|
|
|
|
probeForIdentifier(identifier)
|
|
{
|
|
return this._probesByIdentifier.get(identifier);
|
|
}
|
|
|
|
pause()
|
|
{
|
|
if (this.paused)
|
|
return Promise.resolve();
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.WaitingToPause);
|
|
|
|
let promises = [this.awaitEvent(WI.DebuggerManager.Event.Paused, this)];
|
|
for (let targetData of this._targetDebuggerDataMap.values())
|
|
promises.push(targetData.pauseIfNeeded());
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
resume()
|
|
{
|
|
if (!this.paused)
|
|
return Promise.resolve();
|
|
|
|
let promises = [this.awaitEvent(WI.DebuggerManager.Event.Resumed, this)];
|
|
for (let targetData of this._targetDebuggerDataMap.values())
|
|
promises.push(targetData.resumeIfNeeded());
|
|
return Promise.all(promises);
|
|
}
|
|
|
|
stepNext()
|
|
{
|
|
if (!this.paused)
|
|
return Promise.reject(new Error("Cannot step next because debugger is not paused."));
|
|
|
|
return Promise.all([
|
|
this.awaitEvent(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this),
|
|
this._activeCallFrame.target.DebuggerAgent.stepNext(),
|
|
]);
|
|
}
|
|
|
|
stepOver()
|
|
{
|
|
if (!this.paused)
|
|
return Promise.reject(new Error("Cannot step over because debugger is not paused."));
|
|
|
|
return Promise.all([
|
|
this.awaitEvent(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this),
|
|
this._activeCallFrame.target.DebuggerAgent.stepOver(),
|
|
]);
|
|
}
|
|
|
|
stepInto()
|
|
{
|
|
if (!this.paused)
|
|
return Promise.reject(new Error("Cannot step into because debugger is not paused."));
|
|
|
|
return Promise.all([
|
|
this.awaitEvent(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this),
|
|
this._activeCallFrame.target.DebuggerAgent.stepInto(),
|
|
]);
|
|
}
|
|
|
|
stepOut()
|
|
{
|
|
if (!this.paused)
|
|
return Promise.reject(new Error("Cannot step out because debugger is not paused."));
|
|
|
|
return Promise.all([
|
|
this.awaitEvent(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this),
|
|
this._activeCallFrame.target.DebuggerAgent.stepOut(),
|
|
]);
|
|
}
|
|
|
|
continueUntilNextRunLoop(target)
|
|
{
|
|
return this.dataForTarget(target).continueUntilNextRunLoop();
|
|
}
|
|
|
|
continueToLocation(script, lineNumber, columnNumber)
|
|
{
|
|
return script.target.DebuggerAgent.continueToLocation({scriptId: script.id, lineNumber, columnNumber});
|
|
}
|
|
|
|
addBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.JavaScriptBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
if (breakpoint.special)
|
|
this._updateSpecialBreakpoint(breakpoint);
|
|
else {
|
|
if (breakpoint.contentIdentifier)
|
|
this._breakpointContentIdentifierMap.add(breakpoint.contentIdentifier, breakpoint);
|
|
|
|
if (breakpoint.scriptIdentifier)
|
|
this._breakpointScriptIdentifierMap.add(breakpoint.scriptIdentifier, breakpoint);
|
|
|
|
this._breakpoints.push(breakpoint);
|
|
|
|
if (!breakpoint.disabled)
|
|
this._setBreakpoint(breakpoint);
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.breakpoints.putObject(breakpoint);
|
|
}
|
|
|
|
this.addProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointAdded, {breakpoint});
|
|
}
|
|
|
|
removeBreakpoint(breakpoint)
|
|
{
|
|
console.assert(breakpoint instanceof WI.JavaScriptBreakpoint, breakpoint);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
console.assert(breakpoint.removable, breakpoint);
|
|
if (!breakpoint.removable)
|
|
return;
|
|
|
|
let special = breakpoint.special;
|
|
|
|
if (!special) {
|
|
this._breakpoints.remove(breakpoint);
|
|
|
|
if (breakpoint.identifier)
|
|
this._removeBreakpoint(breakpoint);
|
|
|
|
if (breakpoint.contentIdentifier)
|
|
this._breakpointContentIdentifierMap.delete(breakpoint.contentIdentifier, breakpoint);
|
|
|
|
if (breakpoint.scriptIdentifier)
|
|
this._breakpointScriptIdentifierMap.delete(breakpoint.scriptIdentifier, breakpoint);
|
|
}
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
if (special) {
|
|
switch (breakpoint) {
|
|
case this._assertionFailuresBreakpoint:
|
|
this._assertionFailuresBreakpointSetting.reset();
|
|
this._assertionFailuresBreakpoint = null;
|
|
break;
|
|
|
|
case this._allMicrotasksBreakpoint:
|
|
this._allMicrotasksBreakpointSetting.reset();
|
|
this._allMicrotasksBreakpoint = null;
|
|
break;
|
|
}
|
|
} else if (!this._restoringBreakpoints)
|
|
WI.objectStores.breakpoints.deleteObject(breakpoint);
|
|
|
|
this.removeProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointRemoved, {breakpoint});
|
|
}
|
|
|
|
addSymbolicBreakpoint(breakpoint)
|
|
{
|
|
console.assert(WI.SymbolicBreakpoint.supported());
|
|
console.assert(breakpoint instanceof WI.SymbolicBreakpoint, breakpoint);
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
|
|
if (this._symbolicBreakpoints.some((existingBreakpoint) => existingBreakpoint.equals(breakpoint)))
|
|
return false;
|
|
|
|
this._symbolicBreakpoints.push(breakpoint);
|
|
|
|
if (!breakpoint.disabled) {
|
|
for (let target of WI.targets)
|
|
this._setSymbolicBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.symbolicBreakpoints.putObject(breakpoint);
|
|
|
|
this.addProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.SymbolicBreakpointAdded, {breakpoint});
|
|
|
|
return true;
|
|
}
|
|
|
|
removeSymbolicBreakpoint(breakpoint)
|
|
{
|
|
console.assert(WI.SymbolicBreakpoint.supported());
|
|
console.assert(breakpoint instanceof WI.SymbolicBreakpoint, breakpoint);
|
|
console.assert(breakpoint.removable, breakpoint);
|
|
console.assert(!breakpoint.special, breakpoint);
|
|
|
|
// Disable the breakpoint first, so removing actions doesn't re-add the breakpoint.
|
|
breakpoint.disabled = true;
|
|
breakpoint.clearActions();
|
|
|
|
this._symbolicBreakpoints.remove(breakpoint);
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.symbolicBreakpoints.deleteObject(breakpoint);
|
|
|
|
this.removeProbesForBreakpoint(breakpoint);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.SymbolicBreakpointRemoved, {breakpoint});
|
|
}
|
|
|
|
nextBreakpointActionIdentifier()
|
|
{
|
|
return this._nextBreakpointActionIdentifier++;
|
|
}
|
|
|
|
addProbesForBreakpoint(breakpoint)
|
|
{
|
|
if (this._knownProbeIdentifiersForBreakpoint.has(breakpoint))
|
|
return;
|
|
|
|
this._knownProbeIdentifiersForBreakpoint.set(breakpoint, new Set);
|
|
|
|
this.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
removeProbesForBreakpoint(breakpoint)
|
|
{
|
|
console.assert(this._knownProbeIdentifiersForBreakpoint.has(breakpoint));
|
|
|
|
this.updateProbesForBreakpoint(breakpoint);
|
|
this._knownProbeIdentifiersForBreakpoint.delete(breakpoint);
|
|
}
|
|
|
|
updateProbesForBreakpoint(breakpoint)
|
|
{
|
|
let knownProbeIdentifiers = this._knownProbeIdentifiersForBreakpoint.get(breakpoint);
|
|
if (!knownProbeIdentifiers) {
|
|
// Sometimes actions change before the added breakpoint is fully dispatched.
|
|
this.addProbesForBreakpoint(breakpoint);
|
|
return;
|
|
}
|
|
|
|
let seenProbeIdentifiers = new Set;
|
|
|
|
for (let probeAction of breakpoint.probeActions) {
|
|
let probeIdentifier = probeAction.id;
|
|
console.assert(probeIdentifier, "Probe added without breakpoint action identifier: ", breakpoint);
|
|
|
|
seenProbeIdentifiers.add(probeIdentifier);
|
|
if (!knownProbeIdentifiers.has(probeIdentifier)) {
|
|
// New probe; find or create relevant probe set.
|
|
knownProbeIdentifiers.add(probeIdentifier);
|
|
let probeSet = this._probeSetForBreakpoint(breakpoint);
|
|
let newProbe = new WI.Probe(probeIdentifier, breakpoint, probeAction.data);
|
|
this._probesByIdentifier.set(probeIdentifier, newProbe);
|
|
probeSet.addProbe(newProbe);
|
|
break;
|
|
}
|
|
|
|
let probe = this._probesByIdentifier.get(probeIdentifier);
|
|
console.assert(probe, "Probe known but couldn't be found by identifier: ", probeIdentifier);
|
|
// Update probe expression; if it differed, change events will fire.
|
|
probe.expression = probeAction.data;
|
|
}
|
|
|
|
// Look for missing probes based on what we saw last.
|
|
for (let probeIdentifier of knownProbeIdentifiers) {
|
|
if (seenProbeIdentifiers.has(probeIdentifier))
|
|
break;
|
|
|
|
// The probe has gone missing, remove it.
|
|
let probeSet = this._probeSetForBreakpoint(breakpoint);
|
|
let probe = this._probesByIdentifier.get(probeIdentifier);
|
|
this._probesByIdentifier.delete(probeIdentifier);
|
|
knownProbeIdentifiers.delete(probeIdentifier);
|
|
probeSet.removeProbe(probe);
|
|
|
|
// Remove the probe set if it has become empty.
|
|
if (!probeSet.probes.length) {
|
|
this._probeSetsByBreakpoint.delete(probeSet.breakpoint);
|
|
probeSet.willRemove();
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ProbeSetRemoved, {probeSet});
|
|
}
|
|
}
|
|
}
|
|
|
|
// DebuggerObserver
|
|
|
|
breakpointResolved(target, breakpointIdentifier, location)
|
|
{
|
|
let breakpoint = this._breakpointIdMap.get(breakpointIdentifier);
|
|
console.assert(breakpoint);
|
|
if (!breakpoint)
|
|
return;
|
|
|
|
console.assert(breakpoint.identifier === breakpointIdentifier);
|
|
|
|
let sourceCodeLocation = this._sourceCodeLocationFromPayload(target, location);
|
|
|
|
if (!breakpoint.sourceCodeLocation.sourceCode)
|
|
breakpoint.sourceCodeLocation.sourceCode = sourceCodeLocation.sourceCode;
|
|
|
|
breakpoint.addResolvedLocation(sourceCodeLocation);
|
|
}
|
|
|
|
globalObjectCleared(target)
|
|
{
|
|
let wasPaused = this.paused;
|
|
|
|
WI.Script.resetUniqueDisplayNameNumbers(target);
|
|
|
|
this._internalWebKitScripts = [];
|
|
this._targetDebuggerDataMap.clear();
|
|
|
|
this._ignoreBreakpointDisplayLocationDidChangeEvent = true;
|
|
|
|
// Mark all the breakpoints as unresolved. They will be reported as resolved when
|
|
// breakpointResolved is called as the page loads.
|
|
for (let breakpoint of this._breakpoints) {
|
|
breakpoint.clearResolvedLocations();
|
|
|
|
if (breakpoint.sourceCodeLocation.sourceCode)
|
|
breakpoint.sourceCodeLocation.sourceCode = null;
|
|
}
|
|
|
|
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptsCleared);
|
|
|
|
if (wasPaused)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
|
|
}
|
|
|
|
debuggerDidPause(target, callFramesPayload, reason, data, asyncStackTracePayload)
|
|
{
|
|
if (this._delayedResumeTimeout) {
|
|
clearTimeout(this._delayedResumeTimeout);
|
|
this._delayedResumeTimeout = undefined;
|
|
}
|
|
|
|
let wasPaused = this.paused;
|
|
let targetData = this._targetDebuggerDataMap.get(target);
|
|
|
|
let callFrames = [];
|
|
let pauseReason = DebuggerManager.pauseReasonFromPayload(reason);
|
|
let pauseData = data || null;
|
|
|
|
for (var i = 0; i < callFramesPayload.length; ++i) {
|
|
var callFramePayload = callFramesPayload[i];
|
|
var sourceCodeLocation = this._sourceCodeLocationFromPayload(target, callFramePayload.location);
|
|
// FIXME: There may be useful call frames without a source code location (native callframes), should we include them?
|
|
if (!sourceCodeLocation)
|
|
continue;
|
|
if (!sourceCodeLocation.sourceCode)
|
|
continue;
|
|
|
|
// Exclude the case where the call frame is in the inspector code.
|
|
if (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(sourceCodeLocation.sourceCode.sourceURL))
|
|
continue;
|
|
|
|
let scopeChain = this._scopeChainFromPayload(target, callFramePayload.scopeChain);
|
|
let callFrame = WI.CallFrame.fromDebuggerPayload(target, callFramePayload, scopeChain, sourceCodeLocation);
|
|
callFrames.push(callFrame);
|
|
}
|
|
|
|
let activeCallFrame = callFrames[0];
|
|
|
|
if (!activeCallFrame) {
|
|
// FIXME: This may not be safe for multiple threads/targets.
|
|
// This indicates we were pausing in internal scripts only (Injected Scripts).
|
|
// Just resume and skip past this pause. We should be fixing the backend to
|
|
// not send such pauses.
|
|
if (wasPaused)
|
|
target.DebuggerAgent.continueUntilNextRunLoop();
|
|
else
|
|
target.DebuggerAgent.resume();
|
|
this._didResumeInternal(target);
|
|
return;
|
|
}
|
|
|
|
let stackTrace = new WI.StackTrace(callFrames, {
|
|
parentStackTrace: WI.StackTrace.fromPayload(target, asyncStackTracePayload),
|
|
});
|
|
targetData.updateForPause(stackTrace, pauseReason, pauseData);
|
|
|
|
// Pause other targets because at least one target has paused.
|
|
// FIXME: Should this be done on the backend?
|
|
for (let [otherTarget, otherTargetData] of this._targetDebuggerDataMap)
|
|
otherTargetData.pauseIfNeeded();
|
|
|
|
let activeCallFrameDidChange = this._activeCallFrame && this._activeCallFrame.target === target;
|
|
if (activeCallFrameDidChange)
|
|
this._activeCallFrame = activeCallFrame;
|
|
else if (!wasPaused) {
|
|
this._activeCallFrame = activeCallFrame;
|
|
activeCallFrameDidChange = true;
|
|
}
|
|
|
|
if (!wasPaused)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.Paused);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.CallFramesDidChange, {target});
|
|
|
|
if (activeCallFrameDidChange)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
|
|
}
|
|
|
|
debuggerDidResume(target)
|
|
{
|
|
this._didResumeInternal(target);
|
|
}
|
|
|
|
playBreakpointActionSound(breakpointActionIdentifier)
|
|
{
|
|
InspectorFrontendHost.beep();
|
|
}
|
|
|
|
scriptDidParse(target, scriptIdentifier, url, startLine, startColumn, endLine, endColumn, isModule, isContentScript, sourceURL, sourceMapURL)
|
|
{
|
|
// Don't add the script again if it is already known.
|
|
let targetData = this.dataForTarget(target);
|
|
let existingScript = targetData.scriptForIdentifier(scriptIdentifier);
|
|
if (existingScript) {
|
|
console.assert(existingScript.url === (url || null));
|
|
console.assert(existingScript.range.startLine === startLine);
|
|
console.assert(existingScript.range.startColumn === startColumn);
|
|
console.assert(existingScript.range.endLine === endLine);
|
|
console.assert(existingScript.range.endColumn === endColumn);
|
|
return;
|
|
}
|
|
|
|
if (!WI.settings.engineeringShowInternalScripts.value && isWebKitInternalScript(sourceURL))
|
|
return;
|
|
|
|
let range = new WI.TextRange(startLine, startColumn, endLine, endColumn);
|
|
let sourceType = isModule ? WI.Script.SourceType.Module : WI.Script.SourceType.Program;
|
|
let script = new WI.Script(target, scriptIdentifier, range, url, sourceType, isContentScript, sourceURL, sourceMapURL);
|
|
|
|
targetData.addScript(script);
|
|
|
|
// FIXME: <https://webkit.org/b/164427> Web Inspector: WorkerTarget's mainResource should be a Resource not a Script
|
|
// We make the main resource of a WorkerTarget the Script instead of the Resource
|
|
// because the frontend may not be informed of the Resource. We should guarantee
|
|
// the frontend is informed of the Resource.
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) {
|
|
// A ServiceWorker starts with a LocalScript for the main resource but we can replace it during initialization.
|
|
if (target.mainResource instanceof WI.LocalScript) {
|
|
if (script.url === target.name)
|
|
target.mainResource = script;
|
|
}
|
|
} else if (!target.mainResource && target !== WI.mainTarget) {
|
|
// A Worker starts without a main resource and we insert one.
|
|
if (script.url === target.name) {
|
|
target.mainResource = script;
|
|
if (script.resource)
|
|
target.resourceCollection.remove(script.resource);
|
|
}
|
|
}
|
|
|
|
if (isWebKitInternalScript(script.sourceURL)) {
|
|
this._internalWebKitScripts.push(script);
|
|
if (!WI.settings.engineeringShowInternalScripts.value)
|
|
return;
|
|
}
|
|
|
|
if (!WI.settings.debugShowConsoleEvaluations.value && isWebInspectorConsoleEvaluationScript(script.sourceURL))
|
|
return;
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
|
|
|
|
if ((target !== WI.mainTarget || WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) && !script.isMainResource() && !script.resource)
|
|
target.addScript(script);
|
|
}
|
|
|
|
scriptDidFail(target, url, scriptSource)
|
|
{
|
|
const sourceURL = null;
|
|
const sourceType = WI.Script.SourceType.Program;
|
|
let script = new WI.LocalScript(target, url, sourceURL, sourceType, scriptSource);
|
|
|
|
// If there is already a resource we don't need to have the script anymore,
|
|
// we only need a script to use for parser error location links.
|
|
if (script.resource)
|
|
return;
|
|
|
|
let targetData = this.dataForTarget(target);
|
|
targetData.addScript(script);
|
|
target.addScript(script);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
|
|
}
|
|
|
|
didSampleProbe(target, sample)
|
|
{
|
|
console.assert(this._probesByIdentifier.has(sample.probeId), "Unknown probe identifier specified for sample: ", sample);
|
|
let probe = this._probesByIdentifier.get(sample.probeId);
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(sample.timestamp);
|
|
let object = WI.RemoteObject.fromPayload(sample.payload, target);
|
|
probe.addSample(new WI.ProbeSample(sample.sampleId, sample.batchId, elapsedTime, object));
|
|
}
|
|
|
|
// Private
|
|
|
|
_sourceCodeLocationFromPayload(target, payload)
|
|
{
|
|
let targetData = this.dataForTarget(target);
|
|
let script = targetData.scriptForIdentifier(payload.scriptId);
|
|
if (!script)
|
|
return null;
|
|
|
|
return script.createSourceCodeLocation(payload.lineNumber, payload.columnNumber);
|
|
}
|
|
|
|
_scopeChainFromPayload(target, payload)
|
|
{
|
|
let scopeChain = [];
|
|
for (let i = 0; i < payload.length; ++i)
|
|
scopeChain.push(this._scopeChainNodeFromPayload(target, payload[i]));
|
|
return scopeChain;
|
|
}
|
|
|
|
_scopeChainNodeFromPayload(target, payload)
|
|
{
|
|
var type = null;
|
|
switch (payload.type) {
|
|
case InspectorBackend.Enum.Debugger.ScopeType.Global:
|
|
type = WI.ScopeChainNode.Type.Global;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.With:
|
|
type = WI.ScopeChainNode.Type.With;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.Closure:
|
|
type = WI.ScopeChainNode.Type.Closure;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.Catch:
|
|
type = WI.ScopeChainNode.Type.Catch;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.FunctionName:
|
|
type = WI.ScopeChainNode.Type.FunctionName;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.NestedLexical:
|
|
type = WI.ScopeChainNode.Type.Block;
|
|
break;
|
|
case InspectorBackend.Enum.Debugger.ScopeType.GlobalLexicalEnvironment:
|
|
type = WI.ScopeChainNode.Type.GlobalLexicalEnvironment;
|
|
break;
|
|
default:
|
|
console.error("Unknown type: " + payload.type);
|
|
break;
|
|
}
|
|
|
|
let object = WI.RemoteObject.fromPayload(payload.object, target);
|
|
return new WI.ScopeChainNode(type, [object], payload.name, payload.location, payload.empty);
|
|
}
|
|
|
|
_setBreakpoint(breakpoint, specificTarget)
|
|
{
|
|
console.assert(!breakpoint.disabled);
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
if (!this._restoringBreakpoints && !this.breakpointsDisabledTemporarily) {
|
|
// Enable breakpoints since a breakpoint is being set. This eliminates
|
|
// a multi-step process for the user that can be confusing.
|
|
this.breakpointsEnabled = true;
|
|
}
|
|
|
|
function didSetBreakpoint(target, error, breakpointIdentifier, locations) {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._breakpointIdMap.set(breakpointIdentifier, breakpoint);
|
|
|
|
breakpoint.identifier = breakpointIdentifier;
|
|
|
|
// Debugger.setBreakpoint returns a single location.
|
|
if (!(locations instanceof Array))
|
|
locations = [locations];
|
|
|
|
for (let location of locations)
|
|
this.breakpointResolved(target, breakpointIdentifier, location);
|
|
}
|
|
|
|
// The breakpoint will be resolved again by calling DebuggerAgent, so mark it as unresolved.
|
|
// If something goes wrong it will stay unresolved and show up as such in the user interface.
|
|
// When setting for a new target, don't change the resolved target.
|
|
if (!specificTarget)
|
|
breakpoint.clearResolvedLocations();
|
|
|
|
if (breakpoint.contentIdentifier) {
|
|
let targets = specificTarget ? [specificTarget] : WI.targets;
|
|
for (let target of targets) {
|
|
target.DebuggerAgent.setBreakpointByUrl.invoke({
|
|
lineNumber: breakpoint.sourceCodeLocation.lineNumber,
|
|
url: breakpoint.contentIdentifier,
|
|
urlRegex: undefined,
|
|
columnNumber: breakpoint.sourceCodeLocation.columnNumber,
|
|
options: breakpoint.optionsToProtocol(),
|
|
}, didSetBreakpoint.bind(this, target));
|
|
}
|
|
} else if (breakpoint.scriptIdentifier) {
|
|
let target = breakpoint.target;
|
|
target.DebuggerAgent.setBreakpoint.invoke({
|
|
location: {scriptId: breakpoint.scriptIdentifier, lineNumber: breakpoint.sourceCodeLocation.lineNumber, columnNumber: breakpoint.sourceCodeLocation.columnNumber},
|
|
options: breakpoint.optionsToProtocol(),
|
|
}, didSetBreakpoint.bind(this, target));
|
|
} else
|
|
WI.reportInternalError("Unknown source for breakpoint.");
|
|
}
|
|
|
|
_removeBreakpoint(breakpoint, callback)
|
|
{
|
|
if (!breakpoint.identifier)
|
|
return;
|
|
|
|
function didRemoveBreakpoint(target, error)
|
|
{
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._breakpointIdMap.delete(breakpoint.identifier);
|
|
|
|
breakpoint.identifier = null;
|
|
|
|
// Don't reset resolved here since we want to keep disabled breakpoints looking like they
|
|
// are resolved in the user interface. They will get marked as unresolved in reset.
|
|
|
|
if (callback)
|
|
callback(target);
|
|
}
|
|
|
|
if (breakpoint.contentIdentifier) {
|
|
for (let target of WI.targets)
|
|
target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this, target));
|
|
} else if (breakpoint.scriptIdentifier) {
|
|
let target = breakpoint.target;
|
|
target.DebuggerAgent.removeBreakpoint(breakpoint.identifier, didRemoveBreakpoint.bind(this, target));
|
|
}
|
|
}
|
|
|
|
_setSymbolicBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(breakpoint instanceof WI.SymbolicBreakpoint, breakpoint);
|
|
console.assert(!breakpoint.disabled, breakpoint);
|
|
console.assert(WI.SymbolicBreakpoint.supported(target), target);
|
|
|
|
if (!this._restoringBreakpoints && !this.breakpointsDisabledTemporarily)
|
|
this.breakpointsEnabled = true;
|
|
|
|
target.DebuggerAgent.addSymbolicBreakpoint.invoke({
|
|
symbol: breakpoint.symbol,
|
|
caseSensitive: breakpoint.caseSensitive,
|
|
isRegex: breakpoint.isRegex,
|
|
options: breakpoint.optionsToProtocol(),
|
|
});
|
|
}
|
|
|
|
_removeSymbolicBreakpoint(breakpoint, target)
|
|
{
|
|
console.assert(breakpoint instanceof WI.SymbolicBreakpoint, breakpoint);
|
|
console.assert(WI.SymbolicBreakpoint.supported(target), target);
|
|
|
|
target.DebuggerAgent.removeSymbolicBreakpoint.invoke({
|
|
symbol: breakpoint.symbol,
|
|
caseSensitive: breakpoint.caseSensitive,
|
|
isRegex: breakpoint.isRegex,
|
|
});
|
|
}
|
|
|
|
_setPauseOnExceptions(target)
|
|
{
|
|
let commandArguments = {
|
|
state: "none",
|
|
};
|
|
|
|
if (this._breakpointsEnabledSetting.value) {
|
|
if (this._allExceptionsBreakpoint && !this._allExceptionsBreakpoint.disabled) {
|
|
commandArguments.state = "all";
|
|
commandArguments.options = this._allExceptionsBreakpoint.optionsToProtocol();
|
|
} else if (this._uncaughtExceptionsBreakpoint && !this._uncaughtExceptionsBreakpoint.disabled) {
|
|
commandArguments.state = "uncaught";
|
|
commandArguments.options = this._uncaughtExceptionsBreakpoint.optionsToProtocol();
|
|
}
|
|
|
|
// Mark the uncaught breakpoint as unresolved if "all" as it includes "uncaught".
|
|
// That way it is clear in the user interface that the breakpoint is ignored.
|
|
if (this._uncaughtExceptionsBreakpoint)
|
|
this._uncaughtExceptionsBreakpoint.resolved = commandArguments.state !== "all";
|
|
}
|
|
|
|
target.DebuggerAgent.setPauseOnExceptions.invoke(commandArguments);
|
|
}
|
|
|
|
_createSpecialBreakpoint(serializedBreakpoint = {})
|
|
{
|
|
let location = WI.SourceCodeLocation.specialBreakpointLocation;
|
|
serializedBreakpoint.lineNumber = location.lineNumber;
|
|
serializedBreakpoint.columnNumber = location.columnNumber;
|
|
|
|
serializedBreakpoint.resolved = true;
|
|
|
|
return WI.JavaScriptBreakpoint.fromJSON(serializedBreakpoint);
|
|
}
|
|
|
|
_updateSpecialBreakpoint(breakpoint, specificTarget)
|
|
{
|
|
console.assert(breakpoint.special, breakpoint);
|
|
|
|
if (!breakpoint.disabled && !this._restoringBreakpoints && !this.breakpointsDisabledTemporarily)
|
|
this.breakpointsEnabled = true;
|
|
|
|
let targets = specificTarget ? [specificTarget] : WI.targets;
|
|
|
|
let setting = null;
|
|
let command = null;
|
|
switch (breakpoint) {
|
|
case this._debuggerStatementsBreakpoint:
|
|
setting = this._debuggerStatementsBreakpointSetting;
|
|
command = "setPauseOnDebuggerStatements";
|
|
break;
|
|
|
|
case this._allExceptionsBreakpoint:
|
|
setting = this._allExceptionsBreakpointSetting;
|
|
break;
|
|
|
|
case this._uncaughtExceptionsBreakpoint:
|
|
setting = this._uncaughtExceptionsBreakpointSetting;
|
|
break;
|
|
|
|
case this._assertionFailuresBreakpoint:
|
|
setting = this._assertionFailuresBreakpointSetting;
|
|
command = "setPauseOnAssertions";
|
|
break;
|
|
|
|
case this._allMicrotasksBreakpoint:
|
|
setting = this._allMicrotasksBreakpointSetting;
|
|
command = "setPauseOnMicrotasks";
|
|
break;
|
|
}
|
|
console.assert(setting);
|
|
|
|
if (!specificTarget)
|
|
setting.value = breakpoint.toJSON();
|
|
|
|
if (command) {
|
|
let commandArguments = {
|
|
enabled: !breakpoint.disabled,
|
|
};
|
|
if (!breakpoint.disabled)
|
|
commandArguments.options = breakpoint.optionsToProtocol();
|
|
|
|
for (let target of targets)
|
|
target.DebuggerAgent[command].invoke(commandArguments);
|
|
} else {
|
|
console.assert(breakpoint === this._allExceptionsBreakpoint || breakpoint === this._uncaughtExceptionsBreakpoint, breakpoint);
|
|
for (let target of targets)
|
|
this._setPauseOnExceptions(target);
|
|
}
|
|
}
|
|
|
|
_setBlackboxBreakpointEvaluations(target)
|
|
{
|
|
// COMPATIBILITY (macOS 12.3, iOS 15.4): Debugger.setBlackboxBreakpointEvaluations did not exist yet.
|
|
if (target.hasCommand("Debugger.setBlackboxBreakpointEvaluations"))
|
|
target.DebuggerAgent.setBlackboxBreakpointEvaluations(WI.settings.blackboxBreakpointEvaluations.value);
|
|
}
|
|
|
|
_breakpointDisplayLocationDidChange(event)
|
|
{
|
|
if (this._ignoreBreakpointDisplayLocationDidChangeEvent)
|
|
return;
|
|
|
|
let breakpoint = event.target;
|
|
if (!breakpoint.identifier || breakpoint.disabled)
|
|
return;
|
|
|
|
// Remove the breakpoint with its old id.
|
|
this._removeBreakpoint(breakpoint, (target) => {
|
|
// Add the breakpoint at its new lineNumber and get a new id.
|
|
this._restoringBreakpoints = true;
|
|
this._setBreakpoint(breakpoint, target);
|
|
this._restoringBreakpoints = false;
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.BreakpointMoved, {breakpoint});
|
|
});
|
|
}
|
|
|
|
_breakpointDisabledStateDidChange(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (breakpoint.special) {
|
|
this._updateSpecialBreakpoint(breakpoint);
|
|
return;
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.breakpoints.putObject(breakpoint);
|
|
|
|
if (breakpoint.disabled)
|
|
this._removeBreakpoint(breakpoint);
|
|
else
|
|
this._setBreakpoint(breakpoint);
|
|
}
|
|
|
|
_breakpointEditablePropertyDidChange(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (breakpoint.special) {
|
|
this._restoringBreakpoints = true;
|
|
this._updateSpecialBreakpoint(breakpoint);
|
|
this._restoringBreakpoints = false;
|
|
return;
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.breakpoints.putObject(breakpoint);
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
console.assert(breakpoint.editable);
|
|
if (!breakpoint.editable)
|
|
return;
|
|
|
|
// Remove the breakpoint with its old id.
|
|
this._removeBreakpoint(breakpoint, (target) => {
|
|
// Add the breakpoint with its new properties and get a new id.
|
|
this._restoringBreakpoints = true;
|
|
this._setBreakpoint(breakpoint, target);
|
|
this._restoringBreakpoints = false;
|
|
});
|
|
}
|
|
|
|
_handleBreakpointActionsDidChange(event)
|
|
{
|
|
this._breakpointEditablePropertyDidChange(event);
|
|
|
|
this.updateProbesForBreakpoint(event.target);
|
|
}
|
|
|
|
_handleSymbolicBreakpointDisabledStateChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
for (let target of WI.targets) {
|
|
if (breakpoint.disabled)
|
|
this._removeSymbolicBreakpoint(breakpoint, target);
|
|
else
|
|
this._setSymbolicBreakpoint(breakpoint, target);
|
|
}
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.symbolicBreakpoints.putObject(breakpoint);
|
|
}
|
|
|
|
_handleSymbolicBreakpointEditablePropertyChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
if (!this._restoringBreakpoints)
|
|
WI.objectStores.symbolicBreakpoints.putObject(breakpoint);
|
|
|
|
if (breakpoint.disabled)
|
|
return;
|
|
|
|
this._restoringBreakpoints = true;
|
|
for (let target of WI.targets) {
|
|
// Clear the old breakpoint from the backend before setting the new one.
|
|
this._removeSymbolicBreakpoint(breakpoint, target);
|
|
this._setSymbolicBreakpoint(breakpoint, target);
|
|
}
|
|
this._restoringBreakpoints = false;
|
|
}
|
|
|
|
_handleSymbolicBreakpointActionsChanged(event)
|
|
{
|
|
let breakpoint = event.target;
|
|
|
|
this._handleSymbolicBreakpointEditablePropertyChanged(event);
|
|
|
|
this.updateProbesForBreakpoint(breakpoint);
|
|
}
|
|
|
|
_startDisablingBreakpointsTemporarily()
|
|
{
|
|
if (++this._temporarilyDisableBreakpointsRequestCount > 1)
|
|
return;
|
|
|
|
console.assert(!this.breakpointsDisabledTemporarily, "Already temporarily disabling breakpoints.");
|
|
if (this.breakpointsDisabledTemporarily)
|
|
return;
|
|
|
|
|
|
this._temporarilyDisabledBreakpointsRestoreSetting.value = this._breakpointsEnabledSetting.value;
|
|
|
|
this.breakpointsEnabled = false;
|
|
}
|
|
|
|
_stopDisablingBreakpointsTemporarily()
|
|
{
|
|
this._temporarilyDisableBreakpointsRequestCount = Math.max(0, this._temporarilyDisableBreakpointsRequestCount - 1);
|
|
if (this._temporarilyDisableBreakpointsRequestCount > 0)
|
|
return;
|
|
|
|
console.assert(this.breakpointsDisabledTemporarily, "Was not temporarily disabling breakpoints.");
|
|
if (!this.breakpointsDisabledTemporarily)
|
|
return;
|
|
|
|
let restoreState = this._temporarilyDisabledBreakpointsRestoreSetting.value;
|
|
this._temporarilyDisabledBreakpointsRestoreSetting.value = null;
|
|
|
|
this.breakpointsEnabled = restoreState;
|
|
}
|
|
|
|
_handleTimelineCapturingStateChanged(event)
|
|
{
|
|
switch (WI.timelineManager.capturingState) {
|
|
case WI.TimelineManager.CapturingState.Starting:
|
|
this._startDisablingBreakpointsTemporarily();
|
|
if (this.paused)
|
|
this.resume();
|
|
break;
|
|
|
|
case WI.TimelineManager.CapturingState.Inactive:
|
|
this._stopDisablingBreakpointsTemporarily();
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleAuditManagerTestScheduled(event)
|
|
{
|
|
this._startDisablingBreakpointsTemporarily();
|
|
|
|
if (this.paused)
|
|
this.resume();
|
|
}
|
|
|
|
_handleAuditManagerTestCompleted(event)
|
|
{
|
|
this._stopDisablingBreakpointsTemporarily();
|
|
}
|
|
|
|
_targetRemoved(event)
|
|
{
|
|
let wasPaused = this.paused;
|
|
|
|
this._targetDebuggerDataMap.delete(event.data.target);
|
|
|
|
if (!this.paused && wasPaused)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
|
|
}
|
|
|
|
_handleBlackboxBreakpointEvaluationsChange(event)
|
|
{
|
|
for (let target of WI.targets)
|
|
this._setBlackboxBreakpointEvaluations(target);
|
|
}
|
|
|
|
_handleEngineeringShowInternalScriptsSettingChanged(event)
|
|
{
|
|
let eventType = WI.settings.engineeringShowInternalScripts.value ? WI.DebuggerManager.Event.ScriptAdded : WI.DebuggerManager.Event.ScriptRemoved;
|
|
for (let script of this._internalWebKitScripts)
|
|
this.dispatchEventToListeners(eventType, {script});
|
|
}
|
|
|
|
_handleEngineeringPauseForInternalScriptsSettingChanged(event)
|
|
{
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Debugger.setPauseForInternalScripts"))
|
|
target.DebuggerAgent.setPauseForInternalScripts(WI.settings.engineeringPauseForInternalScripts.value);
|
|
}
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
this._didResumeInternal(WI.mainTarget);
|
|
}
|
|
|
|
_didResumeInternal(target)
|
|
{
|
|
if (!this.paused)
|
|
return;
|
|
|
|
if (this._delayedResumeTimeout) {
|
|
clearTimeout(this._delayedResumeTimeout);
|
|
this._delayedResumeTimeout = undefined;
|
|
}
|
|
|
|
let activeCallFrameDidChange = false;
|
|
if (this._activeCallFrame && this._activeCallFrame.target === target) {
|
|
this._activeCallFrame = null;
|
|
activeCallFrameDidChange = true;
|
|
}
|
|
|
|
this._blackboxedCallFrameGroupsToAutoExpand = [];
|
|
|
|
this.dataForTarget(target).updateForResume();
|
|
|
|
if (!this.paused)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.Resumed);
|
|
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.CallFramesDidChange, {target});
|
|
|
|
if (activeCallFrameDidChange)
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ActiveCallFrameDidChange);
|
|
}
|
|
|
|
_associateBreakpointsWithSourceCode(breakpoints, sourceCode)
|
|
{
|
|
this._ignoreBreakpointDisplayLocationDidChangeEvent = true;
|
|
|
|
for (let breakpoint of breakpoints) {
|
|
if (!breakpoint.sourceCodeLocation.sourceCode)
|
|
breakpoint.sourceCodeLocation.sourceCode = sourceCode;
|
|
// SourceCodes can be unequal if the SourceCodeLocation is associated with a Script and we are looking at the Resource.
|
|
console.assert(breakpoint.sourceCodeLocation.sourceCode === sourceCode || breakpoint.sourceCodeLocation.sourceCode.contentIdentifier === sourceCode.contentIdentifier);
|
|
}
|
|
|
|
this._ignoreBreakpointDisplayLocationDidChangeEvent = false;
|
|
}
|
|
|
|
_probeSetForBreakpoint(breakpoint)
|
|
{
|
|
let probeSet = this._probeSetsByBreakpoint.get(breakpoint);
|
|
if (!probeSet) {
|
|
probeSet = new WI.ProbeSet(breakpoint);
|
|
this._probeSetsByBreakpoint.set(breakpoint, probeSet);
|
|
this.dispatchEventToListeners(WI.DebuggerManager.Event.ProbeSetAdded, {probeSet});
|
|
}
|
|
return probeSet;
|
|
}
|
|
};
|
|
|
|
WI.DebuggerManager.Event = {
|
|
BreakpointAdded: "debugger-manager-breakpoint-added",
|
|
BreakpointRemoved: "debugger-manager-breakpoint-removed",
|
|
BreakpointMoved: "debugger-manager-breakpoint-moved",
|
|
SymbolicBreakpointAdded: "debugger-manager-symbolic-breakpoint-added",
|
|
SymbolicBreakpointRemoved: "debugger-manager-symbolic-breakpoint-removed",
|
|
WaitingToPause: "debugger-manager-waiting-to-pause",
|
|
Paused: "debugger-manager-paused",
|
|
Resumed: "debugger-manager-resumed",
|
|
CallFramesDidChange: "debugger-manager-call-frames-did-change",
|
|
ActiveCallFrameDidChange: "debugger-manager-active-call-frame-did-change",
|
|
ScriptAdded: "debugger-manager-script-added",
|
|
ScriptRemoved: "debugger-manager-script-removed",
|
|
ScriptsCleared: "debugger-manager-scripts-cleared",
|
|
BreakpointsEnabledDidChange: "debugger-manager-breakpoints-enabled-did-change",
|
|
ProbeSetAdded: "debugger-manager-probe-set-added",
|
|
ProbeSetRemoved: "debugger-manager-probe-set-removed",
|
|
BlackboxChanged: "blackboxed-urls-changed",
|
|
};
|
|
|
|
WI.DebuggerManager.PauseReason = {
|
|
AnimationFrame: "animation-frame",
|
|
Assertion: "assertion",
|
|
BlackboxedScript: "blackboxed-script",
|
|
Breakpoint: "breakpoint",
|
|
CSPViolation: "CSP-violation",
|
|
DebuggerStatement: "debugger-statement",
|
|
DOM: "DOM",
|
|
Exception: "exception",
|
|
FunctionCall: "function-call",
|
|
Interval: "interval",
|
|
Listener: "listener",
|
|
Microtask: "microtask",
|
|
PauseOnNextStatement: "pause-on-next-statement",
|
|
Timeout: "timeout",
|
|
URL: "url",
|
|
Other: "other",
|
|
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.EventBreakpointType.Timer was replaced by DOMDebugger.EventBreakpointType.Interval and DOMDebugger.EventBreakpointType.Timeout.
|
|
Timer: "timer",
|
|
|
|
// COMPATIBILITY (iOS 13): DOMDebugger.EventBreakpointType.EventListener was replaced by DOMDebugger.EventBreakpointType.Listener.
|
|
EventListener: "event-listener",
|
|
};
|
|
|
|
WI.DebuggerManager.BlackboxType = {
|
|
Pattern: "pattern",
|
|
URL: "url",
|
|
};
|
|
|
|
/* Controllers/DeviceSettingsManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2023 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DeviceSettingsManager = class DeviceSettingsManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._deviceUserAgent = "";
|
|
this._overridenDeviceUserAgent = null;
|
|
this._overridenDeviceScreenSize = null;
|
|
this._overridenDeviceSettings = new Map;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!target.hasDomain("Page"))
|
|
return
|
|
|
|
// COMPATIBILITY (iOS 12.2): Page.overrideUserAgent did not exist.
|
|
if (target.hasCommand("Page.overrideUserAgent") && this._overridenDeviceUserAgent)
|
|
target.PageAgent.overrideUserAgent(this._overridenDeviceUserAgent);
|
|
|
|
// COMPATIBILITY (iOS 12.2): Page.overrideSetting did not exist.
|
|
if (target.hasCommand("Page.overrideSetting")) {
|
|
for (let [setting, value] of this._overridenDeviceSettings)
|
|
target.PageAgent.overrideSetting(setting, value);
|
|
}
|
|
|
|
const objectGroup = "user-agent";
|
|
|
|
WI.runtimeManager.evaluateInInspectedWindow("globalThis.navigator?.userAgent", {objectGroup, returnByValue: true}, (remoteObject, wasThrown, result) => {
|
|
if (!wasThrown && result)
|
|
this._deviceUserAgent = result.value;
|
|
|
|
target.RuntimeAgent.releaseObjectGroup(objectGroup);
|
|
});
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsSetScreenSizeOverride()
|
|
{
|
|
return InspectorBackend.hasCommand("Page.setScreenSizeOverride");
|
|
}
|
|
|
|
// Public
|
|
|
|
get deviceUserAgent() { return this._deviceUserAgent; }
|
|
get overridenDeviceUserAgent() { return this._overridenDeviceUserAgent; }
|
|
get overridenDeviceScreenSize() { return this._overridenDeviceScreenSize; }
|
|
get overridenDeviceSettings() { return this._overridenDeviceSettings; }
|
|
|
|
get hasOverridenDefaultSettings()
|
|
{
|
|
return this._overridenDeviceUserAgent || this._overridenDeviceScreenSize || this._overridenDeviceSettings.size > 0;
|
|
}
|
|
|
|
overrideDeviceSetting(setting, value, callback)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Page.overrideSetting")) {
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
let commandArguments = {
|
|
setting,
|
|
};
|
|
|
|
let shouldOverride = !this._overridenDeviceSettings.has(setting);
|
|
if (shouldOverride)
|
|
commandArguments.value = value;
|
|
|
|
target.PageAgent.overrideSetting.invoke(commandArguments, (error) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
callback(false);
|
|
return;
|
|
}
|
|
|
|
if (shouldOverride)
|
|
this._overridenDeviceSettings.set(setting, value);
|
|
else
|
|
this._overridenDeviceSettings.delete(setting);
|
|
|
|
callback(shouldOverride);
|
|
this.dispatchEventToListeners(WI.DeviceSettingsManager.Event.SettingChanged);
|
|
});
|
|
}
|
|
|
|
overrideUserAgent(value, force)
|
|
{
|
|
if (value === this._overridenDeviceUserAgent)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Page.overrideUserAgent"))
|
|
return;
|
|
|
|
let commandArguments = {};
|
|
|
|
let shouldOverride = value && (value !== WI.DeviceSettingsManager.DefaultValue || force);
|
|
if (shouldOverride)
|
|
commandArguments.value = value;
|
|
|
|
target.PageAgent.overrideUserAgent.invoke(commandArguments, (error) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._overridenDeviceUserAgent = shouldOverride ? value : null;
|
|
|
|
this.dispatchEventToListeners(WI.DeviceSettingsManager.Event.SettingChanged);
|
|
target.PageAgent.reload();
|
|
});
|
|
}
|
|
|
|
overrideScreenSize(value, force)
|
|
{
|
|
if (value === this._overridenDeviceScreenSize)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Page.setScreenSizeOverride"))
|
|
return;
|
|
|
|
let commandArguments = {};
|
|
|
|
let shouldOverride = value && (value !== WI.DeviceSettingsManager.DefaultValue || force);
|
|
if (shouldOverride) {
|
|
let [width, height] = value.split("x");
|
|
width = parseInt(width);
|
|
height = parseInt(height);
|
|
|
|
if (isNaN(width) || isNaN(height))
|
|
shouldOverride = false;
|
|
else {
|
|
commandArguments.width = width;
|
|
commandArguments.height = height;
|
|
}
|
|
}
|
|
|
|
target.PageAgent.setScreenSizeOverride.invoke(commandArguments, (error) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this._overridenDeviceScreenSize = shouldOverride ? value : null;
|
|
|
|
this.dispatchEventToListeners(WI.DeviceSettingsManager.Event.SettingChanged);
|
|
target.PageAgent.reload();
|
|
});
|
|
}
|
|
};
|
|
|
|
WI.DeviceSettingsManager.Event = {
|
|
SettingChanged: "device-setting-changed",
|
|
};
|
|
|
|
WI.DeviceSettingsManager.DefaultValue = "default";
|
|
|
|
/* Controllers/FormatterSourceMap.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.FormatterSourceMap = class FormatterSourceMap extends WI.Object
|
|
{
|
|
constructor(originalLineEndings, formattedLineEndings, mapping)
|
|
{
|
|
super();
|
|
|
|
this._originalLineEndings = originalLineEndings;
|
|
this._formattedLineEndings = formattedLineEndings;
|
|
this._mapping = mapping;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromSourceMapData({originalLineEndings, formattedLineEndings, mapping})
|
|
{
|
|
return new WI.FormatterSourceMap(originalLineEndings, formattedLineEndings, mapping);
|
|
}
|
|
|
|
// Public
|
|
|
|
originalToFormatted(lineNumber, columnNumber)
|
|
{
|
|
var originalPosition = this._locationToPosition(this._originalLineEndings, lineNumber || 0, columnNumber || 0);
|
|
return this.originalPositionToFormatted(originalPosition);
|
|
}
|
|
|
|
originalPositionToFormatted(originalPosition)
|
|
{
|
|
var formattedPosition = this._convertPosition(this._mapping.original, this._mapping.formatted, originalPosition);
|
|
return this._positionToLocation(this._formattedLineEndings, formattedPosition);
|
|
}
|
|
|
|
originalPositionToFormattedPosition(originalPosition)
|
|
{
|
|
return this._convertPosition(this._mapping.original, this._mapping.formatted, originalPosition);
|
|
}
|
|
|
|
formattedToOriginal(lineNumber, columnNumber)
|
|
{
|
|
var originalPosition = this.formattedToOriginalOffset(lineNumber, columnNumber);
|
|
return this._positionToLocation(this._originalLineEndings, originalPosition);
|
|
}
|
|
|
|
formattedToOriginalOffset(lineNumber, columnNumber)
|
|
{
|
|
var formattedPosition = this._locationToPosition(this._formattedLineEndings, lineNumber || 0, columnNumber || 0);
|
|
var originalPosition = this._convertPosition(this._mapping.formatted, this._mapping.original, formattedPosition);
|
|
return originalPosition;
|
|
}
|
|
|
|
formattedPositionToOriginalPosition(formattedPosition)
|
|
{
|
|
return this._convertPosition(this._mapping.formatted, this._mapping.original, formattedPosition);
|
|
}
|
|
|
|
// Private
|
|
|
|
_locationToPosition(lineEndings, lineNumber, columnNumber)
|
|
{
|
|
var lineOffset = lineNumber ? lineEndings[lineNumber - 1] + 1 : 0;
|
|
return lineOffset + columnNumber;
|
|
}
|
|
|
|
_positionToLocation(lineEndings, position)
|
|
{
|
|
var lineNumber = lineEndings.upperBound(position - 1);
|
|
if (!lineNumber)
|
|
var columnNumber = position;
|
|
else
|
|
var columnNumber = position - lineEndings[lineNumber - 1] - 1;
|
|
return {lineNumber, columnNumber};
|
|
}
|
|
|
|
_convertPosition(positions1, positions2, positionInPosition1)
|
|
{
|
|
var index = positions1.upperBound(positionInPosition1) - 1;
|
|
var convertedPosition = positions2[index] + positionInPosition1 - positions1[index];
|
|
if (index < positions2.length - 1 && convertedPosition > positions2[index + 1])
|
|
convertedPosition = positions2[index + 1];
|
|
return convertedPosition;
|
|
}
|
|
};
|
|
|
|
/* Controllers/HARBuilder.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017-2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// HTTP Archive (HAR) format - Version 1.2
|
|
// https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html#sec-har-object-types-creator
|
|
// http://www.softwareishard.com/blog/har-12-spec/
|
|
|
|
WI.HARBuilder = class HARBuilder
|
|
{
|
|
static async buildArchive(resources)
|
|
{
|
|
let promises = [];
|
|
for (let resource of resources) {
|
|
console.assert(resource.finished);
|
|
promises.push(new Promise((resolve, reject) => {
|
|
// Always resolve.
|
|
resource.requestContent().then(
|
|
(x) => resolve(x),
|
|
() => resolve(null)
|
|
);
|
|
}));
|
|
}
|
|
|
|
let contents = await Promise.all(promises);
|
|
console.assert(contents.length === resources.length);
|
|
|
|
return {
|
|
log: {
|
|
version: "1.2",
|
|
creator: HARBuilder.creator(),
|
|
pages: HARBuilder.pages(),
|
|
entries: resources.map((resource, index) => HARBuilder.entry(resource, contents[index])),
|
|
}
|
|
};
|
|
}
|
|
|
|
static creator()
|
|
{
|
|
return {
|
|
name: "WebKit Web Inspector",
|
|
version: "1.0",
|
|
};
|
|
}
|
|
|
|
static pages()
|
|
{
|
|
return [{
|
|
startedDateTime: HARBuilder.date(WI.networkManager.mainFrame.mainResource.requestSentDate),
|
|
id: "page_0",
|
|
title: WI.networkManager.mainFrame.url || "",
|
|
pageTimings: HARBuilder.pageTimings(),
|
|
}];
|
|
}
|
|
|
|
static pageTimings()
|
|
{
|
|
let result = {};
|
|
|
|
let domContentReadyEventTimestamp = WI.networkManager.mainFrame.domContentReadyEventTimestamp;
|
|
if (!isNaN(domContentReadyEventTimestamp))
|
|
result.onContentLoad = domContentReadyEventTimestamp * 1000;
|
|
|
|
let loadEventTimestamp = WI.networkManager.mainFrame.loadEventTimestamp;
|
|
if (!isNaN(loadEventTimestamp))
|
|
result.onLoad = loadEventTimestamp * 1000;
|
|
|
|
return result;
|
|
}
|
|
|
|
static entry(resource, content)
|
|
{
|
|
let entry = {
|
|
pageref: "page_0",
|
|
startedDateTime: HARBuilder.date(resource.requestSentDate),
|
|
time: 0,
|
|
request: HARBuilder.request(resource),
|
|
response: HARBuilder.response(resource, content),
|
|
cache: HARBuilder.cache(resource),
|
|
timings: HARBuilder.timings(resource),
|
|
};
|
|
|
|
if (resource.timingData.startTime && resource.timingData.responseEnd)
|
|
entry.time = (resource.timingData.responseEnd - resource.timingData.startTime) * 1000;
|
|
if (resource.remoteAddress) {
|
|
entry.serverIPAddress = HARBuilder.ipAddress(resource.remoteAddress);
|
|
|
|
// WebKit Custom Field `_serverPort`.
|
|
if (entry.serverIPAddress)
|
|
entry._serverPort = HARBuilder.port(resource.remoteAddress);
|
|
}
|
|
if (resource.connectionIdentifier)
|
|
entry.connection = "" + resource.connectionIdentifier;
|
|
|
|
// CFNetwork Custom Field `_fetchType`.
|
|
if (resource.responseSource !== WI.Resource.ResponseSource.Unknown)
|
|
entry._fetchType = HARBuilder.fetchType(resource.responseSource);
|
|
|
|
// WebKit Custom Field `_priority`.
|
|
if (resource.priority !== WI.Resource.NetworkPriority.Unknown)
|
|
entry._priority = HARBuilder.priority(resource.priority);
|
|
|
|
return entry;
|
|
}
|
|
|
|
static request(resource)
|
|
{
|
|
let result = {
|
|
method: resource.requestMethod || "",
|
|
url: resource.url || "",
|
|
httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
|
|
cookies: HARBuilder.cookies(resource.requestCookies, null),
|
|
headers: HARBuilder.headers(resource.requestHeaders),
|
|
queryString: resource.queryStringParameters || [],
|
|
headersSize: !isNaN(resource.requestHeadersTransferSize) ? resource.requestHeadersTransferSize : -1,
|
|
bodySize: !isNaN(resource.requestBodyTransferSize) ? resource.requestBodyTransferSize : -1,
|
|
};
|
|
|
|
if (resource.requestData)
|
|
result.postData = HARBuilder.postData(resource);
|
|
|
|
return result;
|
|
}
|
|
|
|
static response(resource, content)
|
|
{
|
|
let result = {
|
|
status: resource.statusCode || 0,
|
|
statusText: resource.statusText || "",
|
|
httpVersion: WI.Resource.displayNameForProtocol(resource.protocol) || "",
|
|
cookies: HARBuilder.cookies(resource.responseCookies, resource.requestSentDate),
|
|
headers: HARBuilder.headers(resource.responseHeaders),
|
|
content: HARBuilder.content(resource, content),
|
|
redirectURL: resource.responseHeaders.valueForCaseInsensitiveKey("Location") || "",
|
|
headersSize: !isNaN(resource.responseHeadersTransferSize) ? resource.responseHeadersTransferSize : -1,
|
|
bodySize: !isNaN(resource.responseBodyTransferSize) ? resource.responseBodyTransferSize : -1,
|
|
};
|
|
|
|
// Chrome Custom Field `_transferSize`.
|
|
if (!isNaN(resource.networkTotalTransferSize))
|
|
result._transferSize = resource.networkTotalTransferSize;
|
|
|
|
// Chrome Custom Field `_error`.
|
|
if (resource.failureReasonText)
|
|
result._error = resource.failureReasonText;
|
|
|
|
return result;
|
|
}
|
|
|
|
static cookies(cookies, requestSentDate)
|
|
{
|
|
let result = [];
|
|
|
|
for (let cookie of cookies) {
|
|
let json = {
|
|
name: cookie.name,
|
|
value: cookie.value,
|
|
};
|
|
|
|
if (cookie.type === WI.Cookie.Type.Response) {
|
|
if (cookie.path)
|
|
json.path = cookie.path;
|
|
if (cookie.domain)
|
|
json.domain = cookie.domain;
|
|
json.expires = HARBuilder.date(cookie.expirationDate(requestSentDate));
|
|
json.httpOnly = cookie.httpOnly;
|
|
json.secure = cookie.secure;
|
|
if (cookie.sameSite !== WI.Cookie.SameSiteType.None)
|
|
json.sameSite = cookie.sameSite;
|
|
}
|
|
|
|
result.push(json);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static headers(headers)
|
|
{
|
|
let result = [];
|
|
|
|
for (let key in headers)
|
|
result.push({name: key, value: headers[key]});
|
|
|
|
return result;
|
|
}
|
|
|
|
static content(resource, content)
|
|
{
|
|
let encodedSize = !isNaN(resource.networkEncodedSize) ? resource.networkEncodedSize : resource.estimatedNetworkEncodedSize;
|
|
let decodedSize = !isNaN(resource.networkDecodedSize) ? resource.networkDecodedSize : resource.size;
|
|
|
|
if (isNaN(decodedSize))
|
|
decodedSize = 0;
|
|
if (isNaN(encodedSize))
|
|
encodedSize = 0;
|
|
|
|
let result = {
|
|
size: decodedSize,
|
|
compression: decodedSize - encodedSize,
|
|
mimeType: resource.mimeType || "x-unknown",
|
|
};
|
|
|
|
if (content) {
|
|
if (content.rawContent)
|
|
result.text = content.rawContent;
|
|
if (content.rawBase64Encoded)
|
|
result.encoding = "base64";
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
static postData(resource)
|
|
{
|
|
return {
|
|
mimeType: resource.requestDataContentType || "",
|
|
text: resource.requestData,
|
|
params: resource.requestFormParameters || [],
|
|
};
|
|
}
|
|
|
|
static cache(resource)
|
|
{
|
|
// FIXME: <https://webkit.org/b/178682> Web Inspector: Include <cache> details in HAR Export
|
|
// http://www.softwareishard.com/blog/har-12-spec/#cache
|
|
return {};
|
|
}
|
|
|
|
static timings(resource)
|
|
{
|
|
// FIXME: <https://webkit.org/b/195694> Web Inspector: HAR Extension for Redirect Timing Info
|
|
// Chrome has Custom Fields `_blocked_queueing` and `_blocked_proxy`.
|
|
|
|
let result = {
|
|
blocked: -1,
|
|
dns: -1,
|
|
connect: -1,
|
|
ssl: -1,
|
|
send: 0,
|
|
wait: 0,
|
|
receive: 0,
|
|
};
|
|
|
|
if (resource.timingData.startTime && resource.timingData.responseEnd) {
|
|
let {startTime, domainLookupStart, domainLookupEnd, connectStart, connectEnd, secureConnectionStart, requestStart, responseStart, responseEnd} = resource.timingData;
|
|
result.blocked = ((domainLookupStart || connectStart || requestStart) - startTime) * 1000;
|
|
if (domainLookupStart)
|
|
result.dns = ((domainLookupEnd || connectStart || requestStart) - domainLookupStart) * 1000;
|
|
if (connectStart)
|
|
result.connect = ((connectEnd || requestStart) - connectStart) * 1000;
|
|
if (secureConnectionStart)
|
|
result.ssl = ((connectEnd || requestStart) - secureConnectionStart) * 1000;
|
|
|
|
// If all the time before requestStart was included in blocked, then make send time zero
|
|
// as send time is essentially just blocked time after dns / connection time, and we
|
|
// do not want to double count it.
|
|
result.send = (domainLookupEnd || connectEnd) ? (requestStart - (connectEnd || domainLookupEnd)) * 1000 : 0;
|
|
|
|
result.wait = (responseStart - requestStart) * 1000;
|
|
result.receive = (responseEnd - responseStart) * 1000;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
// Helpers
|
|
|
|
static ipAddress(remoteAddress)
|
|
{
|
|
// IP Address, without port.
|
|
if (!remoteAddress)
|
|
return "";
|
|
|
|
// NOTE: Resource.remoteAddress always includes the port at the end.
|
|
// So this always strips the last part.
|
|
return remoteAddress.replace(/:\d+$/, "");
|
|
}
|
|
|
|
static port(remoteAddress)
|
|
{
|
|
// IP Address, without port.
|
|
if (!remoteAddress)
|
|
return undefined;
|
|
|
|
// NOTE: Resource.remoteAddress always includes the port at the end.
|
|
// So this always matches the last part.
|
|
let index = remoteAddress.lastIndexOf(":");
|
|
if (!index)
|
|
return undefined;
|
|
|
|
let portString = remoteAddress.substr(index + 1);
|
|
let port = parseInt(portString);
|
|
if (isNaN(port))
|
|
return undefined;
|
|
|
|
return port;
|
|
}
|
|
|
|
static date(date)
|
|
{
|
|
// ISO 8601
|
|
if (!date)
|
|
return "";
|
|
|
|
return date.toISOString();
|
|
}
|
|
|
|
static fetchType(responseSource)
|
|
{
|
|
switch (responseSource) {
|
|
case WI.Resource.ResponseSource.Network:
|
|
return "Network Load";
|
|
case WI.Resource.ResponseSource.MemoryCache:
|
|
return "Memory Cache";
|
|
case WI.Resource.ResponseSource.DiskCache:
|
|
return "Disk Cache";
|
|
case WI.Resource.ResponseSource.ServiceWorker:
|
|
return "Service Worker";
|
|
case WI.Resource.ResponseSource.InspectorOverride:
|
|
return "Inspector Override";
|
|
}
|
|
|
|
console.assert();
|
|
return undefined;
|
|
}
|
|
|
|
static priority(priority)
|
|
{
|
|
switch (priority) {
|
|
case WI.Resource.NetworkPriority.Low:
|
|
return "low";
|
|
case WI.Resource.NetworkPriority.Medium:
|
|
return "medium";
|
|
case WI.Resource.NetworkPriority.High:
|
|
return "high";
|
|
}
|
|
|
|
console.assert();
|
|
return undefined;
|
|
}
|
|
|
|
// Consuming.
|
|
|
|
static dateFromHARDate(isoString)
|
|
{
|
|
return Date.parse(isoString);
|
|
}
|
|
|
|
static protocolFromHARProtocol(protocol)
|
|
{
|
|
switch (protocol) {
|
|
case "HTTP/2":
|
|
return "h2";
|
|
case "HTTP/1.0":
|
|
return "http/1.0";
|
|
case "HTTP/1.1":
|
|
return "http/1.1";
|
|
case "SPDY/2":
|
|
return "spdy/2";
|
|
case "SPDY/3":
|
|
return "spdy/3";
|
|
case "SPDY/3.1":
|
|
return "spdy/3.1";
|
|
}
|
|
|
|
if (protocol)
|
|
console.warn("Unknown HAR protocol value", protocol);
|
|
return null;
|
|
}
|
|
|
|
static responseSourceFromHARFetchType(fetchType)
|
|
{
|
|
switch (fetchType) {
|
|
case "Network Load":
|
|
return WI.Resource.ResponseSource.Network;
|
|
case "Memory Cache":
|
|
return WI.Resource.ResponseSource.MemoryCache;
|
|
case "Disk Cache":
|
|
return WI.Resource.ResponseSource.DiskCache;
|
|
case "Service Worker":
|
|
return WI.Resource.ResponseSource.ServiceWorker;
|
|
case "Inspector Override":
|
|
return WI.Resource.ResponseSource.InspectorOverride;
|
|
}
|
|
|
|
if (fetchType)
|
|
console.warn("Unknown HAR _fetchType value", fetchType);
|
|
return WI.Resource.ResponseSource.Other;
|
|
}
|
|
|
|
static networkPriorityFromHARPriority(priority)
|
|
{
|
|
switch (priority) {
|
|
case "low":
|
|
return WI.Resource.NetworkPriority.Low;
|
|
case "medium":
|
|
return WI.Resource.NetworkPriority.Medium;
|
|
case "high":
|
|
return WI.Resource.NetworkPriority.High;
|
|
}
|
|
|
|
if (priority)
|
|
console.warn("Unknown HAR priority value", priority);
|
|
return WI.Resource.NetworkPriority.Unknown;
|
|
}
|
|
};
|
|
|
|
/* Controllers/HeapManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: HeapManager lacks advanced multi-target support. (Instruments/Profilers per-target)
|
|
|
|
WI.HeapManager = class HeapManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Heap"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Heap");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("Heap"))
|
|
target.HeapAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
enable()
|
|
{
|
|
if (this._enabled)
|
|
return;
|
|
|
|
this._enabled = true;
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("Heap"))
|
|
target.HeapAgent.disable();
|
|
}
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
snapshot(callback)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.HeapAgent.snapshot((error, timestamp, snapshotStringData) => {
|
|
if (error)
|
|
console.error(error);
|
|
callback(error, timestamp, snapshotStringData);
|
|
});
|
|
}
|
|
|
|
getPreview(node, callback)
|
|
{
|
|
console.assert(this._enabled);
|
|
console.assert(node instanceof WI.HeapSnapshotNodeProxy);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.HeapAgent.getPreview(node.id, (error, string, functionDetails, preview) => {
|
|
if (error)
|
|
console.error(error);
|
|
callback(error, string, functionDetails, preview);
|
|
});
|
|
}
|
|
|
|
getRemoteObject(node, objectGroup, callback)
|
|
{
|
|
console.assert(this._enabled);
|
|
console.assert(node instanceof WI.HeapSnapshotNodeProxy);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.HeapAgent.getRemoteObject(node.id, objectGroup, (error, result) => {
|
|
if (error)
|
|
console.error(error);
|
|
callback(error, result);
|
|
});
|
|
}
|
|
|
|
// HeapObserver
|
|
|
|
garbageCollected(target, payload)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
// FIXME: <https://webkit.org/b/167323> Web Inspector: Enable Memory profiling in Workers
|
|
if (target !== WI.mainTarget)
|
|
return;
|
|
|
|
let collection = WI.GarbageCollection.fromPayload(payload);
|
|
this.dispatchEventToListeners(WI.HeapManager.Event.GarbageCollected, {collection});
|
|
}
|
|
};
|
|
|
|
WI.HeapManager.Event = {
|
|
GarbageCollected: "heap-manager-garbage-collected"
|
|
};
|
|
|
|
/* Controllers/IndexedDBManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2013 Samsung Electronics. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: IndexedDBManager lacks advanced multi-target support. (IndexedDatabase per-target)
|
|
|
|
WI.IndexedDBManager = class IndexedDBManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
this._requestedSecurityOrigins = new Set;
|
|
|
|
this._reset();
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["IndexedDB"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "IndexedDB");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("IndexedDB"))
|
|
target.IndexedDBAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
get indexedDatabases() { return this._indexedDatabases; }
|
|
|
|
enable()
|
|
{
|
|
console.assert(!this._enabled);
|
|
|
|
this._enabled = true;
|
|
|
|
this._reset();
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.SecurityOriginDidChange, this._securityOriginDidChange, this);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
this._enabled = false;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("IndexedDB"))
|
|
target.IndexedDBAgent.disable();
|
|
}
|
|
|
|
WI.Frame.removeEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
WI.Frame.removeEventListener(WI.Frame.Event.SecurityOriginDidChange, this._securityOriginDidChange, this);
|
|
|
|
this._reset();
|
|
}
|
|
|
|
requestIndexedDatabaseData(objectStore, objectStoreIndex, startEntryIndex, maximumEntryCount, callback)
|
|
{
|
|
console.assert(this._enabled);
|
|
console.assert(InspectorBackend.hasDomain("IndexedDB"));
|
|
console.assert(objectStore);
|
|
console.assert(callback);
|
|
|
|
function processData(error, entryPayloads, moreAvailable)
|
|
{
|
|
if (error) {
|
|
callback(null, false);
|
|
return;
|
|
}
|
|
|
|
var entries = [];
|
|
|
|
for (var entryPayload of entryPayloads) {
|
|
var entry = {};
|
|
entry.primaryKey = WI.RemoteObject.fromPayload(entryPayload.primaryKey);
|
|
entry.key = WI.RemoteObject.fromPayload(entryPayload.key);
|
|
entry.value = WI.RemoteObject.fromPayload(entryPayload.value);
|
|
entries.push(entry);
|
|
}
|
|
|
|
callback(entries, moreAvailable);
|
|
}
|
|
|
|
var requestArguments = {
|
|
securityOrigin: objectStore.parentDatabase.securityOrigin,
|
|
databaseName: objectStore.parentDatabase.name,
|
|
objectStoreName: objectStore.name,
|
|
indexName: objectStoreIndex && objectStoreIndex.name || "",
|
|
skipCount: startEntryIndex || 0,
|
|
pageSize: maximumEntryCount || 100
|
|
};
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.IndexedDBAgent.requestData.invoke(requestArguments, processData);
|
|
}
|
|
|
|
clearObjectStore(objectStore)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let securityOrigin = objectStore.parentDatabase.securityOrigin;
|
|
let databaseName = objectStore.parentDatabase.name;
|
|
let objectStoreName = objectStore.name;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.IndexedDBAgent.clearObjectStore(securityOrigin, databaseName, objectStoreName);
|
|
}
|
|
|
|
// Private
|
|
|
|
_reset()
|
|
{
|
|
this._indexedDatabases = [];
|
|
this._requestedSecurityOrigins.clear();
|
|
this.dispatchEventToListeners(WI.IndexedDBManager.Event.Cleared);
|
|
|
|
let mainFrame = WI.networkManager.mainFrame;
|
|
if (mainFrame)
|
|
this._addIndexedDBDatabasesIfNeeded(mainFrame);
|
|
}
|
|
|
|
_addIndexedDBDatabasesIfNeeded(frame)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasDomain("IndexedDB"))
|
|
return;
|
|
|
|
var securityOrigin = frame.securityOrigin;
|
|
|
|
// Don't show storage if we don't have a security origin (about:blank).
|
|
if (!securityOrigin || securityOrigin === "://")
|
|
return;
|
|
|
|
if (this._requestedSecurityOrigins.has(securityOrigin))
|
|
return;
|
|
|
|
this._requestedSecurityOrigins.add(securityOrigin);
|
|
|
|
function processDatabaseNames(error, names)
|
|
{
|
|
if (error || !names)
|
|
return;
|
|
|
|
for (var name of names)
|
|
target.IndexedDBAgent.requestDatabase(securityOrigin, name, processDatabase.bind(this));
|
|
}
|
|
|
|
function processDatabase(error, databasePayload)
|
|
{
|
|
if (error || !databasePayload)
|
|
return;
|
|
|
|
var objectStores = databasePayload.objectStores.map(processObjectStore);
|
|
var indexedDatabase = new WI.IndexedDatabase(databasePayload.name, securityOrigin, databasePayload.version, objectStores);
|
|
|
|
this._indexedDatabases.push(indexedDatabase);
|
|
this.dispatchEventToListeners(WI.IndexedDBManager.Event.IndexedDatabaseWasAdded, {indexedDatabase});
|
|
}
|
|
|
|
function processKeyPath(keyPathPayload)
|
|
{
|
|
switch (keyPathPayload.type) {
|
|
case InspectorBackend.Enum.IndexedDB.KeyPathType.Null:
|
|
return null;
|
|
case InspectorBackend.Enum.IndexedDB.KeyPathType.String:
|
|
return keyPathPayload.string;
|
|
case InspectorBackend.Enum.IndexedDB.KeyPathType.Array:
|
|
return keyPathPayload.array;
|
|
default:
|
|
console.error("Unknown KeyPath type:", keyPathPayload.type);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function processObjectStore(objectStorePayload)
|
|
{
|
|
var keyPath = processKeyPath(objectStorePayload.keyPath);
|
|
var indexes = objectStorePayload.indexes.map(processObjectStoreIndex);
|
|
return new WI.IndexedDatabaseObjectStore(objectStorePayload.name, keyPath, objectStorePayload.autoIncrement, indexes);
|
|
}
|
|
|
|
function processObjectStoreIndex(objectStoreIndexPayload)
|
|
{
|
|
var keyPath = processKeyPath(objectStoreIndexPayload.keyPath);
|
|
return new WI.IndexedDatabaseObjectStoreIndex(objectStoreIndexPayload.name, keyPath, objectStoreIndexPayload.unique, objectStoreIndexPayload.multiEntry);
|
|
}
|
|
|
|
target.IndexedDBAgent.requestDatabaseNames(securityOrigin, processDatabaseNames.bind(this));
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
if (event.target.isMainFrame())
|
|
this._reset();
|
|
}
|
|
|
|
_securityOriginDidChange(event)
|
|
{
|
|
console.assert(event.target instanceof WI.Frame);
|
|
|
|
this._addIndexedDBDatabasesIfNeeded(event.target);
|
|
}
|
|
};
|
|
|
|
WI.IndexedDBManager.Event = {
|
|
IndexedDatabaseWasAdded: "indexed-db-manager-indexed-database-was-added",
|
|
Cleared: "indexed-db-manager-cleared",
|
|
};
|
|
|
|
/* Controllers/LayerTreeManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: LayerTreeManager lacks advanced multi-target support. (Layers per-target)
|
|
|
|
WI.LayerTreeManager = class LayerTreeManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._showPaintRects = false;
|
|
this._compositingBordersVisible = false;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (target.hasDomain("LayerTree"))
|
|
target.LayerTreeAgent.enable();
|
|
|
|
if (target.hasDomain("Page")) {
|
|
if (target.hasCommand("Page.setShowPaintRects") && this._showPaintRects)
|
|
target.PageAgent.setShowPaintRects(this._showPaintRects);
|
|
|
|
if (this._compositingBordersVisible) {
|
|
// COMPATIBILITY(iOS 13.1): Page.setCompositingBordersVisible was replaced by Page.Setting.ShowDebugBorders and Page.Setting.ShowRepaintCounter.
|
|
if (target.hasCommand("Page.overrideSetting") && InspectorBackend.Enum.Page.Setting.ShowDebugBorders && InspectorBackend.Enum.Page.Setting.ShowRepaintCounter) {
|
|
target.PageAgent.overrideSetting(InspectorBackend.Enum.Page.Setting.ShowDebugBorders, this._compositingBordersVisible);
|
|
target.PageAgent.overrideSetting(InspectorBackend.Enum.Page.Setting.ShowRepaintCounter, this._compositingBordersVisible);
|
|
} else if (target.hasCommand("Page.setCompositingBordersVisible"))
|
|
target.PageAgent.setCompositingBordersVisible(this._compositingBordersVisible);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsShowingPaintRects()
|
|
{
|
|
return InspectorBackend.hasCommand("Page.setShowPaintRects");
|
|
}
|
|
|
|
static supportsVisibleCompositingBorders()
|
|
{
|
|
return InspectorBackend.hasCommand("Page.setCompositingBordersVisible")
|
|
|| (InspectorBackend.hasCommand("Page.overrideSetting") && InspectorBackend.Enum.Page.Setting.ShowDebugBorders && InspectorBackend.Enum.Page.Setting.ShowRepaintCounter);
|
|
}
|
|
|
|
// Public
|
|
|
|
get supported()
|
|
{
|
|
return InspectorBackend.hasDomain("LayerTree");
|
|
}
|
|
|
|
get showPaintRects()
|
|
{
|
|
return this._showPaintRects;
|
|
}
|
|
|
|
set showPaintRects(showPaintRects)
|
|
{
|
|
if (this._showPaintRects === showPaintRects)
|
|
return;
|
|
|
|
this._showPaintRects = showPaintRects;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Page.setShowPaintRects"))
|
|
target.PageAgent.setShowPaintRects(this._showPaintRects);
|
|
}
|
|
|
|
this.dispatchEventToListeners(LayerTreeManager.Event.ShowPaintRectsChanged);
|
|
}
|
|
|
|
get compositingBordersVisible()
|
|
{
|
|
return this._compositingBordersVisible;
|
|
}
|
|
|
|
set compositingBordersVisible(compositingBordersVisible)
|
|
{
|
|
if (this._compositingBordersVisible === compositingBordersVisible)
|
|
return;
|
|
|
|
this._compositingBordersVisible = compositingBordersVisible;
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY(iOS 13.1): Page.setCompositingBordersVisible was replaced by Page.Setting.ShowDebugBorders and Page.Setting.ShowRepaintCounter.
|
|
if (target.hasCommand("Page.overrideSetting") && InspectorBackend.Enum.Page.Setting.ShowDebugBorders && InspectorBackend.Enum.Page.Setting.ShowRepaintCounter) {
|
|
target.PageAgent.overrideSetting(InspectorBackend.Enum.Page.Setting.ShowDebugBorders, this._compositingBordersVisible);
|
|
target.PageAgent.overrideSetting(InspectorBackend.Enum.Page.Setting.ShowRepaintCounter, this._compositingBordersVisible);
|
|
} else if (target.hasCommand("Page.setCompositingBordersVisible"))
|
|
target.PageAgent.setCompositingBordersVisible(this._compositingBordersVisible);
|
|
}
|
|
|
|
this.dispatchEventToListeners(LayerTreeManager.Event.CompositingBordersVisibleChanged);
|
|
}
|
|
|
|
updateCompositingBordersVisibleFromPageIfNeeded()
|
|
{
|
|
if (!WI.targetsAvailable()) {
|
|
WI.whenTargetsAvailable().then(() => {
|
|
this.updateCompositingBordersVisibleFromPageIfNeeded();
|
|
});
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY(iOS 13.1): Page.getCompositingBordersVisible was replaced by Page.Setting.ShowDebugBorders and Page.Setting.ShowRepaintCounter.
|
|
if (!target.hasCommand("Page.getCompositingBordersVisible"))
|
|
return;
|
|
|
|
target.PageAgent.getCompositingBordersVisible((error, compositingBordersVisible) => {
|
|
if (error) {
|
|
WI.reportInternalError(error);
|
|
return;
|
|
}
|
|
|
|
this.compositingBordersVisible = compositingBordersVisible;
|
|
});
|
|
}
|
|
|
|
layerTreeMutations(previousLayers, newLayers)
|
|
{
|
|
console.assert(this.supported);
|
|
|
|
if (isEmptyObject(previousLayers))
|
|
return {preserved: [], additions: newLayers, removals: []};
|
|
|
|
let previousLayerIds = new Set;
|
|
let newLayerIds = new Set;
|
|
|
|
let preserved = [];
|
|
let additions = [];
|
|
|
|
for (let layer of previousLayers)
|
|
previousLayerIds.add(layer.layerId);
|
|
|
|
for (let layer of newLayers) {
|
|
newLayerIds.add(layer.layerId);
|
|
|
|
if (previousLayerIds.has(layer.layerId))
|
|
preserved.push(layer);
|
|
else
|
|
additions.push(layer);
|
|
}
|
|
|
|
let removals = previousLayers.filter((layer) => !newLayerIds.has(layer.layerId));
|
|
|
|
return {preserved, additions, removals};
|
|
}
|
|
|
|
layersForNode(node, callback)
|
|
{
|
|
console.assert(this.supported);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.LayerTreeAgent.layersForNode(node.id, (error, layers) => {
|
|
callback(error ? [] : layers.map(WI.Layer.fromPayload));
|
|
});
|
|
}
|
|
|
|
reasonsForCompositingLayer(layer, callback)
|
|
{
|
|
console.assert(this.supported);
|
|
|
|
let target = WI.assumingMainTarget();
|
|
target.LayerTreeAgent.reasonsForCompositingLayer(layer.layerId, function(error, reasons) {
|
|
callback(error ? 0 : reasons);
|
|
});
|
|
}
|
|
|
|
// LayerTreeObserver
|
|
|
|
layerTreeDidChange()
|
|
{
|
|
this.dispatchEventToListeners(WI.LayerTreeManager.Event.LayerTreeDidChange);
|
|
}
|
|
};
|
|
|
|
WI.LayerTreeManager.Event = {
|
|
ShowPaintRectsChanged: "show-paint-rects-changed",
|
|
CompositingBordersVisibleChanged: "compositing-borders-visible-changed",
|
|
LayerTreeDidChange: "layer-tree-did-change",
|
|
};
|
|
|
|
/* Controllers/MemoryManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.MemoryManager = class MemoryManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Memory"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Memory");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("Memory"))
|
|
target.MemoryAgent.enable();
|
|
}
|
|
|
|
// Public
|
|
|
|
enable()
|
|
{
|
|
if (this._enabled)
|
|
return;
|
|
|
|
this._enabled = true;
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasDomain("Memory"))
|
|
target.MemoryAgent.disable();
|
|
}
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
// MemoryObserver
|
|
|
|
memoryPressure(timestamp, protocolSeverity)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let memoryPressureEvent = WI.MemoryPressureEvent.fromPayload(timestamp, protocolSeverity);
|
|
this.dispatchEventToListeners(WI.MemoryManager.Event.MemoryPressure, {memoryPressureEvent});
|
|
}
|
|
};
|
|
|
|
WI.MemoryManager.Event = {
|
|
MemoryPressure: "memory-manager-memory-pressure",
|
|
};
|
|
|
|
/* Controllers/NetworkManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013-2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: NetworkManager lacks advanced multi-target support. (Network.loadResource invocations per-target)
|
|
|
|
WI.NetworkManager = class NetworkManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._frameIdentifierMap = new Map;
|
|
this._mainFrame = null;
|
|
this._resourceRequestIdentifierMap = new Map;
|
|
this._orphanedResources = new Map;
|
|
this._webSocketIdentifierToURL = new Map;
|
|
|
|
this._waitingForMainFrameResourceTreePayload = true;
|
|
this._transitioningPageTarget = false;
|
|
|
|
this._sourceMapURLMap = new Map;
|
|
this._downloadingSourceMaps = new Set;
|
|
this._failedSourceMapURLs = new Set;
|
|
|
|
this._localResourceOverrides = [];
|
|
this._harImportLocalResourceMap = new Set;
|
|
|
|
this._pendingLocalResourceOverrideSaves = null;
|
|
this._saveLocalResourceOverridesDebouncer = null;
|
|
|
|
// FIXME: Provide dedicated UI to toggle Network Interception globally?
|
|
this._interceptionEnabled = true;
|
|
|
|
this._emulatedCondition = WI.NetworkManager.EmulatedCondition.None;
|
|
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
WI.notifications.addEventListener(WI.Notification.ExtraDomainsActivated, this._extraDomainsActivated, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleFrameMainResourceDidChange, this);
|
|
|
|
if (NetworkManager.supportsOverridingResponses()) {
|
|
WI.Resource.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleResourceContentChangedForLocalResourceOverride, this);
|
|
WI.Resource.addEventListener(WI.Resource.Event.RequestDataDidChange, this._handleResourceContentChangedForLocalResourceOverride, this);
|
|
WI.LocalResourceOverride.addEventListener(WI.LocalResourceOverride.Event.DisabledChanged, this._handleResourceOverrideDisabledChanged, this);
|
|
WI.LocalResourceOverride.addEventListener(WI.LocalResourceOverride.Event.ResourceErrorTypeChanged, this._handleResourceOverrideResourceErrorTypeChanged, this);
|
|
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
let serializedLocalResourceOverrides = await WI.objectStores.localResourceOverrides.getAll();
|
|
|
|
this._restoringLocalResourceOverrides = true;
|
|
for (let serializedLocalResourceOverride of serializedLocalResourceOverrides) {
|
|
let localResourceOverride = WI.LocalResourceOverride.fromJSON(serializedLocalResourceOverride);
|
|
|
|
let supported = false;
|
|
switch (localResourceOverride.type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
supported = WI.NetworkManager.supportsBlockingRequests();
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Request:
|
|
supported = WI.NetworkManager.supportsOverridingRequests();
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
supported = WI.NetworkManager.supportsOverridingResponses();
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory:
|
|
supported = WI.NetworkManager.supportsOverridingResponses() && WI.LocalResource.canMapToFile();
|
|
break;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
supported = WI.NetworkManager.supportsOverridingRequestsWithResponses();
|
|
break;
|
|
}
|
|
if (!supported)
|
|
continue;
|
|
|
|
const key = null;
|
|
WI.objectStores.localResourceOverrides.associateObject(localResourceOverride, key, serializedLocalResourceOverride);
|
|
|
|
this.addLocalResourceOverride(localResourceOverride);
|
|
}
|
|
this._restoringLocalResourceOverrides = false;
|
|
})());
|
|
}
|
|
|
|
this._bootstrapScript = null;
|
|
if (NetworkManager.supportsBootstrapScript()) {
|
|
this._bootstrapScriptEnabledSetting = new WI.Setting("bootstrap-script-enabled", true);
|
|
|
|
WI.Target.registerInitializationPromise((async () => {
|
|
let bootstrapScriptSource = await WI.objectStores.general.get(NetworkManager.bootstrapScriptSourceObjectStoreKey);
|
|
if (bootstrapScriptSource !== undefined)
|
|
this.createBootstrapScript(bootstrapScriptSource);
|
|
})());
|
|
}
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsShowCertificate()
|
|
{
|
|
return InspectorFrontendHost.supportsShowCertificate
|
|
&& InspectorBackend.hasCommand("Network.getSerializedCertificate");
|
|
}
|
|
|
|
static supportsBlockingRequests()
|
|
{
|
|
// COMPATIBILITY (iOS 13.4): Network.interceptRequestWithError did not exist yet.
|
|
return InspectorBackend.hasCommand("Network.interceptRequestWithError");
|
|
}
|
|
|
|
static supportsOverridingRequests()
|
|
{
|
|
// COMPATIBILITY (iOS 13.4): Network.interceptWithRequest did not exist yet.
|
|
return InspectorBackend.hasCommand("Network.interceptWithRequest");
|
|
}
|
|
|
|
static supportsOverridingRequestsWithResponses()
|
|
{
|
|
// COMPATIBILITY (iOS 13.4): Network.interceptRequestWithResponse did not exist yet.
|
|
return InspectorBackend.hasCommand("Network.interceptRequestWithResponse");
|
|
}
|
|
|
|
static supportsOverridingResponses()
|
|
{
|
|
// COMPATIBILITY (iOS 13.0): Network.interceptWithResponse did not exist yet.
|
|
return InspectorBackend.hasCommand("Network.interceptWithResponse");
|
|
}
|
|
|
|
static supportsBootstrapScript()
|
|
{
|
|
return InspectorBackend.hasCommand("Page.setBootstrapScript");
|
|
}
|
|
|
|
static get bootstrapScriptURL()
|
|
{
|
|
return "web-inspector://bootstrap.js";
|
|
}
|
|
|
|
static get bootstrapScriptSourceObjectStoreKey()
|
|
{
|
|
return "bootstrap-script-source";
|
|
}
|
|
|
|
static synthesizeImportError(message)
|
|
{
|
|
message = WI.UIString("HAR Import Error: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.error(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (target.hasDomain("Page")) {
|
|
target.PageAgent.enable();
|
|
target.PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this));
|
|
|
|
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
|
|
if (target.hasCommand("Page.setBootstrapScript") && this._bootstrapScript && this._bootstrapScriptEnabledSetting.value)
|
|
target.PageAgent.setBootstrapScript(this._bootstrapScript.content);
|
|
}
|
|
|
|
if (target.hasDomain("ServiceWorker"))
|
|
target.ServiceWorkerAgent.getInitializationInfo(this._processServiceWorkerConfiguration.bind(this));
|
|
|
|
if (target.hasDomain("Network")) {
|
|
target.NetworkAgent.enable();
|
|
target.NetworkAgent.setResourceCachingDisabled(WI.settings.resourceCachingDisabled.value);
|
|
|
|
// COMPATIBILITY (iOS 13.0): Network.setInterceptionEnabled did not exist.
|
|
if (target.hasCommand("Network.setInterceptionEnabled")) {
|
|
if (this._interceptionEnabled)
|
|
target.NetworkAgent.setInterceptionEnabled(this._interceptionEnabled);
|
|
|
|
for (let localResourceOverride of this._localResourceOverrides) {
|
|
if (!localResourceOverride.disabled)
|
|
this._addInterception(localResourceOverride, target);
|
|
}
|
|
}
|
|
}
|
|
|
|
this._applyEmulatedCondition(target);
|
|
|
|
if (target.type === WI.TargetType.Worker)
|
|
this.adoptOrphanedResourcesForTarget(target);
|
|
}
|
|
|
|
transitionPageTarget()
|
|
{
|
|
this._transitioningPageTarget = true;
|
|
this._waitingForMainFrameResourceTreePayload = true;
|
|
}
|
|
|
|
// Public
|
|
|
|
get mainFrame() { return this._mainFrame; }
|
|
get localResourceOverrides() { return this._localResourceOverrides; }
|
|
get bootstrapScript() { return this._bootstrapScript; }
|
|
|
|
get frames()
|
|
{
|
|
return Array.from(this._frameIdentifierMap.values());
|
|
}
|
|
|
|
get interceptionEnabled()
|
|
{
|
|
return this._interceptionEnabled;
|
|
}
|
|
|
|
set interceptionEnabled(enabled)
|
|
{
|
|
if (this._interceptionEnabled === enabled)
|
|
return;
|
|
|
|
this._interceptionEnabled = enabled;
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13.0): Network.setInterceptionEnabled did not exist.
|
|
if (target.hasCommand("Network.setInterceptionEnabled"))
|
|
target.NetworkAgent.setInterceptionEnabled(this._interceptionEnabled);
|
|
}
|
|
}
|
|
|
|
get emulatedCondition()
|
|
{
|
|
return this._emulatedCondition;
|
|
}
|
|
|
|
set emulatedCondition(condition)
|
|
{
|
|
console.assert(Object.values(WI.NetworkManager.EmulatedCondition).includes(condition), condition);
|
|
console.assert(WI.settings.experimentalEnableNetworkEmulatedCondition.value);
|
|
console.assert(InspectorBackend.hasCommand("Network.setEmulatedConditions"));
|
|
|
|
if (condition === this._emulatedCondition)
|
|
return;
|
|
|
|
this._emulatedCondition = condition;
|
|
|
|
for (let target of WI.targets)
|
|
this._applyEmulatedCondition(target);
|
|
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.EmulatedConditionChanged);
|
|
}
|
|
|
|
frameForIdentifier(frameId)
|
|
{
|
|
return this._frameIdentifierMap.get(frameId) || null;
|
|
}
|
|
|
|
resourceForRequestIdentifier(requestIdentifier)
|
|
{
|
|
return this._resourceRequestIdentifierMap.get(requestIdentifier) || null;
|
|
}
|
|
|
|
downloadSourceMap(sourceMapURL, baseURL, originalSourceCode)
|
|
{
|
|
if (!WI.settings.sourceMapsEnabled.value)
|
|
return;
|
|
|
|
// The baseURL could have come from a "//# sourceURL". Attempt to get a
|
|
// reasonable absolute URL for the base by using the main resource's URL.
|
|
if (WI.networkManager.mainFrame)
|
|
baseURL = absoluteURL(baseURL, WI.networkManager.mainFrame.url);
|
|
|
|
if (sourceMapURL.startsWith("data:")) {
|
|
this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode);
|
|
return;
|
|
}
|
|
|
|
sourceMapURL = absoluteURL(sourceMapURL, baseURL);
|
|
if (!sourceMapURL)
|
|
return;
|
|
|
|
console.assert(originalSourceCode.url);
|
|
if (!originalSourceCode.url)
|
|
return;
|
|
|
|
// FIXME: <rdar://problem/13265694> Source Maps: Better handle when multiple resources reference the same SourceMap
|
|
|
|
if (this._sourceMapURLMap.has(sourceMapURL) || this._downloadingSourceMaps.has(sourceMapURL))
|
|
return;
|
|
|
|
let loadAndParseSourceMap = () => {
|
|
this._loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode);
|
|
};
|
|
|
|
if (!WI.networkManager.mainFrame) {
|
|
// If we don't have a main frame, then we are likely in the middle of building the resource tree.
|
|
// Delaying until the next runloop is enough in this case to then start loading the source map.
|
|
setTimeout(loadAndParseSourceMap, 0);
|
|
return;
|
|
}
|
|
|
|
loadAndParseSourceMap();
|
|
}
|
|
|
|
isSourceMapURL(url)
|
|
{
|
|
return this._downloadingSourceMaps.has(url) || this._failedSourceMapURLs.has(url);
|
|
}
|
|
|
|
get bootstrapScriptEnabled()
|
|
{
|
|
console.assert(NetworkManager.supportsBootstrapScript());
|
|
console.assert(this._bootstrapScript);
|
|
|
|
return this._bootstrapScriptEnabledSetting.value;
|
|
}
|
|
|
|
set bootstrapScriptEnabled(enabled)
|
|
{
|
|
console.assert(NetworkManager.supportsBootstrapScript());
|
|
console.assert(this._bootstrapScript);
|
|
|
|
this._bootstrapScriptEnabledSetting.value = !!enabled;
|
|
|
|
let source = this._bootstrapScriptEnabledSetting.value ? this._bootstrapScript.content : undefined;
|
|
|
|
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Page.setBootstrapScript"))
|
|
target.PageAgent.setBootstrapScript(source);
|
|
}
|
|
|
|
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptEnabledChanged, {bootstrapScript: this._bootstrapScript});
|
|
}
|
|
|
|
async createBootstrapScript(source)
|
|
{
|
|
console.assert(NetworkManager.supportsBootstrapScript());
|
|
|
|
if (this._bootstrapScript)
|
|
return;
|
|
|
|
if (!arguments.length)
|
|
source = await WI.objectStores.general.get(NetworkManager.bootstrapScriptSourceObjectStoreKey);
|
|
|
|
if (!source) {
|
|
source = `
|
|
/*
|
|
* ${WI.UIString("The Inspector Bootstrap Script is guaranteed to be the first script evaluated in any page, as well as any sub-frames.")}
|
|
* ${WI.UIString("It is evaluated immediately after the global object is created, before any other content has loaded.")}
|
|
*
|
|
* ${WI.UIString("Modifications made here will take effect on the next load of any page or sub-frame.")}
|
|
* ${WI.UIString("The contents and enabled state will be preserved across Web Inspector sessions.")}
|
|
*
|
|
* ${WI.UIString("Some examples of ways to use this script include (but are not limited to):")}
|
|
* - ${WI.UIString("overriding built-in functions to log call traces or add %s statements").format(WI.unlocalizedString("`debugger`"))}
|
|
* - ${WI.UIString("ensuring that common debugging functions are available on every page via the Console")}
|
|
*
|
|
* ${WI.UIString("More information is available at <https://webkit.org/web-inspector/inspector-bootstrap-script/>.")}
|
|
*/
|
|
`.trimStart();
|
|
}
|
|
|
|
const target = null;
|
|
const url = null;
|
|
const sourceURL = NetworkManager.bootstrapScriptURL;
|
|
this._bootstrapScript = new WI.LocalScript(target, url, sourceURL, WI.Script.SourceType.Program, source, {injected: true, editable: true});
|
|
this._bootstrapScript.addEventListener(WI.SourceCode.Event.ContentDidChange, this._handleBootstrapScriptContentDidChange, this);
|
|
this._handleBootstrapScriptContentDidChange();
|
|
|
|
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptCreated, {bootstrapScript: this._bootstrapScript});
|
|
}
|
|
|
|
destroyBootstrapScript()
|
|
{
|
|
console.assert(NetworkManager.supportsBootstrapScript());
|
|
|
|
if (!this._bootstrapScript)
|
|
return;
|
|
|
|
let bootstrapScript = this._bootstrapScript;
|
|
|
|
this._bootstrapScript = null;
|
|
WI.objectStores.general.delete(NetworkManager.bootstrapScriptSourceObjectStoreKey);
|
|
|
|
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Page.setBootstrapScript"))
|
|
target.PageAgent.setBootstrapScript();
|
|
}
|
|
|
|
this.dispatchEventToListeners(NetworkManager.Event.BootstrapScriptDestroyed, {bootstrapScript});
|
|
}
|
|
|
|
addLocalResourceOverride(localResourceOverride)
|
|
{
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
|
|
|
|
console.assert(!this._localResourceOverrides.includes(localResourceOverride));
|
|
this._localResourceOverrides.push(localResourceOverride);
|
|
|
|
if (!this._restoringLocalResourceOverrides)
|
|
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
|
|
|
|
if (!localResourceOverride.disabled)
|
|
this._addInterception(localResourceOverride);
|
|
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.LocalResourceOverrideAdded, {localResourceOverride});
|
|
}
|
|
|
|
removeLocalResourceOverride(localResourceOverride)
|
|
{
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
|
|
|
|
if (!this._localResourceOverrides.remove(localResourceOverride)) {
|
|
console.assert(false, "Attempted to remove a local resource override that was not known.");
|
|
return;
|
|
}
|
|
|
|
if (this._pendingLocalResourceOverrideSaves)
|
|
this._pendingLocalResourceOverrideSaves.delete(localResourceOverride);
|
|
|
|
if (!this._restoringLocalResourceOverrides)
|
|
WI.objectStores.localResourceOverrides.deleteObject(localResourceOverride);
|
|
|
|
if (!localResourceOverride.disabled)
|
|
this._removeInterception(localResourceOverride);
|
|
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.LocalResourceOverrideRemoved, {localResourceOverride});
|
|
}
|
|
|
|
localResourceOverridesForURL(url)
|
|
{
|
|
// Order local resource overrides based on how closely they match the given URL. As an example,
|
|
// a regular expression is likely going to match more URLs than a case-insensitive string.
|
|
const rankFunctions = [
|
|
(localResourceOverride) => localResourceOverride.isCaseSensitive && !localResourceOverride.isRegex, // exact match
|
|
(localResourceOverride) => !localResourceOverride.isCaseSensitive && !localResourceOverride.isRegex, // case-insensitive
|
|
(localResourceOverride) => localResourceOverride.isCaseSensitive && localResourceOverride.isRegex, // case-sensitive regex
|
|
(localResourceOverride) => !localResourceOverride.isCaseSensitive && localResourceOverride.isRegex, // case-insensitive regex
|
|
];
|
|
return this._localResourceOverrides
|
|
.filter((localResourceOverride) => localResourceOverride.matches(url))
|
|
.sort((a, b) => {
|
|
let aRank = rankFunctions.findIndex((rankFunction) => rankFunction(a));
|
|
let bRank = rankFunctions.findIndex((rankFunction) => rankFunction(b));
|
|
return aRank - bRank;
|
|
});
|
|
}
|
|
|
|
canBeOverridden(resource)
|
|
{
|
|
if (!(resource instanceof WI.Resource))
|
|
return false;
|
|
|
|
if (resource instanceof WI.SourceMapResource)
|
|
return false;
|
|
|
|
if (resource.localResourceOverride)
|
|
return false;
|
|
|
|
const schemes = ["http:", "https:", "file:"];
|
|
if (!schemes.some((scheme) => resource.url.startsWith(scheme)))
|
|
return false;
|
|
|
|
if (this.localResourceOverridesForURL(resource.url).length)
|
|
return false;
|
|
|
|
switch (resource.type) {
|
|
case WI.Resource.Type.Document:
|
|
case WI.Resource.Type.StyleSheet:
|
|
case WI.Resource.Type.Script:
|
|
case WI.Resource.Type.XHR:
|
|
case WI.Resource.Type.Fetch:
|
|
case WI.Resource.Type.Image:
|
|
case WI.Resource.Type.Font:
|
|
case WI.Resource.Type.EventSource:
|
|
case WI.Resource.Type.Other:
|
|
break;
|
|
case WI.Resource.Type.Ping:
|
|
case WI.Resource.Type.Beacon:
|
|
// Responses aren't really expected for Ping/Beacon.
|
|
return false;
|
|
case WI.Resource.Type.WebSocket:
|
|
// Non-HTTP traffic.
|
|
console.assert(false, "Scheme check above should have been sufficient.");
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
resourcesForURL(url)
|
|
{
|
|
let resources = new Set;
|
|
if (this._mainFrame) {
|
|
if (this._mainFrame.mainResource.url === url)
|
|
resources.add(this._mainFrame.mainResource);
|
|
|
|
const recursivelySearchChildFrames = true;
|
|
resources.addAll(this._mainFrame.resourcesForURL(url, recursivelySearchChildFrames));
|
|
}
|
|
return resources;
|
|
}
|
|
|
|
adoptOrphanedResourcesForTarget(target)
|
|
{
|
|
let resources = this._orphanedResources.take(target.identifier);
|
|
if (!resources)
|
|
return;
|
|
|
|
for (let resource of resources)
|
|
target.adoptResource(resource);
|
|
}
|
|
|
|
processHAR({json, error})
|
|
{
|
|
if (error) {
|
|
WI.NetworkManager.synthesizeImportError(error);
|
|
return null;
|
|
}
|
|
|
|
if (typeof json !== "object" || json === null) {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid JSON"));
|
|
return null;
|
|
}
|
|
|
|
if (typeof json.log !== "object" || typeof json.log.version !== "string") {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
|
|
return null;
|
|
}
|
|
|
|
if (json.log.version !== "1.2") {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("unsupported HAR version"));
|
|
return null;
|
|
}
|
|
|
|
if (!Array.isArray(json.log.entries) || !Array.isArray(json.log.pages) || !json.log.pages[0] || !json.log.pages[0].startedDateTime) {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
|
|
return null;
|
|
}
|
|
|
|
let mainResourceSentWalltime = WI.HARBuilder.dateFromHARDate(json.log.pages[0].startedDateTime) / 1000;
|
|
if (isNaN(mainResourceSentWalltime)) {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("invalid HAR"));
|
|
return null;
|
|
}
|
|
|
|
let localResources = [];
|
|
|
|
for (let entry of json.log.entries) {
|
|
let localResource = WI.LocalResource.fromHAREntry(entry, mainResourceSentWalltime);
|
|
this._harImportLocalResourceMap.add(localResource);
|
|
localResources.push(localResource);
|
|
}
|
|
|
|
return localResources;
|
|
}
|
|
|
|
// PageObserver
|
|
|
|
frameDidNavigate(framePayload)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
var frameWasLoadedInstantly = false;
|
|
|
|
var frame = this.frameForIdentifier(framePayload.id);
|
|
if (!frame) {
|
|
// If the frame wasn't known before now, then the main resource was loaded instantly (about:blank, etc.)
|
|
// Make a new resource (which will make the frame). Mark will mark it as loaded at the end too since we
|
|
// don't expect any more events about the load finishing for these frames.
|
|
let resourceOptions = {
|
|
loaderIdentifier: framePayload.loaderId,
|
|
};
|
|
let frameOptions = {
|
|
name: framePayload.name,
|
|
securityOrigin: framePayload.securityOrigin,
|
|
};
|
|
let frameResource = this._addNewResourceToFrameOrTarget(framePayload.url, framePayload.id, resourceOptions, frameOptions);
|
|
frame = frameResource.parentFrame;
|
|
frameWasLoadedInstantly = true;
|
|
|
|
console.assert(frame);
|
|
if (!frame)
|
|
return;
|
|
}
|
|
|
|
if (framePayload.loaderId === frame.provisionalLoaderIdentifier) {
|
|
// There was a provisional load in progress, commit it.
|
|
frame.commitProvisionalLoad(framePayload.securityOrigin);
|
|
} else {
|
|
let mainResource = null;
|
|
if (frame.mainResource.url !== framePayload.url || frame.loaderIdentifier !== framePayload.loaderId) {
|
|
// Navigations like back/forward do not have provisional loads, so create a new main resource here.
|
|
mainResource = new WI.Resource(framePayload.url, {
|
|
mimeType: framePayload.mimeType,
|
|
loaderIdentifier: framePayload.loaderId,
|
|
});
|
|
} else {
|
|
// The main resource is already correct, so reuse it.
|
|
mainResource = frame.mainResource;
|
|
}
|
|
|
|
frame.initialize(framePayload.name, framePayload.securityOrigin, framePayload.loaderId, mainResource);
|
|
}
|
|
|
|
var oldMainFrame = this._mainFrame;
|
|
|
|
if (framePayload.parentId) {
|
|
var parentFrame = this.frameForIdentifier(framePayload.parentId);
|
|
console.assert(parentFrame);
|
|
|
|
if (frame === this._mainFrame)
|
|
this._mainFrame = null;
|
|
|
|
if (frame.parentFrame !== parentFrame)
|
|
parentFrame.addChildFrame(frame);
|
|
} else {
|
|
if (frame.parentFrame)
|
|
frame.parentFrame.removeChildFrame(frame);
|
|
this._mainFrame = frame;
|
|
}
|
|
|
|
if (this._mainFrame !== oldMainFrame)
|
|
this._mainFrameDidChange(oldMainFrame);
|
|
|
|
if (frameWasLoadedInstantly)
|
|
frame.mainResource.markAsFinished();
|
|
}
|
|
|
|
frameDidDetach(frameId)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
var frame = this.frameForIdentifier(frameId);
|
|
if (!frame)
|
|
return;
|
|
|
|
if (frame.parentFrame)
|
|
frame.parentFrame.removeChildFrame(frame);
|
|
|
|
this._frameIdentifierMap.delete(frame.id);
|
|
|
|
var oldMainFrame = this._mainFrame;
|
|
|
|
if (frame === this._mainFrame)
|
|
this._mainFrame = null;
|
|
|
|
frame.clearExecutionContexts();
|
|
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.FrameWasRemoved, {frame});
|
|
|
|
if (this._mainFrame !== oldMainFrame)
|
|
this._mainFrameDidChange(oldMainFrame);
|
|
}
|
|
|
|
// NetworkObserver
|
|
|
|
resourceRequestWillBeSent(requestIdentifier, frameIdentifier, loaderIdentifier, request, type, redirectResponse, timestamp, walltime, initiator, targetId)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
|
|
if (resource) {
|
|
// This is an existing request which is being redirected, update the resource.
|
|
console.assert(resource.parentFrame.id === frameIdentifier);
|
|
console.assert(resource.loaderIdentifier === loaderIdentifier);
|
|
console.assert(!targetId);
|
|
resource.updateForRedirectResponse(request, redirectResponse, elapsedTime, walltime);
|
|
return;
|
|
}
|
|
|
|
// This is a new request, make a new resource and add it to the right frame.
|
|
resource = this._addNewResourceToFrameOrTarget(request.url, frameIdentifier, {
|
|
type,
|
|
loaderIdentifier,
|
|
targetId,
|
|
requestIdentifier,
|
|
requestMethod: request.method,
|
|
requestHeaders: request.headers,
|
|
requestData: request.postData,
|
|
requestSentTimestamp: elapsedTime,
|
|
requestSentWalltime: walltime,
|
|
referrerPolicy: request.referrerPolicy,
|
|
integrity: request.integrity,
|
|
initiatorStackTrace: this._initiatorStackTraceFromPayload(initiator),
|
|
initiatorSourceCodeLocation: this._initiatorSourceCodeLocationFromPayload(initiator),
|
|
initiatorNode: this._initiatorNodeFromPayload(initiator),
|
|
});
|
|
|
|
// Associate the resource with the requestIdentifier so it can be found in future loading events.
|
|
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
|
|
}
|
|
|
|
webSocketCreated(requestId, url)
|
|
{
|
|
this._webSocketIdentifierToURL.set(requestId, url);
|
|
}
|
|
|
|
webSocketWillSendHandshakeRequest(requestId, timestamp, walltime, request)
|
|
{
|
|
let url = this._webSocketIdentifierToURL.get(requestId);
|
|
console.assert(url);
|
|
if (!url)
|
|
return;
|
|
|
|
// FIXME: <webkit.org/b/168475> Web Inspector: Correctly display iframe's and worker's WebSockets
|
|
|
|
let resource = new WI.WebSocketResource(url, {
|
|
loaderIdentifier: WI.networkManager.mainFrame.id,
|
|
requestIdentifier: requestId,
|
|
requestHeaders: request.headers,
|
|
timestamp,
|
|
walltime,
|
|
requestSentTimestamp: WI.timelineManager.computeElapsedTime(timestamp),
|
|
});
|
|
|
|
let frame = this.frameForIdentifier(WI.networkManager.mainFrame.id);
|
|
frame.addResource(resource);
|
|
|
|
this._resourceRequestIdentifierMap.set(requestId, resource);
|
|
}
|
|
|
|
webSocketHandshakeResponseReceived(requestId, timestamp, response)
|
|
{
|
|
let resource = this._resourceRequestIdentifierMap.get(requestId);
|
|
console.assert(resource);
|
|
if (!resource)
|
|
return;
|
|
|
|
resource.readyState = WI.WebSocketResource.ReadyState.Open;
|
|
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
|
|
// FIXME: <webkit.org/b/169166> Web Inspector: WebSockets: Implement timing information
|
|
let responseTiming = response.timing || null;
|
|
|
|
resource.updateForResponse(resource.url, resource.mimeType, resource.type, response.headers, response.status, response.statusText, elapsedTime, responseTiming);
|
|
|
|
resource.markAsFinished(elapsedTime);
|
|
}
|
|
|
|
webSocketFrameReceived(requestId, timestamp, response)
|
|
{
|
|
this._webSocketFrameReceivedOrSent(requestId, timestamp, response);
|
|
}
|
|
|
|
webSocketFrameSent(requestId, timestamp, response)
|
|
{
|
|
this._webSocketFrameReceivedOrSent(requestId, timestamp, response);
|
|
}
|
|
|
|
webSocketClosed(requestId, timestamp)
|
|
{
|
|
let resource = this._resourceRequestIdentifierMap.get(requestId);
|
|
console.assert(resource);
|
|
if (!resource)
|
|
return;
|
|
|
|
resource.readyState = WI.WebSocketResource.ReadyState.Closed;
|
|
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
resource.markAsFinished(elapsedTime);
|
|
|
|
this._webSocketIdentifierToURL.delete(requestId);
|
|
this._resourceRequestIdentifierMap.delete(requestId);
|
|
}
|
|
|
|
_webSocketFrameReceivedOrSent(requestId, timestamp, response)
|
|
{
|
|
let resource = this._resourceRequestIdentifierMap.get(requestId);
|
|
console.assert(resource);
|
|
if (!resource)
|
|
return;
|
|
|
|
// Data going from the client to the server is always masked.
|
|
let isOutgoing = !!response.mask;
|
|
|
|
let {payloadData, payloadLength, opcode} = response;
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
|
|
resource.addFrame(payloadData, payloadLength, isOutgoing, opcode, timestamp, elapsedTime);
|
|
}
|
|
|
|
resourceRequestWasServedFromMemoryCache(requestIdentifier, frameIdentifier, loaderIdentifier, cachedResourcePayload, timestamp, initiator)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
console.assert(!this._resourceRequestIdentifierMap.has(requestIdentifier));
|
|
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
let response = cachedResourcePayload.response;
|
|
const responseSource = InspectorBackend.Enum.Network.ResponseSource.MemoryCache;
|
|
|
|
let resource = this._addNewResourceToFrameOrTarget(cachedResourcePayload.url, frameIdentifier, {
|
|
type: cachedResourcePayload.type,
|
|
loaderIdentifier,
|
|
requestIdentifier,
|
|
requestMethod: "GET",
|
|
requestSentTimestamp: elapsedTime,
|
|
initiatorStackTrace: this._initiatorStackTraceFromPayload(initiator),
|
|
initiatorSourceCodeLocation: this._initiatorSourceCodeLocationFromPayload(initiator),
|
|
initiatorNode: this._initiatorNodeFromPayload(initiator),
|
|
});
|
|
resource.updateForResponse(cachedResourcePayload.url, response.mimeType, cachedResourcePayload.type, response.headers, response.status, response.statusText, elapsedTime, response.timing, responseSource, response.security);
|
|
resource.increaseSize(cachedResourcePayload.bodySize, elapsedTime);
|
|
resource.increaseTransferSize(cachedResourcePayload.bodySize);
|
|
resource.setCachedResponseBodySize(cachedResourcePayload.bodySize);
|
|
resource.markAsFinished(elapsedTime);
|
|
|
|
console.assert(resource.cached, "This resource should be classified as cached since it was served from the MemoryCache", resource);
|
|
|
|
if (cachedResourcePayload.sourceMapURL)
|
|
this.downloadSourceMap(cachedResourcePayload.sourceMapURL, resource.url, resource);
|
|
|
|
// No need to associate the resource with the requestIdentifier, since this is the only event
|
|
// sent for memory cache resource loads.
|
|
}
|
|
|
|
resourceRequestDidReceiveResponse(requestIdentifier, frameIdentifier, loaderIdentifier, type, response, timestamp)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
|
|
|
|
// We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called).
|
|
// We don't want to assert in this case since we do likely have the resource, via Page.getResourceTree. The Resource
|
|
// just doesn't have a requestIdentifier for us to look it up, but we can try to look it up by its URL.
|
|
if (!resource) {
|
|
var frame = this.frameForIdentifier(frameIdentifier);
|
|
if (frame)
|
|
resource = frame.resourcesForURL(response.url).firstValue;
|
|
|
|
// If we find the resource this way we had marked it earlier as finished via Page.getResourceTree.
|
|
// Associate the resource with the requestIdentifier so it can be found in future loading events.
|
|
// and roll it back to an unfinished state, we know now it is still loading.
|
|
if (resource) {
|
|
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
|
|
resource.revertMarkAsFinished();
|
|
}
|
|
}
|
|
|
|
// If we haven't found an existing Resource by now, then it is a resource that was loading when the inspector
|
|
// opened and we just missed the resourceRequestWillBeSent for it. So make a new resource and add it.
|
|
if (!resource) {
|
|
resource = this._addNewResourceToFrameOrTarget(response.url, frameIdentifier, {
|
|
type,
|
|
loaderIdentifier,
|
|
requestIdentifier,
|
|
requestHeaders: response.requestHeaders,
|
|
requestSentTimestamp: elapsedTime,
|
|
});
|
|
|
|
// Associate the resource with the requestIdentifier so it can be found in future loading events.
|
|
this._resourceRequestIdentifierMap.set(requestIdentifier, resource);
|
|
}
|
|
|
|
resource.updateForResponse(response.url, response.mimeType, type, response.headers, response.status, response.statusText, elapsedTime, response.timing, response.source, response.security);
|
|
}
|
|
|
|
resourceRequestDidReceiveData(requestIdentifier, dataLength, encodedDataLength, timestamp)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
|
|
var elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
|
|
// We might not have a resource if the inspector was opened during the page load (after resourceRequestWillBeSent is called).
|
|
// We don't want to assert in this case since we do likely have the resource, via Page.getResourceTree. The Resource
|
|
// just doesn't have a requestIdentifier for us to look it up.
|
|
if (!resource)
|
|
return;
|
|
|
|
resource.increaseSize(dataLength, elapsedTime);
|
|
|
|
if (encodedDataLength !== -1)
|
|
resource.increaseTransferSize(encodedDataLength);
|
|
}
|
|
|
|
resourceRequestDidFinishLoading(requestIdentifier, timestamp, sourceMapURL, metrics)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
// By now we should always have the Resource. Either it was fetched when the inspector first opened with
|
|
// Page.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse.
|
|
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
|
|
console.assert(resource);
|
|
if (!resource)
|
|
return;
|
|
|
|
if (metrics)
|
|
resource.updateWithMetrics(metrics);
|
|
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
resource.markAsFinished(elapsedTime);
|
|
|
|
if (sourceMapURL)
|
|
this.downloadSourceMap(sourceMapURL, resource.url, resource);
|
|
|
|
this._resourceRequestIdentifierMap.delete(requestIdentifier);
|
|
}
|
|
|
|
resourceRequestDidFailLoading(requestIdentifier, canceled, timestamp, errorText)
|
|
{
|
|
// Ignore this while waiting for the whole frame/resource tree.
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
// By now we should always have the Resource. Either it was fetched when the inspector first opened with
|
|
// Page.getResourceTree, or it was a currently loading resource that we learned about in resourceRequestDidReceiveResponse.
|
|
let resource = this._resourceRequestIdentifierMap.get(requestIdentifier);
|
|
console.assert(resource);
|
|
if (!resource)
|
|
return;
|
|
|
|
let elapsedTime = WI.timelineManager.computeElapsedTime(timestamp);
|
|
resource.markAsFailed(canceled, elapsedTime, errorText);
|
|
|
|
if (resource.parentFrame && resource === resource.parentFrame.provisionalMainResource)
|
|
resource.parentFrame.clearProvisionalLoad();
|
|
|
|
this._resourceRequestIdentifierMap.delete(requestIdentifier);
|
|
}
|
|
|
|
async requestIntercepted(target, requestId, request)
|
|
{
|
|
for (let localResourceOverride of this.localResourceOverridesForURL(request.url)) {
|
|
if (localResourceOverride.disabled)
|
|
continue;
|
|
|
|
if (localResourceOverride.networkStage !== WI.NetworkManager.NetworkStage.Request)
|
|
continue;
|
|
|
|
let isPassthrough = localResourceOverride.isPassthrough;
|
|
let originalHeaders = isPassthrough ? request.headers : {};
|
|
|
|
let localResource = localResourceOverride.localResource;
|
|
await localResource.requestContent();
|
|
|
|
let revision = localResource.currentRevision;
|
|
|
|
switch (localResourceOverride.type) {
|
|
case WI.LocalResourceOverride.InterceptType.Block:
|
|
target.NetworkAgent.interceptRequestWithError.invoke({
|
|
requestId,
|
|
errorType: localResourceOverride.resourceErrorType,
|
|
});
|
|
return;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.Request: {
|
|
target.NetworkAgent.interceptWithRequest.invoke({
|
|
requestId,
|
|
url: localResourceOverride.generateRequestRedirectURL(request.url) ?? undefined,
|
|
method: localResource.requestMethod ?? (isPassthrough ? request.method : ""),
|
|
headers: {...originalHeaders, ...localResource.requestHeaders},
|
|
postData: (function() {
|
|
if (!WI.HTTPUtilities.RequestMethodsWithBody.has(localResource.requestMethod))
|
|
return undefined;
|
|
if (localResource.requestData ?? false)
|
|
return btoa(localResource.requestData);
|
|
if (isPassthrough)
|
|
return request.data;
|
|
return "";
|
|
})(),
|
|
});
|
|
return;
|
|
}
|
|
|
|
case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork:
|
|
console.assert(revision.mimeType === localResource.mimeType);
|
|
target.NetworkAgent.interceptRequestWithResponse.invoke({
|
|
requestId,
|
|
content: revision.content,
|
|
base64Encoded: !!revision.base64Encoded,
|
|
mimeType: revision.mimeType ?? "text/plain",
|
|
status: !isNaN(localResource.statusCode) ? localResource.statusCode : 200,
|
|
statusText: (function() {
|
|
if (localResource.statusText ?? false)
|
|
return localResource.statusText;
|
|
|
|
if (!isNaN(localResource.statusCode))
|
|
return WI.HTTPUtilities.statusTextForStatusCode(localResource.statusCode);
|
|
|
|
return WI.HTTPUtilities.statusTextForStatusCode(200);
|
|
})(),
|
|
headers: {...originalHeaders, ...localResource.responseHeaders},
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
// It's possible for a response regex override to overlap a request regex override, in
|
|
// which case we should silently continue the request if the response regex override was
|
|
// used instead (e.g. it was added first).
|
|
target.NetworkAgent.interceptContinue.invoke({
|
|
requestId,
|
|
stage: WI.NetworkManager.NetworkStage.Request,
|
|
});
|
|
}
|
|
|
|
async responseIntercepted(target, requestId, response)
|
|
{
|
|
for (let localResourceOverride of this.localResourceOverridesForURL(response.url)) {
|
|
if (localResourceOverride.disabled)
|
|
continue;
|
|
|
|
if (localResourceOverride.networkStage !== WI.NetworkManager.NetworkStage.Response)
|
|
continue;
|
|
|
|
let isPassthrough = localResourceOverride.isPassthrough;
|
|
let originalHeaders = isPassthrough ? response.headers : {};
|
|
|
|
let localResource = localResourceOverride.localResource;
|
|
await localResource.requestContent();
|
|
|
|
let revision = localResource.currentRevision;
|
|
|
|
switch (localResourceOverride.type) {
|
|
case WI.LocalResourceOverride.InterceptType.Response:
|
|
console.assert(revision.mimeType === localResource.mimeType);
|
|
target.NetworkAgent.interceptWithResponse.invoke({
|
|
requestId,
|
|
content: revision.content,
|
|
base64Encoded: !!revision.base64Encoded,
|
|
mimeType: revision.mimeType ?? (isPassthrough ? response.mimeType : "text/plain"),
|
|
status: (function() {
|
|
if (!isNaN(localResource.statusCode))
|
|
return localResource.statusCode;
|
|
|
|
if (isPassthrough)
|
|
return response.status;
|
|
|
|
return 200;
|
|
})(),
|
|
statusText: (function() {
|
|
if (localResource.statusText ?? false)
|
|
return localResource.statusText;
|
|
|
|
if (isPassthrough)
|
|
return response.statusText;
|
|
|
|
if (!isNaN(localResource.statusCode))
|
|
return WI.HTTPUtilities.statusTextForStatusCode(localResource.statusCode);
|
|
|
|
return WI.HTTPUtilities.statusTextForStatusCode(200);
|
|
})(),
|
|
headers: {...originalHeaders, ...localResource.responseHeaders},
|
|
});
|
|
return;
|
|
|
|
case WI.LocalResourceOverride.InterceptType.ResponseMappedDirectory: {
|
|
let subpath = localResourceOverride.generateSubpathForMappedDirectory(WI.urlWithoutUserQueryOrFragment(response.url));
|
|
let content = await localResource.requestContentFromMappedDirectory(subpath);
|
|
if (typeof content === "string") {
|
|
let mimeType = WI.mimeTypeForFileExtension(WI.fileExtensionForURL(response.url));
|
|
target.NetworkAgent.interceptWithResponse.invoke({
|
|
requestId,
|
|
content,
|
|
base64Encoded: !WI.shouldTreatMIMETypeAsText(mimeType),
|
|
mimeType,
|
|
status: (function() {
|
|
if (response.status < 400)
|
|
return response.status;
|
|
return 200;
|
|
})(),
|
|
statusText: (function() {
|
|
if (response.status < 400) {
|
|
if (response.statusText)
|
|
return response.statusText;
|
|
return WI.HTTPUtilities.statusTextForStatusCode(response.status);
|
|
}
|
|
return WI.HTTPUtilities.statusTextForStatusCode(200);
|
|
})(),
|
|
});
|
|
} else {
|
|
// Be lenient by allowing for a very general directory mapping to not have to
|
|
// contain files for every single possible request that could be intercepted.
|
|
target.NetworkAgent.interceptContinue.invoke({
|
|
requestId,
|
|
stage: WI.NetworkManager.NetworkStage.Response,
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// It's possible for a request regex override to overlap a response regex override, in
|
|
// which case we should silently continue the response if the request regex override was
|
|
// used instead (e.g. it was added first).
|
|
target.NetworkAgent.interceptContinue.invoke({
|
|
requestId,
|
|
stage: WI.NetworkManager.NetworkStage.Response,
|
|
});
|
|
}
|
|
|
|
// RuntimeObserver
|
|
|
|
executionContextCreated(payload)
|
|
{
|
|
let frame = this.frameForIdentifier(payload.frameId);
|
|
console.assert(frame);
|
|
if (!frame)
|
|
return;
|
|
|
|
let type = WI.ExecutionContext.typeFromPayload(payload);
|
|
let target = frame.mainResource.target;
|
|
let executionContext = new WI.ExecutionContext(target, payload.id, type, payload.name, frame);
|
|
frame.addExecutionContext(executionContext);
|
|
}
|
|
|
|
// Private
|
|
|
|
_addNewResourceToFrameOrTarget(url, frameIdentifier, resourceOptions = {}, frameOptions = {})
|
|
{
|
|
console.assert(!this._waitingForMainFrameResourceTreePayload);
|
|
|
|
let resource = null;
|
|
|
|
if (!frameIdentifier && resourceOptions.targetId) {
|
|
// This is a new resource for a ServiceWorker target.
|
|
console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker);
|
|
console.assert(resourceOptions.targetId === WI.mainTarget.identifier);
|
|
resource = new WI.Resource(url, resourceOptions);
|
|
resource.target.addResource(resource);
|
|
return resource;
|
|
}
|
|
|
|
let frame = this.frameForIdentifier(frameIdentifier);
|
|
if (frame) {
|
|
if (resourceOptions.type === InspectorBackend.Enum.Page.ResourceType.Document && frame.provisionalMainResource && frame.provisionalMainResource.url === url && frame.provisionalLoaderIdentifier === resourceOptions.loaderIdentifier)
|
|
resource = frame.provisionalMainResource;
|
|
else {
|
|
resource = new WI.Resource(url, resourceOptions);
|
|
if (resource.target === WI.pageTarget)
|
|
this._addResourceToFrame(frame, resource);
|
|
else if (resource.target)
|
|
resource.target.addResource(resource);
|
|
else
|
|
this._addOrphanedResource(resource, resourceOptions.targetId);
|
|
}
|
|
} else {
|
|
// This is a new request for a new frame, which is always the main resource.
|
|
console.assert(WI.sharedApp.debuggableType !== WI.DebuggableType.ServiceWorker);
|
|
console.assert(!resourceOptions.targetId);
|
|
resource = new WI.Resource(url, resourceOptions);
|
|
frame = new WI.Frame(frameIdentifier, frameOptions.name, frameOptions.securityOrigin, resourceOptions.loaderIdentifier, resource);
|
|
this._frameIdentifierMap.set(frame.id, frame);
|
|
|
|
// If we don't have a main frame, assume this is it. This can change later in
|
|
// frameDidNavigate when the parent frame is known.
|
|
if (!this._mainFrame) {
|
|
this._mainFrame = frame;
|
|
this._mainFrameDidChange(null);
|
|
}
|
|
|
|
this._dispatchFrameWasAddedEvent(frame);
|
|
}
|
|
|
|
console.assert(resource);
|
|
|
|
return resource;
|
|
}
|
|
|
|
_addResourceToFrame(frame, resource)
|
|
{
|
|
console.assert(!this._waitingForMainFrameResourceTreePayload);
|
|
if (this._waitingForMainFrameResourceTreePayload)
|
|
return;
|
|
|
|
console.assert(frame);
|
|
console.assert(resource);
|
|
|
|
if (resource.loaderIdentifier !== frame.loaderIdentifier && !frame.provisionalLoaderIdentifier) {
|
|
// This is the start of a provisional load which happens before frameDidNavigate is called.
|
|
// This resource will be the new mainResource if frameDidNavigate is called.
|
|
frame.startProvisionalLoad(resource);
|
|
return;
|
|
}
|
|
|
|
// This is just another resource, either for the main loader or the provisional loader.
|
|
console.assert(resource.loaderIdentifier === frame.loaderIdentifier || resource.loaderIdentifier === frame.provisionalLoaderIdentifier);
|
|
frame.addResource(resource);
|
|
}
|
|
|
|
_addResourceToTarget(target, resource)
|
|
{
|
|
console.assert(target !== WI.pageTarget);
|
|
console.assert(resource);
|
|
|
|
target.addResource(resource);
|
|
}
|
|
|
|
_initiatorStackTraceFromPayload(initiatorPayload)
|
|
{
|
|
if (!initiatorPayload)
|
|
return null;
|
|
|
|
let stackTrace = initiatorPayload.stackTrace;
|
|
if (!stackTrace)
|
|
return null;
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `stackTrace` was an array of `Console.CallFrame`.
|
|
if (Array.isArray(stackTrace))
|
|
stackTrace = {callFrames: stackTrace};
|
|
|
|
return WI.StackTrace.fromPayload(WI.assumingMainTarget(), stackTrace);
|
|
}
|
|
|
|
_initiatorSourceCodeLocationFromPayload(initiatorPayload)
|
|
{
|
|
if (!initiatorPayload)
|
|
return null;
|
|
|
|
var url = null;
|
|
var lineNumber = NaN;
|
|
var columnNumber = 0;
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `stackTrace` was an array of `Console.CallFrame`.
|
|
let callFramesPayload = Array.isArray(initiatorPayload.stackTrace) ? initiatorPayload.stackTrace : initiatorPayload.stackTrace?.callFrames;
|
|
if (callFramesPayload?.length) {
|
|
for (let callFramePayload of callFramesPayload) {
|
|
if (!callFramePayload.url || callFramePayload.url === "[native code]")
|
|
continue;
|
|
|
|
url = callFramePayload.url;
|
|
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
lineNumber = callFramePayload.lineNumber - 1;
|
|
|
|
columnNumber = callFramePayload.columnNumber;
|
|
|
|
break;
|
|
}
|
|
} else if (initiatorPayload.url) {
|
|
url = initiatorPayload.url;
|
|
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
lineNumber = initiatorPayload.lineNumber - 1;
|
|
}
|
|
|
|
if (!url || isNaN(lineNumber) || lineNumber < 0)
|
|
return null;
|
|
|
|
let sourceCode = WI.networkManager.resourcesForURL(url).firstValue;
|
|
if (!sourceCode)
|
|
sourceCode = WI.debuggerManager.scriptsForURL(url, WI.mainTarget)[0];
|
|
|
|
if (!sourceCode)
|
|
return null;
|
|
|
|
return sourceCode.createSourceCodeLocation(lineNumber, columnNumber);
|
|
}
|
|
|
|
_initiatorNodeFromPayload(initiatorPayload)
|
|
{
|
|
return WI.domManager.nodeForId(initiatorPayload.nodeId);
|
|
}
|
|
|
|
_processServiceWorkerConfiguration(error, initializationPayload)
|
|
{
|
|
console.assert(this._waitingForMainFrameResourceTreePayload);
|
|
this._waitingForMainFrameResourceTreePayload = false;
|
|
|
|
if (error) {
|
|
console.error(JSON.stringify(error));
|
|
return;
|
|
}
|
|
|
|
console.assert(initializationPayload.targetId.startsWith("serviceworker:"));
|
|
|
|
WI.mainTarget.identifier = initializationPayload.targetId;
|
|
WI.mainTarget.name = initializationPayload.url;
|
|
|
|
// Create a main resource with this content in case the content never shows up as a WI.Script.
|
|
const sourceURL = null;
|
|
const sourceType = WI.Script.SourceType.Program;
|
|
let script = new WI.LocalScript(WI.mainTarget, initializationPayload.url, sourceURL, sourceType, initializationPayload.content);
|
|
WI.mainTarget.mainResource = script;
|
|
|
|
InspectorBackend.runAfterPendingDispatches(() => {
|
|
if (WI.mainTarget.mainResource === script) {
|
|
// We've now received all the scripts, if we don't have a better main resource use this LocalScript.
|
|
WI.debuggerManager.dataForTarget(WI.mainTarget).addScript(script);
|
|
WI.debuggerManager.dispatchEventToListeners(WI.DebuggerManager.Event.ScriptAdded, {script});
|
|
}
|
|
});
|
|
}
|
|
|
|
_processMainFrameResourceTreePayload(error, mainFramePayload)
|
|
{
|
|
console.assert(this._waitingForMainFrameResourceTreePayload);
|
|
this._waitingForMainFrameResourceTreePayload = false;
|
|
|
|
if (error) {
|
|
console.error(JSON.stringify(error));
|
|
return;
|
|
}
|
|
|
|
console.assert(mainFramePayload);
|
|
console.assert(mainFramePayload.frame);
|
|
|
|
this._resourceRequestIdentifierMap = new Map;
|
|
this._frameIdentifierMap = new Map;
|
|
|
|
var oldMainFrame = this._mainFrame;
|
|
|
|
this._mainFrame = this._addFrameTreeFromFrameResourceTreePayload(mainFramePayload, true);
|
|
|
|
if (this._mainFrame !== oldMainFrame)
|
|
this._mainFrameDidChange(oldMainFrame);
|
|
|
|
// Emulate a main resource change within this page even though we are swapping out main frames.
|
|
// This is because many managers listen only for main resource change events to perform work,
|
|
// but they don't listen for main frame changes.
|
|
if (this._transitioningPageTarget) {
|
|
this._transitioningPageTarget = false;
|
|
this._mainFrame._dispatchMainResourceDidChangeEvent(oldMainFrame.mainResource);
|
|
}
|
|
}
|
|
|
|
_createFrame(payload)
|
|
{
|
|
// If payload.url is missing or empty then this page is likely the special empty page. In that case
|
|
// we will just say it is "about:blank" so we have a URL, which is required for resources.
|
|
let mainResource = new WI.Resource(payload.url || "about:blank", {
|
|
mimeType: payload.mimeType,
|
|
loaderIdentifier: payload.loaderId,
|
|
});
|
|
var frame = new WI.Frame(payload.id, payload.name, payload.securityOrigin, payload.loaderId, mainResource);
|
|
|
|
this._frameIdentifierMap.set(frame.id, frame);
|
|
|
|
mainResource.markAsFinished();
|
|
|
|
return frame;
|
|
}
|
|
|
|
_createResource(payload, framePayload)
|
|
{
|
|
let resource = new WI.Resource(payload.url, {
|
|
mimeType: payload.mimeType,
|
|
type: payload.type,
|
|
loaderIdentifier: framePayload.loaderId,
|
|
targetId: payload.targetId,
|
|
});
|
|
|
|
if (payload.sourceMapURL)
|
|
this.downloadSourceMap(payload.sourceMapURL, resource.url, resource);
|
|
|
|
return resource;
|
|
}
|
|
|
|
_addFrameTreeFromFrameResourceTreePayload(payload, isMainFrame)
|
|
{
|
|
var frame = this._createFrame(payload.frame);
|
|
if (isMainFrame)
|
|
frame.markAsMainFrame();
|
|
|
|
for (var i = 0; payload.childFrames && i < payload.childFrames.length; ++i)
|
|
frame.addChildFrame(this._addFrameTreeFromFrameResourceTreePayload(payload.childFrames[i], false));
|
|
|
|
for (var i = 0; payload.resources && i < payload.resources.length; ++i) {
|
|
var resourcePayload = payload.resources[i];
|
|
|
|
// The main resource is included as a resource. We can skip it since we already created
|
|
// a main resource when we created the Frame. The resource payload does not include anything
|
|
// didn't already get from the frame payload.
|
|
if (resourcePayload.type === "Document" && resourcePayload.url === payload.frame.url)
|
|
continue;
|
|
|
|
var resource = this._createResource(resourcePayload, payload);
|
|
if (resource.target === WI.pageTarget)
|
|
frame.addResource(resource);
|
|
else if (resource.target)
|
|
resource.target.addResource(resource);
|
|
else
|
|
this._addOrphanedResource(resource, resourcePayload.targetId);
|
|
|
|
if (resourcePayload.failed || resourcePayload.canceled)
|
|
resource.markAsFailed(resourcePayload.canceled);
|
|
else
|
|
resource.markAsFinished();
|
|
}
|
|
|
|
this._dispatchFrameWasAddedEvent(frame);
|
|
|
|
return frame;
|
|
}
|
|
|
|
_addOrphanedResource(resource, targetId)
|
|
{
|
|
let resources = this._orphanedResources.get(targetId);
|
|
if (!resources) {
|
|
resources = [];
|
|
this._orphanedResources.set(targetId, resources);
|
|
}
|
|
|
|
resources.push(resource);
|
|
}
|
|
|
|
_commandArgumentsForInterception(localResourceOverride)
|
|
{
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride, localResourceOverride);
|
|
|
|
return {
|
|
url: localResourceOverride.url,
|
|
stage: localResourceOverride.networkStage,
|
|
caseSensitive: localResourceOverride.isCaseSensitive,
|
|
isRegex: localResourceOverride.isRegex,
|
|
};
|
|
}
|
|
|
|
_addInterception(localResourceOverride, specificTarget)
|
|
{
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride, localResourceOverride);
|
|
console.assert(!localResourceOverride.disabled, localResourceOverride);
|
|
|
|
let targets = specificTarget ? [specificTarget] : WI.targets;
|
|
for (let target of targets) {
|
|
// COMPATIBILITY (iOS 13.0): Network.addInterception did not exist yet.
|
|
if (!target.hasCommand("Network.addInterception"))
|
|
continue;
|
|
|
|
target.NetworkAgent.addInterception.invoke(this._commandArgumentsForInterception(localResourceOverride));
|
|
}
|
|
}
|
|
|
|
_removeInterception(localResourceOverride, specificTarget)
|
|
{
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride, localResourceOverride);
|
|
|
|
let targets = specificTarget ? [specificTarget] : WI.targets;
|
|
for (let target of targets) {
|
|
// COMPATIBILITY (iOS 13.0): Network.removeInterception did not exist yet.
|
|
if (!target.hasCommand("Network.removeInterception"))
|
|
continue;
|
|
|
|
target.NetworkAgent.removeInterception.invoke(this._commandArgumentsForInterception(localResourceOverride));
|
|
}
|
|
}
|
|
|
|
_applyEmulatedCondition(target)
|
|
{
|
|
if (!WI.settings.experimentalEnableNetworkEmulatedCondition.value)
|
|
return;
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): Network.setEmulatedConditions did not exist.
|
|
if (!target.hasCommand("Network.setEmulatedConditions"))
|
|
return;
|
|
|
|
target.NetworkAgent.setEmulatedConditions(this._emulatedCondition.bytesPerSecondLimit);
|
|
}
|
|
|
|
_dispatchFrameWasAddedEvent(frame)
|
|
{
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.FrameWasAdded, {frame});
|
|
}
|
|
|
|
_mainFrameDidChange(oldMainFrame)
|
|
{
|
|
if (oldMainFrame)
|
|
oldMainFrame.unmarkAsMainFrame();
|
|
if (this._mainFrame)
|
|
this._mainFrame.markAsMainFrame();
|
|
|
|
this.dispatchEventToListeners(WI.NetworkManager.Event.MainFrameDidChange, {oldMainFrame});
|
|
}
|
|
|
|
_loadAndParseSourceMap(sourceMapURL, baseURL, originalSourceCode)
|
|
{
|
|
this._downloadingSourceMaps.add(sourceMapURL);
|
|
|
|
let sourceMapLoaded = (error, content, mimeType, statusCode) => {
|
|
if (error || statusCode >= 400) {
|
|
this._sourceMapLoadAndParseFailed(sourceMapURL);
|
|
return;
|
|
}
|
|
|
|
if (content.slice(0, 3) === ")]}") {
|
|
let firstNewlineIndex = content.indexOf("\n");
|
|
if (firstNewlineIndex === -1) {
|
|
this._sourceMapLoadAndParseFailed(sourceMapURL);
|
|
return;
|
|
}
|
|
|
|
content = content.substring(firstNewlineIndex);
|
|
}
|
|
|
|
try {
|
|
let payload = JSON.parse(content);
|
|
let baseURL = sourceMapURL.startsWith("data:") ? originalSourceCode.url : sourceMapURL;
|
|
let sourceMap = new WI.SourceMap(baseURL, payload, originalSourceCode);
|
|
this._sourceMapLoadAndParseSucceeded(sourceMapURL, sourceMap);
|
|
} catch {
|
|
this._sourceMapLoadAndParseFailed(sourceMapURL);
|
|
}
|
|
};
|
|
|
|
if (sourceMapURL.startsWith("data:")) {
|
|
let {mimeType, base64, data} = parseDataURL(sourceMapURL);
|
|
let content = base64 ? atob(data) : data;
|
|
sourceMapLoaded(null, content, mimeType, 0);
|
|
return;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Network.loadResource")) {
|
|
this._sourceMapLoadAndParseFailed(sourceMapURL);
|
|
return;
|
|
}
|
|
|
|
let frameIdentifier = null;
|
|
if (originalSourceCode instanceof WI.Resource && originalSourceCode.parentFrame)
|
|
frameIdentifier = originalSourceCode.parentFrame.id;
|
|
|
|
if (!frameIdentifier)
|
|
frameIdentifier = WI.networkManager.mainFrame ? WI.networkManager.mainFrame.id : "";
|
|
|
|
target.NetworkAgent.loadResource(frameIdentifier, sourceMapURL, sourceMapLoaded);
|
|
}
|
|
|
|
_sourceMapLoadAndParseFailed(sourceMapURL)
|
|
{
|
|
this._downloadingSourceMaps.delete(sourceMapURL);
|
|
this._failedSourceMapURLs.add(sourceMapURL);
|
|
}
|
|
|
|
_sourceMapLoadAndParseSucceeded(sourceMapURL, sourceMap)
|
|
{
|
|
if (!this._downloadingSourceMaps.has(sourceMapURL))
|
|
return;
|
|
|
|
this._downloadingSourceMaps.delete(sourceMapURL);
|
|
|
|
this._sourceMapURLMap.set(sourceMapURL, sourceMap);
|
|
|
|
for (let source of sourceMap.sources())
|
|
sourceMap.addResource(new WI.SourceMapResource(source, sourceMap));
|
|
|
|
// Associate the SourceMap with the originalSourceCode.
|
|
sourceMap.originalSourceCode.addSourceMap(sourceMap);
|
|
|
|
// If the originalSourceCode was not a Resource, be sure to also associate with the Resource if one exists.
|
|
// FIXME: We should try to use the right frame instead of a global lookup by URL.
|
|
if (!(sourceMap.originalSourceCode instanceof WI.Resource)) {
|
|
console.assert(sourceMap.originalSourceCode instanceof WI.Script);
|
|
let resource = sourceMap.originalSourceCode.resource;
|
|
if (resource)
|
|
resource.addSourceMap(sourceMap);
|
|
}
|
|
}
|
|
|
|
_handleResourceContentChangedForLocalResourceOverride(event)
|
|
{
|
|
let localResourceOverride = event.target.localResourceOverride;
|
|
if (!localResourceOverride)
|
|
return;
|
|
|
|
if (!this._saveLocalResourceOverridesDebouncer) {
|
|
this._pendingLocalResourceOverrideSaves = new Set;
|
|
this._saveLocalResourceOverridesDebouncer = new Debouncer(() => {
|
|
for (let localResourceOverride of this._pendingLocalResourceOverrideSaves) {
|
|
console.assert(localResourceOverride instanceof WI.LocalResourceOverride);
|
|
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
|
|
}
|
|
});
|
|
}
|
|
|
|
this._pendingLocalResourceOverrideSaves.add(localResourceOverride);
|
|
this._saveLocalResourceOverridesDebouncer.delayForTime(500);
|
|
}
|
|
|
|
_handleResourceOverrideDisabledChanged(event)
|
|
{
|
|
console.assert(WI.NetworkManager.supportsOverridingResponses());
|
|
|
|
let localResourceOverride = event.target;
|
|
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
|
|
|
|
if (localResourceOverride.disabled)
|
|
this._removeInterception(localResourceOverride);
|
|
else
|
|
this._addInterception(localResourceOverride);
|
|
}
|
|
|
|
_handleResourceOverrideResourceErrorTypeChanged(event)
|
|
{
|
|
console.assert(WI.NetworkManager.supportsBlockingRequests());
|
|
|
|
let localResourceOverride = event.target;
|
|
WI.objectStores.localResourceOverrides.putObject(localResourceOverride);
|
|
}
|
|
|
|
_handleBootstrapScriptContentDidChange(event)
|
|
{
|
|
let source = this._bootstrapScript.content || "";
|
|
|
|
WI.objectStores.general.put(source, NetworkManager.bootstrapScriptSourceObjectStoreKey);
|
|
|
|
if (!this._bootstrapScriptEnabledSetting.value)
|
|
return;
|
|
|
|
// COMPATIBILITY (iOS 13.0): Page.setBootstrapScript did not exist yet.
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Page.setBootstrapScript"))
|
|
target.PageAgent.setBootstrapScript(source);
|
|
}
|
|
}
|
|
|
|
_extraDomainsActivated(event)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (target.hasDomain("Page") && event.data.domains.includes("Page"))
|
|
target.PageAgent.getResourceTree(this._processMainFrameResourceTreePayload.bind(this));
|
|
}
|
|
|
|
_handleFrameMainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
WI.LocalResource.resetPathsThatFailedToLoadFromFileSystem();
|
|
|
|
this._sourceMapURLMap.clear();
|
|
this._downloadingSourceMaps.clear();
|
|
this._failedSourceMapURLs.clear();
|
|
}
|
|
};
|
|
|
|
// Keep this in sync with `Network.NetworkStage`.
|
|
WI.NetworkManager.NetworkStage = {
|
|
Request: "request",
|
|
Response: "response",
|
|
};
|
|
|
|
WI.NetworkManager.EmulatedCondition = {
|
|
// Keep this first.
|
|
None: {
|
|
id: "none",
|
|
bytesPerSecondLimit: 0,
|
|
get displayName() { return WI.UIString("No throttling", "Label indicating that network throttling is inactive."); }
|
|
},
|
|
|
|
Mobile3G: {
|
|
id: "mobile-3g",
|
|
bytesPerSecondLimit: 780 * 1000 / 8, // 780kbps
|
|
get displayName() { return WI.UIString("3G", "Label indicating that network activity is being simulated with 3G connectivity."); }
|
|
},
|
|
|
|
DSL: {
|
|
id: "dsl",
|
|
bytesPerSecondLimit: 2 * 1000 * 1000 / 8, // 2mbps
|
|
get displayName() { return WI.UIString("DSL", "Label indicating that network activity is being simulated with DSL connectivity."); }
|
|
},
|
|
|
|
Edge: {
|
|
id: "edge",
|
|
bytesPerSecondLimit: 240 * 1000 / 8, // 240kbps
|
|
get displayName() { return WI.UIString("Edge", "Label indicating that network activity is being simulated with Edge connectivity."); }
|
|
},
|
|
|
|
LTE: {
|
|
id: "lte",
|
|
bytesPerSecondLimit: 50 * 1000 * 1000 / 8, // 50mbps
|
|
get displayName() { return WI.UIString("LTE", "Label indicating that network activity is being simulated with LTE connectivity"); }
|
|
},
|
|
|
|
WiFi: {
|
|
id: "wifi",
|
|
bytesPerSecondLimit: 40 * 1000 * 1000 / 8, // 40mbps
|
|
get displayName() { return WI.UIString("Wi-Fi", "Label indicating that network activity is being simulated with Wi-Fi connectivity"); }
|
|
},
|
|
|
|
WiFi802_11ac: {
|
|
id: "wifi-802_11ac",
|
|
bytesPerSecondLimit: 250 * 1000 * 1000 / 8, // 250mbps
|
|
get displayName() { return WI.UIString("Wi-Fi 802.11ac", "Label indicating that network activity is being simulated with Wi-Fi 802.11ac connectivity"); }
|
|
},
|
|
};
|
|
|
|
WI.NetworkManager.Event = {
|
|
FrameWasAdded: "network-manager-frame-was-added",
|
|
FrameWasRemoved: "network-manager-frame-was-removed",
|
|
MainFrameDidChange: "network-manager-main-frame-did-change",
|
|
BootstrapScriptCreated: "network-manager-bootstrap-script-created",
|
|
BootstrapScriptEnabledChanged: "network-manager-bootstrap-script-enabled-changed",
|
|
BootstrapScriptDestroyed: "network-manager-bootstrap-script-destroyed",
|
|
LocalResourceOverrideAdded: "network-manager-local-resource-override-added",
|
|
LocalResourceOverrideRemoved: "network-manager-local-resource-override-removed",
|
|
EmulatedConditionChanged: "network-manager-emulated-condition-changed",
|
|
};
|
|
|
|
/* Controllers/RuntimeManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.RuntimeManager = class RuntimeManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._activeExecutionContext = null;
|
|
|
|
WI.settings.consoleSavedResultAlias.addEventListener(WI.Setting.Event.Changed, function(event) {
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 12.2): Runtime.setSavedResultAlias did not exist.
|
|
if (target.hasCommand("Runtime.setSavedResultAlias"))
|
|
target.RuntimeAgent.setSavedResultAlias(WI.settings.consoleSavedResultAlias.value);
|
|
}
|
|
}, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static supportsAwaitPromise()
|
|
{
|
|
// COMPATIBILITY (iOS 12): Runtime.awaitPromise did not exist.
|
|
return InspectorBackend.hasCommand("Runtime.awaitPromise");
|
|
}
|
|
|
|
static preferredSavedResultPrefix()
|
|
{
|
|
// COMPATIBILITY (iOS 12.2): Runtime.setSavedResultAlias did not exist.
|
|
if (!InspectorBackend.hasCommand("Runtime.setSavedResultAlias"))
|
|
return "$";
|
|
return WI.settings.consoleSavedResultAlias.value || "$";
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
target.RuntimeAgent.enable();
|
|
|
|
if (WI.settings.showJavaScriptTypeInformation.value)
|
|
target.RuntimeAgent.enableTypeProfiler();
|
|
|
|
if (WI.settings.enableControlFlowProfiler.value)
|
|
target.RuntimeAgent.enableControlFlowProfiler();
|
|
|
|
// COMPATIBILITY (iOS 12.2): Runtime.setSavedResultAlias did not exist.
|
|
if (target.hasCommand("Runtime.setSavedResultAlias") && WI.settings.consoleSavedResultAlias.value)
|
|
target.RuntimeAgent.setSavedResultAlias(WI.settings.consoleSavedResultAlias.value);
|
|
}
|
|
|
|
// Public
|
|
|
|
get activeExecutionContext()
|
|
{
|
|
return this._activeExecutionContext;
|
|
}
|
|
|
|
set activeExecutionContext(executionContext)
|
|
{
|
|
if (this._activeExecutionContext === executionContext)
|
|
return;
|
|
|
|
this._activeExecutionContext = executionContext;
|
|
|
|
this.dispatchEventToListeners(WI.RuntimeManager.Event.ActiveExecutionContextChanged);
|
|
}
|
|
|
|
evaluateInInspectedWindow(expression, options, callback)
|
|
{
|
|
if (!this._activeExecutionContext) {
|
|
callback(null, false);
|
|
return;
|
|
}
|
|
|
|
let {objectGroup, includeCommandLineAPI, doNotPauseOnExceptionsAndMuteConsole, returnByValue, generatePreview, saveResult, emulateUserGesture, sourceURLAppender} = options;
|
|
|
|
includeCommandLineAPI = includeCommandLineAPI || false;
|
|
doNotPauseOnExceptionsAndMuteConsole = doNotPauseOnExceptionsAndMuteConsole || false;
|
|
returnByValue = returnByValue || false;
|
|
generatePreview = generatePreview || false;
|
|
saveResult = saveResult || false;
|
|
emulateUserGesture = emulateUserGesture || false;
|
|
sourceURLAppender = sourceURLAppender || appendWebInspectorSourceURL;
|
|
|
|
console.assert(objectGroup, "RuntimeManager.evaluateInInspectedWindow should always be called with an objectGroup");
|
|
console.assert(typeof sourceURLAppender === "function");
|
|
|
|
if (!expression) {
|
|
// There is no expression, so the completion should happen against global properties.
|
|
expression = "this";
|
|
} else if (/^\s*\{/.test(expression) && /\}\s*$/.test(expression)) {
|
|
// Transform {a:1} to ({a:1}) so it is treated like an object literal instead of a block with a label.
|
|
expression = "(" + expression + ")";
|
|
} else if (/\bawait\b/.test(expression)) {
|
|
// Transform `await <expr>` into an async function assignment.
|
|
expression = this._tryApplyAwaitConvenience(expression);
|
|
}
|
|
|
|
expression = sourceURLAppender(expression);
|
|
|
|
let target = this._activeExecutionContext.target;
|
|
let executionContextId = this._activeExecutionContext.id;
|
|
|
|
if (WI.debuggerManager.activeCallFrame) {
|
|
target = WI.debuggerManager.activeCallFrame.target;
|
|
executionContextId = target.executionContext.id;
|
|
}
|
|
|
|
function evalCallback(error, result, wasThrown, savedResultIndex)
|
|
{
|
|
this.dispatchEventToListeners(WI.RuntimeManager.Event.DidEvaluate, {objectGroup});
|
|
|
|
if (error) {
|
|
console.error(error);
|
|
callback(null, false);
|
|
return;
|
|
}
|
|
|
|
if (returnByValue)
|
|
callback(null, wasThrown, wasThrown ? null : result, savedResultIndex);
|
|
else
|
|
callback(WI.RemoteObject.fromPayload(result, target), wasThrown, savedResultIndex);
|
|
}
|
|
|
|
if (WI.debuggerManager.activeCallFrame) {
|
|
target.DebuggerAgent.evaluateOnCallFrame.invoke({
|
|
callFrameId: WI.debuggerManager.activeCallFrame.id,
|
|
expression,
|
|
objectGroup,
|
|
includeCommandLineAPI,
|
|
doNotPauseOnExceptionsAndMuteConsole,
|
|
returnByValue,
|
|
generatePreview,
|
|
saveResult,
|
|
emulateUserGesture, // COMPATIBILITY (iOS 13): "emulateUserGesture" did not exist yet.
|
|
}, evalCallback.bind(this));
|
|
return;
|
|
}
|
|
|
|
target.RuntimeAgent.evaluate.invoke({
|
|
expression,
|
|
objectGroup,
|
|
includeCommandLineAPI,
|
|
doNotPauseOnExceptionsAndMuteConsole,
|
|
contextId: executionContextId,
|
|
returnByValue,
|
|
generatePreview,
|
|
saveResult,
|
|
emulateUserGesture, // COMPATIBILITY (iOS 12.2): "emulateUserGesture" did not exist yet.
|
|
}, evalCallback.bind(this));
|
|
}
|
|
|
|
saveResult(remoteObject, callback)
|
|
{
|
|
console.assert(remoteObject instanceof WI.RemoteObject);
|
|
|
|
let target = this._activeExecutionContext.target;
|
|
let executionContextId = this._activeExecutionContext.id;
|
|
|
|
function mycallback(error, savedResultIndex)
|
|
{
|
|
callback(savedResultIndex);
|
|
}
|
|
|
|
if (remoteObject.objectId)
|
|
target.RuntimeAgent.saveResult(remoteObject.asCallArgument(), mycallback);
|
|
else
|
|
target.RuntimeAgent.saveResult(remoteObject.asCallArgument(), executionContextId, mycallback);
|
|
}
|
|
|
|
// Private
|
|
|
|
_tryApplyAwaitConvenience(originalExpression)
|
|
{
|
|
let esprimaSyntaxTree;
|
|
|
|
// Do not transform if the original code parses just fine.
|
|
try {
|
|
esprima.parse(originalExpression);
|
|
return originalExpression;
|
|
} catch { }
|
|
|
|
// Do not transform if the async function version does not parse.
|
|
try {
|
|
esprimaSyntaxTree = esprima.parse("(async function(){" + originalExpression + "})");
|
|
} catch {
|
|
return originalExpression;
|
|
}
|
|
|
|
// Assert expected AST produced by our wrapping code.
|
|
console.assert(esprimaSyntaxTree.type === "Program");
|
|
console.assert(esprimaSyntaxTree.body.length === 1);
|
|
console.assert(esprimaSyntaxTree.body[0].type === "ExpressionStatement");
|
|
console.assert(esprimaSyntaxTree.body[0].expression.type === "FunctionExpression");
|
|
console.assert(esprimaSyntaxTree.body[0].expression.async);
|
|
console.assert(esprimaSyntaxTree.body[0].expression.body.type === "BlockStatement");
|
|
|
|
// Do not transform if there is more than one statement.
|
|
let asyncFunctionBlock = esprimaSyntaxTree.body[0].expression.body;
|
|
if (asyncFunctionBlock.body.length !== 1)
|
|
return originalExpression;
|
|
|
|
// Extract the variable name for transformation.
|
|
let variableName;
|
|
let anonymous = false;
|
|
let declarationKind = "var";
|
|
let awaitPortion;
|
|
let statement = asyncFunctionBlock.body[0];
|
|
if (statement.type === "ExpressionStatement"
|
|
&& statement.expression.type === "AwaitExpression") {
|
|
// await <expr>
|
|
anonymous = true;
|
|
} else if (statement.type === "ExpressionStatement"
|
|
&& statement.expression.type === "AssignmentExpression"
|
|
&& statement.expression.right.type === "AwaitExpression"
|
|
&& statement.expression.left.type === "Identifier") {
|
|
// x = await <expr>
|
|
variableName = statement.expression.left.name;
|
|
awaitPortion = originalExpression.substring(originalExpression.indexOf("await"));
|
|
} else if (statement.type === "VariableDeclaration"
|
|
&& statement.declarations.length === 1
|
|
&& statement.declarations[0].init.type === "AwaitExpression"
|
|
&& statement.declarations[0].id.type === "Identifier") {
|
|
// var x = await <expr>
|
|
variableName = statement.declarations[0].id.name;
|
|
declarationKind = statement.kind;
|
|
awaitPortion = originalExpression.substring(originalExpression.indexOf("await"));
|
|
} else {
|
|
// Do not transform if this was not one of the simple supported syntaxes.
|
|
return originalExpression;
|
|
}
|
|
|
|
if (anonymous) {
|
|
return `
|
|
(async function() {
|
|
try {
|
|
let result = ${originalExpression};
|
|
console.info("%o", result);
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
})();
|
|
undefined`;
|
|
}
|
|
|
|
return `${declarationKind} ${variableName};
|
|
(async function() {
|
|
try {
|
|
${variableName} = ${awaitPortion};
|
|
console.info("%o", ${variableName});
|
|
} catch (e) {
|
|
console.error(e);
|
|
}
|
|
})();
|
|
undefined;`;
|
|
}
|
|
};
|
|
|
|
WI.RuntimeManager.ConsoleObjectGroup = "console";
|
|
WI.RuntimeManager.TopLevelExecutionContextIdentifier = undefined;
|
|
|
|
WI.RuntimeManager.Event = {
|
|
DidEvaluate: "runtime-manager-did-evaluate",
|
|
DefaultExecutionContextChanged: "runtime-manager-default-execution-context-changed",
|
|
ActiveExecutionContextChanged: "runtime-manager-active-execution-context-changed",
|
|
};
|
|
|
|
/* Controllers/TargetManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016-2018 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TargetManager = class TargetManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._targets = new Map;
|
|
this._cachedTargetsList = null;
|
|
this._seenPageTarget = false;
|
|
this._transitionTimeoutIdentifier = undefined;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
// COMPATIBILITY (iOS 13): Target.setPauseOnStart did not exist yet.
|
|
if (target.hasCommand("Target.setPauseOnStart"))
|
|
target.TargetAgent.setPauseOnStart(true);
|
|
}
|
|
|
|
// Public
|
|
|
|
get targets()
|
|
{
|
|
if (!this._cachedTargetsList)
|
|
this._cachedTargetsList = Array.from(this._targets.values()).filter((target) => !(target instanceof WI.MultiplexingBackendTarget));
|
|
return this._cachedTargetsList;
|
|
}
|
|
|
|
get workerTargets()
|
|
{
|
|
return this.targets.filter((target) => target.type === WI.TargetType.Worker);
|
|
}
|
|
|
|
get allTargets()
|
|
{
|
|
return Array.from(this._targets.values());
|
|
}
|
|
|
|
targetForIdentifier(targetId)
|
|
{
|
|
if (!targetId)
|
|
return null;
|
|
|
|
for (let target of this._targets.values()) {
|
|
if (target.identifier === targetId)
|
|
return target;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
addTarget(target)
|
|
{
|
|
console.assert(target);
|
|
console.assert(!this._targets.has(target.identifier));
|
|
|
|
this._cachedTargetsList = null;
|
|
this._targets.set(target.identifier, target);
|
|
|
|
this.dispatchEventToListeners(WI.TargetManager.Event.TargetAdded, {target});
|
|
}
|
|
|
|
removeTarget(target)
|
|
{
|
|
console.assert(target);
|
|
console.assert(target !== WI.mainTarget);
|
|
|
|
this._cachedTargetsList = null;
|
|
this._targets.delete(target.identifier);
|
|
target.destroy();
|
|
|
|
this.dispatchEventToListeners(WI.TargetManager.Event.TargetRemoved, {target});
|
|
}
|
|
|
|
createMultiplexingBackendTarget()
|
|
{
|
|
console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.WebPage);
|
|
|
|
let target = new WI.MultiplexingBackendTarget;
|
|
target.initialize();
|
|
|
|
this._initializeBackendTarget(target);
|
|
|
|
// Add the target without dispatching an event.
|
|
this._targets.set(target.identifier, target);
|
|
}
|
|
|
|
createDirectBackendTarget()
|
|
{
|
|
console.assert(WI.sharedApp.debuggableType !== WI.DebuggableType.WebPage);
|
|
|
|
let target = new WI.DirectBackendTarget;
|
|
target.initialize();
|
|
|
|
this._initializeBackendTarget(target);
|
|
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.ITML || WI.sharedApp.debuggableType === WI.DebuggableType.Page)
|
|
this._initializePageTarget(target);
|
|
|
|
this.addTarget(target);
|
|
}
|
|
|
|
// TargetObserver
|
|
|
|
targetCreated(parentTarget, targetInfo)
|
|
{
|
|
let connection = new InspectorBackend.TargetConnection(parentTarget, targetInfo.targetId);
|
|
let subTarget = this._createTarget(parentTarget, targetInfo, connection);
|
|
this._checkAndHandlePageTargetTransition(subTarget);
|
|
subTarget.initialize();
|
|
this.addTarget(subTarget);
|
|
}
|
|
|
|
didCommitProvisionalTarget(parentTarget, previousTargetId, newTargetId)
|
|
{
|
|
this.targetDestroyed(previousTargetId);
|
|
let target = this._targets.get(newTargetId);
|
|
console.assert(target);
|
|
if (!target)
|
|
return;
|
|
|
|
target.didCommitProvisionalTarget();
|
|
this._checkAndHandlePageTargetTransition(target);
|
|
target.connection.dispatchProvisionalMessages();
|
|
|
|
this.dispatchEventToListeners(WI.TargetManager.Event.DidCommitProvisionalTarget, {previousTargetId, target});
|
|
}
|
|
|
|
targetDestroyed(targetId)
|
|
{
|
|
let target = this._targets.get(targetId);
|
|
if (!target)
|
|
return;
|
|
|
|
this._checkAndHandlePageTargetTermination(target);
|
|
this.removeTarget(target);
|
|
}
|
|
|
|
dispatchMessageFromTarget(targetId, message)
|
|
{
|
|
let target = this._targets.get(targetId);
|
|
console.assert(target);
|
|
if (!target)
|
|
return;
|
|
|
|
if (target.isProvisional)
|
|
target.connection.addProvisionalMessage(message);
|
|
else
|
|
target.connection.dispatch(message);
|
|
}
|
|
|
|
// Private
|
|
|
|
_createTarget(parentTarget, targetInfo, connection)
|
|
{
|
|
// COMPATIBILITY (iOS 13.0): `Target.TargetInfo.isProvisional` and `Target.TargetInfo.isPaused` did not exist yet.
|
|
let {targetId, type, isProvisional, isPaused} = targetInfo;
|
|
|
|
switch (type) {
|
|
case InspectorBackend.Enum.Target.TargetInfoType.Page:
|
|
return new WI.PageTarget(parentTarget, targetId, WI.UIString("Page"), connection, {isProvisional, isPaused});
|
|
case InspectorBackend.Enum.Target.TargetInfoType.Worker:
|
|
return new WI.WorkerTarget(parentTarget, targetId, WI.UIString("Worker"), connection, {isPaused});
|
|
case "serviceworker": // COMPATIBILITY (iOS 13): "serviceworker" was renamed to "service-worker".
|
|
case InspectorBackend.Enum.Target.TargetInfoType.ServiceWorker:
|
|
return new WI.WorkerTarget(parentTarget, targetId, WI.UIString("ServiceWorker"), connection, {isPaused});
|
|
}
|
|
|
|
throw "Unknown Target type: " + type;
|
|
}
|
|
|
|
_checkAndHandlePageTargetTransition(target)
|
|
{
|
|
if (target.type !== WI.TargetType.Page)
|
|
return;
|
|
|
|
if (target.isProvisional)
|
|
return;
|
|
|
|
// First page target.
|
|
if (!WI.pageTarget && !this._seenPageTarget) {
|
|
this._seenPageTarget = true;
|
|
this._initializePageTarget(target);
|
|
return;
|
|
}
|
|
|
|
// Transitioning page target.
|
|
this._transitionPageTarget(target);
|
|
}
|
|
|
|
_checkAndHandlePageTargetTermination(target)
|
|
{
|
|
if (target.type !== WI.TargetType.Page)
|
|
return;
|
|
|
|
if (target.isProvisional)
|
|
return;
|
|
|
|
console.assert(target === WI.pageTarget);
|
|
console.assert(this._seenPageTarget);
|
|
|
|
// Terminating the page target.
|
|
this._terminatePageTarget(target);
|
|
|
|
// Ensure we transition in a reasonable amount of time, otherwise close.
|
|
const timeToTransition = 2000;
|
|
clearTimeout(this._transitionTimeoutIdentifier);
|
|
this._transitionTimeoutIdentifier = setTimeout(() => {
|
|
this._transitionTimeoutIdentifier = undefined;
|
|
if (WI.pageTarget)
|
|
return;
|
|
if (WI.isEngineeringBuild)
|
|
throw new Error("Error: No new pageTarget some time after last page target was terminated. Failed transition?");
|
|
WI.close();
|
|
}, timeToTransition);
|
|
}
|
|
|
|
_initializeBackendTarget(target)
|
|
{
|
|
console.assert(!WI.mainTarget);
|
|
|
|
WI.backendTarget = target;
|
|
|
|
this._resetMainExecutionContext();
|
|
|
|
WI._backendTargetAvailablePromise.resolve();
|
|
}
|
|
|
|
_initializePageTarget(target)
|
|
{
|
|
console.assert(WI.sharedApp.isWebDebuggable() || WI.sharedApp.debuggableType === WI.DebuggableType.ITML);
|
|
console.assert(target.type === WI.TargetType.Page || target instanceof WI.DirectBackendTarget);
|
|
|
|
WI.pageTarget = target;
|
|
|
|
this._resetMainExecutionContext();
|
|
|
|
WI._pageTargetAvailablePromise.resolve();
|
|
}
|
|
|
|
_transitionPageTarget(target)
|
|
{
|
|
console.assert(!WI.pageTarget);
|
|
console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.WebPage);
|
|
console.assert(target.type === WI.TargetType.Page);
|
|
|
|
WI.pageTarget = target;
|
|
|
|
this._resetMainExecutionContext();
|
|
|
|
// Actions to transition the page target.
|
|
WI.notifications.dispatchEventToListeners(WI.Notification.TransitionPageTarget);
|
|
WI.domManager.transitionPageTarget();
|
|
WI.networkManager.transitionPageTarget();
|
|
WI.timelineManager.transitionPageTarget();
|
|
}
|
|
|
|
_terminatePageTarget(target)
|
|
{
|
|
console.assert(WI.pageTarget);
|
|
console.assert(WI.pageTarget === target);
|
|
console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.WebPage);
|
|
|
|
// Remove any Worker targets associated with this page.
|
|
for (let workerTarget of this.workerTargets)
|
|
WI.workerManager.workerTerminated(workerTarget.identifier);
|
|
|
|
WI.pageTarget = null;
|
|
}
|
|
|
|
_resetMainExecutionContext()
|
|
{
|
|
if (WI.mainTarget instanceof WI.MultiplexingBackendTarget)
|
|
return;
|
|
|
|
if (WI.mainTarget.executionContext)
|
|
WI.runtimeManager.activeExecutionContext = WI.mainTarget.executionContext;
|
|
}
|
|
};
|
|
|
|
WI.TargetManager.Event = {
|
|
TargetAdded: "target-manager-target-added",
|
|
TargetRemoved: "target-manager-target-removed",
|
|
DidCommitProvisionalTarget: "target-manager-provisional-target-committed",
|
|
};
|
|
|
|
/* Controllers/TimelineManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
// FIXME: TimelineManager lacks advanced multi-target support. (Instruments/Profilers per-target)
|
|
|
|
WI.TimelineManager = class TimelineManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._enabled = false;
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.ProvisionalLoadStarted, this._provisionalLoadStarted, this);
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this);
|
|
|
|
WI.consoleManager.addEventListener(WI.ConsoleManager.Event.MessageAdded, this._handleMessageAdded, this);
|
|
|
|
this._enabledTimelineTypesSetting = new WI.Setting("enabled-instrument-types", WI.TimelineManager.defaultTimelineTypes());
|
|
|
|
this._capturingState = TimelineManager.CapturingState.Inactive;
|
|
this._capturingInstrumentCount = 0;
|
|
this._capturingStartTime = NaN;
|
|
this._capturingEndTime = NaN;
|
|
|
|
this._initiatedByBackendStart = false;
|
|
this._initiatedByBackendStop = false;
|
|
|
|
this._isCapturingPageReload = false;
|
|
this._autoCaptureOnPageLoad = false;
|
|
this._mainResourceForAutoCapturing = null;
|
|
this._shouldSetAutoCapturingMainResource = false;
|
|
this._transitioningPageTarget = false;
|
|
|
|
this._webTimelineScriptRecordsExpectingScriptProfilerEvents = null;
|
|
this._scriptProfilerRecords = null;
|
|
|
|
this._boundStopCapturing = this.stopCapturing.bind(this);
|
|
this._stopCapturingTimeout = undefined;
|
|
this._deadTimeTimeout = undefined;
|
|
this._lastDeadTimeTickle = 0;
|
|
}
|
|
|
|
// Agent
|
|
|
|
get domains() { return ["Timeline"]; }
|
|
|
|
activateExtraDomain(domain)
|
|
{
|
|
// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type
|
|
|
|
console.assert(domain === "Timeline");
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
if (target.hasDomain("Timeline")) {
|
|
// COMPATIBILITY (iOS 13): Timeline.enable did not exist yet.
|
|
if (target.hasCommand("Timeline.enable"))
|
|
target.TimelineAgent.enable();
|
|
|
|
this._updateAutoCaptureInstruments([target]);
|
|
|
|
target.TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad);
|
|
}
|
|
}
|
|
|
|
transitionPageTarget()
|
|
{
|
|
this._transitioningPageTarget = true;
|
|
}
|
|
|
|
// Static
|
|
|
|
static defaultTimelineTypes()
|
|
{
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript || WI.sharedApp.debuggableType === WI.DebuggableType.ITML) {
|
|
return [
|
|
WI.TimelineRecord.Type.Script,
|
|
WI.TimelineRecord.Type.HeapAllocations,
|
|
];
|
|
}
|
|
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) {
|
|
// FIXME: Support Network Timeline in ServiceWorker.
|
|
return [
|
|
WI.TimelineRecord.Type.Script,
|
|
WI.TimelineRecord.Type.HeapAllocations,
|
|
];
|
|
}
|
|
|
|
let defaultTypes = [];
|
|
|
|
if (WI.ScreenshotsInstrument.supported())
|
|
defaultTypes.push(WI.TimelineRecord.Type.Screenshots);
|
|
|
|
defaultTypes.push(WI.TimelineRecord.Type.Network);
|
|
defaultTypes.push(WI.TimelineRecord.Type.Layout);
|
|
defaultTypes.push(WI.TimelineRecord.Type.Script);
|
|
defaultTypes.push(WI.TimelineRecord.Type.RenderingFrame);
|
|
|
|
if (WI.CPUInstrument.supported())
|
|
defaultTypes.push(WI.TimelineRecord.Type.CPU);
|
|
|
|
return defaultTypes;
|
|
}
|
|
|
|
static availableTimelineTypes()
|
|
{
|
|
let types = WI.TimelineManager.defaultTimelineTypes();
|
|
if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript || WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker || WI.sharedApp.debuggableType === WI.DebuggableType.ITML)
|
|
return types;
|
|
|
|
types.push(WI.TimelineRecord.Type.Memory);
|
|
types.push(WI.TimelineRecord.Type.HeapAllocations);
|
|
|
|
if (WI.MediaInstrument.supported()) {
|
|
let insertionIndex = types.indexOf(WI.TimelineRecord.Type.Layout) + 1;
|
|
types.insertAtIndex(WI.TimelineRecord.Type.Media, insertionIndex || types.length);
|
|
}
|
|
|
|
return types;
|
|
}
|
|
|
|
static synthesizeImportError(message)
|
|
{
|
|
message = WI.UIString("Timeline Recording Import Error: %s").format(message);
|
|
|
|
if (window.InspectorTest) {
|
|
console.error(message);
|
|
return;
|
|
}
|
|
|
|
let consoleMessage = new WI.ConsoleMessage(WI.mainTarget, WI.ConsoleMessage.MessageSource.Other, WI.ConsoleMessage.MessageLevel.Error, message);
|
|
consoleMessage.shouldRevealConsole = true;
|
|
|
|
WI.consoleLogViewController.appendConsoleMessage(consoleMessage);
|
|
}
|
|
|
|
// Public
|
|
|
|
get capturingState() { return this._capturingState; }
|
|
|
|
reset()
|
|
{
|
|
if (this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active)
|
|
this.stopCapturing();
|
|
|
|
this._recordings = [];
|
|
this._activeRecording = null;
|
|
this._nextRecordingIdentifier = 1;
|
|
|
|
this._loadNewRecording();
|
|
}
|
|
|
|
// The current recording that new timeline records will be appended to, if any.
|
|
get activeRecording()
|
|
{
|
|
console.assert(this._activeRecording || !this.isCapturing());
|
|
return this._activeRecording;
|
|
}
|
|
|
|
get autoCaptureOnPageLoad()
|
|
{
|
|
return this._autoCaptureOnPageLoad;
|
|
}
|
|
|
|
set autoCaptureOnPageLoad(autoCapture)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
autoCapture = !!autoCapture;
|
|
|
|
if (this._autoCaptureOnPageLoad === autoCapture)
|
|
return;
|
|
|
|
this._autoCaptureOnPageLoad = autoCapture;
|
|
|
|
for (let target of WI.targets) {
|
|
if (target.hasCommand("Timeline.setAutoCaptureEnabled"))
|
|
target.TimelineAgent.setAutoCaptureEnabled(this._autoCaptureOnPageLoad);
|
|
}
|
|
}
|
|
|
|
get enabledTimelineTypes()
|
|
{
|
|
let availableTimelineTypes = WI.TimelineManager.availableTimelineTypes();
|
|
return this._enabledTimelineTypesSetting.value.filter((type) => availableTimelineTypes.includes(type));
|
|
}
|
|
|
|
set enabledTimelineTypes(x)
|
|
{
|
|
this._enabledTimelineTypesSetting.value = x || [];
|
|
|
|
this._updateAutoCaptureInstruments(WI.targets);
|
|
}
|
|
|
|
isCapturing()
|
|
{
|
|
return this._capturingState !== TimelineManager.CapturingState.Inactive;
|
|
}
|
|
|
|
isCapturingPageReload()
|
|
{
|
|
return this._isCapturingPageReload;
|
|
}
|
|
|
|
willAutoStop()
|
|
{
|
|
return !!this._stopCapturingTimeout;
|
|
}
|
|
|
|
relaxAutoStop()
|
|
{
|
|
if (this._stopCapturingTimeout) {
|
|
clearTimeout(this._stopCapturingTimeout);
|
|
this._stopCapturingTimeout = undefined;
|
|
}
|
|
|
|
if (this._deadTimeTimeout) {
|
|
clearTimeout(this._deadTimeTimeout);
|
|
this._deadTimeTimeout = undefined;
|
|
}
|
|
}
|
|
|
|
enable()
|
|
{
|
|
if (this._enabled)
|
|
return;
|
|
|
|
this._enabled = true;
|
|
|
|
this.reset();
|
|
|
|
for (let target of WI.targets)
|
|
this.initializeTarget(target);
|
|
}
|
|
|
|
disable()
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
this.reset();
|
|
|
|
for (let target of WI.targets) {
|
|
// COMPATIBILITY (iOS 13): Timeline.disable did not exist yet.
|
|
if (target.hasCommand("Timeline.disable"))
|
|
target.TimelineAgent.disable();
|
|
}
|
|
|
|
this._enabled = false;
|
|
}
|
|
|
|
startCapturing(shouldCreateRecording)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
console.assert(this._capturingState === TimelineManager.CapturingState.Stopping || this._capturingState === TimelineManager.CapturingState.Inactive, "TimelineManager is already capturing.");
|
|
if (this._capturingState !== TimelineManager.CapturingState.Stopping && this._capturingState !== TimelineManager.CapturingState.Inactive)
|
|
return;
|
|
|
|
if (!this._activeRecording || shouldCreateRecording)
|
|
this._loadNewRecording();
|
|
|
|
this._updateCapturingState(TimelineManager.CapturingState.Starting);
|
|
|
|
this._capturingStartTime = NaN;
|
|
this._activeRecording.start(this._initiatedByBackendStart);
|
|
}
|
|
|
|
stopCapturing()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
console.assert(this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active, "TimelineManager is not capturing.");
|
|
if (this._capturingState !== TimelineManager.CapturingState.Starting && this._capturingState !== TimelineManager.CapturingState.Active)
|
|
return;
|
|
|
|
this._updateCapturingState(TimelineManager.CapturingState.Stopping);
|
|
|
|
this._capturingEndTime = NaN;
|
|
this._activeRecording.stop(this._initiatedByBackendStop);
|
|
}
|
|
|
|
async processJSON({filename, json, error})
|
|
{
|
|
if (error) {
|
|
WI.TimelineManager.synthesizeImportError(error);
|
|
return;
|
|
}
|
|
|
|
if (typeof json !== "object" || json === null) {
|
|
WI.TimelineManager.synthesizeImportError(WI.UIString("invalid JSON"));
|
|
return;
|
|
}
|
|
|
|
if (!json.recording || typeof json.recording !== "object" || !json.overview || typeof json.overview !== "object" || typeof json.version !== "number") {
|
|
WI.TimelineManager.synthesizeImportError(WI.UIString("invalid JSON"));
|
|
return;
|
|
}
|
|
|
|
if (json.version !== WI.TimelineRecording.SerializationVersion) {
|
|
WI.NetworkManager.synthesizeImportError(WI.UIString("unsupported version"));
|
|
return;
|
|
}
|
|
|
|
let recordingData = json.recording;
|
|
let overviewData = json.overview;
|
|
|
|
let identifier = this._nextRecordingIdentifier++;
|
|
let newRecording = await WI.TimelineRecording.import(identifier, recordingData, filename);
|
|
this._recordings.push(newRecording);
|
|
|
|
this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active)
|
|
this.stopCapturing();
|
|
|
|
let oldRecording = this._activeRecording;
|
|
if (oldRecording) {
|
|
const importing = true;
|
|
oldRecording.unloaded(importing);
|
|
}
|
|
|
|
this._activeRecording = newRecording;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingLoaded, {oldRecording});
|
|
this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingImported, {overviewData});
|
|
}
|
|
|
|
computeElapsedTime(timestamp)
|
|
{
|
|
if (!this._activeRecording)
|
|
return 0;
|
|
|
|
return this._activeRecording.computeElapsedTime(timestamp);
|
|
}
|
|
|
|
scriptProfilerIsTracking()
|
|
{
|
|
return this._scriptProfilerRecords !== null;
|
|
}
|
|
|
|
// ConsoleObserver
|
|
|
|
heapSnapshotAdded(timestamp, snapshot)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
|
|
}
|
|
|
|
// TimelineObserver
|
|
|
|
capturingStarted(startTime)
|
|
{
|
|
// The frontend didn't start capturing, so this was a programmatic start.
|
|
if (this._capturingState === TimelineManager.CapturingState.Inactive) {
|
|
this._initiatedByBackendStart = true;
|
|
this._activeRecording.addScriptInstrumentForProgrammaticCapture();
|
|
this.startCapturing();
|
|
}
|
|
|
|
if (!isNaN(startTime)) {
|
|
if (isNaN(this._capturingStartTime) || startTime < this._capturingStartTime)
|
|
this._capturingStartTime = startTime;
|
|
|
|
this._activeRecording.initializeTimeBoundsIfNecessary(startTime);
|
|
}
|
|
|
|
this._capturingInstrumentCount++;
|
|
console.assert(this._capturingInstrumentCount);
|
|
if (this._capturingInstrumentCount > 1)
|
|
return;
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Active)
|
|
return;
|
|
|
|
this._lastDeadTimeTickle = 0;
|
|
|
|
this._webTimelineScriptRecordsExpectingScriptProfilerEvents = [];
|
|
|
|
this._activeRecording.capturingStarted(this._capturingStartTime);
|
|
|
|
WI.settings.timelinesAutoStop.addEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
|
|
WI.Target.addEventListener(WI.Target.Event.ResourceAdded, this._resourceWasAdded, this);
|
|
|
|
WI.heapManager.addEventListener(WI.HeapManager.Event.GarbageCollected, this._garbageCollected, this);
|
|
|
|
WI.memoryManager.addEventListener(WI.MemoryManager.Event.MemoryPressure, this._memoryPressure, this);
|
|
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
|
|
WI.DOMNode.addEventListener(WI.DOMNode.Event.PowerEfficientPlaybackStateChanged, this._handleDOMNodePowerEfficientPlaybackStateChanged, this);
|
|
|
|
this._updateCapturingState(TimelineManager.CapturingState.Active, {startTime: this._capturingStartTime});
|
|
}
|
|
|
|
capturingStopped(endTime)
|
|
{
|
|
// The frontend didn't stop capturing, so this was a programmatic stop.
|
|
if (this._capturingState === TimelineManager.CapturingState.Active) {
|
|
this._initiatedByBackendStop = true;
|
|
this.stopCapturing();
|
|
}
|
|
|
|
if (!isNaN(endTime)) {
|
|
if (isNaN(this._capturingEndTime) || endTime > this._capturingEndTime)
|
|
this._capturingEndTime = endTime;
|
|
}
|
|
|
|
this._capturingInstrumentCount--;
|
|
console.assert(this._capturingInstrumentCount >= 0);
|
|
if (this._capturingInstrumentCount)
|
|
return;
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Inactive)
|
|
return;
|
|
|
|
WI.DOMNode.removeEventListener(WI.DOMNode.Event.DidFireEvent, this._handleDOMNodeDidFireEvent, this);
|
|
WI.DOMNode.removeEventListener(WI.DOMNode.Event.PowerEfficientPlaybackStateChanged, this._handleDOMNodePowerEfficientPlaybackStateChanged, this);
|
|
|
|
WI.heapManager.removeEventListener(WI.HeapManager.Event.GarbageCollected, this._garbageCollected, this);
|
|
|
|
WI.memoryManager.removeEventListener(WI.MemoryManager.Event.MemoryPressure, this._memoryPressure, this);
|
|
|
|
WI.Target.removeEventListener(WI.Target.Event.ResourceAdded, this._resourceWasAdded, this);
|
|
WI.Frame.removeEventListener(WI.Frame.Event.ResourceWasAdded, this._resourceWasAdded, this);
|
|
|
|
WI.settings.timelinesAutoStop.removeEventListener(WI.Setting.Event.Changed, this._handleTimelinesAutoStopSettingChanged, this);
|
|
|
|
this._activeRecording.capturingStopped(this._capturingEndTime);
|
|
|
|
this.relaxAutoStop();
|
|
|
|
this._isCapturingPageReload = false;
|
|
this._shouldSetAutoCapturingMainResource = false;
|
|
this._mainResourceForAutoCapturing = null;
|
|
this._initiatedByBackendStart = false;
|
|
this._initiatedByBackendStop = false;
|
|
|
|
this._updateCapturingState(TimelineManager.CapturingState.Inactive, {endTime: this._capturingEndTime});
|
|
}
|
|
|
|
autoCaptureStarted()
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let waitingForCapturingStartedEvent = this._capturingState === TimelineManager.CapturingState.Starting;
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active)
|
|
this.stopCapturing();
|
|
|
|
this._initiatedByBackendStart = true;
|
|
|
|
// We may already have an fresh TimelineRecording created if autoCaptureStarted is received
|
|
// between sending the Timeline.start command and receiving Timeline.capturingStarted event.
|
|
// In that case, there is no need to call startCapturing again. Reuse the fresh recording.
|
|
if (!waitingForCapturingStartedEvent) {
|
|
const createNewRecording = true;
|
|
this.startCapturing(createNewRecording);
|
|
}
|
|
|
|
this._shouldSetAutoCapturingMainResource = true;
|
|
}
|
|
|
|
eventRecorded(recordPayload)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this.isCapturing());
|
|
if (!this.isCapturing())
|
|
return;
|
|
|
|
var records = [];
|
|
|
|
// Iterate over the records tree using a stack. Doing this recursively has
|
|
// been known to cause a call stack overflow. https://webkit.org/b/79106
|
|
var stack = [{array: [recordPayload], parent: null, parentRecord: null, index: 0}];
|
|
while (stack.length) {
|
|
var entry = stack.lastValue;
|
|
var recordPayloads = entry.array;
|
|
|
|
if (entry.index < recordPayloads.length) {
|
|
var recordPayload = recordPayloads[entry.index];
|
|
var record = this._processEvent(recordPayload, entry.parent);
|
|
if (record) {
|
|
record.parent = entry.parentRecord;
|
|
records.push(record);
|
|
if (entry.parentRecord)
|
|
entry.parentRecord.children.push(record);
|
|
}
|
|
|
|
if (recordPayload.children && recordPayload.children.length)
|
|
stack.push({array: recordPayload.children, parent: recordPayload, parentRecord: record || entry.parentRecord, index: 0});
|
|
++entry.index;
|
|
} else
|
|
stack.pop();
|
|
}
|
|
|
|
for (var record of records) {
|
|
if (record.type === WI.TimelineRecord.Type.RenderingFrame) {
|
|
if (!record.children.length)
|
|
continue;
|
|
record.setupFrameIndex();
|
|
}
|
|
|
|
this._addRecord(record);
|
|
}
|
|
}
|
|
|
|
// PageObserver
|
|
|
|
pageDOMContentLoadedEventFired(timestamp)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this._activeRecording);
|
|
|
|
let computedTimestamp = this._activeRecording.computeElapsedTime(timestamp);
|
|
|
|
if (WI.networkManager.mainFrame)
|
|
WI.networkManager.mainFrame.markDOMContentReadyEvent(computedTimestamp);
|
|
|
|
let eventMarker = new WI.TimelineMarker(computedTimestamp, WI.TimelineMarker.Type.DOMContentEvent);
|
|
this._activeRecording.addEventMarker(eventMarker);
|
|
}
|
|
|
|
pageLoadEventFired(timestamp)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this._activeRecording);
|
|
|
|
let computedTimestamp = this._activeRecording.computeElapsedTime(timestamp);
|
|
|
|
if (WI.networkManager.mainFrame)
|
|
WI.networkManager.mainFrame.markLoadEvent(computedTimestamp);
|
|
|
|
let eventMarker = new WI.TimelineMarker(computedTimestamp, WI.TimelineMarker.Type.LoadEvent);
|
|
this._activeRecording.addEventMarker(eventMarker);
|
|
|
|
this._stopAutoRecordingSoon();
|
|
}
|
|
|
|
// CPUProfilerObserver
|
|
|
|
cpuProfilerTrackingStarted(timestamp)
|
|
{
|
|
this.capturingStarted(timestamp);
|
|
}
|
|
|
|
cpuProfilerTrackingUpdated(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this.isCapturing());
|
|
if (!this.isCapturing())
|
|
return;
|
|
|
|
this._addRecord(new WI.CPUTimelineRecord(event));
|
|
}
|
|
|
|
cpuProfilerTrackingCompleted(timestamp)
|
|
{
|
|
this.capturingStopped(timestamp);
|
|
}
|
|
|
|
// ScriptProfilerObserver
|
|
|
|
scriptProfilerTrackingStarted(timestamp)
|
|
{
|
|
this._scriptProfilerRecords = [];
|
|
|
|
this.capturingStarted(timestamp);
|
|
}
|
|
|
|
scriptProfilerTrackingUpdated(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let {startTime, endTime, type} = event;
|
|
let scriptRecordType = this._scriptProfilerTypeToScriptTimelineRecordType(type);
|
|
let record = new WI.ScriptTimelineRecord(scriptRecordType, startTime, endTime, null, null, null, null);
|
|
record.__scriptProfilerType = type;
|
|
this._scriptProfilerRecords.push(record);
|
|
|
|
// "Other" events, generated by Web content, will have wrapping Timeline records
|
|
// and need to be merged. Non-Other events, generated purely by the JavaScript
|
|
// engine or outside of the page via APIs, will not have wrapping Timeline
|
|
// records, so these records can just be added right now.
|
|
if (type !== InspectorBackend.Enum.ScriptProfiler.EventType.Other)
|
|
this._addRecord(record);
|
|
}
|
|
|
|
scriptProfilerTrackingCompleted(timestamp, samples)
|
|
{
|
|
if (this._enabled) {
|
|
console.assert(!this._webTimelineScriptRecordsExpectingScriptProfilerEvents || this._scriptProfilerRecords.length >= this._webTimelineScriptRecordsExpectingScriptProfilerEvents.length);
|
|
|
|
if (samples) {
|
|
let {stackTraces} = samples;
|
|
let topDownCallingContextTree = this._activeRecording.topDownCallingContextTree;
|
|
|
|
// Calculate a per-sample duration.
|
|
let timestampIndex = 0;
|
|
let timestampCount = stackTraces.length;
|
|
let sampleDurations = new Array(timestampCount);
|
|
let sampleDurationIndex = 0;
|
|
const defaultDuration = 1 / 1000; // 1ms.
|
|
for (let i = 0; i < this._scriptProfilerRecords.length; ++i) {
|
|
let record = this._scriptProfilerRecords[i];
|
|
|
|
// Use a default duration for timestamps recorded outside of ScriptProfiler events.
|
|
while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.startTime) {
|
|
sampleDurations[sampleDurationIndex++] = defaultDuration;
|
|
timestampIndex++;
|
|
}
|
|
|
|
// Average the duration per sample across all samples during the record.
|
|
let samplesInRecord = 0;
|
|
while (timestampIndex < timestampCount && stackTraces[timestampIndex].timestamp < record.endTime) {
|
|
timestampIndex++;
|
|
samplesInRecord++;
|
|
}
|
|
if (samplesInRecord) {
|
|
let averageDuration = (record.endTime - record.startTime) / samplesInRecord;
|
|
sampleDurations.fill(averageDuration, sampleDurationIndex, sampleDurationIndex + samplesInRecord);
|
|
sampleDurationIndex += samplesInRecord;
|
|
}
|
|
}
|
|
|
|
// Use a default duration for timestamps recorded outside of ScriptProfiler events.
|
|
if (timestampIndex < timestampCount)
|
|
sampleDurations.fill(defaultDuration, sampleDurationIndex);
|
|
|
|
this._activeRecording.initializeCallingContextTrees(stackTraces, sampleDurations);
|
|
|
|
// FIXME: This transformation should not be needed after introducing ProfileView.
|
|
// Once we eliminate ProfileNodeTreeElements and ProfileNodeDataGridNodes.
|
|
// <https://webkit.org/b/154973> Web Inspector: Timelines UI redesign: Remove TimelineSidebarPanel
|
|
for (let i = 0; i < this._scriptProfilerRecords.length; ++i) {
|
|
let record = this._scriptProfilerRecords[i];
|
|
record.profilePayload = topDownCallingContextTree.toCPUProfilePayload(record.startTime, record.endTime);
|
|
}
|
|
}
|
|
|
|
// Associate the ScriptProfiler created records with Web Timeline records.
|
|
// Filter out the already added ScriptProfiler events which should not have been wrapped.
|
|
if (WI.sharedApp.debuggableType !== WI.DebuggableType.JavaScript && WI.sharedApp.debuggableType !== WI.DebuggableType.ITML) {
|
|
this._scriptProfilerRecords = this._scriptProfilerRecords.filter((x) => x.__scriptProfilerType === InspectorBackend.Enum.ScriptProfiler.EventType.Other);
|
|
this._mergeScriptProfileRecords();
|
|
}
|
|
|
|
this._scriptProfilerRecords = null;
|
|
|
|
let timeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Script);
|
|
timeline.refresh();
|
|
}
|
|
|
|
this.capturingStopped(timestamp);
|
|
}
|
|
|
|
// MemoryObserver
|
|
|
|
memoryTrackingStarted(timestamp)
|
|
{
|
|
this.capturingStarted(timestamp);
|
|
}
|
|
|
|
memoryTrackingUpdated(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this.isCapturing());
|
|
if (!this.isCapturing())
|
|
return;
|
|
|
|
this._addRecord(new WI.MemoryTimelineRecord(event.timestamp, event.categories));
|
|
}
|
|
|
|
memoryTrackingCompleted(timestamp)
|
|
{
|
|
this.capturingStopped(timestamp);
|
|
}
|
|
|
|
// HeapObserver
|
|
|
|
heapTrackingStarted(timestamp, snapshot)
|
|
{
|
|
this.capturingStarted(timestamp);
|
|
|
|
if (this._enabled)
|
|
this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
|
|
}
|
|
|
|
heapTrackingCompleted(timestamp, snapshot)
|
|
{
|
|
if (this._enabled)
|
|
this._addRecord(new WI.HeapAllocationsTimelineRecord(timestamp, snapshot));
|
|
|
|
this.capturingStopped();
|
|
}
|
|
|
|
// AnimationObserver
|
|
|
|
animationTrackingStarted(timestamp)
|
|
{
|
|
this.capturingStarted(timestamp);
|
|
}
|
|
|
|
animationTrackingUpdated(timestamp, event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
console.assert(this.isCapturing());
|
|
if (!this.isCapturing())
|
|
return;
|
|
|
|
let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
|
|
console.assert(mediaTimeline);
|
|
|
|
let record = mediaTimeline.recordForTrackingAnimationId(event.trackingAnimationId);
|
|
if (!record) {
|
|
let details = {
|
|
trackingAnimationId: event.trackingAnimationId,
|
|
};
|
|
|
|
let eventType;
|
|
if (event.animationName) {
|
|
eventType = WI.MediaTimelineRecord.EventType.CSSAnimation;
|
|
details.animationName = event.animationName;
|
|
} else if (event.transitionProperty) {
|
|
eventType = WI.MediaTimelineRecord.EventType.CSSTransition;
|
|
details.transitionProperty = event.transitionProperty;
|
|
} else {
|
|
WI.reportInternalError(`Unknown event type for event '${JSON.stringify(event)}'`);
|
|
return;
|
|
}
|
|
|
|
let domNode = WI.domManager.nodeForId(event.nodeId);
|
|
console.assert(domNode);
|
|
|
|
record = new WI.MediaTimelineRecord(eventType, domNode, details);
|
|
this._addRecord(record);
|
|
}
|
|
|
|
record.updateAnimationState(timestamp, event.animationState);
|
|
}
|
|
|
|
animationTrackingCompleted(timestamp)
|
|
{
|
|
this.capturingStopped(timestamp);
|
|
}
|
|
|
|
// Private
|
|
|
|
_updateCapturingState(state, data = {})
|
|
{
|
|
if (this._capturingState === state)
|
|
return;
|
|
|
|
this._capturingState = state;
|
|
|
|
this.dispatchEventToListeners(TimelineManager.Event.CapturingStateChanged, data);
|
|
}
|
|
|
|
_processRecord(recordPayload, parentRecordPayload)
|
|
{
|
|
console.assert(this.isCapturing());
|
|
|
|
var startTime = this._activeRecording.computeElapsedTime(recordPayload.startTime);
|
|
var endTime = this._activeRecording.computeElapsedTime(recordPayload.endTime);
|
|
let stackTrace = this._stackTraceFromPayload(recordPayload.stackTrace);
|
|
|
|
var significantCallFrame = null;
|
|
if (stackTrace) {
|
|
for (let callFrame of stackTrace.callFrames) {
|
|
if (callFrame.nativeCode)
|
|
continue;
|
|
significantCallFrame = callFrame;
|
|
break;
|
|
}
|
|
}
|
|
|
|
var sourceCodeLocation = significantCallFrame && significantCallFrame.sourceCodeLocation;
|
|
|
|
switch (recordPayload.type) {
|
|
case InspectorBackend.Enum.Timeline.EventType.ScheduleStyleRecalculation:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.LayoutTimelineRecord(WI.LayoutTimelineRecord.EventType.InvalidateStyles, startTime, startTime, stackTrace, sourceCodeLocation);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.RecalculateStyles:
|
|
return new WI.LayoutTimelineRecord(WI.LayoutTimelineRecord.EventType.RecalculateStyles, startTime, endTime, stackTrace, sourceCodeLocation);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.InvalidateLayout:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.LayoutTimelineRecord(WI.LayoutTimelineRecord.EventType.InvalidateLayout, startTime, startTime, stackTrace, sourceCodeLocation);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.Layout:
|
|
var layoutRecordType = sourceCodeLocation ? WI.LayoutTimelineRecord.EventType.ForcedLayout : WI.LayoutTimelineRecord.EventType.Layout;
|
|
var quad = new WI.Quad(recordPayload.data.root);
|
|
return new WI.LayoutTimelineRecord(layoutRecordType, startTime, endTime, stackTrace, sourceCodeLocation, quad);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.Paint:
|
|
var quad = new WI.Quad(recordPayload.data.clip);
|
|
return new WI.LayoutTimelineRecord(WI.LayoutTimelineRecord.EventType.Paint, startTime, endTime, stackTrace, sourceCodeLocation, quad);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.Composite:
|
|
return new WI.LayoutTimelineRecord(WI.LayoutTimelineRecord.EventType.Composite, startTime, endTime, stackTrace, sourceCodeLocation);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.RenderingFrame:
|
|
if (!recordPayload.children || !recordPayload.children.length)
|
|
return null;
|
|
|
|
return new WI.RenderingFrameTimelineRecord(startTime, endTime, recordPayload.data.name);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.EvaluateScript:
|
|
if (!sourceCodeLocation) {
|
|
var mainFrame = WI.networkManager.mainFrame;
|
|
const recursivelySearchChildFrames = true;
|
|
let scriptResource = mainFrame.url === recordPayload.data.url ? mainFrame.mainResource : mainFrame.resourcesForURL(recordPayload.data.url, recursivelySearchChildFrames).lastValue;
|
|
if (scriptResource) {
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
let lineNumber = recordPayload.data.lineNumber - 1;
|
|
let columnNumber = "columnNumber" in recordPayload.data ? recordPayload.data.columnNumber - 1 : 0;
|
|
sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, columnNumber);
|
|
}
|
|
}
|
|
|
|
var profileData = recordPayload.data.profile;
|
|
|
|
var record;
|
|
switch (parentRecordPayload && parentRecordPayload.type) {
|
|
case InspectorBackend.Enum.Timeline.EventType.TimerFire:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.timerId, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.ObserverCallback:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ObserverCallback, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.type, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.FireAnimationFrame:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.id, profileData);
|
|
break;
|
|
default:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, stackTrace, sourceCodeLocation, null, profileData);
|
|
break;
|
|
}
|
|
|
|
this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record);
|
|
return record;
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.ConsoleProfile:
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ConsoleProfileRecorded, startTime, endTime, stackTrace, sourceCodeLocation, recordPayload.data.title);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.TimerFire:
|
|
case InspectorBackend.Enum.Timeline.EventType.EventDispatch:
|
|
case InspectorBackend.Enum.Timeline.EventType.FireAnimationFrame:
|
|
case InspectorBackend.Enum.Timeline.EventType.ObserverCallback:
|
|
// These are handled when we see the child FunctionCall or EvaluateScript.
|
|
break;
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.FunctionCall:
|
|
// FunctionCall always happens as a child of another record, and since the FunctionCall record
|
|
// has useful info we just make the timeline record here (combining the data from both records).
|
|
if (!parentRecordPayload) {
|
|
console.warn("Unexpectedly received a FunctionCall timeline record without a parent record");
|
|
break;
|
|
}
|
|
|
|
if (!sourceCodeLocation) {
|
|
var mainFrame = WI.networkManager.mainFrame;
|
|
const recursivelySearchChildFrames = true;
|
|
let scriptResource = mainFrame.url === recordPayload.data.scriptName ? mainFrame.mainResource : mainFrame.resourcesForURL(recordPayload.data.scriptName, recursivelySearchChildFrames).lastValue;
|
|
if (scriptResource) {
|
|
// The lineNumber is 1-based, but we expect 0-based.
|
|
let lineNumber = recordPayload.data.scriptLine - 1;
|
|
let columnNumber = "scriptColumn" in recordPayload.data ? recordPayload.data.scriptColumn - 1 : 0;
|
|
sourceCodeLocation = scriptResource.createSourceCodeLocation(lineNumber, columnNumber);
|
|
}
|
|
}
|
|
|
|
var profileData = recordPayload.data.profile;
|
|
|
|
var record;
|
|
switch (parentRecordPayload.type) {
|
|
case InspectorBackend.Enum.Timeline.EventType.TimerFire:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.TimerFired, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.timerId, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.EventDispatch:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.EventDispatched, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.type, profileData, parentRecordPayload.data);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.ObserverCallback:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ObserverCallback, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.type, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.FireAnimationFrame:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.AnimationFrameFired, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.id, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.FunctionCall:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.id, profileData);
|
|
break;
|
|
case InspectorBackend.Enum.Timeline.EventType.RenderingFrame:
|
|
record = new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ScriptEvaluated, startTime, endTime, stackTrace, sourceCodeLocation, parentRecordPayload.data.id, profileData);
|
|
break;
|
|
|
|
default:
|
|
console.assert(false, "Missed FunctionCall embedded inside of: " + parentRecordPayload.type);
|
|
break;
|
|
}
|
|
|
|
if (record) {
|
|
this._webTimelineScriptRecordsExpectingScriptProfilerEvents.push(record);
|
|
return record;
|
|
}
|
|
break;
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.ProbeSample: {
|
|
let probe = WI.debuggerManager.probeForIdentifier(recordPayload.data.probeId);
|
|
if (probe.breakpoint instanceof WI.JavaScriptBreakpoint)
|
|
sourceCodeLocation = probe.breakpoint.sourceCodeLocation;
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.ProbeSampleRecorded, startTime, startTime, stackTrace, sourceCodeLocation, recordPayload.data.probeId);
|
|
}
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.TimerInstall:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
var timerDetails = {timerId: recordPayload.data.timerId, timeout: recordPayload.data.timeout, repeating: !recordPayload.data.singleShot};
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.TimerInstalled, startTime, startTime, stackTrace, sourceCodeLocation, timerDetails);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.TimerRemove:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.TimerRemoved, startTime, startTime, stackTrace, sourceCodeLocation, recordPayload.data.timerId);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.RequestAnimationFrame:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.AnimationFrameRequested, startTime, startTime, stackTrace, sourceCodeLocation, recordPayload.data.id);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.CancelAnimationFrame:
|
|
console.assert(isNaN(endTime));
|
|
|
|
// Pass the startTime as the endTime since this record type has no duration.
|
|
return new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.AnimationFrameCanceled, startTime, startTime, stackTrace, sourceCodeLocation, recordPayload.data.id);
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.Screenshot:
|
|
console.assert(isNaN(endTime));
|
|
|
|
return new WI.ScreenshotsTimelineRecord(startTime, recordPayload.data.imageData);
|
|
|
|
default:
|
|
console.error("Missing handling of Timeline Event Type: " + recordPayload.type);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_processEvent(recordPayload, parentRecordPayload)
|
|
{
|
|
console.assert(this.isCapturing());
|
|
|
|
switch (recordPayload.type) {
|
|
case InspectorBackend.Enum.Timeline.EventType.TimeStamp:
|
|
var timestamp = this._activeRecording.computeElapsedTime(recordPayload.startTime);
|
|
var eventMarker = new WI.TimelineMarker(timestamp, WI.TimelineMarker.Type.TimeStamp, recordPayload.data.message);
|
|
this._activeRecording.addEventMarker(eventMarker);
|
|
break;
|
|
|
|
case InspectorBackend.Enum.Timeline.EventType.Time:
|
|
case InspectorBackend.Enum.Timeline.EventType.TimeEnd:
|
|
// FIXME: <https://webkit.org/b/150690> Web Inspector: Show console.time/timeEnd ranges in Timeline
|
|
// FIXME: Make use of "message" payload properties.
|
|
break;
|
|
|
|
default:
|
|
return this._processRecord(recordPayload, parentRecordPayload);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
_loadNewRecording()
|
|
{
|
|
if (this._activeRecording && this._activeRecording.isEmpty())
|
|
return;
|
|
|
|
let instruments = this.enabledTimelineTypes.map((type) => WI.Instrument.createForTimelineType(type));
|
|
let identifier = this._nextRecordingIdentifier++;
|
|
let newRecording = new WI.TimelineRecording(identifier, WI.UIString("Timeline Recording %d").format(identifier), instruments);
|
|
|
|
this._recordings.push(newRecording);
|
|
this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingCreated, {recording: newRecording});
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active)
|
|
this.stopCapturing();
|
|
|
|
var oldRecording = this._activeRecording;
|
|
if (oldRecording)
|
|
oldRecording.unloaded();
|
|
|
|
this._activeRecording = newRecording;
|
|
|
|
this.dispatchEventToListeners(WI.TimelineManager.Event.RecordingLoaded, {oldRecording});
|
|
}
|
|
|
|
_stackTraceFromPayload(payload)
|
|
{
|
|
let target = WI.assumingMainTarget();
|
|
|
|
// COMPATIBILITY (macOS 13.0, iOS 16.0): `stackTrace` was an array of `Console.CallFrame`.
|
|
if (Array.isArray(payload))
|
|
payload = {callFrames: payload};
|
|
|
|
return WI.StackTrace.fromPayload(target, payload);
|
|
}
|
|
|
|
_addRecord(record)
|
|
{
|
|
this._activeRecording.addRecord(record);
|
|
|
|
// Only worry about dead time after the load event.
|
|
if (WI.networkManager.mainFrame && isNaN(WI.networkManager.mainFrame.loadEventTimestamp))
|
|
this._resetAutoRecordingDeadTimeTimeout();
|
|
}
|
|
|
|
_attemptAutoCapturingForFrame(frame)
|
|
{
|
|
if (!this._autoCaptureOnPageLoad)
|
|
return false;
|
|
|
|
if (!frame.isMainFrame())
|
|
return false;
|
|
|
|
if (!InspectorBackend.hasDomain("Timeline"))
|
|
return false;
|
|
|
|
if (!this._shouldSetAutoCapturingMainResource)
|
|
return false;
|
|
|
|
console.assert(this.isCapturing(), "We saw autoCaptureStarted so we should already be capturing");
|
|
|
|
let mainResource = frame.provisionalMainResource || frame.mainResource;
|
|
if (mainResource === this._mainResourceForAutoCapturing)
|
|
return false;
|
|
|
|
let oldMainResource = frame.mainResource || null;
|
|
this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url;
|
|
|
|
this._mainResourceForAutoCapturing = mainResource;
|
|
|
|
this._addRecord(new WI.ResourceTimelineRecord(mainResource));
|
|
|
|
this._resetAutoRecordingMaxTimeTimeout();
|
|
|
|
this._shouldSetAutoCapturingMainResource = false;
|
|
|
|
return true;
|
|
}
|
|
|
|
_legacyAttemptStartAutoCapturingForFrame(frame)
|
|
{
|
|
if (this.isCapturing() && !this._mainResourceForAutoCapturing)
|
|
return false;
|
|
|
|
let mainResource = frame.provisionalMainResource || frame.mainResource;
|
|
if (mainResource === this._mainResourceForAutoCapturing)
|
|
return false;
|
|
|
|
let oldMainResource = frame.mainResource || null;
|
|
this._isCapturingPageReload = oldMainResource !== null && oldMainResource.url === mainResource.url;
|
|
|
|
if (this._capturingState === TimelineManager.CapturingState.Starting || this._capturingState === TimelineManager.CapturingState.Active)
|
|
this.stopCapturing();
|
|
|
|
this._mainResourceForAutoCapturing = mainResource;
|
|
|
|
this._loadNewRecording();
|
|
|
|
this.startCapturing();
|
|
|
|
this._addRecord(new WI.ResourceTimelineRecord(mainResource));
|
|
|
|
this._resetAutoRecordingMaxTimeTimeout();
|
|
|
|
return true;
|
|
}
|
|
|
|
_stopAutoRecordingSoon()
|
|
{
|
|
if (!WI.settings.timelinesAutoStop.value)
|
|
return;
|
|
|
|
// Only auto stop when auto capturing.
|
|
if (!this.isCapturing() || !this._mainResourceForAutoCapturing)
|
|
return;
|
|
|
|
if (this._stopCapturingTimeout)
|
|
clearTimeout(this._stopCapturingTimeout);
|
|
this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WI.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent);
|
|
}
|
|
|
|
_resetAutoRecordingMaxTimeTimeout()
|
|
{
|
|
if (!WI.settings.timelinesAutoStop.value)
|
|
return;
|
|
|
|
if (this._stopCapturingTimeout)
|
|
clearTimeout(this._stopCapturingTimeout);
|
|
this._stopCapturingTimeout = setTimeout(this._boundStopCapturing, WI.TimelineManager.MaximumAutoRecordDuration);
|
|
}
|
|
|
|
_resetAutoRecordingDeadTimeTimeout()
|
|
{
|
|
if (!WI.settings.timelinesAutoStop.value)
|
|
return;
|
|
|
|
// Only monitor dead time when auto capturing.
|
|
if (!this.isCapturing() || !this._mainResourceForAutoCapturing)
|
|
return;
|
|
|
|
// Avoid unnecessary churning of timeout identifier by not tickling until 10ms have passed.
|
|
let now = Date.now();
|
|
if (now <= this._lastDeadTimeTickle)
|
|
return;
|
|
this._lastDeadTimeTickle = now + 10;
|
|
|
|
if (this._deadTimeTimeout)
|
|
clearTimeout(this._deadTimeTimeout);
|
|
this._deadTimeTimeout = setTimeout(this._boundStopCapturing, WI.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly);
|
|
}
|
|
|
|
_provisionalLoadStarted(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
this._attemptAutoCapturingForFrame(event.target);
|
|
}
|
|
|
|
_mainResourceDidChange(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
// Ignore resource events when there isn't a main frame yet. Those events are triggered by
|
|
// loading the cached resources when the inspector opens, and they do not have timing information.
|
|
if (!WI.networkManager.mainFrame)
|
|
return;
|
|
|
|
let frame = event.target;
|
|
|
|
// When performing a page transition start a recording once the main resource changes.
|
|
// We start a legacy capture because the backend wasn't available to automatically
|
|
// initiate the capture, so the frontend must start the capture.
|
|
if (this._transitioningPageTarget) {
|
|
this._transitioningPageTarget = false;
|
|
if (this._autoCaptureOnPageLoad)
|
|
this._legacyAttemptStartAutoCapturingForFrame(frame);
|
|
return;
|
|
}
|
|
|
|
if (this._attemptAutoCapturingForFrame(frame))
|
|
return;
|
|
|
|
if (!this.isCapturing())
|
|
return;
|
|
|
|
let mainResource = frame.mainResource;
|
|
if (mainResource === this._mainResourceForAutoCapturing)
|
|
return;
|
|
|
|
this._addRecord(new WI.ResourceTimelineRecord(mainResource));
|
|
}
|
|
|
|
_handleMessageAdded(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let {message} = event.data;
|
|
|
|
if (WI.ScreenshotsInstrument.supported() && message.source === WI.ConsoleMessage.MessageSource.ConsoleAPI && message.type === WI.ConsoleMessage.MessageType.Image && message.level === WI.ConsoleMessage.MessageLevel.Log && message.messageText)
|
|
this._addRecord(new WI.ScreenshotsTimelineRecord(message.timestamp, message.messageText));
|
|
}
|
|
|
|
_resourceWasAdded(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
// Ignore resource events when there isn't a main frame yet. Those events are triggered by
|
|
// loading the cached resources when the inspector opens, and they do not have timing information.
|
|
if (!WI.networkManager.mainFrame)
|
|
return;
|
|
|
|
this._addRecord(new WI.ResourceTimelineRecord(event.data.resource));
|
|
}
|
|
|
|
_garbageCollected(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let {collection} = event.data;
|
|
this._addRecord(new WI.ScriptTimelineRecord(WI.ScriptTimelineRecord.EventType.GarbageCollected, collection.startTime, collection.endTime, null, null, collection));
|
|
}
|
|
|
|
_memoryPressure(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
this._activeRecording.addMemoryPressureEvent(event.data.memoryPressureEvent);
|
|
}
|
|
|
|
_handleTimelinesAutoStopSettingChanged(event)
|
|
{
|
|
if (WI.settings.timelinesAutoStop.value) {
|
|
if (this._mainResourceForAutoCapturing && !isNaN(this._mainResourceForAutoCapturing.parentFrame.loadEventTimestamp))
|
|
this._stopAutoRecordingSoon();
|
|
else
|
|
this._resetAutoRecordingMaxTimeTimeout();
|
|
this._resetAutoRecordingDeadTimeTimeout();
|
|
} else
|
|
this.relaxAutoStop();
|
|
}
|
|
|
|
_scriptProfilerTypeToScriptTimelineRecordType(type)
|
|
{
|
|
switch (type) {
|
|
case InspectorBackend.Enum.ScriptProfiler.EventType.API:
|
|
return WI.ScriptTimelineRecord.EventType.APIScriptEvaluated;
|
|
case InspectorBackend.Enum.ScriptProfiler.EventType.Microtask:
|
|
return WI.ScriptTimelineRecord.EventType.MicrotaskDispatched;
|
|
case InspectorBackend.Enum.ScriptProfiler.EventType.Other:
|
|
return WI.ScriptTimelineRecord.EventType.ScriptEvaluated;
|
|
}
|
|
}
|
|
|
|
_mergeScriptProfileRecords()
|
|
{
|
|
let nextRecord = function(list) { return list.shift() || null; };
|
|
let nextWebTimelineRecord = nextRecord.bind(null, this._webTimelineScriptRecordsExpectingScriptProfilerEvents);
|
|
let nextScriptProfilerRecord = nextRecord.bind(null, this._scriptProfilerRecords);
|
|
let recordEnclosesRecord = function(record1, record2) {
|
|
return record1.startTime <= record2.startTime && record1.endTime >= record2.endTime;
|
|
};
|
|
|
|
let webRecord = nextWebTimelineRecord();
|
|
let profilerRecord = nextScriptProfilerRecord();
|
|
|
|
while (webRecord && profilerRecord) {
|
|
// Skip web records with parent web records. For example an EvaluateScript with an EvaluateScript parent.
|
|
if (webRecord.parent instanceof WI.ScriptTimelineRecord) {
|
|
console.assert(recordEnclosesRecord(webRecord.parent, webRecord), "Timeline Record incorrectly wrapping another Timeline Record");
|
|
webRecord = nextWebTimelineRecord();
|
|
continue;
|
|
}
|
|
|
|
// Normal case of a Web record wrapping a Script record.
|
|
if (recordEnclosesRecord(webRecord, profilerRecord)) {
|
|
webRecord.profilePayload = profilerRecord.profilePayload;
|
|
profilerRecord = nextScriptProfilerRecord();
|
|
|
|
// If there are more script profile records in the same time interval, add them
|
|
// as individual script evaluated records with profiles. This can happen with
|
|
// web microtask checkpoints that are technically inside of other web records.
|
|
// FIXME: <https://webkit.org/b/152903> Web Inspector: Timeline Cleanup: Better Timeline Record for Microtask Checkpoints
|
|
while (profilerRecord && recordEnclosesRecord(webRecord, profilerRecord)) {
|
|
this._addRecord(profilerRecord);
|
|
profilerRecord = nextScriptProfilerRecord();
|
|
}
|
|
|
|
webRecord = nextWebTimelineRecord();
|
|
continue;
|
|
}
|
|
|
|
// Profiler Record is entirely after the Web Record. This would mean an empty web record.
|
|
if (profilerRecord.startTime > webRecord.endTime) {
|
|
console.warn("Unexpected case of a Timeline record not containing a ScriptProfiler event and profile data");
|
|
webRecord = nextWebTimelineRecord();
|
|
continue;
|
|
}
|
|
|
|
// Non-wrapped profiler record.
|
|
console.warn("Unexpected case of a ScriptProfiler event not being contained by a Timeline record");
|
|
this._addRecord(profilerRecord);
|
|
profilerRecord = nextScriptProfilerRecord();
|
|
}
|
|
|
|
// Skipping the remaining ScriptProfiler events to match the current UI for handling Timeline records.
|
|
// However, the remaining ScriptProfiler records are valid and could be shown.
|
|
// FIXME: <https://webkit.org/b/152904> Web Inspector: Timeline UI should keep up with processing all incoming records
|
|
}
|
|
|
|
_updateAutoCaptureInstruments(targets)
|
|
{
|
|
console.assert(this._enabled);
|
|
|
|
let enabledTimelineTypes = this.enabledTimelineTypes;
|
|
|
|
for (let target of targets) {
|
|
if (!target.hasCommand("Timeline.setInstruments"))
|
|
continue;
|
|
|
|
let instrumentSet = new Set;
|
|
for (let timelineType of enabledTimelineTypes) {
|
|
switch (timelineType) {
|
|
case WI.TimelineRecord.Type.Script:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.ScriptProfiler);
|
|
break;
|
|
case WI.TimelineRecord.Type.HeapAllocations:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Heap);
|
|
break;
|
|
case WI.TimelineRecord.Type.Network:
|
|
case WI.TimelineRecord.Type.RenderingFrame:
|
|
case WI.TimelineRecord.Type.Layout:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Timeline);
|
|
break;
|
|
case WI.TimelineRecord.Type.CPU:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.CPU);
|
|
break;
|
|
case WI.TimelineRecord.Type.Screenshots:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Screenshot);
|
|
break;
|
|
case WI.TimelineRecord.Type.Memory:
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Memory);
|
|
break;
|
|
case WI.TimelineRecord.Type.Media:
|
|
// COMPATIBILITY (iOS 13): Animation domain did not exist yet.
|
|
if (InspectorBackend.hasDomain("Animation"))
|
|
instrumentSet.add(InspectorBackend.Enum.Timeline.Instrument.Animation);
|
|
break;
|
|
}
|
|
}
|
|
|
|
target.TimelineAgent.setInstruments(Array.from(instrumentSet));
|
|
}
|
|
}
|
|
|
|
_handleDOMNodeDidFireEvent(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let domNode = event.target;
|
|
if (!domNode.isMediaElement())
|
|
return;
|
|
|
|
let {domEvent} = event.data;
|
|
|
|
let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
|
|
console.assert(mediaTimeline);
|
|
|
|
let record = mediaTimeline.recordForMediaElementEvents(domNode);
|
|
if (!record) {
|
|
record = new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.MediaElement, domNode);
|
|
this._addRecord(record);
|
|
}
|
|
|
|
record.addDOMEvent(domEvent.timestamp, domEvent);
|
|
}
|
|
|
|
_handleDOMNodePowerEfficientPlaybackStateChanged(event)
|
|
{
|
|
if (!this._enabled)
|
|
return;
|
|
|
|
let domNode = event.target;
|
|
console.assert(domNode.isMediaElement());
|
|
|
|
let {timestamp, isPowerEfficient} = event.data;
|
|
|
|
let mediaTimeline = this._activeRecording.timelineForRecordType(WI.TimelineRecord.Type.Media);
|
|
console.assert(mediaTimeline);
|
|
|
|
let record = mediaTimeline.recordForMediaElementEvents(domNode);
|
|
if (!record) {
|
|
record = new WI.MediaTimelineRecord(WI.MediaTimelineRecord.EventType.MediaElement, domNode);
|
|
this._addRecord(record);
|
|
}
|
|
|
|
record.powerEfficientPlaybackStateChanged(timestamp, isPowerEfficient);
|
|
}
|
|
};
|
|
|
|
WI.TimelineManager.CapturingState = {
|
|
Inactive: "inactive",
|
|
Starting: "starting",
|
|
Active: "active",
|
|
Stopping: "stopping",
|
|
};
|
|
|
|
WI.TimelineManager.Event = {
|
|
CapturingStateChanged: "timeline-manager-capturing-started",
|
|
RecordingCreated: "timeline-manager-recording-created",
|
|
RecordingLoaded: "timeline-manager-recording-loaded",
|
|
RecordingImported: "timeline-manager-recording-imported",
|
|
};
|
|
|
|
WI.TimelineManager.MaximumAutoRecordDuration = 90_000; // 90 seconds
|
|
WI.TimelineManager.MaximumAutoRecordDurationAfterLoadEvent = 10_000; // 10 seconds
|
|
WI.TimelineManager.DeadTimeRequiredToStopAutoRecordingEarly = 2_000; // 2 seconds
|
|
|
|
/* Controllers/WebInspectorExtensionController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2020-2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WebInspectorExtensionController = class WebInspectorExtensionController extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._extensionForExtensionIDMap = new Map;
|
|
this._extensionTabContentViewForExtensionTabIDMap = new Map;
|
|
this._tabIDsForExtensionIDMap = new Multimap;
|
|
this._nextExtensionTabID = 1;
|
|
|
|
this._extensionTabPositions = null;
|
|
this._saveTabPositionsDebouncer = null;
|
|
|
|
WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._handleMainResourceDidChange, this);
|
|
}
|
|
|
|
// Static
|
|
|
|
static get extensionTabPositionsObjectStoreKey()
|
|
{
|
|
return "extension-tab-positions";
|
|
}
|
|
|
|
// Public
|
|
|
|
get registeredExtensionIDs()
|
|
{
|
|
return new Set(this._extensionForExtensionIDMap.keys());
|
|
}
|
|
|
|
registerExtension(extensionID, extensionBundleIdentifier, displayName)
|
|
{
|
|
if (this._extensionForExtensionIDMap.has(extensionID)) {
|
|
WI.reportInternalError("Unable to register extension, it's already registered: " + extensionID);
|
|
return WI.WebInspectorExtension.ErrorCode.RegistrationFailed;
|
|
}
|
|
|
|
if (!this._extensionForExtensionIDMap.size) {
|
|
WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this);
|
|
WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this);
|
|
WI.tabBrowser.tabBar.addEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this);
|
|
}
|
|
|
|
let extension = new WI.WebInspectorExtension(extensionID, extensionBundleIdentifier, displayName);
|
|
this._extensionForExtensionIDMap.set(extensionID, extension);
|
|
|
|
this.dispatchEventToListeners(WI.WebInspectorExtensionController.Event.ExtensionAdded, {extension});
|
|
}
|
|
|
|
unregisterExtension(extensionID)
|
|
{
|
|
let extension = this._extensionForExtensionIDMap.take(extensionID);
|
|
if (!extension) {
|
|
WI.reportInternalError("Unable to unregister extension with unknown ID: " + extensionID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
if (!this._extensionForExtensionIDMap.size) {
|
|
WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemAdded, this._saveExtensionTabPositions, this);
|
|
WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemRemoved, this._saveExtensionTabPositions, this);
|
|
WI.tabBrowser.tabBar.removeEventListener(WI.TabBar.Event.TabBarItemsReordered, this._saveExtensionTabPositions, this);
|
|
}
|
|
|
|
let extensionTabIDsToRemove = this._tabIDsForExtensionIDMap.take(extensionID) || [];
|
|
for (let extensionTabID of extensionTabIDsToRemove) {
|
|
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.take(extensionTabID);
|
|
|
|
// Ensure that the iframe is actually detached and does not leak.
|
|
WI.tabBrowser.closeTabForContentView(tabContentView, {suppressAnimations: true});
|
|
tabContentView.dispose();
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.WebInspectorExtensionController.Event.ExtensionRemoved, {extension});
|
|
}
|
|
|
|
async createTabForExtension(extensionID, tabName, tabIconURL, sourceURL)
|
|
{
|
|
let extension = this._extensionForExtensionIDMap.get(extensionID);
|
|
if (!extension) {
|
|
WI.reportInternalError("Unable to create tab for extension with unknown ID: " + extensionID + " sourceURL: " + sourceURL);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
let extensionTabID = `WebExtensionTab-${extensionID}-${this._nextExtensionTabID++}`;
|
|
let tabContentView = new WI.WebInspectorExtensionTabContentView(extension, extensionTabID, tabName, tabIconURL, sourceURL);
|
|
|
|
this._tabIDsForExtensionIDMap.add(extensionID, extensionTabID);
|
|
this._extensionTabContentViewForExtensionTabIDMap.set(extensionTabID, tabContentView);
|
|
|
|
if (!this._extensionTabPositions)
|
|
await this._loadExtensionTabPositions();
|
|
|
|
WI.tabBrowser.addTabForContentView(tabContentView, {
|
|
suppressAnimations: true,
|
|
insertionIndex: this._insertionIndexForExtensionTab(tabContentView),
|
|
});
|
|
|
|
// The calling convention is to return an error string or a result object.
|
|
return {"result": extensionTabID};
|
|
}
|
|
|
|
async evaluateScriptForExtension(extensionID, scriptSource, {frameURL, contextSecurityOrigin, useContentScriptContext} = {})
|
|
{
|
|
let extension = this._extensionForExtensionIDMap.get(extensionID);
|
|
if (!extension) {
|
|
WI.reportInternalError("Unable to evaluate script for extension with unknown ID: " + extensionID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
let frame = await retryUntil(() => this._frameForFrameURL(frameURL));
|
|
if (!frame) {
|
|
WI.reportInternalError("evaluateScriptForExtension: No frame matched provided frameURL: " + frameURL);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
// FIXME: <https://webkit.org/b/269349> Implement `contextSecurityOrigin` and `useContentScriptContext` options for `devtools.inspectedWindow.eval` command
|
|
if (contextSecurityOrigin) {
|
|
WI.reportInternalError("evaluateScriptForExtension: the 'contextSecurityOrigin' option is not yet implemented.");
|
|
return WI.WebInspectorExtension.ErrorCode.NotImplemented;
|
|
}
|
|
|
|
if (useContentScriptContext) {
|
|
WI.reportInternalError("evaluateScriptForExtension: the 'useContentScriptContext' option is not yet implemented.");
|
|
return WI.WebInspectorExtension.ErrorCode.NotImplemented;
|
|
}
|
|
|
|
let evaluationContext = await retryUntil(() => frame.pageExecutionContext);
|
|
if (!evaluationContext) {
|
|
WI.reportInternalError("evaluateScriptForExtension: No 'pageExecutionContext' was present for frame with URL: " + frame.url);
|
|
return WI.WebInspectorExtension.ErrorCode.ContextDestroyed;
|
|
}
|
|
|
|
return evaluationContext.target.RuntimeAgent.evaluate.invoke({
|
|
expression: scriptSource,
|
|
objectGroup: "extension-evaluation",
|
|
includeCommandLineAPI: true,
|
|
returnByValue: true,
|
|
generatePreview: false,
|
|
saveResult: false,
|
|
contextId: evaluationContext.id,
|
|
}).then((payload) => {
|
|
let resultOrError = payload.result;
|
|
let wasThrown = payload.wasThrown;
|
|
let {type, value} = resultOrError;
|
|
return wasThrown ? {"error": resultOrError.description} : {"result": value};
|
|
}).catch((error) => error.description);
|
|
}
|
|
|
|
reloadForExtension(extensionID, {ignoreCache, userAgent, injectedScript} = {})
|
|
{
|
|
let extension = this._extensionForExtensionIDMap.get(extensionID);
|
|
if (!extension) {
|
|
WI.reportInternalError("Unable to evaluate script for extension with unknown ID: " + extensionID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
// FIXME: <https://webkit.org/b/222328> Implement `userAgent` and `injectedScript` options for `devtools.inspectedWindow.reload` command
|
|
if (userAgent) {
|
|
WI.reportInternalError("reloadForExtension: the 'userAgent' option is not yet implemented.");
|
|
return WI.WebInspectorExtension.ErrorCode.NotImplemented;
|
|
}
|
|
|
|
if (injectedScript) {
|
|
WI.reportInternalError("reloadForExtension: the 'injectedScript' option is not yet implemented.");
|
|
return WI.WebInspectorExtension.ErrorCode.NotImplemented;
|
|
}
|
|
|
|
let target = WI.assumingMainTarget();
|
|
if (!target.hasCommand("Page.reload"))
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
|
|
return target.PageAgent.reload.invoke({ignoreCache});
|
|
}
|
|
|
|
navigateTabForExtension(extensionTabID, sourceURL)
|
|
{
|
|
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID);
|
|
if (!tabContentView) {
|
|
WI.reportInternalError("Unable to navigate extension tab with unknown extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
tabContentView.iframeURL = sourceURL;
|
|
}
|
|
|
|
showExtensionTab(extensionTabID, options = {})
|
|
{
|
|
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID);
|
|
if (!tabContentView) {
|
|
WI.reportInternalError("Unable to show extension tab with unknown extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
tabContentView.visible = true;
|
|
let success = WI.tabBrowser.showTabForContentView(tabContentView, {
|
|
...options,
|
|
insertionIndex: this._insertionIndexForExtensionTab(tabContentView),
|
|
initiatorHint: WI.TabBrowser.TabNavigationInitiator.FrontendAPI,
|
|
});
|
|
|
|
if (!success) {
|
|
WI.reportInternalError("Unable to show extension tab with extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InternalError;
|
|
}
|
|
|
|
tabContentView.visible = true;
|
|
|
|
// Clients expect to be able to use evaluateScriptInExtensionTab() when this method
|
|
// returns, so wait for the extension tab to finish its loading sequence. Wrap the result.
|
|
return tabContentView.whenPageAvailable().then((sourceURL) => { return {"result": sourceURL}; });
|
|
}
|
|
|
|
hideExtensionTab(extensionTabID, options = {})
|
|
{
|
|
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID);
|
|
if (!tabContentView) {
|
|
WI.reportInternalError("Unable to show extension tab with unknown extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
tabContentView.visible = false;
|
|
WI.tabBrowser.closeTabForContentView(tabContentView, options);
|
|
|
|
console.assert(!tabContentView.visible);
|
|
console.assert(!tabContentView.isClosed);
|
|
}
|
|
|
|
addContextMenuItemsForClosedExtensionTabs(contextMenu)
|
|
{
|
|
contextMenu.appendSeparator();
|
|
|
|
for (let tabContentView of this._extensionTabContentViewForExtensionTabIDMap.values()) {
|
|
// If the extension tab has been unchecked in the TabBar context menu, then the tabBarItem
|
|
// for the extension tab will not be connected to a parent TabBar.
|
|
let shouldIncludeTab = !tabContentView.visible || !tabContentView.tabBarItem.parentTabBar;
|
|
if (!shouldIncludeTab)
|
|
continue;
|
|
|
|
contextMenu.appendItem(tabContentView.tabInfo().displayName, () => {
|
|
this.showExtensionTab(tabContentView.extensionTabID);
|
|
});
|
|
}
|
|
}
|
|
|
|
addContextMenuItemsForAllExtensionTabs(contextMenu)
|
|
{
|
|
contextMenu.appendSeparator();
|
|
|
|
for (let tabContentView of this._extensionTabContentViewForExtensionTabIDMap.values()) {
|
|
let checked = tabContentView.visible || !!tabContentView.tabBarItem.parentTabBar;
|
|
contextMenu.appendCheckboxItem(tabContentView.tabInfo().displayName, () => {
|
|
if (!checked)
|
|
this.showExtensionTab(tabContentView.extensionTabID);
|
|
else
|
|
this.hideExtensionTab(tabContentView.extensionTabID);
|
|
}, checked);
|
|
}
|
|
}
|
|
|
|
activeExtensionTabContentViews()
|
|
{
|
|
return Array.from(this._extensionTabContentViewForExtensionTabIDMap.values()).filter((tab) => tab.visible || tab.tabBarItem.parentTabBar);
|
|
}
|
|
|
|
evaluateScriptInExtensionTab(extensionTabID, scriptSource)
|
|
{
|
|
let tabContentView = this._extensionTabContentViewForExtensionTabIDMap.get(extensionTabID);
|
|
if (!tabContentView) {
|
|
WI.reportInternalError("Unable to evaluate with unknown extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
let iframe = tabContentView.iframeElement;
|
|
if (!(iframe instanceof HTMLIFrameElement)) {
|
|
WI.reportInternalError("Unable to evaluate without an <iframe> for extensionTabID: " + extensionTabID);
|
|
return WI.WebInspectorExtension.ErrorCode.InvalidRequest;
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
try {
|
|
// If `result` is a promise, then it came from a different frame and `instanceof Promise` won't work.
|
|
let result = InspectorFrontendHost.evaluateScriptInExtensionTab(iframe, scriptSource);
|
|
if (result?.then) {
|
|
result.then((resolvedValue) => resolve({result: resolvedValue}), (errorValue) => reject({error: errorValue}));
|
|
return;
|
|
}
|
|
|
|
resolve({result});
|
|
} catch (error) {
|
|
// Include more context in the stringification of the error.
|
|
const stackIndent = " ";
|
|
let stackLines = (error.stack?.split("\n") || []).map((line) => `${stackIndent}${line}`);
|
|
let formattedMessage = [
|
|
`Caught Exception: ${error.name}`,
|
|
`at ${error.sourceURL || "(unknown)"}:${error.line || 0}:${error.column || 0}:`,
|
|
error.message,
|
|
"",
|
|
"Backtrace:",
|
|
...stackLines,
|
|
].join("\n");
|
|
reject({error: formattedMessage});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
async _loadExtensionTabPositions()
|
|
{
|
|
let savedTabPositions = await WI.objectStores.general.get(WebInspectorExtensionController.extensionTabPositionsObjectStoreKey);
|
|
this._extensionTabPositions = savedTabPositions || {};
|
|
}
|
|
|
|
_saveExtensionTabPositions()
|
|
{
|
|
if (!this._extensionTabPositions)
|
|
return;
|
|
|
|
this._saveTabPositionsDebouncer ||= new Debouncer(() => {
|
|
for (let tabBarItem of WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight) {
|
|
if (!(tabBarItem.representedObject instanceof WI.WebInspectorExtensionTabContentView))
|
|
continue;
|
|
|
|
let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabBarItem.representedObject, {recomputePositions: true});
|
|
this._extensionTabPositions[tabBarItem.representedObject.savedTabPositionKey] = {anchorTabType, distanceFromAnchorTab};
|
|
}
|
|
|
|
WI.objectStores.general.put(this._extensionTabPositions, WebInspectorExtensionController.extensionTabPositionsObjectStoreKey);
|
|
});
|
|
this._saveTabPositionsDebouncer.delayForTime(5000);
|
|
}
|
|
|
|
_insertionIndexForExtensionTab(tabContentView, options = {})
|
|
{
|
|
let {anchorTabType, anchorTabIndex, distanceFromAnchorTab} = this._computeIndicesForExtensionTab(tabContentView, options);
|
|
return anchorTabIndex + distanceFromAnchorTab + 1;
|
|
}
|
|
|
|
_computeIndicesForExtensionTab(tabContentView, {recomputePositions} = {})
|
|
{
|
|
// Note: pinned tabs always appear on the trailing edge, so we can ignore them
|
|
// for the purposes of computing an `insertionIndex`` for `tabContentView`.
|
|
let anchorTabIndex = 0;
|
|
let savedPositions = this._extensionTabPositions[tabContentView.savedTabPositionKey] || {};
|
|
let anchorTabType = (recomputePositions && savedPositions.anchorTabType) || null;
|
|
let distanceFromAnchorTab = (recomputePositions && savedPositions.distanceFromAnchorTab) || 0;
|
|
|
|
let visibleTabBarItems = WI.tabBrowser.tabBar.visibleTabBarItemsFromLeftToRight;
|
|
for (let i = 0; i < visibleTabBarItems.length; ++i) {
|
|
let visibleTab = visibleTabBarItems[i].representedObject;
|
|
if (!visibleTab)
|
|
continue;
|
|
|
|
if (visibleTab === tabContentView)
|
|
break;
|
|
|
|
if (visibleTab instanceof WI.WebInspectorExtensionTabContentView)
|
|
continue;
|
|
|
|
if (recomputePositions) {
|
|
anchorTabType = visibleTab.type || null;
|
|
continue;
|
|
}
|
|
|
|
if (visibleTab.type !== anchorTabType)
|
|
continue;
|
|
|
|
anchorTabIndex = i;
|
|
break;
|
|
}
|
|
|
|
// Find the count of extension tabs after the anchor tab to compute the real distanceFromAnchorTab.
|
|
// Adding `distanceFromAnchorTab` to `anchorTabIndex` should not insert the tab after a different anchor tab.
|
|
for (let i = 1; i < visibleTabBarItems.length - anchorTabIndex; ++i) {
|
|
if (visibleTabBarItems[anchorTabIndex + i].representedObject?.constructor?.shouldSaveTab?.()) {
|
|
distanceFromAnchorTab = Number.constrain(distanceFromAnchorTab, 0, Math.max(0, i - 1));
|
|
return {anchorTabType, anchorTabIndex, distanceFromAnchorTab};
|
|
}
|
|
}
|
|
|
|
// If the anchor tab is now hidden upon restoring, place the extension at the end.
|
|
// This could happen if a smaller set of tabs are enabled for the inspection target.
|
|
anchorTabIndex = visibleTabBarItems.length - 1;
|
|
return {anchorTabType, anchorTabIndex, distanceFromAnchorTab};
|
|
}
|
|
|
|
_frameForFrameURL(frameURL)
|
|
{
|
|
if (!frameURL)
|
|
return WI.networkManager.mainFrame;
|
|
|
|
function findFrame(frameURL, adjustKnownFrameURL) {
|
|
return WI.networkManager.frames.find((knownFrame) => {
|
|
let knownFrameURL = new URL(knownFrame.url);
|
|
adjustKnownFrameURL?.(knownFrameURL);
|
|
return knownFrameURL.toString() === frameURL;
|
|
});
|
|
}
|
|
|
|
let frame = findFrame(frameURL);
|
|
if (frame)
|
|
return frame;
|
|
|
|
let frameURLParts = new URL(frameURL);
|
|
if (frameURLParts.hash.length)
|
|
return null;
|
|
|
|
frame = findFrame(frameURL, (knownFrameURL) => {
|
|
knownFrameURL.hash = "";
|
|
});
|
|
if (frame)
|
|
return frame;
|
|
|
|
if (frameURLParts.search.length)
|
|
return null;
|
|
|
|
return findFrame(frameURL, (knownFrameURL) => {
|
|
knownFrameURL.hash = "";
|
|
knownFrameURL.search = "";
|
|
});
|
|
}
|
|
|
|
_handleMainResourceDidChange(event)
|
|
{
|
|
if (!event.target.isMainFrame())
|
|
return;
|
|
|
|
// Don't fire the event unless one or more extensions are registered.
|
|
if (!this._extensionForExtensionIDMap.size)
|
|
return;
|
|
|
|
InspectorFrontendHost.inspectedPageDidNavigate(WI.networkManager.mainFrame.url);
|
|
}
|
|
};
|
|
|
|
WI.WebInspectorExtensionController.Event = {
|
|
ExtensionAdded: "extension-added",
|
|
ExtensionRemoved: "extension-removed",
|
|
};
|
|
|
|
/* Controllers/WorkerManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.WorkerManager = class WorkerManager extends WI.Object
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._connections = new Map;
|
|
}
|
|
|
|
// Target
|
|
|
|
initializeTarget(target)
|
|
{
|
|
if (target.hasDomain("Worker"))
|
|
target.WorkerAgent.enable();
|
|
}
|
|
|
|
// WorkerObserver
|
|
|
|
workerCreated(target, workerId, url, name)
|
|
{
|
|
console.assert(target.hasCommand("Worker.sendMessageToWorker"));
|
|
let connection = new InspectorBackend.WorkerConnection;
|
|
let workerTarget = new WI.WorkerTarget(target, workerId, url, name, connection);
|
|
workerTarget.initialize();
|
|
|
|
WI.targetManager.addTarget(workerTarget);
|
|
|
|
this._connections.set(workerId, connection);
|
|
|
|
// Unpause the worker now that we have sent all initialization messages.
|
|
// Ignore errors if a worker went away quickly.
|
|
target.WorkerAgent.initialized(workerId).catch(function(){});
|
|
}
|
|
|
|
workerTerminated(workerId)
|
|
{
|
|
let connection = this._connections.take(workerId);
|
|
|
|
WI.targetManager.removeTarget(connection.target);
|
|
}
|
|
|
|
dispatchMessageFromWorker(workerId, message)
|
|
{
|
|
let connection = this._connections.get(workerId);
|
|
|
|
console.assert(connection);
|
|
if (!connection)
|
|
return;
|
|
|
|
connection.dispatch(message);
|
|
}
|
|
};
|
|
|
|
/* Controllers/ResourceQueryController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2016 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.ResourceQueryController = class ResourceQueryController extends WI.QueryController
|
|
{
|
|
constructor()
|
|
{
|
|
super();
|
|
|
|
this._resourceDataMap = new Map;
|
|
}
|
|
|
|
// Public
|
|
|
|
addResource(resource)
|
|
{
|
|
this._resourceDataMap.set(resource, {});
|
|
}
|
|
|
|
removeResource(resource)
|
|
{
|
|
this._resourceDataMap.delete(resource);
|
|
}
|
|
|
|
reset()
|
|
{
|
|
this._resourceDataMap.clear();
|
|
}
|
|
|
|
executeQuery(query)
|
|
{
|
|
if (!query || !this._resourceDataMap.size)
|
|
return [];
|
|
|
|
query = query.removeWhitespace().toLowerCase();
|
|
|
|
let cookie = null;
|
|
if (query.includes(":")) {
|
|
let [newQuery, lineNumber, columnNumber] = query.split(":");
|
|
query = newQuery;
|
|
lineNumber = lineNumber ? parseInt(lineNumber, 10) - 1 : 0;
|
|
columnNumber = columnNumber ? parseInt(columnNumber, 10) - 1 : 0;
|
|
cookie = {lineNumber, columnNumber};
|
|
}
|
|
|
|
let results = [];
|
|
for (let [resource, cachedData] of this._resourceDataMap) {
|
|
if (isEmptyObject(cachedData)) {
|
|
let displayName = resource.displayName;
|
|
cachedData.displayName = {
|
|
searchString: displayName.toLowerCase(),
|
|
specialCharacterIndices: this._findSpecialCharacterIndicesInDisplayName(displayName),
|
|
};
|
|
|
|
let url = resource.url;
|
|
cachedData.url = {
|
|
searchString: url.toLowerCase(),
|
|
specialCharacterIndices: this._findSpecialCharacterIndicesInURL(url),
|
|
};
|
|
}
|
|
|
|
let resourceResult = null;
|
|
|
|
let findQueryMatches = ({searchString, specialCharacterIndices}) => {
|
|
let matches = this.findQueryMatches(query, searchString, specialCharacterIndices);
|
|
if (!matches.length)
|
|
return;
|
|
|
|
let queryResult = new WI.ResourceQueryResult(resource, searchString, matches, cookie);
|
|
if (!resourceResult || resourceResult.rank < queryResult.rank)
|
|
resourceResult = queryResult;
|
|
};
|
|
findQueryMatches(cachedData.displayName);
|
|
findQueryMatches(cachedData.url);
|
|
|
|
if (resourceResult)
|
|
results.push(resourceResult);
|
|
}
|
|
|
|
// Resources are sorted in descending order by rank. Resources of equal
|
|
// rank are sorted by display name.
|
|
return results.sort((a, b) => {
|
|
if (a.rank === b.rank)
|
|
return a.resource.displayName.extendedLocaleCompare(b.resource.displayName);
|
|
return b.rank - a.rank;
|
|
});
|
|
}
|
|
|
|
// Private
|
|
|
|
_findSpecialCharacterIndicesInDisplayName(displayName)
|
|
{
|
|
return this.findSpecialCharacterIndices(displayName, "_.-");
|
|
}
|
|
|
|
_findSpecialCharacterIndicesInURL(url)
|
|
{
|
|
return this.findSpecialCharacterIndices(url, "_.-/");
|
|
}
|
|
};
|
|
|
|
/* Controllers/SelectionController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2018, 2019 Apple Inc. All Rights Reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.SelectionController = class SelectionController extends WI.Object
|
|
{
|
|
constructor(delegate, comparator)
|
|
{
|
|
super();
|
|
|
|
console.assert(delegate);
|
|
console.assert(typeof comparator === "function");
|
|
|
|
this._delegate = delegate;
|
|
this._comparator = comparator;
|
|
|
|
this._allowsEmptySelection = true;
|
|
this._allowsMultipleSelection = false;
|
|
this._lastSelectedItem = null;
|
|
this._shiftAnchorItem = null;
|
|
this._selectedItems = new Set;
|
|
this._suppressSelectionDidChange = false;
|
|
|
|
console.assert(this._delegate.selectionControllerFirstSelectableItem, "SelectionController delegate must implement selectionControllerFirstSelectableItem.");
|
|
console.assert(this._delegate.selectionControllerLastSelectableItem, "SelectionController delegate must implement selectionControllerLastSelectableItem.");
|
|
console.assert(this._delegate.selectionControllerNextSelectableItem, "SelectionController delegate must implement selectionControllerNextSelectableItem.");
|
|
console.assert(this._delegate.selectionControllerPreviousSelectableItem, "SelectionController delegate must implement selectionControllerPreviousSelectableItem.");
|
|
}
|
|
|
|
// Static
|
|
|
|
static createTreeComparator(itemForRepresentedObject)
|
|
{
|
|
return (a, b) => {
|
|
a = itemForRepresentedObject(a);
|
|
b = itemForRepresentedObject(b);
|
|
if (!a || !b)
|
|
return 0;
|
|
|
|
let getLevel = (item) => {
|
|
let level = 0;
|
|
while (item = item.parent)
|
|
level++;
|
|
return level;
|
|
};
|
|
|
|
let compareSiblings = (s, t) => {
|
|
return s.parent.children.indexOf(s) - s.parent.children.indexOf(t);
|
|
};
|
|
|
|
if (a.parent === b.parent)
|
|
return compareSiblings(a, b);
|
|
|
|
let aLevel = getLevel(a);
|
|
let bLevel = getLevel(b);
|
|
while (aLevel > bLevel) {
|
|
if (a.parent === b)
|
|
return 1;
|
|
a = a.parent;
|
|
aLevel--;
|
|
}
|
|
while (bLevel > aLevel) {
|
|
if (b.parent === a)
|
|
return -1;
|
|
b = b.parent;
|
|
bLevel--;
|
|
}
|
|
|
|
while (a.parent !== b.parent) {
|
|
a = a.parent;
|
|
b = b.parent;
|
|
}
|
|
|
|
console.assert(a.parent === b.parent, "Missing common ancestor.", a, b);
|
|
return compareSiblings(a, b);
|
|
};
|
|
}
|
|
|
|
static createListComparator(indexForRepresentedObject)
|
|
{
|
|
console.assert(indexForRepresentedObject);
|
|
|
|
return (a, b) => {
|
|
return indexForRepresentedObject(a) - indexForRepresentedObject(b);
|
|
};
|
|
}
|
|
|
|
// Public
|
|
|
|
get delegate() { return this._delegate; }
|
|
get lastSelectedItem() { return this._lastSelectedItem; }
|
|
get selectedItems() { return this._selectedItems; }
|
|
|
|
get allowsEmptySelection() { return this._allowsEmptySelection; }
|
|
set allowsEmptySelection(flag) { this._allowsEmptySelection = flag; }
|
|
|
|
get allowsMultipleSelection()
|
|
{
|
|
return this._allowsMultipleSelection;
|
|
}
|
|
|
|
set allowsMultipleSelection(flag)
|
|
{
|
|
if (this._allowsMultipleSelection === flag)
|
|
return;
|
|
|
|
this._allowsMultipleSelection = flag;
|
|
if (this._allowsMultipleSelection)
|
|
return;
|
|
|
|
if (this._selectedItems.size > 1)
|
|
this._updateSelectedItems(new Set([this._lastSelectedItem]));
|
|
}
|
|
|
|
hasSelectedItem(item)
|
|
{
|
|
return this._selectedItems.has(item);
|
|
}
|
|
|
|
selectItem(item, extendSelection = false)
|
|
{
|
|
console.assert(item, "Invalid item for selection.");
|
|
console.assert(!extendSelection || this._allowsMultipleSelection, "Cannot extend selection with multiple selection disabled.");
|
|
|
|
if (!this._allowsMultipleSelection)
|
|
extendSelection = false;
|
|
|
|
this._lastSelectedItem = item;
|
|
this._shiftAnchorItem = null;
|
|
|
|
let newItems = new Set(extendSelection ? this._selectedItems : null);
|
|
newItems.add(item);
|
|
|
|
this._updateSelectedItems(newItems);
|
|
}
|
|
|
|
selectItems(items)
|
|
{
|
|
console.assert(this._allowsMultipleSelection, "Cannot select multiple items with multiple selection disabled.");
|
|
if (!this._allowsMultipleSelection)
|
|
return;
|
|
|
|
if (!this._lastSelectedItem || !items.has(this._lastSelectedItem))
|
|
this._lastSelectedItem = items.lastValue;
|
|
|
|
if (!this._shiftAnchorItem || !items.has(this._shiftAnchorItem))
|
|
this._shiftAnchorItem = this._lastSelectedItem;
|
|
|
|
this._updateSelectedItems(items);
|
|
}
|
|
|
|
deselectItem(item)
|
|
{
|
|
console.assert(item, "Invalid item for selection.");
|
|
|
|
if (!this.hasSelectedItem(item))
|
|
return;
|
|
|
|
if (!this._allowsEmptySelection && this._selectedItems.size === 1)
|
|
return;
|
|
|
|
let newItems = new Set(this._selectedItems);
|
|
newItems.delete(item);
|
|
|
|
if (this._lastSelectedItem === item) {
|
|
this._lastSelectedItem = null;
|
|
|
|
if (newItems.size) {
|
|
console.assert(this._allowsMultipleSelection);
|
|
|
|
const operation = WI.SelectionController.Operation.Extend;
|
|
|
|
// Find selected item closest to deselected item.
|
|
let previous = item;
|
|
let next = item;
|
|
while (!this._lastSelectedItem && previous && next) {
|
|
previous = this._previousSelectableItem(previous, operation);
|
|
if (this.hasSelectedItem(previous)) {
|
|
this._lastSelectedItem = previous;
|
|
break;
|
|
}
|
|
|
|
next = this._nextSelectableItem(next, operation);
|
|
if (this.hasSelectedItem(next)) {
|
|
this._lastSelectedItem = next;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._shiftAnchorItem === item)
|
|
this._shiftAnchorItem = null;
|
|
|
|
this._updateSelectedItems(newItems);
|
|
}
|
|
|
|
selectAll()
|
|
{
|
|
if (!this._allowsMultipleSelection)
|
|
return;
|
|
|
|
const operation = WI.SelectionController.Operation.Extend;
|
|
|
|
let newItems = new Set;
|
|
this._addRange(newItems, this._firstSelectableItem(operation), this._lastSelectableItem(operation));
|
|
this.selectItems(newItems);
|
|
}
|
|
|
|
deselectAll()
|
|
{
|
|
this._deselectAllAndSelect(null);
|
|
}
|
|
|
|
removeSelectedItems()
|
|
{
|
|
if (!this._selectedItems.size)
|
|
return;
|
|
|
|
let operation = this._allowsMultipleSelection ? WI.SelectionController.Operation.Extend : WI.SelectionController.Operation.Direct;
|
|
|
|
let orderedSelection = Array.from(this._selectedItems).sort(this._comparator);
|
|
|
|
// Try selecting the item preceding the selection.
|
|
let firstSelectedItem = orderedSelection[0];
|
|
let itemToSelect = this._previousSelectableItem(firstSelectedItem, operation);
|
|
if (!itemToSelect) {
|
|
// If no item exists before the first item in the selection, try selecting
|
|
// a deselected item (hole) within the selection.
|
|
itemToSelect = firstSelectedItem;
|
|
while (itemToSelect && this.hasSelectedItem(itemToSelect))
|
|
itemToSelect = this._nextSelectableItem(itemToSelect, operation);
|
|
|
|
if (!itemToSelect || this.hasSelectedItem(itemToSelect)) {
|
|
// If the selection contains no holes, try selecting the item
|
|
// following the selection.
|
|
itemToSelect = this._nextSelectableItem(orderedSelection.lastValue, operation);
|
|
}
|
|
}
|
|
|
|
this._deselectAllAndSelect(itemToSelect);
|
|
}
|
|
|
|
reset()
|
|
{
|
|
this._lastSelectedItem = null;
|
|
this._shiftAnchorItem = null;
|
|
this._selectedItems.clear();
|
|
}
|
|
|
|
didRemoveItems(items)
|
|
{
|
|
console.assert(items instanceof Set);
|
|
|
|
if (!items.size || !this._selectedItems.size)
|
|
return;
|
|
|
|
this._updateSelectedItems(this._selectedItems.difference(items));
|
|
}
|
|
|
|
handleKeyDown(event)
|
|
{
|
|
if (event.key === "a" && event.commandOrControlKey) {
|
|
this.selectAll();
|
|
return true;
|
|
}
|
|
|
|
if (event.metaKey || event.ctrlKey)
|
|
return false;
|
|
|
|
if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down") {
|
|
this._selectItemsFromArrowKey(event.keyIdentifier === "Up", event.shiftKey);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
handleItemMouseDown(item, event)
|
|
{
|
|
console.assert(item, "Invalid item for selection.");
|
|
|
|
if (event.button !== 0 || event.ctrlKey)
|
|
return;
|
|
|
|
// Command (macOS) or Control (Windows) key takes precedence over shift
|
|
// whether or not multiple selection is enabled, so handle it first.
|
|
if (event.commandOrControlKey) {
|
|
if (this.hasSelectedItem(item))
|
|
this.deselectItem(item);
|
|
else
|
|
this.selectItem(item, this._allowsMultipleSelection);
|
|
return;
|
|
}
|
|
|
|
let shiftExtendSelection = this._allowsMultipleSelection && event.shiftKey;
|
|
if (!shiftExtendSelection) {
|
|
this.selectItem(item);
|
|
return;
|
|
}
|
|
|
|
let newItems = new Set(this._selectedItems);
|
|
|
|
// Shift-clicking when nothing is selected should cause the first item
|
|
// through the clicked item to be selected.
|
|
if (!newItems.size) {
|
|
this._lastSelectedItem = item;
|
|
this._shiftAnchorItem = this._firstSelectableItem(WI.SelectionController.Operation.Extend);
|
|
|
|
this._addRange(newItems, this._shiftAnchorItem, this._lastSelectedItem);
|
|
this._updateSelectedItems(newItems);
|
|
return;
|
|
}
|
|
|
|
if (!this._shiftAnchorItem)
|
|
this._shiftAnchorItem = this._lastSelectedItem;
|
|
|
|
// Shift-clicking will add to or delete from the current selection, or
|
|
// pivot the selection around the anchor (a delete followed by an add).
|
|
// We could check for all three cases, and add or delete only those items
|
|
// that are necessary, but it is simpler to throw out the previous shift-
|
|
// selected range and add the new range between the anchor and clicked item.
|
|
|
|
let sortItemPair = (a, b) => {
|
|
return [a, b].sort(this._comparator);
|
|
};
|
|
|
|
if (this._shiftAnchorItem !== this._lastSelectedItem) {
|
|
let [startItem, endItem] = sortItemPair(this._shiftAnchorItem, this._lastSelectedItem);
|
|
this._deleteRange(newItems, startItem, endItem);
|
|
}
|
|
|
|
let [startItem, endItem] = sortItemPair(this._shiftAnchorItem, item);
|
|
this._addRange(newItems, startItem, endItem);
|
|
|
|
this._lastSelectedItem = item;
|
|
|
|
this._updateSelectedItems(newItems);
|
|
}
|
|
|
|
// Private
|
|
|
|
_deselectAllAndSelect(item)
|
|
{
|
|
if (!this._selectedItems.size && !item)
|
|
return;
|
|
|
|
if (this._selectedItems.size === 1 && this.hasSelectedItem(item))
|
|
return;
|
|
|
|
this._lastSelectedItem = item;
|
|
this._shiftAnchorItem = null;
|
|
|
|
let newItems = new Set;
|
|
if (item)
|
|
newItems.add(item);
|
|
|
|
this._updateSelectedItems(newItems);
|
|
}
|
|
|
|
_selectItemsFromArrowKey(goingUp, shiftKey)
|
|
{
|
|
let extendSelection = shiftKey && this._allowsMultipleSelection;
|
|
let operation = extendSelection ? WI.SelectionController.Operation.Extend : WI.SelectionController.Operation.Direct;
|
|
|
|
if (!this._selectedItems.size) {
|
|
this.selectItem(goingUp ? this._lastSelectableItem(operation) : this._firstSelectableItem(operation));
|
|
return;
|
|
}
|
|
|
|
let item = goingUp ? this._previousSelectableItem(this._lastSelectedItem, operation) : this._nextSelectableItem(this._lastSelectedItem, operation);
|
|
if (!item)
|
|
return;
|
|
|
|
if (!extendSelection || !this.hasSelectedItem(item)) {
|
|
this.selectItem(item, extendSelection);
|
|
return;
|
|
}
|
|
|
|
// Since the item in the direction of movement is selected, we are either
|
|
// extending the selection into the item, or deselecting. Determine which
|
|
// by checking whether the item opposite the anchor item is selected.
|
|
let priorItem = goingUp ? this._nextSelectableItem(this._lastSelectedItem, operation) : this._previousSelectableItem(this._lastSelectedItem, operation);
|
|
if (!priorItem || !this.hasSelectedItem(priorItem)) {
|
|
this.deselectItem(this._lastSelectedItem);
|
|
return;
|
|
}
|
|
|
|
// The selection is being extended into the item; make it the new
|
|
// anchor item then continue searching in the direction of movement
|
|
// for an unselected item to select.
|
|
while (item) {
|
|
if (!this.hasSelectedItem(item)) {
|
|
this.selectItem(item, extendSelection);
|
|
break;
|
|
}
|
|
|
|
this._lastSelectedItem = item;
|
|
item = goingUp ? this._previousSelectableItem(item, operation) : this._nextSelectableItem(item, operation);
|
|
}
|
|
}
|
|
|
|
_firstSelectableItem(operation)
|
|
{
|
|
return this._delegate.selectionControllerFirstSelectableItem(this, operation);
|
|
}
|
|
|
|
_lastSelectableItem(operation)
|
|
{
|
|
return this._delegate.selectionControllerLastSelectableItem(this, operation);
|
|
}
|
|
|
|
_previousSelectableItem(item, operation)
|
|
{
|
|
return this._delegate.selectionControllerPreviousSelectableItem(this, item, operation);
|
|
}
|
|
|
|
_nextSelectableItem(item, operation)
|
|
{
|
|
return this._delegate.selectionControllerNextSelectableItem(this, item, operation);
|
|
}
|
|
|
|
_updateSelectedItems(items)
|
|
{
|
|
let oldSelectedItems = this._selectedItems;
|
|
this._selectedItems = items;
|
|
|
|
if (this._suppressSelectionDidChange || !this._delegate.selectionControllerSelectionDidChange)
|
|
return;
|
|
|
|
let deselectedItems = oldSelectedItems.difference(items);
|
|
let selectedItems = items.difference(oldSelectedItems);
|
|
if (deselectedItems.size || selectedItems.size)
|
|
this._delegate.selectionControllerSelectionDidChange(this, deselectedItems, selectedItems);
|
|
}
|
|
|
|
_addRange(items, firstItem, lastItem)
|
|
{
|
|
console.assert(this._allowsMultipleSelection);
|
|
|
|
const operation = WI.SelectionController.Operation.Extend;
|
|
|
|
let current = firstItem;
|
|
while (current) {
|
|
items.add(current);
|
|
if (current === lastItem)
|
|
break;
|
|
current = this._nextSelectableItem(current, operation);
|
|
}
|
|
|
|
console.assert(!lastItem || items.has(lastItem), "End of range could not be reached.");
|
|
}
|
|
|
|
_deleteRange(items, firstItem, lastItem)
|
|
{
|
|
console.assert(this._allowsMultipleSelection);
|
|
|
|
const operation = WI.SelectionController.Operation.Extend;
|
|
|
|
let current = firstItem;
|
|
while (current) {
|
|
items.delete(current);
|
|
if (current === lastItem)
|
|
break;
|
|
current = this._nextSelectableItem(current, operation);
|
|
}
|
|
|
|
console.assert(!lastItem || !items.has(lastItem), "End of range could not be reached.");
|
|
}
|
|
};
|
|
|
|
WI.SelectionController.Operation = {
|
|
Direct: Symbol("selection-operation-direct"),
|
|
Extend: Symbol("selection-operation-extend"),
|
|
};
|
|
|
|
/* Views/CodeMirrorAdditions.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
(function () {
|
|
// By default CodeMirror defines syntax highlighting styles based on token
|
|
// only and shared styles between modes. This limiting and does not match
|
|
// what we have done in the Web Inspector. So this modifies the XML, CSS
|
|
// and JavaScript modes to supply two styles for each token. One for the
|
|
// token and one with the mode name.
|
|
|
|
function tokenizeLinkString(stream, state)
|
|
{
|
|
console.assert(state._linkQuoteCharacter !== undefined);
|
|
|
|
// Eat the string until the same quote is found that started the string.
|
|
// If this is unquoted, then eat until whitespace or common parse errors.
|
|
if (state._linkQuoteCharacter)
|
|
stream.eatWhile(new RegExp("[^" + state._linkQuoteCharacter + "]"));
|
|
else
|
|
stream.eatWhile(/[^\s\u00a0=<>\"\']/);
|
|
|
|
// If the stream isn't at the end of line then we found the end quote.
|
|
// In the case, change _linkTokenize to parse the end of the link next.
|
|
// Otherwise _linkTokenize will stay as-is to parse more of the link.
|
|
if (!stream.eol())
|
|
state._linkTokenize = tokenizeEndOfLinkString;
|
|
|
|
return "link";
|
|
}
|
|
|
|
function tokenizeEndOfLinkString(stream, state)
|
|
{
|
|
console.assert(state._linkQuoteCharacter !== undefined);
|
|
console.assert(state._linkBaseStyle);
|
|
|
|
// Eat the quote character to style it with the base style.
|
|
if (state._linkQuoteCharacter)
|
|
stream.eat(state._linkQuoteCharacter);
|
|
|
|
var style = state._linkBaseStyle;
|
|
|
|
// Clean up the state.
|
|
delete state._linkTokenize;
|
|
delete state._linkQuoteCharacter;
|
|
delete state._linkBaseStyle;
|
|
delete state._srcSetTokenizeState;
|
|
|
|
return style;
|
|
}
|
|
|
|
function tokenizeSrcSetString(stream, state)
|
|
{
|
|
console.assert(state._linkQuoteCharacter !== undefined);
|
|
|
|
if (state._srcSetTokenizeState === "link") {
|
|
// Eat the string until a space, comma, or ending quote.
|
|
// If this is unquoted, then eat until whitespace or common parse errors.
|
|
if (state._linkQuoteCharacter)
|
|
stream.eatWhile(new RegExp("[^\\s," + state._linkQuoteCharacter + "]"));
|
|
else
|
|
stream.eatWhile(/[^\s,\u00a0=<>\"\']/);
|
|
} else {
|
|
// Eat the string until a comma, or ending quote.
|
|
// If this is unquoted, then eat until whitespace or common parse errors.
|
|
stream.eatSpace();
|
|
if (state._linkQuoteCharacter)
|
|
stream.eatWhile(new RegExp("[^," + state._linkQuoteCharacter + "]"));
|
|
else
|
|
stream.eatWhile(/[^\s\u00a0=<>\"\']/);
|
|
stream.eatWhile(/[\s,]/);
|
|
}
|
|
|
|
// If the stream isn't at the end of line and we found the end quote
|
|
// change _linkTokenize to parse the end of the link next. Otherwise
|
|
// _linkTokenize will stay as-is to parse more of the srcset.
|
|
if (stream.eol() || (!state._linkQuoteCharacter || stream.peek() === state._linkQuoteCharacter))
|
|
state._linkTokenize = tokenizeEndOfLinkString;
|
|
|
|
// Link portion.
|
|
if (state._srcSetTokenizeState === "link") {
|
|
state._srcSetTokenizeState = "descriptor";
|
|
return "link";
|
|
}
|
|
|
|
// Descriptor portion.
|
|
state._srcSetTokenizeState = "link";
|
|
return state._linkBaseStyle;
|
|
}
|
|
|
|
function extendedXMLToken(stream, state)
|
|
{
|
|
if (state._linkTokenize) {
|
|
// Call the link tokenizer instead.
|
|
var style = state._linkTokenize(stream, state);
|
|
return style && (style + " m-" + this.name);
|
|
}
|
|
|
|
// Remember the start position so we can rewind if needed.
|
|
var startPosition = stream.pos;
|
|
var style = this._token(stream, state);
|
|
if (style === "attribute") {
|
|
// Look for "href" or "src" attributes. If found then we should
|
|
// expect a string later that should get the "link" style instead.
|
|
var text = stream.current().toLowerCase();
|
|
if (text === "src" || /\bhref\b/.test(text))
|
|
state._expectLink = true;
|
|
else if (text === "srcset")
|
|
state._expectSrcSet = true;
|
|
else {
|
|
delete state._expectLink;
|
|
delete state._expectSrcSet;
|
|
}
|
|
} else if (state._expectLink && style === "string") {
|
|
var current = stream.current();
|
|
|
|
// Unless current token is empty quotes, consume quote character
|
|
// and tokenize link next.
|
|
if (current !== "\"\"" && current !== "''") {
|
|
delete state._expectLink;
|
|
|
|
// This is a link, so setup the state to process it next.
|
|
state._linkTokenize = tokenizeLinkString;
|
|
state._linkBaseStyle = style;
|
|
|
|
// The attribute may or may not be quoted.
|
|
var quote = current[0];
|
|
|
|
state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
|
|
|
|
// Rewind the stream to the start of this token.
|
|
stream.pos = startPosition;
|
|
|
|
// Eat the open quote of the string so the string style
|
|
// will be used for the quote character.
|
|
if (state._linkQuoteCharacter)
|
|
stream.eat(state._linkQuoteCharacter);
|
|
}
|
|
} else if (state._expectSrcSet && style === "string") {
|
|
var current = stream.current();
|
|
|
|
// Unless current token is empty quotes, consume quote character
|
|
// and tokenize link next.
|
|
if (current !== "\"\"" && current !== "''") {
|
|
delete state._expectSrcSet;
|
|
|
|
// This is a link, so setup the state to process it next.
|
|
state._srcSetTokenizeState = "link";
|
|
state._linkTokenize = tokenizeSrcSetString;
|
|
state._linkBaseStyle = style;
|
|
|
|
// The attribute may or may not be quoted.
|
|
var quote = current[0];
|
|
|
|
state._linkQuoteCharacter = quote === "'" || quote === "\"" ? quote : null;
|
|
|
|
// Rewind the stream to the start of this token.
|
|
stream.pos = startPosition;
|
|
|
|
// Eat the open quote of the string so the string style
|
|
// will be used for the quote character.
|
|
if (state._linkQuoteCharacter)
|
|
stream.eat(state._linkQuoteCharacter);
|
|
}
|
|
} else if (style) {
|
|
// We don't expect other tokens between attribute and string since
|
|
// spaces and the equal character are not tokenized. So if we get
|
|
// another token before a string then we stop expecting a link.
|
|
delete state._expectLink;
|
|
delete state._expectSrcSet;
|
|
}
|
|
|
|
return style && (style + " m-" + this.name);
|
|
}
|
|
|
|
function tokenizeCSSURLString(stream, state)
|
|
{
|
|
console.assert(state._urlQuoteCharacter);
|
|
|
|
// If we are an unquoted url string, return whitespace blocks as a whitespace token (null).
|
|
if (state._unquotedURLString && stream.eatSpace())
|
|
return null;
|
|
|
|
var ch = null;
|
|
var escaped = false;
|
|
var reachedEndOfURL = false;
|
|
var lastNonWhitespace = stream.pos;
|
|
var quote = state._urlQuoteCharacter;
|
|
|
|
// Parse characters until the end of the stream/line or a proper end quote character.
|
|
while ((ch = stream.next()) != null) {
|
|
if (ch === quote && !escaped) {
|
|
reachedEndOfURL = true;
|
|
break;
|
|
}
|
|
escaped = !escaped && ch === "\\";
|
|
if (!/[\s\u00a0]/.test(ch))
|
|
lastNonWhitespace = stream.pos;
|
|
}
|
|
|
|
// If we are an unquoted url string, do not include trailing whitespace, rewind to the last real character.
|
|
if (state._unquotedURLString)
|
|
stream.pos = lastNonWhitespace;
|
|
|
|
// If we have reached the proper the end of the url string, switch to the end tokenizer to reset the state.
|
|
if (reachedEndOfURL) {
|
|
if (!state._unquotedURLString)
|
|
stream.backUp(1);
|
|
this._urlTokenize = tokenizeEndOfCSSURLString;
|
|
}
|
|
|
|
return "link";
|
|
}
|
|
|
|
function tokenizeEndOfCSSURLString(stream, state)
|
|
{
|
|
console.assert(state._urlQuoteCharacter);
|
|
console.assert(state._urlBaseStyle);
|
|
|
|
// Eat the quote character to style it with the base style.
|
|
if (!state._unquotedURLString)
|
|
stream.eat(state._urlQuoteCharacter);
|
|
|
|
var style = state._urlBaseStyle;
|
|
|
|
delete state._urlTokenize;
|
|
delete state._urlQuoteCharacter;
|
|
delete state._urlBaseStyle;
|
|
|
|
return style;
|
|
}
|
|
|
|
function extendedCSSToken(stream, state)
|
|
{
|
|
var hexColorRegex = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{3,4})\b/g;
|
|
|
|
if (state._urlTokenize) {
|
|
// Call the link tokenizer instead.
|
|
var style = state._urlTokenize(stream, state);
|
|
return style && (style + " m-" + (this.alternateName || this.name));
|
|
}
|
|
|
|
// Remember the start position so we can rewind if needed.
|
|
var startPosition = stream.pos;
|
|
var style = this._token(stream, state);
|
|
|
|
if (style) {
|
|
if (style === "atom") {
|
|
if (stream.current() === "url") {
|
|
// If the current text is "url" then we should expect the next string token to be a link.
|
|
state._expectLink = true;
|
|
} else if (hexColorRegex.test(stream.current()))
|
|
style = style + " hex-color";
|
|
} else if (style === "error") {
|
|
if (state.state=== "atBlock" || state.state === "atBlock_parens") {
|
|
switch (stream.current()) {
|
|
case "prefers-color-scheme":
|
|
case "light":
|
|
case "dark":
|
|
case "prefers-reduced-motion":
|
|
case "reduce":
|
|
case "no-preference":
|
|
case "inverted-colors":
|
|
case "inverted":
|
|
case "color-gamut":
|
|
case "p3":
|
|
case "rec2020":
|
|
case "display-mode":
|
|
case "fullscreen":
|
|
case "standalone":
|
|
case "minimal-ui":
|
|
case "browser":
|
|
case /*-webkit-*/"video-playable-inline":
|
|
case /*-webkit-*/"transform-2d":
|
|
case /*-webkit-*/"transform-3d":
|
|
style = "property";
|
|
break;
|
|
}
|
|
}
|
|
} else if (state._expectLink) {
|
|
delete state._expectLink;
|
|
|
|
if (style === "string") {
|
|
// This is a link, so setup the state to process it next.
|
|
state._urlTokenize = tokenizeCSSURLString;
|
|
state._urlBaseStyle = style;
|
|
|
|
// The url may or may not be quoted.
|
|
var quote = stream.current()[0];
|
|
state._urlQuoteCharacter = quote === "'" || quote === "\"" ? quote : ")";
|
|
state._unquotedURLString = state._urlQuoteCharacter === ")";
|
|
|
|
// Rewind the stream to the start of this token.
|
|
stream.pos = startPosition;
|
|
|
|
// Eat the open quote of the string so the string style
|
|
// will be used for the quote character.
|
|
if (!state._unquotedURLString)
|
|
stream.eat(state._urlQuoteCharacter);
|
|
}
|
|
}
|
|
}
|
|
|
|
return style && (style + " m-" + (this.alternateName || this.name));
|
|
}
|
|
|
|
function extendedJavaScriptToken(stream, state)
|
|
{
|
|
// CodeMirror moves the original token function to _token when we extended it.
|
|
// So call it to get the style that we will add an additional class name to.
|
|
var style = this._token(stream, state);
|
|
|
|
if (style === "number" && stream.current().endsWith("n"))
|
|
style += " bigint";
|
|
|
|
return style && (style + " m-" + (this.alternateName || this.name));
|
|
}
|
|
|
|
function scrollCursorIntoView(codeMirror, event)
|
|
{
|
|
// We don't want to use the default implementation since it can cause massive jumping
|
|
// when the editor is contained inside overflow elements.
|
|
event.preventDefault();
|
|
|
|
function delayedWork()
|
|
{
|
|
// Don't try to scroll unless the editor is focused.
|
|
if (!codeMirror.getWrapperElement().classList.contains("CodeMirror-focused"))
|
|
return;
|
|
|
|
// The cursor element can contain multiple cursors. The first one is the blinky cursor,
|
|
// which is the one we want to scroll into view. It can be missing, so check first.
|
|
var cursorElement = codeMirror.getScrollerElement().getElementsByClassName("CodeMirror-cursor")[0];
|
|
if (cursorElement)
|
|
cursorElement.scrollIntoViewIfNeeded(false);
|
|
}
|
|
|
|
// We need to delay this because CodeMirror can fire scrollCursorIntoView as a view is being blurred
|
|
// and another is being focused. The blurred editor still has the focused state when this event fires.
|
|
// We don't want to scroll the blurred editor into view, only the focused editor.
|
|
setTimeout(delayedWork, 0);
|
|
}
|
|
|
|
CodeMirror.extendMode("css", {token: extendedCSSToken});
|
|
CodeMirror.extendMode("xml", {token: extendedXMLToken});
|
|
CodeMirror.extendMode("javascript", {token: extendedJavaScriptToken});
|
|
|
|
CodeMirror.defineInitHook(function(codeMirror) {
|
|
codeMirror.on("scrollCursorIntoView", scrollCursorIntoView);
|
|
});
|
|
|
|
let whitespaceStyleElement = null;
|
|
let whitespaceCountsWithStyling = new Set;
|
|
CodeMirror.defineOption("showWhitespaceCharacters", false, function(cm, value, old) {
|
|
if (!value || (old && old !== CodeMirror.Init)) {
|
|
cm.removeOverlay("whitespace");
|
|
return;
|
|
}
|
|
|
|
cm.addOverlay({
|
|
name: "whitespace",
|
|
token(stream) {
|
|
if (stream.peek() === " ") {
|
|
let count = 0;
|
|
while (stream.peek() === " ") {
|
|
++count;
|
|
stream.next();
|
|
}
|
|
|
|
if (!whitespaceCountsWithStyling.has(count)) {
|
|
whitespaceCountsWithStyling.add(count);
|
|
|
|
if (!whitespaceStyleElement)
|
|
whitespaceStyleElement = document.head.appendChild(document.createElement("style"));
|
|
|
|
const middleDot = "\\00B7";
|
|
|
|
let styleText = whitespaceStyleElement.textContent;
|
|
styleText += `.show-whitespace-characters .CodeMirror .cm-whitespace-${count}::before {`;
|
|
styleText += `content: "${middleDot.repeat(count)}";`;
|
|
styleText += `}`;
|
|
|
|
whitespaceStyleElement.textContent = styleText;
|
|
}
|
|
|
|
return `whitespace whitespace-${count}`;
|
|
}
|
|
|
|
while (!stream.eol() && stream.peek() !== " ")
|
|
stream.next();
|
|
|
|
return null;
|
|
}
|
|
});
|
|
});
|
|
|
|
CodeMirror.defineExtension("hasLineClass", function(line, where, className) {
|
|
// This matches the arguments to addLineClass and removeLineClass.
|
|
var classProperty = where === "text" ? "textClass" : (where === "background" ? "bgClass" : "wrapClass");
|
|
var lineInfo = this.lineInfo(line);
|
|
if (!lineInfo)
|
|
return false;
|
|
|
|
if (!lineInfo[classProperty])
|
|
return false;
|
|
|
|
// Test for the simple case.
|
|
if (lineInfo[classProperty] === className)
|
|
return true;
|
|
|
|
// Do a quick check for the substring. This is faster than a regex, which requires escaping the input first.
|
|
var index = lineInfo[classProperty].indexOf(className);
|
|
if (index === -1)
|
|
return false;
|
|
|
|
// Check that it is surrounded by spaces. Add padding spaces first to work with beginning and end of string cases.
|
|
var paddedClass = " " + lineInfo[classProperty] + " ";
|
|
return paddedClass.indexOf(" " + className + " ", index) !== -1;
|
|
});
|
|
|
|
CodeMirror.defineExtension("setUniqueBookmark", function(position, options) {
|
|
var marks = this.findMarksAt(position);
|
|
for (var i = 0; i < marks.length; ++i) {
|
|
if (marks[i].__uniqueBookmark) {
|
|
marks[i].clear();
|
|
break;
|
|
}
|
|
}
|
|
|
|
var uniqueBookmark = this.setBookmark(position, options);
|
|
uniqueBookmark.__uniqueBookmark = true;
|
|
return uniqueBookmark;
|
|
});
|
|
|
|
CodeMirror.defineExtension("toggleLineClass", function(line, where, className) {
|
|
if (this.hasLineClass(line, where, className)) {
|
|
this.removeLineClass(line, where, className);
|
|
return false;
|
|
}
|
|
|
|
this.addLineClass(line, where, className);
|
|
return true;
|
|
});
|
|
|
|
CodeMirror.defineExtension("alterNumberInRange", function(amount, startPosition, endPosition, updateSelection) {
|
|
// We don't try if the range is multiline, pass to another key handler.
|
|
if (startPosition.line !== endPosition.line)
|
|
return false;
|
|
|
|
if (updateSelection) {
|
|
// Remember the cursor position/selection.
|
|
var selectionStart = this.getCursor("start");
|
|
var selectionEnd = this.getCursor("end");
|
|
}
|
|
|
|
var line = this.getLine(startPosition.line);
|
|
|
|
var foundPeriod = false;
|
|
|
|
var start = NaN;
|
|
var end = NaN;
|
|
|
|
for (var i = startPosition.ch; i >= 0; --i) {
|
|
var character = line.charAt(i);
|
|
|
|
if (character === ".") {
|
|
if (foundPeriod)
|
|
break;
|
|
foundPeriod = true;
|
|
} else if (character !== "-" && character !== "+" && isNaN(parseInt(character))) {
|
|
// Found the end already, just scan backwards.
|
|
if (i === startPosition.ch) {
|
|
end = i;
|
|
continue;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
start = i;
|
|
}
|
|
|
|
if (isNaN(end)) {
|
|
for (var i = startPosition.ch + 1; i < line.length; ++i) {
|
|
var character = line.charAt(i);
|
|
|
|
if (character === ".") {
|
|
if (foundPeriod) {
|
|
end = i;
|
|
break;
|
|
}
|
|
|
|
foundPeriod = true;
|
|
} else if (isNaN(parseInt(character))) {
|
|
end = i;
|
|
break;
|
|
}
|
|
|
|
end = i + 1;
|
|
}
|
|
}
|
|
|
|
// No number range found, pass to another key handler.
|
|
if (isNaN(start) || isNaN(end))
|
|
return false;
|
|
|
|
var number = parseFloat(line.substring(start, end));
|
|
|
|
// Make the new number and constrain it to a precision of 6, this matches numbers the engine returns.
|
|
// Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1.
|
|
var alteredNumber = Number((number + amount).toFixed(6));
|
|
var alteredNumberString = alteredNumber.toString();
|
|
|
|
var from = {line: startPosition.line, ch: start};
|
|
var to = {line: startPosition.line, ch: end};
|
|
|
|
this.replaceRange(alteredNumberString, from, to);
|
|
|
|
if (updateSelection) {
|
|
var previousLength = to.ch - from.ch;
|
|
var newLength = alteredNumberString.length;
|
|
|
|
// Fix up the selection so it follows the increase or decrease in the replacement length.
|
|
// selectionStart/End may the same object if there is no selection. If that is the case
|
|
// make only one modification to prevent a double adjustment, and keep it a single object
|
|
// to avoid CodeMirror inadvertently creating an actual selection range.
|
|
let diff = newLength - previousLength;
|
|
if (selectionStart === selectionEnd)
|
|
selectionStart.ch += diff;
|
|
else {
|
|
if (selectionStart.ch > from.ch)
|
|
selectionStart.ch += diff;
|
|
if (selectionEnd.ch > from.ch)
|
|
selectionEnd.ch += diff;
|
|
}
|
|
|
|
this.setSelection(selectionStart, selectionEnd);
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
function alterNumber(amount, codeMirror)
|
|
{
|
|
function findNumberToken(position)
|
|
{
|
|
// CodeMirror includes the unit in the number token, so searching for
|
|
// number tokens is the best way to get both the number and unit.
|
|
var token = codeMirror.getTokenAt(position);
|
|
if (token && token.type && /\bnumber\b/.test(token.type))
|
|
return token;
|
|
return null;
|
|
}
|
|
|
|
var position = codeMirror.getCursor("head");
|
|
var token = findNumberToken(position);
|
|
|
|
if (!token) {
|
|
// If the cursor is at the outside beginning of the token, the previous
|
|
// findNumberToken wont find it. So check the next column for a number too.
|
|
position.ch += 1;
|
|
token = findNumberToken(position);
|
|
}
|
|
|
|
if (!token)
|
|
return CodeMirror.Pass;
|
|
|
|
var foundNumber = codeMirror.alterNumberInRange(amount, {ch: token.start, line: position.line}, {ch: token.end, line: position.line}, true);
|
|
if (!foundNumber)
|
|
return CodeMirror.Pass;
|
|
}
|
|
|
|
CodeMirror.defineExtension("rectsForRange", function(range) {
|
|
var lineRects = [];
|
|
|
|
for (var line = range.start.line; line <= range.end.line; ++line) {
|
|
var lineContent = this.getLine(line);
|
|
|
|
var startChar = line === range.start.line ? range.start.ch : (lineContent.length - lineContent.trimLeft().length);
|
|
var endChar = line === range.end.line ? range.end.ch : lineContent.length;
|
|
var firstCharCoords = this.cursorCoords({ch: startChar, line});
|
|
var endCharCoords = this.cursorCoords({ch: endChar, line});
|
|
|
|
// Handle line wrapping.
|
|
if (firstCharCoords.bottom !== endCharCoords.bottom) {
|
|
var maxY = -Number.MAX_VALUE;
|
|
for (var ch = startChar; ch <= endChar; ++ch) {
|
|
var coords = this.cursorCoords({ch, line});
|
|
if (coords.bottom > maxY) {
|
|
if (ch > startChar) {
|
|
var maxX = Math.ceil(this.cursorCoords({ch: ch - 1, line}).right);
|
|
lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
|
|
}
|
|
var minX = Math.floor(coords.left);
|
|
var minY = Math.floor(coords.top);
|
|
maxY = Math.ceil(coords.bottom);
|
|
}
|
|
}
|
|
maxX = Math.ceil(coords.right);
|
|
lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
|
|
} else {
|
|
var minX = Math.floor(firstCharCoords.left);
|
|
var minY = Math.floor(firstCharCoords.top);
|
|
var maxX = Math.ceil(endCharCoords.right);
|
|
var maxY = Math.ceil(endCharCoords.bottom);
|
|
lineRects.push(new WI.Rect(minX, minY, maxX - minX, maxY - minY));
|
|
}
|
|
}
|
|
return lineRects;
|
|
});
|
|
|
|
let mac = WI.Platform.name === "mac";
|
|
|
|
CodeMirror.keyMap["default"] = {
|
|
"Alt-Up": alterNumber.bind(null, 1),
|
|
"Ctrl-Alt-Up": alterNumber.bind(null, 0.1),
|
|
"Shift-Alt-Up": alterNumber.bind(null, 10),
|
|
"Alt-PageUp": alterNumber.bind(null, 10),
|
|
"Shift-Alt-PageUp": alterNumber.bind(null, 100),
|
|
"Alt-Down": alterNumber.bind(null, -1),
|
|
"Ctrl-Alt-Down": alterNumber.bind(null, -0.1),
|
|
"Shift-Alt-Down": alterNumber.bind(null, -10),
|
|
"Alt-PageDown": alterNumber.bind(null, -10),
|
|
"Shift-Alt-PageDown": alterNumber.bind(null, -100),
|
|
"Cmd-/": "toggleComment",
|
|
"Cmd-D": "selectNextOccurrence",
|
|
"Shift-Tab": "indentLess",
|
|
fallthrough: mac ? "macDefault" : "pcDefault"
|
|
};
|
|
|
|
{
|
|
// CodeMirror's default behavior is to always insert a tab ("\t") regardless of `indentWithTabs`.
|
|
let original = CodeMirror.commands.insertTab;
|
|
CodeMirror.commands.insertTab = function(cm) {
|
|
if (cm.options.indentWithTabs)
|
|
original(cm);
|
|
else
|
|
CodeMirror.commands.insertSoftTab(cm);
|
|
};
|
|
}
|
|
|
|
// Register some extra MIME-types for CodeMirror. These are in addition to the
|
|
// ones CodeMirror already registers, like text/html, text/javascript, etc.
|
|
var extraXMLTypes = ["text/xml", "text/xsl"];
|
|
extraXMLTypes.forEach(function(type) {
|
|
CodeMirror.defineMIME(type, "xml");
|
|
});
|
|
|
|
var extraHTMLTypes = ["application/xhtml+xml", "image/svg+xml"];
|
|
extraHTMLTypes.forEach(function(type) {
|
|
CodeMirror.defineMIME(type, "htmlmixed");
|
|
});
|
|
|
|
var extraJavaScriptTypes = ["text/ecmascript", "application/javascript", "application/ecmascript", "application/x-javascript",
|
|
"text/x-javascript", "text/javascript1.1", "text/javascript1.2", "text/javascript1.3", "text/jscript", "text/livescript"];
|
|
extraJavaScriptTypes.forEach(function(type) {
|
|
CodeMirror.defineMIME(type, "javascript");
|
|
});
|
|
|
|
var extraJSONTypes = ["application/x-json", "text/x-json", "application/vnd.api+json"];
|
|
extraJSONTypes.forEach(function(type) {
|
|
CodeMirror.defineMIME(type, {name: "javascript", json: true});
|
|
});
|
|
|
|
// FIXME: Add WHLSL specific modes.
|
|
CodeMirror.defineMIME("x-pipeline/x-compute", CodeMirror.resolveMode("x-shader/x-vertex"));
|
|
CodeMirror.defineMIME("x-pipeline/x-render", CodeMirror.resolveMode("x-shader/x-vertex"));
|
|
})();
|
|
|
|
WI.compareCodeMirrorPositions = function(a, b)
|
|
{
|
|
var lineCompare = a.line - b.line;
|
|
if (lineCompare !== 0)
|
|
return lineCompare;
|
|
|
|
var aColumn = "ch" in a ? a.ch : Number.MAX_VALUE;
|
|
var bColumn = "ch" in b ? b.ch : Number.MAX_VALUE;
|
|
return aColumn - bColumn;
|
|
};
|
|
|
|
WI.walkTokens = function(cm, mode, initialPosition, callback)
|
|
{
|
|
let state = CodeMirror.copyState(mode, cm.getTokenAt(initialPosition).state);
|
|
if (state.localState)
|
|
state = state.localState;
|
|
|
|
let lineCount = cm.lineCount();
|
|
let abort = false;
|
|
for (let lineNumber = initialPosition.line; !abort && lineNumber < lineCount; ++lineNumber) {
|
|
let line = cm.getLine(lineNumber);
|
|
let stream = new CodeMirror.StringStream(line);
|
|
if (lineNumber === initialPosition.line)
|
|
stream.start = stream.pos = initialPosition.ch;
|
|
|
|
while (!stream.eol()) {
|
|
let tokenType = mode.token(stream, state);
|
|
if (!callback(tokenType, stream.current())) {
|
|
abort = true;
|
|
break;
|
|
}
|
|
stream.start = stream.pos;
|
|
}
|
|
}
|
|
|
|
if (!abort)
|
|
callback(null);
|
|
};
|
|
|
|
WI.tokenizeCSSValue = function(cssValue)
|
|
{
|
|
const rulePrefix = "*{X:";
|
|
let cssRule = rulePrefix + cssValue + "}";
|
|
let tokens = [];
|
|
|
|
let mode = CodeMirror.getMode({indentUnit: 0}, "text/css");
|
|
let state = CodeMirror.startState(mode);
|
|
let stream = new CodeMirror.StringStream(cssRule);
|
|
|
|
function processToken(token, tokenType, column) {
|
|
if (column < rulePrefix.length)
|
|
return;
|
|
|
|
if (token === "}" && !tokenType)
|
|
return;
|
|
|
|
tokens.push({value: token, type: tokenType});
|
|
}
|
|
|
|
while (!stream.eol()) {
|
|
let style = mode.token(stream, state);
|
|
let value = stream.current();
|
|
processToken(value, style, stream.start);
|
|
stream.start = stream.pos;
|
|
}
|
|
|
|
return tokens;
|
|
};
|
|
|
|
/* Views/EditingSupport.js */
|
|
|
|
/*
|
|
* Copyright (C) 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.enclosingCodeMirror = function(element)
|
|
{
|
|
while (element) {
|
|
if (element.CodeMirror)
|
|
return element.CodeMirror;
|
|
element = element.parentNode;
|
|
}
|
|
return null;
|
|
};
|
|
|
|
WI.isBeingEdited = function(element)
|
|
{
|
|
while (element) {
|
|
if (element.__editing)
|
|
return true;
|
|
element = element.parentNode;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
WI.markBeingEdited = function(element, value)
|
|
{
|
|
if (value) {
|
|
if (element.__editing)
|
|
return false;
|
|
element.__editing = true;
|
|
WI.__editingCount = (WI.__editingCount || 0) + 1;
|
|
} else {
|
|
if (!element.__editing)
|
|
return false;
|
|
delete element.__editing;
|
|
--WI.__editingCount;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
WI.isEditingAnyField = function()
|
|
{
|
|
return !!WI.__editingCount;
|
|
};
|
|
|
|
WI.isEventTargetAnEditableField = function(event)
|
|
{
|
|
if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement)
|
|
return true;
|
|
|
|
if (event.target.isContentEditable)
|
|
return true;
|
|
|
|
if (WI.isBeingEdited(event.target))
|
|
return true;
|
|
|
|
let codeMirror = WI.enclosingCodeMirror(event.target);
|
|
if (codeMirror)
|
|
return !codeMirror.getOption("readOnly");
|
|
|
|
return false;
|
|
};
|
|
|
|
WI.EditingConfig = class EditingConfig
|
|
{
|
|
constructor(commitHandler, cancelHandler, context)
|
|
{
|
|
this.commitHandler = commitHandler;
|
|
this.cancelHandler = cancelHandler;
|
|
this.context = context;
|
|
this.spellcheck = false;
|
|
}
|
|
|
|
setPasteHandler(pasteHandler)
|
|
{
|
|
this.pasteHandler = pasteHandler;
|
|
}
|
|
|
|
setMultiline(multiline)
|
|
{
|
|
this.multiline = multiline;
|
|
}
|
|
|
|
setCustomFinishHandler(customFinishHandler)
|
|
{
|
|
this.customFinishHandler = customFinishHandler;
|
|
}
|
|
|
|
setNumberCommitHandler(numberCommitHandler)
|
|
{
|
|
this.numberCommitHandler = numberCommitHandler;
|
|
}
|
|
};
|
|
|
|
WI.startEditing = function(element, config)
|
|
{
|
|
if (!WI.markBeingEdited(element, true))
|
|
return null;
|
|
|
|
config = config || new WI.EditingConfig(function() {}, function() {});
|
|
var committedCallback = config.commitHandler;
|
|
var cancelledCallback = config.cancelHandler;
|
|
var pasteCallback = config.pasteHandler;
|
|
var context = config.context;
|
|
var oldText = getContent(element);
|
|
var moveDirection = "";
|
|
|
|
element.classList.add("editing");
|
|
element.contentEditable = "plaintext-only";
|
|
|
|
var oldSpellCheck = element.hasAttribute("spellcheck") ? element.spellcheck : undefined;
|
|
element.spellcheck = config.spellcheck;
|
|
|
|
if (config.multiline)
|
|
element.classList.add("multiline");
|
|
|
|
var oldTabIndex = element.tabIndex;
|
|
if (element.tabIndex < 0)
|
|
element.tabIndex = 0;
|
|
|
|
function blurEventListener() {
|
|
editingCommitted.call(element);
|
|
}
|
|
|
|
function getContent(element) {
|
|
if (element.tagName === "INPUT" && element.type === "text")
|
|
return element.value;
|
|
else
|
|
return element.textContent;
|
|
}
|
|
|
|
function cleanUpAfterEditing()
|
|
{
|
|
WI.markBeingEdited(element, false);
|
|
|
|
this.classList.remove("editing");
|
|
this.contentEditable = false;
|
|
|
|
this.scrollTop = 0;
|
|
this.scrollLeft = 0;
|
|
|
|
if (oldSpellCheck === undefined)
|
|
element.removeAttribute("spellcheck");
|
|
else
|
|
element.spellcheck = oldSpellCheck;
|
|
|
|
if (oldTabIndex === -1)
|
|
this.removeAttribute("tabindex");
|
|
else
|
|
this.tabIndex = oldTabIndex;
|
|
|
|
element.removeEventListener("blur", blurEventListener, false);
|
|
element.removeEventListener("keydown", keyDownEventListener, true);
|
|
if (pasteCallback)
|
|
element.removeEventListener("paste", pasteEventListener, true);
|
|
|
|
WI.restoreFocusFromElement(element);
|
|
}
|
|
|
|
function editingCancelled()
|
|
{
|
|
if (this.tagName === "INPUT" && this.type === "text")
|
|
this.value = oldText;
|
|
else
|
|
this.textContent = oldText;
|
|
|
|
cleanUpAfterEditing.call(this);
|
|
|
|
cancelledCallback(this, context);
|
|
}
|
|
|
|
function editingCommitted()
|
|
{
|
|
cleanUpAfterEditing.call(this);
|
|
|
|
committedCallback(this, getContent(this), oldText, context, moveDirection);
|
|
}
|
|
|
|
function defaultFinishHandler(event)
|
|
{
|
|
var hasOnlyMetaModifierKey = event.metaKey && !event.shiftKey && !event.ctrlKey && !event.altKey;
|
|
if (isEnterKey(event) && (!config.multiline || hasOnlyMetaModifierKey))
|
|
return "commit";
|
|
else if (event.keyCode === WI.KeyboardShortcut.Key.Escape.keyCode || event.keyIdentifier === "U+001B")
|
|
return "cancel";
|
|
else if (event.keyIdentifier === "U+0009") // Tab key
|
|
return "move-" + (event.shiftKey ? "backward" : "forward");
|
|
else if (event.altKey) {
|
|
if (event.keyIdentifier === "Up" || event.keyIdentifier === "Down")
|
|
return "modify-" + (event.keyIdentifier === "Up" ? "up" : "down");
|
|
if (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown")
|
|
return "modify-" + (event.keyIdentifier === "PageUp" ? "up-big" : "down-big");
|
|
}
|
|
}
|
|
|
|
function handleEditingResult(result, event)
|
|
{
|
|
if (result === "commit") {
|
|
editingCommitted.call(element);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
} else if (result === "cancel") {
|
|
editingCancelled.call(element);
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
} else if (result && result.startsWith("move-")) {
|
|
moveDirection = result.substring(5);
|
|
if (event.keyIdentifier !== "U+0009")
|
|
blurEventListener();
|
|
} else if (result && result.startsWith("modify-")) {
|
|
let direction = result.substring(7);
|
|
let delta = direction.startsWith("up") ? 1 : -1;
|
|
if (direction.endsWith("big"))
|
|
delta *= 10;
|
|
|
|
if (event.shiftKey)
|
|
delta *= 10;
|
|
else if (event.ctrlKey)
|
|
delta /= 10;
|
|
|
|
let modified = WI.incrementElementValue(element, delta);
|
|
if (!modified)
|
|
return;
|
|
|
|
if (typeof config.numberCommitHandler === "function")
|
|
config.numberCommitHandler(element, getContent(element), oldText, context, moveDirection);
|
|
|
|
event.preventDefault();
|
|
}
|
|
}
|
|
|
|
function pasteEventListener(event)
|
|
{
|
|
var result = pasteCallback(event);
|
|
handleEditingResult(result, event);
|
|
}
|
|
|
|
function keyDownEventListener(event)
|
|
{
|
|
var handler = config.customFinishHandler || defaultFinishHandler;
|
|
var result = handler(event);
|
|
handleEditingResult(result, event);
|
|
}
|
|
|
|
element.addEventListener("blur", blurEventListener, false);
|
|
element.addEventListener("keydown", keyDownEventListener, true);
|
|
if (pasteCallback)
|
|
element.addEventListener("paste", pasteEventListener, true);
|
|
|
|
element.focus();
|
|
|
|
return {
|
|
cancel: editingCancelled.bind(element),
|
|
commit: editingCommitted.bind(element)
|
|
};
|
|
};
|
|
|
|
WI.incrementElementValue = function(element, delta)
|
|
{
|
|
let selection = element.ownerDocument.defaultView.getSelection();
|
|
if (!selection.rangeCount)
|
|
return false;
|
|
|
|
let range = selection.getRangeAt(0);
|
|
if (!element.contains(range.commonAncestorContainer))
|
|
return false;
|
|
|
|
let wordRange = range.startContainer.rangeOfWord(range.startOffset, WI.EditingSupport.StyleValueDelimiters, element);
|
|
let word = wordRange.toString();
|
|
let wordPrefix = "";
|
|
let wordSuffix = "";
|
|
let nonNumberInWord = /[^\d-\.]+/.exec(word);
|
|
if (nonNumberInWord) {
|
|
let nonNumberEndOffset = nonNumberInWord.index + nonNumberInWord[0].length;
|
|
if (range.startOffset > wordRange.startOffset + nonNumberInWord.index && nonNumberEndOffset < word.length && range.startOffset !== wordRange.startOffset) {
|
|
wordPrefix = word.substring(0, nonNumberEndOffset);
|
|
word = word.substring(nonNumberEndOffset);
|
|
} else {
|
|
wordSuffix = word.substring(nonNumberInWord.index);
|
|
word = word.substring(0, nonNumberInWord.index);
|
|
}
|
|
}
|
|
|
|
let matches = WI.EditingSupport.CSSNumberRegex.exec(word);
|
|
if (!matches || matches.length !== 4)
|
|
return false;
|
|
|
|
let replacement = matches[1] + (Math.round((parseFloat(matches[2]) + delta) * 100) / 100) + matches[3];
|
|
|
|
selection.removeAllRanges();
|
|
selection.addRange(wordRange);
|
|
document.execCommand("insertText", false, wordPrefix + replacement + wordSuffix);
|
|
|
|
let container = range.commonAncestorContainer;
|
|
let startOffset = range.startOffset;
|
|
// This check is for the situation when the cursor is in the space between the
|
|
// opening quote of the attribute and the first character. In that spot, the
|
|
// commonAncestorContainer is actually the entire attribute node since `="` is
|
|
// added as a simple text node. Since the opening quote is immediately before
|
|
// the attribute, the node for that attribute must be the next sibling and the
|
|
// text of the attribute's value must be the first child of that sibling.
|
|
if (container.parentNode.classList.contains("editing") && container.nextSibling) {
|
|
container = container.nextSibling.firstChild;
|
|
startOffset = 0;
|
|
}
|
|
startOffset += wordPrefix.length;
|
|
|
|
if (!container)
|
|
return false;
|
|
|
|
let replacementSelectionRange = document.createRange();
|
|
replacementSelectionRange.setStart(container, startOffset);
|
|
replacementSelectionRange.setEnd(container, startOffset + replacement.length);
|
|
|
|
selection.removeAllRanges();
|
|
selection.addRange(replacementSelectionRange);
|
|
|
|
return true;
|
|
};
|
|
|
|
WI.EditingSupport = {
|
|
StyleValueDelimiters: " \xA0\t\n\"':;,/()",
|
|
CSSNumberRegex: /(.*?)(-?(?:\d+(?:\.\d+)?|\.\d+))(.*)/,
|
|
NumberRegex: /^(-?(?:\d+(?:\.\d+)?|\.\d+))$/
|
|
};
|
|
|
|
/* Views/View.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.View = class View extends WI.Object
|
|
{
|
|
constructor(element)
|
|
{
|
|
super();
|
|
|
|
this._element = element || document.createElement("div");
|
|
this._element.__view = this;
|
|
this._parentView = null;
|
|
this._subviews = [];
|
|
this._dirty = false;
|
|
this._dirtyDescendantsCount = 0;
|
|
this._isAttachedToRoot = false;
|
|
this._layoutReason = null;
|
|
this._didInitialLayout = false;
|
|
}
|
|
|
|
// Static
|
|
|
|
static fromElement(element)
|
|
{
|
|
if (!element || !(element instanceof HTMLElement))
|
|
return null;
|
|
|
|
if (element.__view instanceof WI.View)
|
|
return element.__view;
|
|
return null;
|
|
}
|
|
|
|
static rootView()
|
|
{
|
|
if (!WI.View._rootView) {
|
|
// Since the root view is attached by definition, it does not go through the
|
|
// normal view attachment process. Simply mark it as attached.
|
|
WI.View._rootView = new WI.View(document.body);
|
|
WI.View._rootView._isAttachedToRoot = true;
|
|
}
|
|
|
|
return WI.View._rootView;
|
|
}
|
|
|
|
// Public
|
|
|
|
get element() { return this._element; }
|
|
get layoutPending() { return this._dirty; }
|
|
get parentView() { return this._parentView; }
|
|
get subviews() { return this._subviews; }
|
|
get isAttached() { return this._isAttachedToRoot; }
|
|
|
|
isDescendantOf(view)
|
|
{
|
|
let parentView = this._parentView;
|
|
while (parentView) {
|
|
if (parentView === view)
|
|
return true;
|
|
parentView = parentView.parentView;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
addSubview(view)
|
|
{
|
|
this.insertSubviewBefore(view, null);
|
|
}
|
|
|
|
insertSubviewBefore(view, referenceView)
|
|
{
|
|
console.assert(view instanceof WI.View);
|
|
console.assert(!referenceView || referenceView instanceof WI.View);
|
|
console.assert(view !== WI.View._rootView, "Root view cannot be a subview.");
|
|
|
|
console.assert(!view.parentView, view);
|
|
|
|
if (this._subviews.includes(view)) {
|
|
console.assert(false, "Cannot add view that is already a subview.", view);
|
|
return;
|
|
}
|
|
|
|
const beforeIndex = referenceView ? this._subviews.indexOf(referenceView) : this._subviews.length;
|
|
if (beforeIndex === -1) {
|
|
console.assert(false, "Cannot insert view. Invalid reference view.", referenceView);
|
|
return;
|
|
}
|
|
|
|
this._subviews.insertAtIndex(view, beforeIndex);
|
|
|
|
console.assert(!view.element.parentNode || this._element.contains(view.element.parentNode), "Subview DOM element must be a descendant of the parent view element.");
|
|
if (!view.element.parentNode)
|
|
this._element.insertBefore(view.element, referenceView ? referenceView.element : null);
|
|
|
|
view._didMoveToParent(this);
|
|
}
|
|
|
|
removeSubview(view)
|
|
{
|
|
console.assert(view instanceof WI.View);
|
|
console.assert(this._element.contains(view.element), "Subview DOM element must be a child of the parent view element.");
|
|
|
|
let index = this._subviews.lastIndexOf(view);
|
|
if (index === -1) {
|
|
console.assert(false, "Cannot remove view which isn't a subview.", view);
|
|
return;
|
|
}
|
|
|
|
view._didMoveToParent(null);
|
|
|
|
this._subviews.splice(index, 1);
|
|
view.element.remove();
|
|
}
|
|
|
|
removeAllSubviews()
|
|
{
|
|
for (let subview of this._subviews)
|
|
subview._didMoveToParent(null);
|
|
|
|
this._subviews = [];
|
|
this._element.removeChildren();
|
|
}
|
|
|
|
replaceSubview(oldView, newView)
|
|
{
|
|
console.assert(oldView !== newView, "Cannot replace subview with itself.");
|
|
if (oldView === newView)
|
|
return;
|
|
|
|
this.insertSubviewBefore(newView, oldView);
|
|
this.removeSubview(oldView);
|
|
}
|
|
|
|
updateLayout(layoutReason)
|
|
{
|
|
this._setLayoutReason(layoutReason);
|
|
this._layoutSubtree();
|
|
this._parentView?.didLayoutSubtree();
|
|
}
|
|
|
|
updateLayoutIfNeeded(layoutReason)
|
|
{
|
|
if (!this._dirty && this._didInitialLayout)
|
|
return;
|
|
|
|
this.updateLayout(layoutReason);
|
|
}
|
|
|
|
needsLayout(layoutReason)
|
|
{
|
|
this._setLayoutReason(layoutReason);
|
|
|
|
WI.View._scheduleLayoutForView(this);
|
|
}
|
|
|
|
// Protected
|
|
|
|
get layoutReason() { return this._layoutReason; }
|
|
get didInitialLayout() { return this._didInitialLayout; }
|
|
|
|
attached()
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
detached()
|
|
{
|
|
// Implemented by subclasses.
|
|
}
|
|
|
|
initialLayout()
|
|
{
|
|
// Implemented by subclasses.
|
|
|
|
// Called once when the view is shown for the first time.
|
|
// Views with complex DOM subtrees should create UI elements in
|
|
// initialLayout rather than at construction time.
|
|
}
|
|
|
|
layout()
|
|
{
|
|
// Implemented by subclasses.
|
|
|
|
// Not responsible for recursing to child views.
|
|
// Should not be called directly; use updateLayout() instead.
|
|
}
|
|
|
|
didLayoutSubtree()
|
|
{
|
|
// Implemented by subclasses.
|
|
|
|
// Called after the view and its entire subtree have finished layout.
|
|
// Also called on the immediate parent view that did not layout because it wasn't dirty.
|
|
}
|
|
|
|
sizeDidChange()
|
|
{
|
|
// Implemented by subclasses.
|
|
|
|
// Called after initialLayout, and before layout.
|
|
}
|
|
|
|
// Private
|
|
|
|
_setDirty(dirty)
|
|
{
|
|
if (this._dirty === dirty)
|
|
return;
|
|
|
|
this._dirty = dirty;
|
|
|
|
for (let parentView = this.parentView; parentView; parentView = parentView.parentView) {
|
|
parentView._dirtyDescendantsCount += this._dirty ? 1 : -1;
|
|
console.assert(parentView._dirtyDescendantsCount >= 0);
|
|
}
|
|
}
|
|
|
|
_didMoveToParent(parentView)
|
|
{
|
|
if (this._parentView === parentView)
|
|
return;
|
|
|
|
let dirtyDescendantsCount = this._dirtyDescendantsCount;
|
|
if (this._dirty)
|
|
++dirtyDescendantsCount;
|
|
|
|
if (dirtyDescendantsCount) {
|
|
for (let view = this.parentView; view; view = view.parentView) {
|
|
view._dirtyDescendantsCount -= dirtyDescendantsCount;
|
|
console.assert(view._dirtyDescendantsCount >= 0);
|
|
}
|
|
}
|
|
|
|
this._parentView = parentView;
|
|
let isAttachedToRoot = this.isDescendantOf(WI.View._rootView);
|
|
|
|
let views = [this];
|
|
for (let i = 0; i < views.length; ++i) {
|
|
let view = views[i];
|
|
views.pushAll(view.subviews);
|
|
|
|
view._dirty = false;
|
|
view._dirtyDescendantsCount = 0;
|
|
|
|
if (view._isAttachedToRoot === isAttachedToRoot)
|
|
continue;
|
|
|
|
view._isAttachedToRoot = isAttachedToRoot;
|
|
if (view._isAttachedToRoot)
|
|
view.attached();
|
|
else
|
|
view.detached();
|
|
}
|
|
|
|
if (isAttachedToRoot)
|
|
WI.View._scheduleLayoutForView(this);
|
|
}
|
|
|
|
_layoutSubtree()
|
|
{
|
|
this._setDirty(false);
|
|
let isInitialLayout = !this._didInitialLayout;
|
|
if (isInitialLayout) {
|
|
console.assert(WI.setReentrantCheck(this, "initialLayout"), "ERROR: calling `initialLayout` while already in it", this);
|
|
this.initialLayout();
|
|
this._didInitialLayout = true;
|
|
}
|
|
|
|
if (this._layoutReason === WI.View.LayoutReason.Resize || isInitialLayout) {
|
|
console.assert(WI.setReentrantCheck(this, "sizeDidChange"), "ERROR: calling `sizeDidChange` while already in it", this);
|
|
this.sizeDidChange();
|
|
console.assert(WI.clearReentrantCheck(this, "sizeDidChange"), "ERROR: missing return from `sizeDidChange`", this);
|
|
}
|
|
|
|
let savedLayoutReason = this._layoutReason;
|
|
if (isInitialLayout) {
|
|
// The initial layout should always be treated as dirty.
|
|
this._setLayoutReason();
|
|
}
|
|
|
|
console.assert(WI.setReentrantCheck(this, "layout"), "ERROR: calling `layout` while already in it", this);
|
|
this.layout();
|
|
console.assert(WI.clearReentrantCheck(this, "layout"), "ERROR: missing return from `layout`", this);
|
|
|
|
// Ensure that the initial layout override doesn't affects to subviews.
|
|
this._layoutReason = savedLayoutReason;
|
|
|
|
if (WI.settings.debugEnableLayoutFlashing.value)
|
|
this._drawLayoutFlashingOutline(isInitialLayout);
|
|
|
|
for (let view of this._subviews) {
|
|
view._setLayoutReason(this._layoutReason);
|
|
view._layoutSubtree();
|
|
}
|
|
|
|
this._layoutReason = null;
|
|
|
|
console.assert(WI.setReentrantCheck(this, "didLayoutSubtree"), "ERROR: calling `didLayoutSubtree` while already in it", this);
|
|
this.didLayoutSubtree();
|
|
console.assert(WI.clearReentrantCheck(this, "didLayoutSubtree"), "ERROR: missing return from `didLayoutSubtree`", this);
|
|
}
|
|
|
|
_setLayoutReason(layoutReason)
|
|
{
|
|
this._layoutReason = layoutReason || WI.View.LayoutReason.Dirty;
|
|
}
|
|
|
|
_drawLayoutFlashingOutline(isInitialLayout)
|
|
{
|
|
if (this._layoutFlashingTimeout)
|
|
clearTimeout(this._layoutFlashingTimeout);
|
|
else
|
|
this._layoutFlashingPreviousOutline = this._element.style.outline;
|
|
|
|
let hue = isInitialLayout ? 20 : 40;
|
|
this._element.style.outline = `1px solid hsla(${hue}, 100%, 51%, 0.8)`;
|
|
|
|
this._layoutFlashingTimeout = setTimeout(() => {
|
|
if (this._element)
|
|
this._element.style.outline = this._layoutFlashingPreviousOutline;
|
|
|
|
this._layoutFlashingTimeout = undefined;
|
|
this._layoutFlashingPreviousOutline = null;
|
|
}, 500);
|
|
}
|
|
|
|
// Layout controller logic
|
|
|
|
static _scheduleLayoutForView(view)
|
|
{
|
|
view._setDirty(true);
|
|
|
|
if (!view._isAttachedToRoot)
|
|
return;
|
|
|
|
if (WI.View._scheduledLayoutUpdateIdentifier)
|
|
return;
|
|
|
|
WI.View._scheduledLayoutUpdateIdentifier = requestAnimationFrame(WI.View._visitViewTreeForLayout);
|
|
}
|
|
|
|
static _visitViewTreeForLayout()
|
|
{
|
|
console.assert(WI.View._rootView, "Cannot layout view tree without a root.");
|
|
|
|
WI.View._scheduledLayoutUpdateIdentifier = undefined;
|
|
|
|
let views = [WI.View._rootView];
|
|
let cleanViews = new Set;
|
|
for (let i = 0; i < views.length; ++i) {
|
|
let view = views[i];
|
|
|
|
if (cleanViews.has(view)) {
|
|
console.assert(WI.setReentrantCheck(view, "didLayoutSubtree"), "ERROR: calling `didLayoutSubtree` while already in it", view);
|
|
view.didLayoutSubtree();
|
|
console.assert(WI.clearReentrantCheck(view, "didLayoutSubtree"), "ERROR: missing return from `didLayoutSubtree`", view);
|
|
continue;
|
|
}
|
|
|
|
if (view.layoutPending) {
|
|
view._layoutSubtree();
|
|
continue;
|
|
}
|
|
|
|
if (view._dirtyDescendantsCount) {
|
|
cleanViews.add(view);
|
|
|
|
let hasDirtySubview = false;
|
|
for (let subview of view.subviews) {
|
|
hasDirtySubview ||= subview.layoutPending;
|
|
views.push(subview);
|
|
}
|
|
|
|
// Add again the parent view to the list we're iterating over, right after its subviews.
|
|
// By the time it is encountered again, all its subviews will have done _layoutSubtree() and then we can call didLayoutSubtree() on it.
|
|
if (hasDirtySubview)
|
|
views.push(view);
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.View.LayoutReason = {
|
|
Dirty: Symbol("layout-reason-dirty"),
|
|
Resize: Symbol("layout-reason-resize")
|
|
};
|
|
|
|
WI.View._rootView = null;
|
|
WI.View._scheduledLayoutUpdateIdentifier = undefined;
|
|
|
|
/* Controllers/DiagnosticController.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DiagnosticController = class DiagnosticController
|
|
{
|
|
constructor()
|
|
{
|
|
this._diagnosticLoggingAvailable = false;
|
|
this._recorders = new Set;
|
|
|
|
this._autoLogDiagnosticEventsToConsole = WI.settings.debugAutoLogDiagnosticEvents.value;
|
|
this._logToConsoleMethod = window.InspectorTest ? InspectorTest.log.bind(InspectorTest) : console.log;
|
|
|
|
WI.settings.debugEnableDiagnosticLogging.addEventListener(WI.Setting.Event.Changed, this._debugEnableDiagnosticLoggingSettingDidChange, this);
|
|
WI.settings.debugAutoLogDiagnosticEvents.addEventListener(WI.Setting.Event.Changed, this._debugAutoLogDiagnosticEventsSettingDidChange, this);
|
|
}
|
|
|
|
// Public
|
|
|
|
get diagnosticLoggingAvailable()
|
|
{
|
|
return this._diagnosticLoggingAvailable;
|
|
}
|
|
|
|
set diagnosticLoggingAvailable(available)
|
|
{
|
|
if (this._diagnosticLoggingAvailable === available)
|
|
return;
|
|
|
|
this._diagnosticLoggingAvailable = available;
|
|
this._updateRecorderStates();
|
|
}
|
|
|
|
addRecorder(recorder)
|
|
{
|
|
console.assert(!this._recorders.has(recorder), "Tried to add the same diagnostic recorder more than once.");
|
|
this._recorders.add(recorder);
|
|
this._updateRecorderStates();
|
|
}
|
|
|
|
logDiagnosticEvent(eventName, payload)
|
|
{
|
|
// Don't rely on a diagnostic logging delegate to unit test frontend diagnostics code.
|
|
if (window.InspectorTest) {
|
|
this._logToConsoleMethod(`Received diagnostic event: ${eventName} => ${JSON.stringify(payload)}`);
|
|
return;
|
|
}
|
|
|
|
if (this._autoLogDiagnosticEventsToConsole)
|
|
this._logToConsoleMethod(eventName, payload);
|
|
|
|
InspectorFrontendHost.logDiagnosticEvent(eventName, JSON.stringify(payload));
|
|
}
|
|
|
|
// Private
|
|
|
|
_debugEnableDiagnosticLoggingSettingDidChange()
|
|
{
|
|
this._updateRecorderStates();
|
|
}
|
|
|
|
_debugAutoLogDiagnosticEventsSettingDidChange()
|
|
{
|
|
this._autoLogDiagnosticEventsToConsole = WI.settings.debugAutoLogDiagnosticEvents.value;
|
|
}
|
|
|
|
_updateRecorderStates()
|
|
{
|
|
let isActive = this._diagnosticLoggingAvailable && WI.settings.debugEnableDiagnosticLogging.value;
|
|
for (let recorder of this._recorders)
|
|
recorder.active = isActive;
|
|
}
|
|
};
|
|
|
|
/* Controllers/DiagnosticEventRecorder.js */
|
|
|
|
/*
|
|
* Copyright (C) 2019 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DiagnosticEventRecorder = class DiagnosticEventRecorder
|
|
{
|
|
constructor(name, controller)
|
|
{
|
|
console.assert(controller instanceof WI.DiagnosticController, controller);
|
|
|
|
this._name = name;
|
|
this._active = false;
|
|
this._controller = controller;
|
|
}
|
|
|
|
// Public
|
|
|
|
get name() { return this._name; }
|
|
|
|
get active()
|
|
{
|
|
return this._active;
|
|
}
|
|
|
|
set active(value)
|
|
{
|
|
if (this._active === value)
|
|
return;
|
|
|
|
this._active = value;
|
|
|
|
if (this._active)
|
|
this.setup();
|
|
else
|
|
this.teardown();
|
|
}
|
|
|
|
// Protected
|
|
|
|
logDiagnosticEvent(eventName, payload)
|
|
{
|
|
if (this._active)
|
|
this._controller.logDiagnosticEvent(eventName, payload);
|
|
}
|
|
|
|
setup()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
|
|
teardown()
|
|
{
|
|
throw WI.NotImplementedError.subclassMustOverride();
|
|
}
|
|
};
|
|
|
|
/* Views/Resizer.js */
|
|
|
|
/*
|
|
* Copyright (C) 2015 Apple Inc. All rights reserved.
|
|
* Copyright (C) 2015 University of Washington.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Resizer = class Resizer extends WI.Object
|
|
{
|
|
constructor(ruleOrientation, delegate)
|
|
{
|
|
console.assert(delegate);
|
|
|
|
super();
|
|
|
|
this._delegate = delegate;
|
|
this._orientation = ruleOrientation;
|
|
this._element = document.createElement("div");
|
|
this._element.classList.add("resizer");
|
|
|
|
if (this._orientation === WI.Resizer.RuleOrientation.Horizontal)
|
|
this._element.classList.add("horizontal-rule");
|
|
else if (this._orientation === WI.Resizer.RuleOrientation.Vertical)
|
|
this._element.classList.add("vertical-rule");
|
|
|
|
this._element.addEventListener("mousedown", this._resizerMouseDown.bind(this), false);
|
|
this._resizerMouseMovedEventListener = this._resizerMouseMoved.bind(this);
|
|
this._resizerMouseUpEventListener = this._resizerMouseUp.bind(this);
|
|
}
|
|
|
|
// Public
|
|
|
|
get element()
|
|
{
|
|
return this._element;
|
|
}
|
|
|
|
get orientation()
|
|
{
|
|
return this._orientation;
|
|
}
|
|
|
|
get initialPosition()
|
|
{
|
|
return this._resizerMouseDownPosition || NaN;
|
|
}
|
|
|
|
// Private
|
|
|
|
_currentPosition()
|
|
{
|
|
if (this._orientation === WI.Resizer.RuleOrientation.Vertical)
|
|
return event.pageX;
|
|
if (this._orientation === WI.Resizer.RuleOrientation.Horizontal)
|
|
return event.pageY;
|
|
|
|
console.assert(false, "Unexpected Resizer orientation.", this._orientation);
|
|
}
|
|
|
|
_resizerMouseDown(event)
|
|
{
|
|
if (event.button !== 0 || event.ctrlKey)
|
|
return;
|
|
|
|
this._resizerMouseDownPosition = this._currentPosition();
|
|
|
|
var delegateRequestedAbort = false;
|
|
if (typeof this._delegate.resizerDragStarted === "function")
|
|
delegateRequestedAbort = this._delegate.resizerDragStarted(this, event.target);
|
|
|
|
if (delegateRequestedAbort) {
|
|
delete this._resizerMouseDownPosition;
|
|
return;
|
|
}
|
|
|
|
if (this._orientation === WI.Resizer.RuleOrientation.Vertical)
|
|
document.body.style.cursor = "col-resize";
|
|
else {
|
|
console.assert(this._orientation === WI.Resizer.RuleOrientation.Horizontal);
|
|
document.body.style.cursor = "row-resize";
|
|
}
|
|
|
|
// Register these listeners on the document so we can track the mouse if it leaves the resizer.
|
|
document.addEventListener("mousemove", this._resizerMouseMovedEventListener, false);
|
|
document.addEventListener("mouseup", this._resizerMouseUpEventListener, false);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
// Install a global "glass pane" which prevents cursor from changing during the drag interaction.
|
|
// The cursor could change when hovering over links, text, or other elements with cursor cues.
|
|
// FIXME: when Pointer Events support is available this could be implemented by drawing the cursor ourselves.
|
|
if (WI._elementDraggingGlassPane)
|
|
WI._elementDraggingGlassPane.remove();
|
|
|
|
var glassPaneElement = document.createElement("div");
|
|
glassPaneElement.className = "glass-pane-for-drag";
|
|
document.body.appendChild(glassPaneElement);
|
|
WI._elementDraggingGlassPane = glassPaneElement;
|
|
}
|
|
|
|
_resizerMouseMoved(event)
|
|
{
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (typeof this._delegate.resizerDragging === "function")
|
|
this._delegate.resizerDragging(this, this._resizerMouseDownPosition - this._currentPosition());
|
|
}
|
|
|
|
_resizerMouseUp(event)
|
|
{
|
|
if (event.button !== 0 || event.ctrlKey)
|
|
return;
|
|
|
|
document.body.style.removeProperty("cursor");
|
|
|
|
if (WI._elementDraggingGlassPane) {
|
|
WI._elementDraggingGlassPane.remove();
|
|
delete WI._elementDraggingGlassPane;
|
|
}
|
|
|
|
document.removeEventListener("mousemove", this._resizerMouseMovedEventListener, false);
|
|
document.removeEventListener("mouseup", this._resizerMouseUpEventListener, false);
|
|
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (typeof this._delegate.resizerDragEnded === "function")
|
|
this._delegate.resizerDragEnded(this);
|
|
|
|
delete this._resizerMouseDownPosition;
|
|
}
|
|
};
|
|
|
|
WI.Resizer.RuleOrientation = {
|
|
Horizontal: Symbol("resizer-rule-orientation-horizontal"),
|
|
Vertical: Symbol("resizer-rule-orientation-vertical"),
|
|
};
|
|
|
|
/* Views/Table.js */
|
|
|
|
/*
|
|
* Copyright (C) 2008-2018 Apple Inc. All Rights Reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.Table = class Table extends WI.View
|
|
{
|
|
constructor(identifier, dataSource, delegate, rowHeight)
|
|
{
|
|
super();
|
|
|
|
console.assert(typeof identifier === "string");
|
|
console.assert(dataSource);
|
|
console.assert(delegate);
|
|
console.assert(rowHeight > 0);
|
|
|
|
this._identifier = identifier;
|
|
this._dataSource = dataSource;
|
|
this._delegate = delegate;
|
|
this._rowHeight = rowHeight;
|
|
|
|
// FIXME: Should be able to horizontally scroll non-locked table contents.
|
|
// To do this smoothly (without tearing) will require synchronous scroll events, or
|
|
// synchronized scrolling between multiple elements, or making `position: sticky`
|
|
// respect different vertical / horizontal scroll containers.
|
|
|
|
this.element.classList.add("table", identifier);
|
|
this.element.tabIndex = 0;
|
|
this.element.addEventListener("keydown", this._handleKeyDown.bind(this));
|
|
|
|
this._headerElement = this.element.appendChild(document.createElement("div"));
|
|
this._headerElement.className = "header";
|
|
|
|
let scrollHandler = this._handleScroll.bind(this);
|
|
this._scrollContainerElement = this.element.appendChild(document.createElement("div"));
|
|
this._scrollContainerElement.className = "data-container";
|
|
this._scrollContainerElement.addEventListener("scroll", scrollHandler);
|
|
this._scrollContainerElement.addEventListener("mousewheel", scrollHandler);
|
|
this._scrollContainerElement.addEventListener("mousedown", this._handleMouseDown.bind(this));
|
|
if (this._delegate.tableCellContextMenuClicked)
|
|
this._scrollContainerElement.addEventListener("contextmenu", this._handleContextMenu.bind(this));
|
|
|
|
this._topSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
|
|
this._topSpacerElement.className = "spacer";
|
|
|
|
this._listElement = this._scrollContainerElement.appendChild(document.createElement("ul"));
|
|
this._listElement.className = "data-list";
|
|
|
|
this._bottomSpacerElement = this._scrollContainerElement.appendChild(document.createElement("div"));
|
|
this._bottomSpacerElement.className = "spacer";
|
|
|
|
this._fillerRow = this._listElement.appendChild(document.createElement("li"));
|
|
this._fillerRow.className = "filler";
|
|
|
|
this._resizersElement = this._element.appendChild(document.createElement("div"));
|
|
this._resizersElement.className = "resizers";
|
|
|
|
this._cachedRows = new Map;
|
|
|
|
this._columnSpecs = new Map;
|
|
this._columnOrder = [];
|
|
this._visibleColumns = [];
|
|
this._hiddenColumns = [];
|
|
|
|
this._widthGeneration = 1;
|
|
this._columnWidths = null; // Calculated in _resizeColumnsAndFiller.
|
|
this._fillerHeight = 0; // Calculated in _resizeColumnsAndFiller.
|
|
|
|
let selectionComparator = WI.SelectionController.createListComparator(this._indexForRepresentedObject.bind(this));
|
|
this._selectionController = new WI.SelectionController(this, selectionComparator);
|
|
|
|
this._resizers = [];
|
|
this._currentResizer = null;
|
|
this._resizeLeftColumns = null;
|
|
this._resizeRightColumns = null;
|
|
this._resizeOriginalColumnWidths = null;
|
|
this._lastColumnIndexToAcceptRemainderPixel = 0;
|
|
|
|
this._sortOrderSetting = new WI.Setting(this._identifier + "-sort-order", WI.Table.SortOrder.Indeterminate);
|
|
this._sortColumnIdentifierSetting = new WI.Setting(this._identifier + "-sort", null);
|
|
this._columnVisibilitySetting = new WI.Setting(this._identifier + "-column-visibility", {});
|
|
|
|
this._sortOrder = this._sortOrderSetting.value;
|
|
this._sortColumnIdentifier = this._sortColumnIdentifierSetting.value;
|
|
|
|
this._cachedWidth = NaN;
|
|
this._cachedHeight = NaN;
|
|
this._cachedScrollTop = NaN;
|
|
this._previousCachedWidth = NaN;
|
|
this._previousRevealedRowCount = NaN;
|
|
this._topSpacerHeight = NaN;
|
|
this._bottomSpacerHeight = NaN;
|
|
this._visibleRowIndexStart = NaN;
|
|
this._visibleRowIndexEnd = NaN;
|
|
|
|
console.assert(this._dataSource.tableNumberOfRows, "Table data source must implement tableNumberOfRows.");
|
|
console.assert(this._dataSource.tableIndexForRepresentedObject, "Table data source must implement tableIndexForRepresentedObject.");
|
|
console.assert(this._dataSource.tableRepresentedObjectForIndex, "Table data source must implement tableRepresentedObjectForIndex.");
|
|
|
|
console.assert(this._delegate.tablePopulateCell, "Table delegate must implement tablePopulateCell.");
|
|
}
|
|
|
|
// Public
|
|
|
|
get identifier() { return this._identifier; }
|
|
get dataSource() { return this._dataSource; }
|
|
get delegate() { return this._delegate; }
|
|
get rowHeight() { return this._rowHeight; }
|
|
|
|
get selectedRow()
|
|
{
|
|
let item = this._selectionController.lastSelectedItem;
|
|
let index = this._indexForRepresentedObject(item);
|
|
return index >= 0 ? index : NaN;
|
|
}
|
|
|
|
get selectedRows()
|
|
{
|
|
let rowIndexes = [];
|
|
for (let item of this._selectionController.selectedItems)
|
|
rowIndexes.push(this._indexForRepresentedObject(item));
|
|
return rowIndexes;
|
|
}
|
|
|
|
get scrollContainer() { return this._scrollContainerElement; }
|
|
|
|
get numberOfRows()
|
|
{
|
|
return this._dataSource.tableNumberOfRows(this);
|
|
}
|
|
|
|
get sortOrder()
|
|
{
|
|
return this._sortOrder;
|
|
}
|
|
|
|
set sortOrder(sortOrder)
|
|
{
|
|
if (sortOrder === this._sortOrder && this.didInitialLayout)
|
|
return;
|
|
|
|
console.assert(sortOrder === WI.Table.SortOrder.Indeterminate || sortOrder === WI.Table.SortOrder.Ascending || sortOrder === WI.Table.SortOrder.Descending);
|
|
|
|
this._sortOrder = sortOrder;
|
|
this._sortOrderSetting.value = sortOrder;
|
|
|
|
if (this._sortColumnIdentifier) {
|
|
let column = this._columnSpecs.get(this._sortColumnIdentifier);
|
|
let columnIndex = this._visibleColumns.indexOf(column);
|
|
if (columnIndex !== -1) {
|
|
let headerCell = this._headerElement.children[columnIndex];
|
|
headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
|
|
headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
|
|
}
|
|
|
|
if (this._dataSource.tableSortChanged)
|
|
this._dataSource.tableSortChanged(this);
|
|
}
|
|
}
|
|
|
|
get sortColumnIdentifier()
|
|
{
|
|
return this._sortColumnIdentifier;
|
|
}
|
|
|
|
set sortColumnIdentifier(columnIdentifier)
|
|
{
|
|
if (columnIdentifier === this._sortColumnIdentifier && this.didInitialLayout)
|
|
return;
|
|
|
|
let column = this._columnSpecs.get(columnIdentifier);
|
|
|
|
console.assert(column, "Column not found.", columnIdentifier);
|
|
if (!column)
|
|
return;
|
|
|
|
console.assert(column.sortable, "Column is not sortable.", columnIdentifier);
|
|
if (!column.sortable)
|
|
return;
|
|
|
|
let oldSortColumnIdentifier = this._sortColumnIdentifier;
|
|
this._sortColumnIdentifier = columnIdentifier;
|
|
this._sortColumnIdentifierSetting.value = columnIdentifier;
|
|
|
|
if (oldSortColumnIdentifier) {
|
|
let oldColumn = this._columnSpecs.get(oldSortColumnIdentifier);
|
|
let oldColumnIndex = this._visibleColumns.indexOf(oldColumn);
|
|
if (oldColumnIndex !== -1) {
|
|
let headerCell = this._headerElement.children[oldColumnIndex];
|
|
headerCell.classList.remove("sort-ascending", "sort-descending");
|
|
}
|
|
}
|
|
|
|
if (this._sortColumnIdentifier) {
|
|
let newColumnIndex = this._visibleColumns.indexOf(column);
|
|
if (newColumnIndex !== -1) {
|
|
let headerCell = this._headerElement.children[newColumnIndex];
|
|
headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
|
|
headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
|
|
} else
|
|
this._sortColumnIdentifier = null;
|
|
}
|
|
|
|
if (this._dataSource.tableSortChanged)
|
|
this._dataSource.tableSortChanged(this);
|
|
}
|
|
|
|
get allowsMultipleSelection()
|
|
{
|
|
return this._selectionController.allowsMultipleSelection;
|
|
}
|
|
|
|
set allowsMultipleSelection(flag)
|
|
{
|
|
this._selectionController.allowsMultipleSelection = flag;
|
|
}
|
|
|
|
get columns()
|
|
{
|
|
return Array.from(this._columnSpecs.values());
|
|
}
|
|
|
|
isRowSelected(rowIndex)
|
|
{
|
|
return this._selectionController.hasSelectedItem(this._representedObjectForIndex(rowIndex));
|
|
}
|
|
|
|
reloadData()
|
|
{
|
|
this._cachedRows.clear();
|
|
|
|
this._selectionController.reset();
|
|
|
|
this._previousRevealedRowCount = NaN;
|
|
this.needsLayout();
|
|
}
|
|
|
|
reloadDataAddedToEndOnly()
|
|
{
|
|
this._previousRevealedRowCount = NaN;
|
|
this.needsLayout();
|
|
}
|
|
|
|
reloadRow(rowIndex)
|
|
{
|
|
// Visible row, repopulate the cell.
|
|
if (this._isRowVisible(rowIndex)) {
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (!row)
|
|
return;
|
|
this._populateRow(row);
|
|
return;
|
|
}
|
|
|
|
// Non-visible row, will populate when it becomes visible.
|
|
this._cachedRows.delete(rowIndex);
|
|
}
|
|
|
|
restyleRow(rowIndex)
|
|
{
|
|
if (!this._isRowVisible(rowIndex))
|
|
return;
|
|
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (!row)
|
|
return;
|
|
|
|
this._styleRow(row);
|
|
}
|
|
|
|
reloadVisibleColumnCells(column)
|
|
{
|
|
let columnIndex = this._visibleColumns.indexOf(column);
|
|
if (columnIndex === -1)
|
|
return;
|
|
|
|
let numberOfRows = Math.min(this._visibleRowIndexEnd, this.numberOfRows);
|
|
for (let rowIndex = this._visibleRowIndexStart; rowIndex < numberOfRows; ++rowIndex) {
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (!row)
|
|
continue;
|
|
let cell = row.children[columnIndex];
|
|
if (!cell)
|
|
continue;
|
|
this._delegate.tablePopulateCell(this, cell, column, rowIndex);
|
|
}
|
|
}
|
|
|
|
reloadCell(rowIndex, columnIdentifier)
|
|
{
|
|
let column = this._columnSpecs.get(columnIdentifier);
|
|
let columnIndex = this._visibleColumns.indexOf(column);
|
|
if (columnIndex === -1)
|
|
return;
|
|
|
|
// Visible row, repopulate the cell.
|
|
if (this._isRowVisible(rowIndex)) {
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (!row)
|
|
return;
|
|
let cell = row.children[columnIndex];
|
|
if (!cell)
|
|
return;
|
|
this._delegate.tablePopulateCell(this, cell, column, rowIndex);
|
|
return;
|
|
}
|
|
|
|
// Non-visible row, will populate when it becomes visible.
|
|
this._cachedRows.delete(rowIndex);
|
|
}
|
|
|
|
selectRow(rowIndex, extendSelection = false)
|
|
{
|
|
this._selectionController.selectItem(this._representedObjectForIndex(rowIndex), extendSelection);
|
|
}
|
|
|
|
deselectRow(rowIndex)
|
|
{
|
|
this._selectionController.deselectItem(this._representedObjectForIndex(rowIndex));
|
|
}
|
|
|
|
selectAll()
|
|
{
|
|
this._selectionController.selectAll();
|
|
}
|
|
|
|
deselectAll()
|
|
{
|
|
this._selectionController.deselectAll();
|
|
}
|
|
|
|
removeRow(rowIndex)
|
|
{
|
|
console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
|
|
|
|
if (this.isRowSelected(rowIndex))
|
|
this.deselectRow(rowIndex);
|
|
|
|
this._removeRows(new Set([this._representedObjectForIndex(rowIndex)]));
|
|
}
|
|
|
|
removeSelectedRows()
|
|
{
|
|
let selectedItems = this._selectionController.selectedItems;
|
|
if (!selectedItems.size)
|
|
return;
|
|
|
|
// Change the selection before removing rows. This matches the behavior
|
|
// of macOS Finder (in list and column modes) when removing selected items.
|
|
this._selectionController.removeSelectedItems();
|
|
|
|
this._removeRows(selectedItems);
|
|
}
|
|
|
|
revealRow(rowIndex)
|
|
{
|
|
console.assert(rowIndex >= 0 && rowIndex < this.numberOfRows);
|
|
if (rowIndex < 0 || rowIndex >= this.numberOfRows)
|
|
return;
|
|
|
|
if (this._isRowVisible(rowIndex)) {
|
|
let row = this._cachedRows.get(rowIndex);
|
|
console.assert(row, "Visible rows should always be in the cache.");
|
|
if (row) {
|
|
row.scrollIntoViewIfNeeded(false);
|
|
this._cachedScrollTop = NaN;
|
|
this.needsLayout();
|
|
}
|
|
} else {
|
|
let rowPosition = rowIndex * this._rowHeight;
|
|
let scrollableOffsetHeight = this._calculateOffsetHeight();
|
|
let scrollTop = this._calculateScrollTop();
|
|
let newScrollTop = NaN;
|
|
if (rowPosition + this._rowHeight < scrollTop)
|
|
newScrollTop = rowPosition;
|
|
else if (rowPosition > scrollTop + scrollableOffsetHeight)
|
|
newScrollTop = scrollTop + scrollableOffsetHeight - this._rowHeight;
|
|
|
|
if (!isNaN(newScrollTop)) {
|
|
this._scrollContainerElement.scrollTop = newScrollTop;
|
|
this.updateLayout();
|
|
}
|
|
}
|
|
}
|
|
|
|
columnWithIdentifier(identifier)
|
|
{
|
|
return this._columnSpecs.get(identifier);
|
|
}
|
|
|
|
cellForRowAndColumn(rowIndex, column)
|
|
{
|
|
if (!this._isRowVisible(rowIndex))
|
|
return null;
|
|
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (!row)
|
|
return null;
|
|
|
|
let columnIndex = this._visibleColumns.indexOf(column);
|
|
if (columnIndex === -1)
|
|
return null;
|
|
|
|
return row.children[columnIndex];
|
|
}
|
|
|
|
addColumn(column)
|
|
{
|
|
this._columnSpecs.set(column.identifier, column);
|
|
this._columnOrder.push(column.identifier);
|
|
|
|
if (column.hidden) {
|
|
this._hiddenColumns.push(column);
|
|
column.width = NaN;
|
|
} else {
|
|
this._visibleColumns.push(column);
|
|
this._headerElement.appendChild(this._createHeaderCell(column));
|
|
this._fillerRow.appendChild(this._createFillerCell(column));
|
|
if (column.headerView)
|
|
this.addSubview(column.headerView);
|
|
}
|
|
|
|
// Restore saved user-specified column visibility.
|
|
let savedColumnVisibility = this._columnVisibilitySetting.value;
|
|
if (column.identifier in savedColumnVisibility) {
|
|
let visible = savedColumnVisibility[column.identifier];
|
|
if (visible)
|
|
this.showColumn(column);
|
|
else
|
|
this.hideColumn(column);
|
|
}
|
|
|
|
this.reloadData();
|
|
}
|
|
|
|
showColumn(column)
|
|
{
|
|
console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
|
|
console.assert(!column.locked, "Locked columns should always be shown.");
|
|
if (column.locked)
|
|
return;
|
|
|
|
if (!column.hidden)
|
|
return;
|
|
|
|
column.hidden = false;
|
|
|
|
let columnIndex = this._hiddenColumns.indexOf(column);
|
|
this._hiddenColumns.splice(columnIndex, 1);
|
|
|
|
let newColumnIndex = this._indexToInsertColumn(column);
|
|
this._visibleColumns.insertAtIndex(column, newColumnIndex);
|
|
|
|
// Save user preference for this column to be visible.
|
|
let savedColumnVisibility = this._columnVisibilitySetting.value;
|
|
if (savedColumnVisibility[column.identifier] !== true) {
|
|
let copy = Object.shallowCopy(savedColumnVisibility);
|
|
if (column.defaultHidden)
|
|
copy[column.identifier] = true;
|
|
else
|
|
delete copy[column.identifier];
|
|
this._columnVisibilitySetting.value = copy;
|
|
}
|
|
|
|
this._headerElement.insertBefore(this._createHeaderCell(column), this._headerElement.children[newColumnIndex]);
|
|
this._fillerRow.insertBefore(this._createFillerCell(column), this._fillerRow.children[newColumnIndex]);
|
|
|
|
if (column.headerView)
|
|
this.addSubview(column.headerView);
|
|
|
|
if (this._sortColumnIdentifier === column.identifier) {
|
|
let headerCell = this._headerElement.children[newColumnIndex];
|
|
headerCell.classList.toggle("sort-ascending", this._sortOrder === WI.Table.SortOrder.Ascending);
|
|
headerCell.classList.toggle("sort-descending", this._sortOrder === WI.Table.SortOrder.Descending);
|
|
}
|
|
|
|
// We haven't yet done any layout, nothing to do.
|
|
if (!this._columnWidths)
|
|
return;
|
|
|
|
// To avoid recreating all the cells in the row we create empty cells,
|
|
// size them, and then populate them. We always populate a cell after
|
|
// it has been sized.
|
|
let cellsToPopulate = [];
|
|
for (let row of this._listElement.children) {
|
|
if (row !== this._fillerRow) {
|
|
let unpopulatedCell = this._createCell(column, newColumnIndex);
|
|
cellsToPopulate.push(unpopulatedCell);
|
|
row.insertBefore(unpopulatedCell, row.children[newColumnIndex]);
|
|
}
|
|
}
|
|
|
|
// Re-layout all columns to make space.
|
|
this._widthGeneration++;
|
|
this._columnWidths = null;
|
|
this._resizeColumnsAndFiller();
|
|
|
|
// Now populate only the new cells for this column.
|
|
for (let cell of cellsToPopulate)
|
|
this._delegate.tablePopulateCell(this, cell, column, cell.parentElement.__index);
|
|
|
|
// Now populate columns that may be sensitive to resizes.
|
|
for (let visibleColumn of this._visibleColumns) {
|
|
if (visibleColumn !== column) {
|
|
if (visibleColumn.needsReloadOnResize)
|
|
this.reloadVisibleColumnCells(visibleColumn);
|
|
}
|
|
}
|
|
}
|
|
|
|
hideColumn(column)
|
|
{
|
|
console.assert(this._columnSpecs.get(column.identifier) === column, "Column not in this table.");
|
|
console.assert(!column.locked, "Locked columns should always be shown.");
|
|
if (column.locked)
|
|
return;
|
|
|
|
console.assert(column.hideable, "Column is not hideable so should always be shown.");
|
|
if (!column.hideable)
|
|
return;
|
|
|
|
if (column.hidden)
|
|
return;
|
|
|
|
column.hidden = true;
|
|
|
|
this._hiddenColumns.push(column);
|
|
|
|
let columnIndex = this._visibleColumns.indexOf(column);
|
|
this._visibleColumns.splice(columnIndex, 1);
|
|
|
|
// Save user preference for this column to be hidden.
|
|
let savedColumnVisibility = this._columnVisibilitySetting.value;
|
|
if (savedColumnVisibility[column.identifier] !== false) {
|
|
let copy = Object.shallowCopy(savedColumnVisibility);
|
|
if (column.defaultHidden)
|
|
delete copy[column.identifier];
|
|
else
|
|
copy[column.identifier] = false;
|
|
this._columnVisibilitySetting.value = copy;
|
|
}
|
|
|
|
this._headerElement.removeChild(this._headerElement.children[columnIndex]);
|
|
this._fillerRow.removeChild(this._fillerRow.children[columnIndex]);
|
|
|
|
if (column.headerView)
|
|
this.removeSubview(column.headerView);
|
|
|
|
// We haven't yet done any layout, nothing to do.
|
|
if (!this._columnWidths)
|
|
return;
|
|
|
|
for (let row of this._listElement.children) {
|
|
if (row !== this._fillerRow)
|
|
row.removeChild(row.children[columnIndex]);
|
|
}
|
|
|
|
// Re-layout all columns to make space.
|
|
this._widthGeneration++;
|
|
this._columnWidths = null;
|
|
this._resizeColumnsAndFiller();
|
|
|
|
// Now populate columns that may be sensitive to resizes.
|
|
for (let visibleColumn of this._visibleColumns) {
|
|
if (visibleColumn.needsReloadOnResize)
|
|
this.reloadVisibleColumnCells(visibleColumn);
|
|
}
|
|
}
|
|
|
|
// Protected
|
|
|
|
attached()
|
|
{
|
|
super.attached();
|
|
|
|
if (this._cachedScrollTop && !this._scrollContainerElement.scrollTop)
|
|
this._scrollContainerElement.scrollTop = this._cachedScrollTop;
|
|
}
|
|
|
|
initialLayout()
|
|
{
|
|
this.sortOrder = this._sortOrderSetting.value;
|
|
|
|
let restoreSortColumnIdentifier = this._sortColumnIdentifierSetting.value;
|
|
if (!this._columnSpecs.has(restoreSortColumnIdentifier))
|
|
this._sortColumnIdentifierSetting.value = null;
|
|
else
|
|
this.sortColumnIdentifier = restoreSortColumnIdentifier;
|
|
}
|
|
|
|
layout()
|
|
{
|
|
this._updateVisibleRows();
|
|
this._resizeColumnsAndFiller();
|
|
}
|
|
|
|
sizeDidChange()
|
|
{
|
|
super.sizeDidChange();
|
|
|
|
this._previousCachedWidth = this._cachedWidth;
|
|
this._cachedWidth = NaN;
|
|
this._cachedHeight = NaN;
|
|
}
|
|
|
|
// SelectionController delegate
|
|
|
|
selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
|
|
{
|
|
for (let item of deselectedItems) {
|
|
let rowIndex = this._indexForRepresentedObject(item);
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (row)
|
|
row.classList.toggle("selected", false);
|
|
}
|
|
|
|
for (let item of selectedItems) {
|
|
let rowIndex = this._indexForRepresentedObject(item);
|
|
let row = this._cachedRows.get(rowIndex);
|
|
if (row)
|
|
row.classList.toggle("selected", true);
|
|
}
|
|
|
|
if (this._selectionController.lastSelectedItem) {
|
|
let rowIndex = this._indexForRepresentedObject(this._selectionController.lastSelectedItem);
|
|
this.revealRow(rowIndex);
|
|
}
|
|
|
|
if (this._delegate.tableSelectionDidChange)
|
|
this._delegate.tableSelectionDidChange(this);
|
|
}
|
|
|
|
selectionControllerFirstSelectableItem(controller)
|
|
{
|
|
return this._representedObjectForIndex(0);
|
|
}
|
|
|
|
selectionControllerLastSelectableItem(controller)
|
|
{
|
|
return this._representedObjectForIndex(this.numberOfRows - 1);
|
|
}
|
|
|
|
selectionControllerPreviousSelectableItem(controller, item)
|
|
{
|
|
let index = this._indexForRepresentedObject(item);
|
|
console.assert(index >= 0 && index < this.numberOfRows);
|
|
|
|
return index > 0 ? this._representedObjectForIndex(index - 1) : null;
|
|
}
|
|
|
|
selectionControllerNextSelectableItem(controller, item)
|
|
{
|
|
let index = this._indexForRepresentedObject(item);
|
|
console.assert(index >= 0 && index < this.numberOfRows);
|
|
|
|
return index < this.numberOfRows - 1 ? this._representedObjectForIndex(index + 1) : null;
|
|
}
|
|
|
|
// Resizer delegate
|
|
|
|
resizerDragStarted(resizer)
|
|
{
|
|
console.assert(!this._currentResizer, resizer, this._currentResizer);
|
|
|
|
let resizerIndex = this._resizers.indexOf(resizer);
|
|
|
|
this._currentResizer = resizer;
|
|
this._resizeLeftColumns = this._visibleColumns.slice(0, resizerIndex + 1).reverse(); // Reversed to simplify iteration.
|
|
this._resizeRightColumns = this._visibleColumns.slice(resizerIndex + 1);
|
|
this._resizeOriginalColumnWidths = [].concat(this._columnWidths);
|
|
}
|
|
|
|
resizerDragging(resizer, positionDelta)
|
|
{
|
|
console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
|
|
if (resizer !== this._currentResizer)
|
|
return;
|
|
|
|
if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL)
|
|
positionDelta = -positionDelta;
|
|
|
|
// Completely recalculate columns from the original sizes based on the new mouse position.
|
|
this._columnWidths = [].concat(this._resizeOriginalColumnWidths);
|
|
|
|
if (!positionDelta) {
|
|
this._applyColumnWidths();
|
|
return;
|
|
}
|
|
|
|
let delta = Math.abs(positionDelta);
|
|
let leftDirection = positionDelta > 0;
|
|
let rightDirection = !leftDirection;
|
|
|
|
let columnWidths = this._columnWidths;
|
|
let visibleColumns = this._visibleColumns;
|
|
|
|
function growableSize(column) {
|
|
let width = columnWidths[visibleColumns.indexOf(column)];
|
|
if (column.maxWidth)
|
|
return column.maxWidth - width;
|
|
return Infinity;
|
|
}
|
|
|
|
function shrinkableSize(column) {
|
|
let width = columnWidths[visibleColumns.indexOf(column)];
|
|
if (column.minWidth)
|
|
return width - column.minWidth;
|
|
return width;
|
|
}
|
|
|
|
function canGrow(column) {
|
|
return growableSize(column) > 0;
|
|
}
|
|
|
|
function canShrink(column) {
|
|
return shrinkableSize(column) > 0;
|
|
}
|
|
|
|
function columnToResize(columns, isShrinking) {
|
|
// First find a flexible column we can resize.
|
|
for (let column of columns) {
|
|
if (!column.flexible)
|
|
continue;
|
|
if (isShrinking ? canShrink(column) : canGrow(column))
|
|
return column;
|
|
}
|
|
|
|
// Failing that see if we can resize the immediately neighbor.
|
|
let immediateColumn = columns[0];
|
|
if ((isShrinking && canShrink(immediateColumn)) || (!isShrinking && canGrow(immediateColumn)))
|
|
return immediateColumn;
|
|
|
|
// Bail. There isn't anything obvious in the table that can resize.
|
|
return null;
|
|
}
|
|
|
|
while (delta > 0) {
|
|
let leftColumn = columnToResize(this._resizeLeftColumns, leftDirection);
|
|
let rightColumn = columnToResize(this._resizeRightColumns, rightDirection);
|
|
if (!leftColumn || !rightColumn) {
|
|
// No more left or right column to grow or shrink.
|
|
break;
|
|
}
|
|
|
|
let incrementalDelta = Math.min(delta,
|
|
leftDirection ? shrinkableSize(leftColumn) : shrinkableSize(rightColumn),
|
|
leftDirection ? growableSize(rightColumn) : growableSize(leftColumn));
|
|
|
|
let leftIndex = this._visibleColumns.indexOf(leftColumn);
|
|
let rightIndex = this._visibleColumns.indexOf(rightColumn);
|
|
|
|
if (leftDirection) {
|
|
this._columnWidths[leftIndex] -= incrementalDelta;
|
|
this._columnWidths[rightIndex] += incrementalDelta;
|
|
} else {
|
|
this._columnWidths[leftIndex] += incrementalDelta;
|
|
this._columnWidths[rightIndex] -= incrementalDelta;
|
|
}
|
|
|
|
delta -= incrementalDelta;
|
|
}
|
|
|
|
// We have new column widths.
|
|
this._widthGeneration++;
|
|
|
|
this._applyColumnWidths();
|
|
this._positionHeaderViews();
|
|
}
|
|
|
|
resizerDragEnded(resizer)
|
|
{
|
|
console.assert(resizer === this._currentResizer, resizer, this._currentResizer);
|
|
if (resizer !== this._currentResizer)
|
|
return;
|
|
|
|
this._currentResizer = null;
|
|
this._resizeLeftColumns = null;
|
|
this._resizeRightColumns = null;
|
|
this._resizeOriginalColumnWidths = null;
|
|
|
|
this._positionResizerElements();
|
|
this._positionHeaderViews();
|
|
}
|
|
|
|
// Private
|
|
|
|
_createHeaderCell(column)
|
|
{
|
|
let cell = document.createElement("span");
|
|
cell.classList.add("cell", column.identifier);
|
|
cell.textContent = column.name;
|
|
|
|
if (column.align)
|
|
cell.classList.add("align-" + column.align);
|
|
if (column.sortable) {
|
|
cell.classList.add("sortable");
|
|
cell.addEventListener("click", this._handleHeaderCellClicked.bind(this, column));
|
|
}
|
|
|
|
cell.addEventListener("contextmenu", this._handleHeaderContextMenu.bind(this, column));
|
|
|
|
return cell;
|
|
}
|
|
|
|
_createFillerCell(column)
|
|
{
|
|
let cell = document.createElement("span");
|
|
cell.classList.add("cell", column.identifier);
|
|
return cell;
|
|
}
|
|
|
|
_createCell(column, columnIndex)
|
|
{
|
|
let cell = document.createElement("span");
|
|
cell.classList.add("cell", column.identifier);
|
|
if (column.align)
|
|
cell.classList.add("align-" + column.align);
|
|
if (this._columnWidths)
|
|
cell.style.width = this._columnWidths[columnIndex] + "px";
|
|
return cell;
|
|
}
|
|
|
|
_getOrCreateRow(rowIndex)
|
|
{
|
|
let cachedRow = this._cachedRows.get(rowIndex);
|
|
if (cachedRow)
|
|
return cachedRow;
|
|
|
|
let row = document.createElement("li");
|
|
row.__index = rowIndex;
|
|
row.__widthGeneration = 0;
|
|
this._styleRow(row);
|
|
|
|
if (this._delegate.tableRowHovered) {
|
|
this._boundHandleRowMouseEnter ??= this._handleRowMouseEnter.bind(this);
|
|
this._boundHandleRowMouseLeave ??= this._handleRowMouseLeave.bind(this);
|
|
|
|
row.addEventListener("mouseenter", this._boundHandleRowMouseEnter);
|
|
row.addEventListener("mouseleave", this._boundHandleRowMouseLeave);
|
|
}
|
|
|
|
this._cachedRows.set(rowIndex, row);
|
|
return row;
|
|
}
|
|
|
|
_styleRow(row)
|
|
{
|
|
let selected = row.classList.contains("selected");
|
|
|
|
row.className = "";
|
|
|
|
if (selected || this.isRowSelected(row.__index))
|
|
row.classList.add("selected");
|
|
|
|
if (this._delegate.tableRowClassNames)
|
|
row.classList.add(...this._delegate.tableRowClassNames(this, row.__index));
|
|
}
|
|
|
|
_populatedCellForColumnAndRow(column, columnIndex, rowIndex)
|
|
{
|
|
console.assert(rowIndex !== undefined, "Tried to populate a row that did not know its index. Is this the filler row?");
|
|
|
|
let cell = this._createCell(column, columnIndex);
|
|
this._delegate.tablePopulateCell(this, cell, column, rowIndex);
|
|
return cell;
|
|
}
|
|
|
|
_populateRow(row)
|
|
{
|
|
row.removeChildren();
|
|
|
|
let rowIndex = row.__index;
|
|
for (let i = 0; i < this._visibleColumns.length; ++i) {
|
|
let column = this._visibleColumns[i];
|
|
let cell = this._populatedCellForColumnAndRow(column, i, rowIndex);
|
|
row.appendChild(cell);
|
|
}
|
|
}
|
|
|
|
_resizeColumnsAndFiller()
|
|
{
|
|
if (isNaN(this._cachedWidth) || !this._cachedWidth)
|
|
this._cachedWidth = this._scrollContainerElement.realOffsetWidth;
|
|
|
|
// Not visible yet.
|
|
if (!this._cachedWidth)
|
|
return;
|
|
|
|
let availableWidth = this._cachedWidth;
|
|
let availableHeight = this._cachedHeight;
|
|
|
|
let contentHeight = this.numberOfRows * this._rowHeight;
|
|
this._fillerHeight = Math.max(availableHeight - contentHeight, 0);
|
|
|
|
// No change to layout metrics so no resizing is needed.
|
|
if (this._columnWidths && this._cachedWidth === this._previousCachedWidth) {
|
|
this._updateFillerRowWithNewHeight();
|
|
this._applyColumnWidthsToColumnsIfNeeded();
|
|
return;
|
|
}
|
|
|
|
this._previousCachedWidth = this._cachedWidth;
|
|
|
|
let lockedWidth = 0;
|
|
let lockedColumnCount = 0;
|
|
let totalMinimumWidth = 0;
|
|
|
|
for (let column of this._visibleColumns) {
|
|
if (column.locked) {
|
|
lockedWidth += column.width;
|
|
lockedColumnCount++;
|
|
totalMinimumWidth += column.width;
|
|
} else if (column.minWidth)
|
|
totalMinimumWidth += column.minWidth;
|
|
}
|
|
|
|
let flexibleWidth = availableWidth - lockedWidth;
|
|
let flexibleColumnCount = this._visibleColumns.length - lockedColumnCount;
|
|
|
|
// NOTE: We will often distribute pixels evenly across flexible columns in the table.
|
|
// If `availableWidth < totalMinimumWidth` than the table is too small for the minimum
|
|
// sizes of all the columns and we will start crunching the table (removing pixels from
|
|
// all flexible columns). This would be the appropriate time to introduce horizontal
|
|
// scrolling. For now we just remove pixels evenly.
|
|
//
|
|
// When distributing pixels, always start from the last column to accept remainder
|
|
// pixels so we don't always add from one side / to one column.
|
|
function distributeRemainingPixels(remainder, shrinking) {
|
|
// No pixels to distribute.
|
|
if (!remainder)
|
|
return;
|
|
|
|
let indexToStartAddingRemainderPixels = (this._lastColumnIndexToAcceptRemainderPixel + 1) % this._visibleColumns.length;
|
|
|
|
// Handle tables that are too small or too large. If the size constraints
|
|
// cause the columns to be too small or large. A second pass will do the
|
|
// expanding or crunching ignoring constraints.
|
|
let ignoreConstraints = false;
|
|
|
|
while (remainder > 0) {
|
|
let initialRemainder = remainder;
|
|
|
|
for (let i = indexToStartAddingRemainderPixels; i < this._columnWidths.length; ++i) {
|
|
let column = this._visibleColumns[i];
|
|
if (column.locked)
|
|
continue;
|
|
|
|
if (shrinking) {
|
|
if (ignoreConstraints || (column.minWidth && this._columnWidths[i] > column.minWidth)) {
|
|
this._columnWidths[i]--;
|
|
remainder--;
|
|
}
|
|
} else {
|
|
if (ignoreConstraints || (column.maxWidth && this._columnWidths[i] < column.maxWidth)) {
|
|
this._columnWidths[i]++;
|
|
remainder--;
|
|
} else if (!column.maxWidth) {
|
|
this._columnWidths[i]++;
|
|
remainder--;
|
|
}
|
|
}
|
|
|
|
if (!remainder) {
|
|
this._lastColumnIndexToAcceptRemainderPixel = i;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (remainder === initialRemainder && !indexToStartAddingRemainderPixels) {
|
|
// We have remaining pixels. Start crunching if we need to.
|
|
if (ignoreConstraints)
|
|
break;
|
|
ignoreConstraints = true;
|
|
}
|
|
|
|
indexToStartAddingRemainderPixels = 0;
|
|
}
|
|
|
|
console.assert(!remainder, "Should not have undistributed pixels.");
|
|
}
|
|
|
|
// Two kinds of layouts. Autosize or Resize.
|
|
if (!this._columnWidths) {
|
|
// Autosize: Flex all the flexes evenly and trickle out any remaining pixels.
|
|
this._columnWidths = [];
|
|
this._lastColumnIndexToAcceptRemainderPixel = 0;
|
|
|
|
let bestFitWidth = 0;
|
|
let bestFitColumnCount = 0;
|
|
|
|
function bestFit(callback) {
|
|
while (true) {
|
|
let remainingFlexibleColumnCount = flexibleColumnCount - bestFitColumnCount;
|
|
if (!remainingFlexibleColumnCount)
|
|
return;
|
|
|
|
// Fair size to give each flexible column.
|
|
let remainingFlexibleWidth = flexibleWidth - bestFitWidth;
|
|
let flexWidth = Math.floor(remainingFlexibleWidth / remainingFlexibleColumnCount);
|
|
|
|
let didPerformBestFit = false;
|
|
for (let i = 0; i < this._visibleColumns.length; ++i) {
|
|
// Already best fit this column.
|
|
if (this._columnWidths[i])
|
|
continue;
|
|
|
|
let column = this._visibleColumns[i];
|
|
console.assert(column.flexible, "Non-flexible columns should have been sized earlier", column);
|
|
|
|
// Attempt best fit.
|
|
let bestWidth = callback(column, flexWidth);
|
|
if (bestWidth === -1)
|
|
continue;
|
|
|
|
this._columnWidths[i] = bestWidth;
|
|
bestFitWidth += bestWidth;
|
|
bestFitColumnCount++;
|
|
didPerformBestFit = true;
|
|
}
|
|
if (!didPerformBestFit)
|
|
return;
|
|
|
|
// Repeat with a new flex size now that we have fewer flexible columns.
|
|
}
|
|
}
|
|
|
|
// Fit the locked columns.
|
|
for (let i = 0; i < this._visibleColumns.length; ++i) {
|
|
let column = this._visibleColumns[i];
|
|
if (column.locked)
|
|
this._columnWidths[i] = column.width;
|
|
}
|
|
|
|
// Best fit with the preferred initial width for flexible columns.
|
|
bestFit.call(this, (column, width) => {
|
|
if (!column.preferredInitialWidth || width <= column.preferredInitialWidth)
|
|
return -1;
|
|
return column.preferredInitialWidth;
|
|
});
|
|
|
|
// Best fit max size flexible columns. May make more pixels available for other columns.
|
|
bestFit.call(this, (column, width) => {
|
|
if (!column.maxWidth || width <= column.maxWidth)
|
|
return -1;
|
|
return column.maxWidth;
|
|
});
|
|
|
|
// Best fit min size flexible columns. May make less pixels available for other columns.
|
|
bestFit.call(this, (column, width) => {
|
|
if (!column.minWidth || width >= column.minWidth)
|
|
return -1;
|
|
return column.minWidth;
|
|
});
|
|
|
|
// Best fit the remaining flexible columns with the fair remaining size.
|
|
bestFit.call(this, (column, width) => width);
|
|
|
|
// Distribute any remaining pixels evenly.
|
|
let remainder = availableWidth - (lockedWidth + bestFitWidth);
|
|
let shrinking = remainder < 0;
|
|
distributeRemainingPixels.call(this, Math.abs(remainder), shrinking);
|
|
} else {
|
|
// Resize: Distribute pixels evenly across flex columns.
|
|
console.assert(this._columnWidths.length === this._visibleColumns.length, "Number of columns should not change in a resize.");
|
|
|
|
let originalTotalColumnWidth = 0;
|
|
for (let width of this._columnWidths)
|
|
originalTotalColumnWidth += width;
|
|
|
|
let remainder = Math.abs(availableWidth - originalTotalColumnWidth);
|
|
let shrinking = availableWidth < originalTotalColumnWidth;
|
|
distributeRemainingPixels.call(this, remainder, shrinking);
|
|
}
|
|
|
|
// We have new column widths.
|
|
this._widthGeneration++;
|
|
|
|
// Apply widths.
|
|
|
|
this._updateFillerRowWithNewHeight();
|
|
this._applyColumnWidths();
|
|
this._positionResizerElements();
|
|
this._positionHeaderViews();
|
|
}
|
|
|
|
_updateVisibleRows()
|
|
{
|
|
let rowHeight = this._rowHeight;
|
|
let updateOffsetThreshold = rowHeight * 10;
|
|
let overflowPadding = updateOffsetThreshold * 3;
|
|
|
|
let scrollTop = this._calculateScrollTop();
|
|
let scrollableOffsetHeight = this._calculateOffsetHeight();
|
|
|
|
let visibleRowCount = Math.ceil((scrollableOffsetHeight + (overflowPadding * 2)) / rowHeight);
|
|
let currentTopMargin = this._topSpacerHeight;
|
|
let currentBottomMargin = this._bottomSpacerHeight;
|
|
let currentTableBottom = currentTopMargin + (visibleRowCount * rowHeight);
|
|
|
|
let belowTopThreshold = !currentTopMargin || scrollTop > currentTopMargin + updateOffsetThreshold;
|
|
let aboveBottomThreshold = !currentBottomMargin || scrollTop + scrollableOffsetHeight < currentTableBottom - updateOffsetThreshold;
|
|
|
|
if (belowTopThreshold && aboveBottomThreshold && !isNaN(this._previousRevealedRowCount))
|
|
return;
|
|
|
|
let numberOfRows = this.numberOfRows;
|
|
this._previousRevealedRowCount = numberOfRows;
|
|
|
|
// Scroll back up if the number of rows was reduced such that the existing
|
|
// scroll top value is larger than it could otherwise have been. We only
|
|
// need to do this adjustment if there are more rows than would fit on screen,
|
|
// because when the filler row activates it will reset our scroll.
|
|
if (scrollTop) {
|
|
let rowsThatCanFitOnScreen = Math.ceil(scrollableOffsetHeight / rowHeight);
|
|
if (numberOfRows >= rowsThatCanFitOnScreen) {
|
|
let maximumScrollTop = Math.max(0, (numberOfRows * rowHeight) - scrollableOffsetHeight);
|
|
if (scrollTop > maximumScrollTop) {
|
|
this._scrollContainerElement.scrollTop = maximumScrollTop;
|
|
this._cachedScrollTop = maximumScrollTop;
|
|
}
|
|
}
|
|
}
|
|
|
|
let topHiddenRowCount = Math.max(0, Math.floor((scrollTop - overflowPadding) / rowHeight));
|
|
let bottomHiddenRowCount = Math.max(0, this._previousRevealedRowCount - topHiddenRowCount - visibleRowCount);
|
|
|
|
let marginTop = topHiddenRowCount * rowHeight;
|
|
let marginBottom = bottomHiddenRowCount * rowHeight;
|
|
|
|
if (this._topSpacerHeight !== marginTop) {
|
|
this._topSpacerHeight = marginTop;
|
|
this._topSpacerElement.style.height = marginTop + "px";
|
|
}
|
|
|
|
if (this._bottomDataTableMarginElement !== marginBottom) {
|
|
this._bottomSpacerHeight = marginBottom;
|
|
this._bottomSpacerElement.style.height = marginBottom + "px";
|
|
}
|
|
|
|
this._visibleRowIndexStart = topHiddenRowCount;
|
|
this._visibleRowIndexEnd = this._visibleRowIndexStart + visibleRowCount;
|
|
|
|
// Completely remove all rows and add new ones.
|
|
this._listElement.removeChildren();
|
|
|
|
// If there are an odd number of rows hidden, the first visible row must be an even row.
|
|
this._listElement.classList.toggle("even-first-zebra-stripe", !!(topHiddenRowCount % 2));
|
|
|
|
for (let i = this._visibleRowIndexStart; i < this._visibleRowIndexEnd && i < numberOfRows; ++i) {
|
|
let row = this._getOrCreateRow(i);
|
|
this._listElement.appendChild(row);
|
|
}
|
|
|
|
this._listElement.appendChild(this._fillerRow);
|
|
}
|
|
|
|
_updateFillerRowWithNewHeight()
|
|
{
|
|
if (!this._fillerHeight) {
|
|
this._scrollContainerElement.classList.remove("not-scrollable");
|
|
this._fillerRow.remove();
|
|
return;
|
|
}
|
|
|
|
this._scrollContainerElement.classList.add("not-scrollable");
|
|
|
|
// In the event that we just made the table not scrollable then the number
|
|
// of rows can fit on screen. Reset the scroll top.
|
|
if (this._cachedScrollTop) {
|
|
this._scrollContainerElement.scrollTop = 0;
|
|
this._cachedScrollTop = 0;
|
|
}
|
|
|
|
// Extend past edge some reasonable amount. At least 200px.
|
|
const paddingPastTheEdge = 200;
|
|
this._fillerHeight += paddingPastTheEdge;
|
|
|
|
for (let cell of this._fillerRow.children)
|
|
cell.style.height = this._fillerHeight + "px";
|
|
|
|
if (!this._fillerRow.parentElement)
|
|
this._listElement.appendChild(this._fillerRow);
|
|
}
|
|
|
|
_applyColumnWidths()
|
|
{
|
|
for (let i = 0; i < this._headerElement.children.length; ++i)
|
|
this._headerElement.children[i].style.width = this._columnWidths[i] + "px";
|
|
|
|
for (let row of this._listElement.children) {
|
|
for (let i = 0; i < row.children.length; ++i)
|
|
row.children[i].style.width = this._columnWidths[i] + "px";
|
|
row.__widthGeneration = this._widthGeneration;
|
|
}
|
|
|
|
// Update Table Columns after cells since events may respond to this.
|
|
for (let i = 0; i < this._visibleColumns.length; ++i)
|
|
this._visibleColumns[i].width = this._columnWidths[i];
|
|
|
|
// Create missing cells after we've sized.
|
|
for (let row of this._listElement.children) {
|
|
if (row !== this._fillerRow) {
|
|
if (row.children.length !== this._visibleColumns.length)
|
|
this._populateRow(row);
|
|
}
|
|
}
|
|
}
|
|
|
|
_applyColumnWidthsToColumnsIfNeeded()
|
|
{
|
|
// Apply and create missing cells only if row needs a width update.
|
|
for (let row of this._listElement.children) {
|
|
if (row.__widthGeneration !== this._widthGeneration) {
|
|
for (let i = 0; i < row.children.length; ++i)
|
|
row.children[i].style.width = this._columnWidths[i] + "px";
|
|
if (row !== this._fillerRow) {
|
|
if (row.children.length !== this._visibleColumns.length)
|
|
this._populateRow(row);
|
|
}
|
|
row.__widthGeneration = this._widthGeneration;
|
|
}
|
|
}
|
|
}
|
|
|
|
_positionResizerElements()
|
|
{
|
|
console.assert(this._visibleColumns.length === this._columnWidths.length);
|
|
|
|
// Create the appropriate number of resizers.
|
|
let resizersNeededCount = this._visibleColumns.length - 1;
|
|
if (this._resizers.length !== resizersNeededCount) {
|
|
if (this._resizers.length < resizersNeededCount) {
|
|
do {
|
|
let resizer = new WI.Resizer(WI.Resizer.RuleOrientation.Vertical, this);
|
|
this._resizers.push(resizer);
|
|
this._resizersElement.appendChild(resizer.element);
|
|
} while (this._resizers.length < resizersNeededCount);
|
|
} else {
|
|
do {
|
|
let resizer = this._resizers.pop();
|
|
this._resizersElement.removeChild(resizer.element);
|
|
} while (this._resizers.length > resizersNeededCount);
|
|
}
|
|
}
|
|
|
|
// Position them.
|
|
const columnResizerAdjustment = 3;
|
|
let positionAttribute = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
|
|
let totalWidth = 0;
|
|
for (let i = 0; i < resizersNeededCount; ++i) {
|
|
totalWidth += this._columnWidths[i];
|
|
this._resizers[i].element.style[positionAttribute] = (totalWidth - columnResizerAdjustment) + "px";
|
|
}
|
|
}
|
|
|
|
_positionHeaderViews()
|
|
{
|
|
if (!this.subviews.length)
|
|
return;
|
|
|
|
let offset = 0;
|
|
let updates = [];
|
|
for (let i = 0; i < this._visibleColumns.length; ++i) {
|
|
let column = this._visibleColumns[i];
|
|
let width = this._columnWidths[i];
|
|
if (column.headerView)
|
|
updates.push({headerView: column.headerView, offset, width});
|
|
offset += width;
|
|
}
|
|
|
|
let styleProperty = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? "right" : "left";
|
|
for (let {headerView, offset, width} of updates) {
|
|
headerView.element.style.setProperty(styleProperty, offset + "px");
|
|
headerView.element.style.width = width + "px";
|
|
headerView.updateLayout(WI.View.LayoutReason.Resize);
|
|
}
|
|
}
|
|
|
|
_isRowVisible(rowIndex)
|
|
{
|
|
if (!this._previousRevealedRowCount)
|
|
return false;
|
|
|
|
return rowIndex >= this._visibleRowIndexStart && rowIndex <= this._visibleRowIndexEnd;
|
|
}
|
|
|
|
_indexToInsertColumn(column)
|
|
{
|
|
let currentVisibleColumnIndex = 0;
|
|
|
|
for (let columnIdentifier of this._columnOrder) {
|
|
if (columnIdentifier === column.identifier)
|
|
return currentVisibleColumnIndex;
|
|
if (columnIdentifier === this._visibleColumns[currentVisibleColumnIndex].identifier) {
|
|
currentVisibleColumnIndex++;
|
|
if (currentVisibleColumnIndex >= this._visibleColumns.length)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return currentVisibleColumnIndex;
|
|
}
|
|
|
|
_handleScroll(event)
|
|
{
|
|
if (event.type === "mousewheel" && !event.wheelDeltaY)
|
|
return;
|
|
|
|
this._cachedScrollTop = NaN;
|
|
this.needsLayout();
|
|
}
|
|
|
|
_handleKeyDown(event)
|
|
{
|
|
this._selectionController.handleKeyDown(event);
|
|
}
|
|
|
|
_handleMouseDown(event)
|
|
{
|
|
let cell = event.target.closest(".cell");
|
|
if (!cell)
|
|
return;
|
|
|
|
let row = cell.parentElement;
|
|
if (row === this._fillerRow)
|
|
return;
|
|
|
|
let rowIndex = row.__index;
|
|
|
|
// Before checking if multiple selection is allowed, check if clicking the
|
|
// row would cause it to be selected, and whether it is allowed by the delegate.
|
|
if (!this.isRowSelected(rowIndex) && this._delegate.tableShouldSelectRow) {
|
|
let columnIndex = Array.from(row.children).indexOf(cell);
|
|
let column = this._visibleColumns[columnIndex];
|
|
if (!this._delegate.tableShouldSelectRow(this, cell, column, rowIndex))
|
|
return;
|
|
}
|
|
|
|
this._selectionController.handleItemMouseDown(this._representedObjectForIndex(rowIndex), event);
|
|
}
|
|
|
|
_handleContextMenu(event)
|
|
{
|
|
let cell = event.target.closest(".cell");
|
|
if (!cell)
|
|
return;
|
|
|
|
let row = cell.parentElement;
|
|
if (row === this._fillerRow)
|
|
return;
|
|
|
|
let columnIndex = Array.from(row.children).indexOf(cell);
|
|
let column = this._visibleColumns[columnIndex];
|
|
let rowIndex = row.__index;
|
|
|
|
this._delegate.tableCellContextMenuClicked(this, cell, column, rowIndex, event);
|
|
}
|
|
|
|
_handleHeaderCellClicked(column, event)
|
|
{
|
|
let sortOrder = this._sortOrder;
|
|
if (sortOrder === WI.Table.SortOrder.Indeterminate)
|
|
sortOrder = WI.Table.SortOrder.Descending;
|
|
else if (this._sortColumnIdentifier === column.identifier)
|
|
sortOrder = sortOrder === WI.Table.SortOrder.Ascending ? WI.Table.SortOrder.Descending : WI.Table.SortOrder.Ascending;
|
|
|
|
this.sortColumnIdentifier = column.identifier;
|
|
this.sortOrder = sortOrder;
|
|
}
|
|
|
|
_handleHeaderContextMenu(column, event)
|
|
{
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
|
|
if (column.sortable) {
|
|
if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Ascending) {
|
|
contextMenu.appendItem(WI.UIString("Sort Ascending"), () => {
|
|
this.sortColumnIdentifier = column.identifier;
|
|
this.sortOrder = WI.Table.SortOrder.Ascending;
|
|
});
|
|
}
|
|
|
|
if (this.sortColumnIdentifier !== column.identifier || this.sortOrder !== WI.Table.SortOrder.Descending) {
|
|
contextMenu.appendItem(WI.UIString("Sort Descending"), () => {
|
|
this.sortColumnIdentifier = column.identifier;
|
|
this.sortOrder = WI.Table.SortOrder.Descending;
|
|
});
|
|
}
|
|
}
|
|
|
|
contextMenu.appendSeparator();
|
|
|
|
let didAppendHeaderItem = false;
|
|
|
|
for (let [columnIdentifier, column] of this._columnSpecs) {
|
|
if (column.locked)
|
|
continue;
|
|
if (!column.hideable)
|
|
continue;
|
|
|
|
// Add a header item before the list of toggleable columns.
|
|
if (!didAppendHeaderItem) {
|
|
const disabled = true;
|
|
contextMenu.appendItem(WI.UIString("Displayed Columns"), () => {}, disabled);
|
|
didAppendHeaderItem = true;
|
|
}
|
|
|
|
let checked = !column.hidden;
|
|
contextMenu.appendCheckboxItem(column.name, () => {
|
|
if (column.hidden)
|
|
this.showColumn(column);
|
|
else
|
|
this.hideColumn(column);
|
|
}, checked);
|
|
}
|
|
}
|
|
|
|
_handleRowMouseEnter(event)
|
|
{
|
|
let row = event.target;
|
|
|
|
this.delegate.tableRowHovered(this, row.__index);
|
|
}
|
|
|
|
_handleRowMouseLeave(event)
|
|
{
|
|
this.delegate.tableRowHovered(this, NaN);
|
|
}
|
|
|
|
_removeRows(representedObjects)
|
|
{
|
|
let removed = 0;
|
|
|
|
let adjustRowAtIndex = (index) => {
|
|
let row = this._cachedRows.get(index);
|
|
if (row) {
|
|
this._cachedRows.delete(index);
|
|
row.__index -= removed;
|
|
this._cachedRows.set(row.__index, row);
|
|
}
|
|
};
|
|
|
|
let rowIndexes = [];
|
|
for (let object of representedObjects)
|
|
rowIndexes.push(this._indexForRepresentedObject(object));
|
|
|
|
rowIndexes.sort((a, b) => a - b);
|
|
|
|
let lastIndex = rowIndexes.lastValue;
|
|
for (let index = rowIndexes[0]; index <= lastIndex; ++index) {
|
|
if (rowIndexes.binaryIndexOf(index) >= 0) {
|
|
let row = this._cachedRows.get(index);
|
|
if (row) {
|
|
this._cachedRows.delete(index);
|
|
row.remove();
|
|
}
|
|
removed++;
|
|
continue;
|
|
}
|
|
|
|
if (removed)
|
|
adjustRowAtIndex(index);
|
|
}
|
|
|
|
if (!removed)
|
|
return;
|
|
|
|
for (let index = lastIndex + 1; index < this.numberOfRows; ++index)
|
|
adjustRowAtIndex(index);
|
|
|
|
|
|
this._selectionController.didRemoveItems(representedObjects);
|
|
|
|
if (this._delegate.tableDidRemoveRows)
|
|
this._delegate.tableDidRemoveRows(this, rowIndexes);
|
|
}
|
|
|
|
_indexForRepresentedObject(object)
|
|
{
|
|
return this.dataSource.tableIndexForRepresentedObject(this, object);
|
|
}
|
|
|
|
_representedObjectForIndex(index)
|
|
{
|
|
return this.dataSource.tableRepresentedObjectForIndex(this, index);
|
|
}
|
|
|
|
_calculateOffsetHeight()
|
|
{
|
|
if (isNaN(this._cachedHeight))
|
|
this._cachedHeight = this._scrollContainerElement.realOffsetHeight;
|
|
return this._cachedHeight;
|
|
}
|
|
|
|
_calculateScrollTop()
|
|
{
|
|
if (isNaN(this._cachedScrollTop))
|
|
this._cachedScrollTop = this._scrollContainerElement.scrollTop;
|
|
return this._cachedScrollTop;
|
|
}
|
|
};
|
|
|
|
WI.Table.SortOrder = {
|
|
Indeterminate: "table-sort-order-indeterminate",
|
|
Ascending: "table-sort-order-ascending",
|
|
Descending: "table-sort-order-descending",
|
|
};
|
|
|
|
/* Views/TableColumn.js */
|
|
|
|
/*
|
|
* Copyright (C) 2017 Apple Inc. All Rights Reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TableColumn = class TableColumn extends WI.Object
|
|
{
|
|
constructor(identifier, name, {initialWidth, minWidth, maxWidth, hidden, sortable, hideable, align, resizeType, headerView, needsReloadOnResize} = {})
|
|
{
|
|
super();
|
|
|
|
console.assert(identifier);
|
|
console.assert(name);
|
|
console.assert(!initialWidth || initialWidth > 0);
|
|
console.assert(!minWidth || minWidth >= 0);
|
|
console.assert(!maxWidth || maxWidth >= 0);
|
|
|
|
this._identifier = identifier;
|
|
this._name = name;
|
|
|
|
this._width = initialWidth || NaN;
|
|
this._minWidth = minWidth || 50;
|
|
this._maxWidth = maxWidth || 0;
|
|
this._initialWidth = initialWidth || NaN;
|
|
this._hidden = hidden || false;
|
|
this._defaultHidden = hidden || false;
|
|
this._sortable = typeof sortable === "boolean" ? sortable : true;
|
|
this._hideable = typeof hideable === "boolean" ? hideable : true;
|
|
this._align = align || null;
|
|
this._resizeType = resizeType || TableColumn.ResizeType.Auto;
|
|
this._headerView = headerView || null;
|
|
this._needsReloadOnResize = needsReloadOnResize || false;
|
|
|
|
console.assert(!this._minWidth || !this._maxWidth || this._minWidth <= this._maxWidth, "Invalid min/max", this._minWidth, this._maxWidth);
|
|
console.assert(isNaN(this._width) || !this._minWidth || (this._width >= this._minWidth), "Initial width is less than min", this._width, this._minWidth);
|
|
console.assert(isNaN(this._width) || !this._maxWidth || (this._width <= this._maxWidth), "Initial width is greater than max", this._width, this._maxWidth);
|
|
console.assert(!this.locked || this.width, "A locked column should aways have an initial width");
|
|
console.assert(!this.locked || !this.hidden, "A locked column should never be hidden");
|
|
}
|
|
|
|
get identifier() { return this._identifier; }
|
|
get name() { return this._name; }
|
|
get minWidth() { return this._minWidth; }
|
|
get maxWidth() { return this._maxWidth; }
|
|
get preferredInitialWidth() { return this._initialWidth; }
|
|
get defaultHidden() { return this._defaultHidden; }
|
|
get sortable() { return this._sortable; }
|
|
get hideable() { return this._hideable; }
|
|
get align() { return this._align; }
|
|
get headerView() { return this._headerView; }
|
|
get needsReloadOnResize() { return this._needsReloadOnResize; }
|
|
|
|
get locked() { return this._resizeType === TableColumn.ResizeType.Locked; }
|
|
get flexible() { return this._resizeType === TableColumn.ResizeType.Auto; }
|
|
|
|
get width()
|
|
{
|
|
return this._width;
|
|
}
|
|
|
|
set width(width)
|
|
{
|
|
// NOTE: We can't assert this because we resize past the minimum and maximum sizes.
|
|
// If we support horizontal scrolling in the Table then we could assert these.
|
|
// console.assert(isNaN(width) || !this._minWidth || width >= this._minWidth, "New width was less than midWidth.", width, this._minWidth);
|
|
// console.assert(isNaN(width) || !this._maxWidth || width <= this._maxWidth, "New width was greater than maxWidth.", width, this._maxWidth);
|
|
|
|
if (this._width === width)
|
|
return;
|
|
|
|
this._width = width;
|
|
|
|
this.dispatchEventToListeners(WI.TableColumn.Event.WidthDidChange);
|
|
}
|
|
|
|
get hidden()
|
|
{
|
|
return this._hidden;
|
|
}
|
|
|
|
set hidden(x)
|
|
{
|
|
console.assert(!this.locked && this._hideable, "Should not be able to hide a non-hideable column.");
|
|
this._hidden = x;
|
|
}
|
|
};
|
|
|
|
WI.TableColumn.ResizeType = {
|
|
Auto: "auto",
|
|
Locked: "locked",
|
|
};
|
|
|
|
WI.TableColumn.Event = {
|
|
WidthDidChange: "table-column-width-did-change",
|
|
};
|
|
|
|
/* Views/TreeElement.js */
|
|
|
|
/*
|
|
* Copyright (C) 2007, 2013, 2015 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TreeElement = class TreeElement extends WI.Object
|
|
{
|
|
constructor(title, representedObject, options = {})
|
|
{
|
|
super();
|
|
|
|
this._title = title;
|
|
this.representedObject = representedObject || {};
|
|
|
|
if (this.representedObject.__treeElementIdentifier)
|
|
this.identifier = this.representedObject.__treeElementIdentifier;
|
|
else {
|
|
this.identifier = WI.TreeOutline._knownTreeElementNextIdentifier++;
|
|
this.representedObject.__treeElementIdentifier = this.identifier;
|
|
}
|
|
|
|
this._hidden = false;
|
|
this._selectable = true;
|
|
this.expanded = false;
|
|
this.selected = false;
|
|
this.hasChildren = options.hasChildren;
|
|
this.children = [];
|
|
this.treeOutline = null;
|
|
this.parent = null;
|
|
this.previousSibling = null;
|
|
this.nextSibling = null;
|
|
this._listItemNode = null;
|
|
}
|
|
|
|
// Methods
|
|
|
|
appendChild() { return WI.TreeOutline.prototype.appendChild.apply(this, arguments); }
|
|
insertChild() { return WI.TreeOutline.prototype.insertChild.apply(this, arguments); }
|
|
removeChild() { return WI.TreeOutline.prototype.removeChild.apply(this, arguments); }
|
|
removeChildAtIndex() { return WI.TreeOutline.prototype.removeChildAtIndex.apply(this, arguments); }
|
|
removeChildren() { return WI.TreeOutline.prototype.removeChildren.apply(this, arguments); }
|
|
selfOrDescendant() { return WI.TreeOutline.prototype.selfOrDescendant.apply(this, arguments); }
|
|
|
|
get arrowToggleWidth()
|
|
{
|
|
return 10;
|
|
}
|
|
|
|
get selectable()
|
|
{
|
|
if (this._hidden)
|
|
return false;
|
|
return this._selectable;
|
|
}
|
|
|
|
set selectable(x)
|
|
{
|
|
if (x === this._selectable)
|
|
return;
|
|
|
|
this._selectable = x;
|
|
|
|
this._listItemNode?.classList.toggle("non-selectable", !this._selectable);
|
|
}
|
|
|
|
get expandable()
|
|
{
|
|
return this.hasChildren;
|
|
}
|
|
|
|
get listItemElement()
|
|
{
|
|
return this._listItemNode;
|
|
}
|
|
|
|
get title()
|
|
{
|
|
return this._title;
|
|
}
|
|
|
|
set title(x)
|
|
{
|
|
this._title = x;
|
|
this._setListItemNodeContent();
|
|
this.didChange();
|
|
}
|
|
|
|
get titleHTML()
|
|
{
|
|
return this._titleHTML;
|
|
}
|
|
|
|
set titleHTML(x)
|
|
{
|
|
this._titleHTML = x;
|
|
this._setListItemNodeContent();
|
|
this.didChange();
|
|
}
|
|
|
|
get tooltip()
|
|
{
|
|
return this._tooltip;
|
|
}
|
|
|
|
set tooltip(x)
|
|
{
|
|
this._tooltip = x;
|
|
if (this._listItemNode)
|
|
this._listItemNode.title = x ? x : "";
|
|
}
|
|
|
|
get hasChildren()
|
|
{
|
|
return this._hasChildren;
|
|
}
|
|
|
|
set hasChildren(x)
|
|
{
|
|
if (this._hasChildren === x)
|
|
return;
|
|
|
|
this._hasChildren = x;
|
|
|
|
if (!this._listItemNode)
|
|
return;
|
|
|
|
if (x)
|
|
this._listItemNode.classList.add("parent");
|
|
else {
|
|
this._listItemNode.classList.remove("parent");
|
|
this.collapse();
|
|
}
|
|
|
|
this.didChange();
|
|
}
|
|
|
|
get hidden()
|
|
{
|
|
return this._hidden;
|
|
}
|
|
|
|
set hidden(x)
|
|
{
|
|
if (this._hidden === x)
|
|
return;
|
|
|
|
this._hidden = x;
|
|
|
|
if (this._listItemNode)
|
|
this._listItemNode.hidden = this._hidden;
|
|
if (this._childrenListNode)
|
|
this._childrenListNode.hidden = this._hidden;
|
|
|
|
if (this.treeOutline) {
|
|
if (this.treeOutline.virtualized)
|
|
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
|
|
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementVisibilityDidChange, {element: this});
|
|
}
|
|
}
|
|
|
|
get shouldRefreshChildren()
|
|
{
|
|
return this._shouldRefreshChildren;
|
|
}
|
|
|
|
set shouldRefreshChildren(x)
|
|
{
|
|
this._shouldRefreshChildren = x;
|
|
if (x && this.expanded)
|
|
this.expand();
|
|
}
|
|
|
|
get previousSelectableSibling()
|
|
{
|
|
let treeElement = this.previousSibling;
|
|
while (treeElement && !treeElement.selectable)
|
|
treeElement = treeElement.previousSibling;
|
|
return treeElement;
|
|
}
|
|
|
|
get nextSelectableSibling()
|
|
{
|
|
let treeElement = this.nextSibling;
|
|
while (treeElement && !treeElement.selectable)
|
|
treeElement = treeElement.nextSibling;
|
|
return treeElement;
|
|
}
|
|
|
|
canSelectOnMouseDown(event)
|
|
{
|
|
// Overridden by subclasses if needed.
|
|
return true;
|
|
}
|
|
|
|
_fireDidChange()
|
|
{
|
|
if (this.treeOutline)
|
|
this.treeOutline._treeElementDidChange(this);
|
|
}
|
|
|
|
didChange()
|
|
{
|
|
if (!this.treeOutline)
|
|
return;
|
|
|
|
if (!this._fireDidChangeDebouncer) {
|
|
this._fireDidChangeDebouncer = new Debouncer(() => {
|
|
this._fireDidChange();
|
|
});
|
|
}
|
|
|
|
this._fireDidChangeDebouncer.delayForFrame();
|
|
}
|
|
|
|
_setListItemNodeContent()
|
|
{
|
|
if (!this._listItemNode)
|
|
return;
|
|
|
|
if (!this._titleHTML && !this._title)
|
|
this._listItemNode.removeChildren();
|
|
else if (typeof this._titleHTML === "string")
|
|
this._listItemNode.innerHTML = this._titleHTML;
|
|
else if (typeof this._title === "string")
|
|
this._listItemNode.textContent = this._title;
|
|
else {
|
|
this._listItemNode.removeChildren();
|
|
if (this._title.parentNode)
|
|
this._title.parentNode.removeChild(this._title);
|
|
this._listItemNode.appendChild(this._title);
|
|
}
|
|
}
|
|
|
|
_attach()
|
|
{
|
|
if (!this._listItemNode || this.parent._shouldRefreshChildren) {
|
|
if (this.parent._shouldRefreshChildren)
|
|
this._detach();
|
|
|
|
this._listItemNode = this.treeOutline._childrenListNode.ownerDocument.createElement("li");
|
|
this._listItemNode.treeElement = this;
|
|
this._setListItemNodeContent();
|
|
this._listItemNode.title = this._tooltip ? this._tooltip : "";
|
|
this._listItemNode.hidden = this.hidden;
|
|
this._listItemNode.role = "treeitem";
|
|
|
|
if (this.hasChildren)
|
|
this._listItemNode.classList.add("parent");
|
|
if (this.expanded)
|
|
this._listItemNode.classList.add("expanded");
|
|
if (this.selected)
|
|
this._listItemNode.classList.add("selected");
|
|
if (!this.selectable)
|
|
this._listItemNode.classList.add("non-selectable");
|
|
|
|
this._listItemNode.addEventListener("click", WI.TreeElement.treeElementToggled);
|
|
this._listItemNode.addEventListener("dblclick", WI.TreeElement.treeElementDoubleClicked);
|
|
|
|
if (this.onattach)
|
|
this.onattach(this);
|
|
}
|
|
|
|
var nextSibling = null;
|
|
if (this.nextSibling && this.nextSibling._listItemNode && this.nextSibling._listItemNode.parentNode === this.parent._childrenListNode)
|
|
nextSibling = this.nextSibling._listItemNode;
|
|
|
|
if (!this.treeOutline || !this.treeOutline.virtualized) {
|
|
this.parent._childrenListNode.insertBefore(this._listItemNode, nextSibling);
|
|
if (this._childrenListNode)
|
|
this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
|
|
}
|
|
|
|
if (this.selected)
|
|
this.select();
|
|
if (this.expanded)
|
|
this.expand();
|
|
}
|
|
|
|
_detach()
|
|
{
|
|
if (this.ondetach && this._listItemNode)
|
|
this.ondetach(this);
|
|
if (this._listItemNode && this._listItemNode.parentNode)
|
|
this._listItemNode.parentNode.removeChild(this._listItemNode);
|
|
if (this._childrenListNode && this._childrenListNode.parentNode)
|
|
this._childrenListNode.parentNode.removeChild(this._childrenListNode);
|
|
}
|
|
|
|
static treeElementToggled(event)
|
|
{
|
|
let element = event.currentTarget;
|
|
if (!element)
|
|
return;
|
|
|
|
let treeElement = element.treeElement;
|
|
if (!treeElement)
|
|
return;
|
|
|
|
if (treeElement.toggleOnClick || treeElement.isEventWithinDisclosureTriangle(event)) {
|
|
if (treeElement.expanded) {
|
|
if (event.altKey)
|
|
treeElement.collapseRecursively();
|
|
else
|
|
treeElement.collapse();
|
|
} else {
|
|
if (event.altKey)
|
|
treeElement.expandRecursively();
|
|
else
|
|
treeElement.expand();
|
|
}
|
|
event.stopPropagation();
|
|
}
|
|
|
|
if (treeElement.treeOutline && !treeElement.treeOutline.selectable)
|
|
treeElement.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementClicked, {treeElement});
|
|
}
|
|
|
|
static treeElementDoubleClicked(event)
|
|
{
|
|
var element = event.currentTarget;
|
|
if (!element || !element.treeElement)
|
|
return;
|
|
|
|
if (element.treeElement.isEventWithinDisclosureTriangle(event))
|
|
return;
|
|
|
|
if (element.treeElement.dispatchEventToListeners(WI.TreeElement.Event.DoubleClick))
|
|
return;
|
|
|
|
if (element.treeElement.ondblclick)
|
|
element.treeElement.ondblclick.call(element.treeElement, event);
|
|
else if (element.treeElement.hasChildren && !element.treeElement.expanded)
|
|
element.treeElement.expand();
|
|
}
|
|
|
|
collapse()
|
|
{
|
|
if (this._listItemNode) {
|
|
this._listItemNode.classList.remove("expanded");
|
|
this._listItemNode.ariaExpanded = false;
|
|
}
|
|
if (this._childrenListNode) {
|
|
this._childrenListNode.classList.remove("expanded");
|
|
this._childrenListNode.ariaExpanded = false;
|
|
}
|
|
|
|
this.expanded = false;
|
|
if (this.treeOutline)
|
|
this.treeOutline._treeElementsExpandedState[this.identifier] = false;
|
|
|
|
if (this.oncollapse)
|
|
this.oncollapse(this);
|
|
|
|
if (this.treeOutline) {
|
|
if (this.treeOutline.virtualized)
|
|
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
|
|
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementDisclosureDidChanged, {element: this});
|
|
}
|
|
}
|
|
|
|
collapseRecursively()
|
|
{
|
|
var item = this;
|
|
while (item) {
|
|
if (item.expanded)
|
|
item.collapse();
|
|
item = item.traverseNextTreeElement(false, this, true);
|
|
}
|
|
}
|
|
|
|
expand()
|
|
{
|
|
if (this.expanded && !this._shouldRefreshChildren && this._childrenListNode)
|
|
return;
|
|
|
|
// Set this before onpopulate. Since onpopulate can add elements and dispatch an ElementAdded event,
|
|
// this makes sure the expanded flag is true before calling those functions. This prevents the
|
|
// possibility of an infinite loop if onpopulate or an event handler were to call expand.
|
|
|
|
this.expanded = true;
|
|
if (this.treeOutline)
|
|
this.treeOutline._treeElementsExpandedState[this.identifier] = true;
|
|
|
|
// If there are no children, return. We will be expanded once we have children.
|
|
if (!this.hasChildren)
|
|
return;
|
|
|
|
if (this.treeOutline && (!this._childrenListNode || this._shouldRefreshChildren)) {
|
|
if (this._childrenListNode && this._childrenListNode.parentNode)
|
|
this._childrenListNode.parentNode.removeChild(this._childrenListNode);
|
|
|
|
this._childrenListNode = this.treeOutline._childrenListNode.ownerDocument.createElement("ol");
|
|
this._childrenListNode.parentTreeElement = this;
|
|
this._childrenListNode.classList.add("children");
|
|
this._childrenListNode.hidden = this.hidden;
|
|
this._childrenListNode.role = "group";
|
|
|
|
this.onpopulate();
|
|
|
|
// It is necessary to set expanded to true again here because some subclasses will call
|
|
// collapse in onpopulate (via removeChildren), which sets it back to false.
|
|
this.expanded = true;
|
|
|
|
for (var i = 0; i < this.children.length; ++i)
|
|
this.children[i]._attach();
|
|
|
|
this._shouldRefreshChildren = false;
|
|
}
|
|
|
|
if (this._listItemNode) {
|
|
this._listItemNode.classList.add("expanded");
|
|
this._listItemNode.ariaExpanded = true;
|
|
|
|
if (this._childrenListNode && this._childrenListNode.parentNode !== this._listItemNode.parentNode)
|
|
this.parent._childrenListNode.insertBefore(this._childrenListNode, this._listItemNode.nextSibling);
|
|
}
|
|
|
|
if (this._childrenListNode) {
|
|
this._childrenListNode.classList.add("expanded");
|
|
this._childrenListNode.ariaExpanded = true;
|
|
}
|
|
|
|
if (this.onexpand)
|
|
this.onexpand(this);
|
|
|
|
if (this.treeOutline) {
|
|
if (this.treeOutline.virtualized)
|
|
this.treeOutline.updateVirtualizedElementsDebouncer.delayForFrame();
|
|
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementDisclosureDidChanged, {element: this});
|
|
}
|
|
}
|
|
|
|
expandRecursively(maxDepth)
|
|
{
|
|
var item = this;
|
|
var info = {};
|
|
var depth = 0;
|
|
|
|
// The Inspector uses TreeOutlines to represents object properties, so recursive expansion
|
|
// in some case can be infinite, since JavaScript objects can hold circular references.
|
|
// So default to a recursion cap of 3 levels, since that gives fairly good results.
|
|
if (maxDepth === undefined)
|
|
maxDepth = 3;
|
|
|
|
while (item) {
|
|
if (depth < maxDepth)
|
|
item.expand();
|
|
item = item.traverseNextTreeElement(false, this, depth >= maxDepth, info);
|
|
depth += info.depthChange;
|
|
}
|
|
}
|
|
|
|
hasAncestor(ancestor)
|
|
{
|
|
if (!ancestor)
|
|
return false;
|
|
|
|
var currentNode = this.parent;
|
|
while (currentNode) {
|
|
if (ancestor === currentNode)
|
|
return true;
|
|
currentNode = currentNode.parent;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
reveal({skipExpandingAncestors} = {})
|
|
{
|
|
if (!skipExpandingAncestors) {
|
|
let currentAncestor = this.parent;
|
|
while (currentAncestor && !currentAncestor.root) {
|
|
if (!currentAncestor.expanded)
|
|
currentAncestor.expand();
|
|
currentAncestor = currentAncestor.parent;
|
|
}
|
|
}
|
|
|
|
// This must be called before onreveal, as some subclasses will scrollIntoViewIfNeeded and
|
|
// we should update the visible elements before attempting to scroll.
|
|
if (this.treeOutline && this.treeOutline.virtualized)
|
|
this.treeOutline.updateVirtualizedElementsDebouncer.force(this);
|
|
|
|
if (this.onreveal)
|
|
this.onreveal(this);
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRevealed, {element: this});
|
|
}
|
|
|
|
revealed(ignoreHidden)
|
|
{
|
|
if (!ignoreHidden && this.hidden)
|
|
return false;
|
|
|
|
var currentAncestor = this.parent;
|
|
while (currentAncestor && !currentAncestor.root) {
|
|
if (!currentAncestor.expanded)
|
|
return false;
|
|
if (!ignoreHidden && currentAncestor.hidden)
|
|
return false;
|
|
currentAncestor = currentAncestor.parent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
select(omitFocus, selectedByUser, suppressNotification)
|
|
{
|
|
let treeOutline = this.treeOutline;
|
|
if (!treeOutline || !this.selectable)
|
|
return;
|
|
|
|
if (!omitFocus)
|
|
this.focus();
|
|
else if (treeOutline.element.contains(document.activeElement)) {
|
|
// When treeOutline has focus, focus on the newly selected treeElement.
|
|
this.focus();
|
|
}
|
|
|
|
if (this.selected && !this.treeOutline.allowsRepeatSelection)
|
|
return;
|
|
|
|
// Focusing on another node may detach "this" from tree.
|
|
treeOutline = this.treeOutline;
|
|
if (!treeOutline)
|
|
return;
|
|
|
|
this.selected = true;
|
|
treeOutline.selectTreeElementInternal(this, suppressNotification, selectedByUser);
|
|
|
|
if (this._listItemNode)
|
|
this._listItemNode.ariaSelected = true;
|
|
}
|
|
|
|
revealAndSelect(omitFocus, selectedByUser, suppressNotification)
|
|
{
|
|
this.reveal();
|
|
this.select(omitFocus, selectedByUser, suppressNotification);
|
|
}
|
|
|
|
deselect(suppressNotification)
|
|
{
|
|
if (!this.treeOutline || !this.selected)
|
|
return false;
|
|
|
|
this.selected = false;
|
|
this.treeOutline.selectTreeElementInternal(null, suppressNotification);
|
|
|
|
if (this._listItemNode) {
|
|
this.unfocus();
|
|
this._listItemNode.ariaSelected = false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
focus()
|
|
{
|
|
if (!this._listItemNode)
|
|
return;
|
|
|
|
this._listItemNode.tabIndex = 0;
|
|
this._listItemNode.focus();
|
|
}
|
|
|
|
unfocus()
|
|
{
|
|
if (!this._listItemNode)
|
|
return;
|
|
|
|
this._listItemNode.removeAttribute("tabIndex");
|
|
}
|
|
|
|
onpopulate()
|
|
{
|
|
// Overridden by subclasses.
|
|
}
|
|
|
|
traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate, info)
|
|
{
|
|
function shouldSkip(element) {
|
|
return skipUnrevealed && !element.revealed(true);
|
|
}
|
|
|
|
var depthChange = 0;
|
|
var element = this;
|
|
|
|
if (!dontPopulate)
|
|
element.onpopulate();
|
|
|
|
do {
|
|
if (element.hasChildren && element.children[0] && (!skipUnrevealed || element.expanded)) {
|
|
element = element.children[0];
|
|
depthChange += 1;
|
|
} else {
|
|
while (element && !element.nextSibling && element.parent && !element.parent.root && element.parent !== stayWithin) {
|
|
element = element.parent;
|
|
depthChange -= 1;
|
|
}
|
|
|
|
if (element)
|
|
element = element.nextSibling;
|
|
}
|
|
} while (element && shouldSkip(element));
|
|
|
|
if (info)
|
|
info.depthChange = depthChange;
|
|
|
|
return element;
|
|
}
|
|
|
|
traversePreviousTreeElement(skipUnrevealed, dontPopulate)
|
|
{
|
|
function shouldSkip(element) {
|
|
return skipUnrevealed && !element.revealed(true);
|
|
}
|
|
|
|
var element = this;
|
|
|
|
do {
|
|
if (element.previousSibling) {
|
|
element = element.previousSibling;
|
|
|
|
while (element && element.hasChildren && element.expanded && !shouldSkip(element)) {
|
|
if (!dontPopulate)
|
|
element.onpopulate();
|
|
element = element.children.lastValue;
|
|
}
|
|
} else
|
|
element = element.parent && element.parent.root ? null : element.parent;
|
|
} while (element && shouldSkip(element));
|
|
|
|
return element;
|
|
}
|
|
|
|
isEventWithinDisclosureTriangle(event)
|
|
{
|
|
if (!document.contains(this._listItemNode))
|
|
return false;
|
|
|
|
// FIXME: We should not use getComputedStyle(). For that we need to get rid of using ::before for disclosure triangle. (http://webk.it/74446)
|
|
let computedStyle = window.getComputedStyle(this._listItemNode);
|
|
let start = 0;
|
|
if (computedStyle.direction === WI.LayoutDirection.RTL)
|
|
start += this._listItemNode.totalOffsetRight - this._listItemNode.getComputedCSSPropertyNumberValue("padding-right") - this.arrowToggleWidth;
|
|
else
|
|
start += this._listItemNode.totalOffsetLeft + this._listItemNode.getComputedCSSPropertyNumberValue("padding-left");
|
|
|
|
return event.pageX >= start && event.pageX <= start + this.arrowToggleWidth && this.hasChildren;
|
|
}
|
|
|
|
populateContextMenu(contextMenu, event)
|
|
{
|
|
if (this.children.some((child) => child.hasChildren) || (this.hasChildren && !this.children.length)) {
|
|
contextMenu.appendSeparator();
|
|
|
|
contextMenu.appendItem(WI.UIString("Expand All"), this.expandRecursively.bind(this));
|
|
contextMenu.appendItem(WI.UIString("Collapse All"), this.collapseRecursively.bind(this));
|
|
}
|
|
}
|
|
};
|
|
|
|
WI.TreeElement.Event = {
|
|
DoubleClick: "tree-element-double-click",
|
|
};
|
|
|
|
/* Views/TreeOutline.js */
|
|
|
|
/*
|
|
* Copyright (C) 2007-2020 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
* 3. Neither the name of Apple Inc. ("Apple") nor the names of
|
|
* its contributors may be used to endorse or promote products derived
|
|
* from this software without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
|
|
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
|
|
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
|
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.TreeOutline = class TreeOutline extends WI.Object
|
|
{
|
|
constructor(selectable = true)
|
|
{
|
|
super();
|
|
|
|
this.element = document.createElement("ol");
|
|
this.element.classList.add(WI.TreeOutline.ElementStyleClassName);
|
|
this.element.role = "tree";
|
|
this.element.addEventListener("contextmenu", this._handleContextmenu.bind(this));
|
|
|
|
this.children = [];
|
|
this._childrenListNode = this.element;
|
|
this._childrenListNode.removeChildren();
|
|
this._knownTreeElements = [];
|
|
this._treeElementsExpandedState = [];
|
|
this.allowsRepeatSelection = false;
|
|
this.root = true;
|
|
this.hasChildren = false;
|
|
this.expanded = true;
|
|
this.selected = false;
|
|
this.treeOutline = this;
|
|
this._hidden = false;
|
|
this._compact = false;
|
|
this._large = false;
|
|
this._disclosureButtons = true;
|
|
this._customIndent = false;
|
|
this._selectable = selectable;
|
|
|
|
this._cachedNumberOfDescendants = 0;
|
|
|
|
let itemForRepresentedObject = this.getCachedTreeElement.bind(this);
|
|
let selectionComparator = WI.SelectionController.createTreeComparator(itemForRepresentedObject);
|
|
this._selectionController = new WI.SelectionController(this, selectionComparator);
|
|
|
|
this._itemWasSelectedByUser = false;
|
|
this._processingSelectionChange = false;
|
|
this._suppressNextSelectionDidChangeEvent = false;
|
|
|
|
this._virtualizedDebouncer = null;
|
|
this._virtualizedVisibleTreeElements = null;
|
|
this._virtualizedAttachedTreeElements = null;
|
|
this._virtualizedScrollContainer = null;
|
|
this._virtualizedTreeItemHeight = NaN;
|
|
this._virtualizedTopSpacer = null;
|
|
this._virtualizedBottomSpacer = null;
|
|
|
|
this._childrenListNode.tabIndex = 0;
|
|
this._childrenListNode.addEventListener("keydown", this._treeKeyDown.bind(this), true);
|
|
this._childrenListNode.addEventListener("mousedown", this._handleMouseDown.bind(this));
|
|
|
|
WI.TreeOutline._generateStyleRulesIfNeeded();
|
|
|
|
if (!this._selectable)
|
|
this.element.classList.add("non-selectable");
|
|
}
|
|
|
|
// Public
|
|
|
|
get allowsEmptySelection()
|
|
{
|
|
return this._selectionController.allowsEmptySelection;
|
|
}
|
|
|
|
set allowsEmptySelection(flag)
|
|
{
|
|
this._selectionController.allowsEmptySelection = flag;
|
|
}
|
|
|
|
get allowsMultipleSelection()
|
|
{
|
|
return this._selectionController.allowsMultipleSelection;
|
|
}
|
|
|
|
set allowsMultipleSelection(flag)
|
|
{
|
|
this._selectionController.allowsMultipleSelection = flag;
|
|
}
|
|
|
|
get selectedTreeElement()
|
|
{
|
|
return this.getCachedTreeElement(this._selectionController.lastSelectedItem);
|
|
}
|
|
|
|
set selectedTreeElement(treeElement)
|
|
{
|
|
if (treeElement)
|
|
this._selectionController.selectItem(this.objectForSelection(treeElement));
|
|
else
|
|
this._selectionController.deselectAll();
|
|
}
|
|
|
|
get selectedTreeElements()
|
|
{
|
|
if (this.allowsMultipleSelection) {
|
|
let treeElements = [];
|
|
for (let representedObject of this._selectionController.selectedItems)
|
|
treeElements.push(this.getCachedTreeElement(representedObject));
|
|
return treeElements;
|
|
}
|
|
|
|
let selectedTreeElement = this.selectedTreeElement;
|
|
if (selectedTreeElement)
|
|
return [selectedTreeElement];
|
|
|
|
return [];
|
|
}
|
|
|
|
get processingSelectionChange() { return this._processingSelectionChange; }
|
|
|
|
get hidden()
|
|
{
|
|
return this._hidden;
|
|
}
|
|
|
|
set hidden(x)
|
|
{
|
|
if (this._hidden === x)
|
|
return;
|
|
|
|
this._hidden = x;
|
|
this.element.hidden = this._hidden;
|
|
}
|
|
|
|
get compact()
|
|
{
|
|
return this._compact;
|
|
}
|
|
|
|
set compact(x)
|
|
{
|
|
if (this._compact === x)
|
|
return;
|
|
|
|
this._compact = x;
|
|
if (this._compact)
|
|
this.large = false;
|
|
|
|
this.element.classList.toggle("compact", this._compact);
|
|
}
|
|
|
|
get large()
|
|
{
|
|
return this._large;
|
|
}
|
|
|
|
set large(x)
|
|
{
|
|
if (this._large === x)
|
|
return;
|
|
|
|
this._large = x;
|
|
if (this._large)
|
|
this.compact = false;
|
|
|
|
this.element.classList.toggle("large", this._large);
|
|
}
|
|
|
|
get disclosureButtons()
|
|
{
|
|
return this._disclosureButtons;
|
|
}
|
|
|
|
set disclosureButtons(x)
|
|
{
|
|
if (this._disclosureButtons === x)
|
|
return;
|
|
|
|
this._disclosureButtons = x;
|
|
this.element.classList.toggle("hide-disclosure-buttons", !this._disclosureButtons);
|
|
}
|
|
|
|
get customIndent()
|
|
{
|
|
return this._customIndent;
|
|
}
|
|
|
|
set customIndent(x)
|
|
{
|
|
if (this._customIndent === x)
|
|
return;
|
|
|
|
this._customIndent = x;
|
|
this.element.classList.toggle(WI.TreeOutline.CustomIndentStyleClassName, this._customIndent);
|
|
}
|
|
|
|
get selectable() { return this._selectable; }
|
|
|
|
appendChild(child)
|
|
{
|
|
console.assert(child);
|
|
if (!child)
|
|
return;
|
|
|
|
var lastChild = this.children[this.children.length - 1];
|
|
if (lastChild) {
|
|
lastChild.nextSibling = child;
|
|
child.previousSibling = lastChild;
|
|
} else {
|
|
child.previousSibling = null;
|
|
child.nextSibling = null;
|
|
}
|
|
|
|
var isFirstChild = !this.children.length;
|
|
|
|
this.children.push(child);
|
|
this.hasChildren = true;
|
|
child.parent = this;
|
|
child.treeOutline = this.treeOutline;
|
|
child.treeOutline._rememberTreeElement(child);
|
|
|
|
var current = child.children[0];
|
|
while (current) {
|
|
current.treeOutline = this.treeOutline;
|
|
current.treeOutline._rememberTreeElement(current);
|
|
current = current.traverseNextTreeElement(false, child, true);
|
|
}
|
|
|
|
if (child.hasChildren && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
|
|
child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
|
|
|
|
if (this._childrenListNode)
|
|
child._attach();
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child});
|
|
|
|
if (isFirstChild && this.expanded)
|
|
this.expand();
|
|
}
|
|
|
|
insertChild(child, index)
|
|
{
|
|
console.assert(child);
|
|
if (!child)
|
|
return;
|
|
|
|
var previousChild = index > 0 ? this.children[index - 1] : null;
|
|
if (previousChild) {
|
|
previousChild.nextSibling = child;
|
|
child.previousSibling = previousChild;
|
|
} else {
|
|
child.previousSibling = null;
|
|
}
|
|
|
|
var nextChild = this.children[index];
|
|
if (nextChild) {
|
|
nextChild.previousSibling = child;
|
|
child.nextSibling = nextChild;
|
|
} else {
|
|
child.nextSibling = null;
|
|
}
|
|
|
|
var isFirstChild = !this.children.length;
|
|
|
|
this.children.splice(index, 0, child);
|
|
this.hasChildren = true;
|
|
child.parent = this;
|
|
child.treeOutline = this.treeOutline;
|
|
child.treeOutline._rememberTreeElement(child);
|
|
|
|
var current = child.children[0];
|
|
while (current) {
|
|
current.treeOutline = this.treeOutline;
|
|
current.treeOutline._rememberTreeElement(current);
|
|
current = current.traverseNextTreeElement(false, child, true);
|
|
}
|
|
|
|
if (child.expandable && child.treeOutline._treeElementsExpandedState[child.identifier] !== undefined)
|
|
child.expanded = child.treeOutline._treeElementsExpandedState[child.identifier];
|
|
|
|
if (this._childrenListNode)
|
|
child._attach();
|
|
|
|
if (this.treeOutline)
|
|
this.treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementAdded, {element: child});
|
|
|
|
if (isFirstChild && this.expanded)
|
|
this.expand();
|
|
}
|
|
|
|
removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling)
|
|
{
|
|
console.assert(childIndex >= 0 && childIndex < this.children.length);
|
|
if (childIndex < 0 || childIndex >= this.children.length)
|
|
return;
|
|
|
|
let child = this.children[childIndex];
|
|
let parent = child.parent;
|
|
|
|
let childOrDescendantWasSelected = child.deselect(suppressOnDeselect) || child.selfOrDescendant((descendant) => descendant.selected);
|
|
if (childOrDescendantWasSelected && !suppressSelectSibling) {
|
|
const omitFocus = true;
|
|
if (child.previousSibling)
|
|
child.previousSibling.select(omitFocus);
|
|
else if (child.nextSibling)
|
|
child.nextSibling.select(omitFocus);
|
|
else
|
|
parent.select(omitFocus);
|
|
}
|
|
|
|
let treeOutline = child.treeOutline;
|
|
if (treeOutline) {
|
|
treeOutline._forgetTreeElement(child);
|
|
treeOutline._forgetChildrenRecursive(child);
|
|
}
|
|
|
|
if (child.previousSibling)
|
|
child.previousSibling.nextSibling = child.nextSibling;
|
|
if (child.nextSibling)
|
|
child.nextSibling.previousSibling = child.previousSibling;
|
|
|
|
this.children.splice(childIndex, 1);
|
|
|
|
child._detach();
|
|
child.treeOutline = null;
|
|
child.parent = null;
|
|
child.nextSibling = null;
|
|
child.previousSibling = null;
|
|
|
|
if (treeOutline)
|
|
treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child});
|
|
}
|
|
|
|
removeChild(child, suppressOnDeselect, suppressSelectSibling)
|
|
{
|
|
console.assert(child);
|
|
if (!child)
|
|
return;
|
|
|
|
var childIndex = this.children.indexOf(child);
|
|
console.assert(childIndex !== -1);
|
|
if (childIndex === -1)
|
|
return;
|
|
|
|
this.removeChildAtIndex(childIndex, suppressOnDeselect, suppressSelectSibling);
|
|
|
|
if (!this.children.length) {
|
|
if (this._listItemNode)
|
|
this._listItemNode.classList.remove("parent");
|
|
this.hasChildren = false;
|
|
}
|
|
}
|
|
|
|
removeChildren(suppressOnDeselect)
|
|
{
|
|
for (let child of this.children) {
|
|
child.deselect(suppressOnDeselect);
|
|
|
|
let treeOutline = child.treeOutline;
|
|
if (treeOutline) {
|
|
treeOutline._forgetTreeElement(child);
|
|
treeOutline._forgetChildrenRecursive(child);
|
|
}
|
|
|
|
child._detach();
|
|
child.treeOutline = null;
|
|
child.parent = null;
|
|
child.nextSibling = null;
|
|
child.previousSibling = null;
|
|
|
|
if (treeOutline)
|
|
treeOutline.dispatchEventToListeners(WI.TreeOutline.Event.ElementRemoved, {element: child});
|
|
}
|
|
|
|
this.children = [];
|
|
}
|
|
|
|
_rememberTreeElement(element)
|
|
{
|
|
if (!this._knownTreeElements[element.identifier])
|
|
this._knownTreeElements[element.identifier] = [];
|
|
|
|
var elements = this._knownTreeElements[element.identifier];
|
|
if (!elements.includes(element)) {
|
|
elements.push(element);
|
|
this._cachedNumberOfDescendants++;
|
|
}
|
|
|
|
if (this.virtualized)
|
|
this._virtualizedDebouncer.delayForFrame();
|
|
}
|
|
|
|
_forgetTreeElement(element)
|
|
{
|
|
if (this.selectedTreeElement === element) {
|
|
element.deselect(true);
|
|
this.selectedTreeElement = null;
|
|
}
|
|
|
|
if (this._knownTreeElements[element.identifier]) {
|
|
if (this._knownTreeElements[element.identifier].remove(element))
|
|
this._cachedNumberOfDescendants--;
|
|
}
|
|
|
|
if (this.virtualized)
|
|
this._virtualizedDebouncer.delayForFrame();
|
|
}
|
|
|
|
_forgetChildrenRecursive(parentElement)
|
|
{
|
|
var child = parentElement.children[0];
|
|
while (child) {
|
|
this._forgetTreeElement(child);
|
|
child = child.traverseNextTreeElement(false, parentElement, true);
|
|
}
|
|
}
|
|
|
|
getCachedTreeElement(representedObject)
|
|
{
|
|
if (!representedObject)
|
|
return null;
|
|
|
|
// SelectionController requires every selectable object to be unique.
|
|
// A TreeOutline subclass where multiple TreeElements may be associated
|
|
// with one represented object can override objectForSelection, and return
|
|
// a proxy object that is associated with a single TreeElement.
|
|
if (representedObject.__proxyObjectTreeElement)
|
|
return representedObject.__proxyObjectTreeElement;
|
|
|
|
if (representedObject.__treeElementIdentifier) {
|
|
// If this representedObject has a tree element identifier, and it is a known TreeElement
|
|
// in our tree we can just return that tree element.
|
|
var elements = this._knownTreeElements[representedObject.__treeElementIdentifier];
|
|
if (elements) {
|
|
for (var i = 0; i < elements.length; ++i)
|
|
if (elements[i].representedObject === representedObject)
|
|
return elements[i];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
selfOrDescendant(predicate)
|
|
{
|
|
let treeElements = [this];
|
|
while (treeElements.length) {
|
|
let treeElement = treeElements.shift();
|
|
if (predicate(treeElement))
|
|
return treeElement;
|
|
|
|
treeElements.pushAll(treeElement.children);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
findTreeElement(representedObject, isAncestor, getParent)
|
|
{
|
|
if (!representedObject)
|
|
return null;
|
|
|
|
var cachedElement = this.getCachedTreeElement(representedObject);
|
|
if (cachedElement)
|
|
return cachedElement;
|
|
|
|
// The representedObject isn't known, so we start at the top of the tree and work down to find the first
|
|
// tree element that represents representedObject or one of its ancestors.
|
|
var item;
|
|
var found = false;
|
|
for (var i = 0; i < this.children.length; ++i) {
|
|
item = this.children[i];
|
|
if (item.representedObject === representedObject || (isAncestor && isAncestor(item.representedObject, representedObject))) {
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!found)
|
|
return null;
|
|
|
|
// Make sure the item that we found is connected to the root of the tree.
|
|
// Build up a list of representedObject's ancestors that aren't already in our tree.
|
|
var ancestors = [];
|
|
var currentObject = representedObject;
|
|
while (currentObject) {
|
|
ancestors.unshift(currentObject);
|
|
if (currentObject === item.representedObject)
|
|
break;
|
|
currentObject = getParent(currentObject);
|
|
}
|
|
|
|
// For each of those ancestors we populate them to fill in the tree.
|
|
for (var i = 0; i < ancestors.length; ++i) {
|
|
// Make sure we don't call findTreeElement with the same representedObject
|
|
// again, to prevent infinite recursion.
|
|
if (ancestors[i] === representedObject)
|
|
continue;
|
|
|
|
// FIXME: we could do something faster than findTreeElement since we will know the next
|
|
// ancestor exists in the tree.
|
|
item = this.findTreeElement(ancestors[i], isAncestor, getParent);
|
|
if (!item)
|
|
return null;
|
|
|
|
item.onpopulate();
|
|
}
|
|
|
|
return this.getCachedTreeElement(representedObject);
|
|
}
|
|
|
|
_treeElementDidChange(treeElement)
|
|
{
|
|
if (treeElement.treeOutline !== this)
|
|
return;
|
|
|
|
this.dispatchEventToListeners(WI.TreeOutline.Event.ElementDidChange, {element: treeElement});
|
|
}
|
|
|
|
treeElementFromNode(node)
|
|
{
|
|
var listNode = node.closest("ol, li");
|
|
if (listNode)
|
|
return listNode.parentTreeElement || listNode.treeElement;
|
|
return null;
|
|
}
|
|
|
|
treeElementFromPoint(x, y)
|
|
{
|
|
var node = this._childrenListNode.ownerDocument.elementFromPoint(x, y);
|
|
if (!node)
|
|
return null;
|
|
|
|
return this.treeElementFromNode(node);
|
|
}
|
|
|
|
_treeKeyDown(event)
|
|
{
|
|
if (WI.isBeingEdited(event.target))
|
|
return;
|
|
|
|
if (event.target !== this._childrenListNode && event.target.closest("." + WI.TreeOutline.ElementStyleClassName) !== this._childrenListNode)
|
|
return;
|
|
|
|
let isRTL = WI.resolveLayoutDirectionForElement(this.element) === WI.LayoutDirection.RTL;
|
|
let expandKeyIdentifier = isRTL ? "Left" : "Right";
|
|
let collapseKeyIdentifier = isRTL ? "Right" : "Left";
|
|
|
|
var handled = false;
|
|
var nextSelectedElement;
|
|
|
|
if (this.selectedTreeElement) {
|
|
if (event.keyIdentifier === collapseKeyIdentifier) {
|
|
if (this.selectedTreeElement.expanded) {
|
|
if (event.altKey)
|
|
this.selectedTreeElement.collapseRecursively();
|
|
else
|
|
this.selectedTreeElement.collapse();
|
|
handled = true;
|
|
} else if (this.selectedTreeElement.parent && !this.selectedTreeElement.parent.root) {
|
|
handled = true;
|
|
if (this.selectedTreeElement.parent.selectable) {
|
|
nextSelectedElement = this.selectedTreeElement.parent;
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.parent;
|
|
handled = nextSelectedElement ? true : false;
|
|
} else if (this.selectedTreeElement.parent)
|
|
this.selectedTreeElement.parent.collapse();
|
|
}
|
|
} else if (event.keyIdentifier === expandKeyIdentifier) {
|
|
if (!this.selectedTreeElement.revealed()) {
|
|
this.selectedTreeElement.reveal();
|
|
handled = true;
|
|
} else if (this.selectedTreeElement.expandable) {
|
|
handled = true;
|
|
if (this.selectedTreeElement.expanded) {
|
|
nextSelectedElement = this.selectedTreeElement.children[0];
|
|
while (nextSelectedElement && !nextSelectedElement.selectable)
|
|
nextSelectedElement = nextSelectedElement.nextSibling;
|
|
handled = nextSelectedElement ? true : false;
|
|
} else {
|
|
if (event.altKey)
|
|
this.selectedTreeElement.expandRecursively();
|
|
else
|
|
this.selectedTreeElement.expand();
|
|
}
|
|
}
|
|
} else if (event.keyCode === 8 /* Backspace */ || event.keyCode === 46 /* Delete */) {
|
|
for (let treeElement of this.selectedTreeElements) {
|
|
if (treeElement.ondelete && treeElement.ondelete())
|
|
handled = true;
|
|
}
|
|
if (!handled && this.treeOutline.ondelete)
|
|
handled = this.treeOutline.ondelete(this.selectedTreeElement);
|
|
} else if (isEnterKey(event)) {
|
|
if (this.selectedTreeElement.onenter)
|
|
handled = this.selectedTreeElement.onenter();
|
|
if (!handled && this.treeOutline.onenter)
|
|
handled = this.treeOutline.onenter(this.selectedTreeElement);
|
|
} else if (event.keyIdentifier === "U+0020" /* Space */) {
|
|
if (this.selectedTreeElement.onspace)
|
|
handled = this.selectedTreeElement.onspace();
|
|
if (!handled && this.treeOutline.onspace)
|
|
handled = this.treeOutline.onspace(this.selectedTreeElement);
|
|
}
|
|
}
|
|
|
|
if (!handled) {
|
|
this._itemWasSelectedByUser = true;
|
|
handled = this._selectionController.handleKeyDown(event);
|
|
this._itemWasSelectedByUser = false;
|
|
|
|
if (handled)
|
|
nextSelectedElement = this.selectedTreeElement;
|
|
}
|
|
|
|
if (nextSelectedElement) {
|
|
nextSelectedElement.reveal();
|
|
nextSelectedElement.select(false, true);
|
|
}
|
|
|
|
if (handled) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
}
|
|
|
|
expand()
|
|
{
|
|
// this is the root, do nothing
|
|
}
|
|
|
|
collapse()
|
|
{
|
|
// this is the root, do nothing
|
|
}
|
|
|
|
revealed()
|
|
{
|
|
return true;
|
|
}
|
|
|
|
reveal()
|
|
{
|
|
// this is the root, do nothing
|
|
}
|
|
|
|
select()
|
|
{
|
|
// this is the root, do nothing
|
|
}
|
|
|
|
revealAndSelect(omitFocus)
|
|
{
|
|
// this is the root, do nothing
|
|
}
|
|
|
|
selectTreeElements(treeElements)
|
|
{
|
|
if (!treeElements.length)
|
|
return;
|
|
|
|
if (treeElements.length === 1) {
|
|
this.selectedTreeElement = treeElements[0];
|
|
return;
|
|
}
|
|
|
|
console.assert(this.allowsMultipleSelection, "Cannot select TreeElements with multiple selection disabled.");
|
|
if (!this.allowsMultipleSelection)
|
|
return;
|
|
|
|
let selectableObjects = treeElements.map((treeElement) => this.objectForSelection(treeElement));
|
|
this._selectionController.selectItems(new Set(selectableObjects));
|
|
}
|
|
|
|
get virtualized()
|
|
{
|
|
return this._virtualizedScrollContainer && !isNaN(this._virtualizedTreeItemHeight);
|
|
}
|
|
|
|
registerScrollVirtualizer(scrollContainer, treeItemHeight)
|
|
{
|
|
console.assert(scrollContainer);
|
|
console.assert(!isNaN(treeItemHeight));
|
|
console.assert(!this.virtualized);
|
|
|
|
let boundUpdateVirtualizedElements = (focusedTreeElement) => {
|
|
this._updateVirtualizedElements(focusedTreeElement);
|
|
};
|
|
|
|
this._virtualizedDebouncer = new Debouncer(boundUpdateVirtualizedElements);
|
|
this._virtualizedVisibleTreeElements = new Set;
|
|
this._virtualizedAttachedTreeElements = new Set;
|
|
this._virtualizedScrollContainer = scrollContainer;
|
|
this._virtualizedTreeItemHeight = treeItemHeight;
|
|
this._virtualizedTopSpacer = document.createElement("div");
|
|
this._virtualizedBottomSpacer = document.createElement("div");
|
|
|
|
let throttler = new Throttler(boundUpdateVirtualizedElements, 1000 / 16);
|
|
this._virtualizedScrollContainer.addEventListener("scroll", (event) => {
|
|
throttler.fire();
|
|
});
|
|
|
|
this._updateVirtualizedElements();
|
|
}
|
|
|
|
get updateVirtualizedElementsDebouncer()
|
|
{
|
|
return this._virtualizedDebouncer;
|
|
}
|
|
|
|
// SelectionController delegate
|
|
|
|
selectionControllerSelectionDidChange(controller, deselectedItems, selectedItems)
|
|
{
|
|
this._processingSelectionChange = true;
|
|
|
|
for (let representedObject of deselectedItems) {
|
|
let treeElement = this.getCachedTreeElement(representedObject);
|
|
if (!treeElement)
|
|
continue;
|
|
|
|
if (treeElement.listItemElement)
|
|
treeElement.listItemElement.classList.remove("selected");
|
|
|
|
treeElement.deselect();
|
|
}
|
|
|
|
for (let representedObject of selectedItems) {
|
|
let treeElement = this.getCachedTreeElement(representedObject);
|
|
if (!treeElement)
|
|
continue;
|
|
|
|
if (treeElement.listItemElement)
|
|
treeElement.listItemElement.classList.add("selected");
|
|
|
|
const omitFocus = true;
|
|
treeElement.select(omitFocus);
|
|
}
|
|
|
|
this._dispatchSelectionDidChangeEvent();
|
|
|
|
this._processingSelectionChange = false;
|
|
}
|
|
|
|
selectionControllerFirstSelectableItem(controller)
|
|
{
|
|
let firstChild = this.children[0];
|
|
if (firstChild.selectable)
|
|
return firstChild.representedObject;
|
|
return this.selectionControllerNextSelectableItem(controller, firstChild.representedObject);
|
|
}
|
|
|
|
selectionControllerLastSelectableItem(controller)
|
|
{
|
|
let treeElement = this.children.lastValue;
|
|
while (treeElement.expanded && treeElement.children.length)
|
|
treeElement = treeElement.children.lastValue;
|
|
|
|
let item = this.objectForSelection(treeElement);
|
|
if (this.canSelectTreeElement(treeElement))
|
|
return item;
|
|
return this.selectionControllerPreviousSelectableItem(controller, item);
|
|
}
|
|
|
|
selectionControllerPreviousSelectableItem(controller, item)
|
|
{
|
|
let treeElement = this.getCachedTreeElement(item);
|
|
console.assert(treeElement, "Missing TreeElement for representedObject.", item);
|
|
if (!treeElement)
|
|
return null;
|
|
|
|
const skipUnrevealed = true;
|
|
const stayWithin = null;
|
|
const dontPopulate = true;
|
|
|
|
while (treeElement = treeElement.traversePreviousTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
|
|
if (this.canSelectTreeElement(treeElement))
|
|
return this.objectForSelection(treeElement);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
selectionControllerNextSelectableItem(controller, item)
|
|
{
|
|
let treeElement = this.getCachedTreeElement(item);
|
|
console.assert(treeElement, "Missing TreeElement for representedObject.", item);
|
|
if (!treeElement)
|
|
return null;
|
|
|
|
const skipUnrevealed = true;
|
|
const stayWithin = null;
|
|
const dontPopulate = true;
|
|
|
|
while (treeElement = treeElement.traverseNextTreeElement(skipUnrevealed, stayWithin, dontPopulate)) {
|
|
if (this.canSelectTreeElement(treeElement))
|
|
return this.objectForSelection(treeElement);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
// Protected
|
|
|
|
canSelectTreeElement(treeElement)
|
|
{
|
|
// Can be overridden by subclasses.
|
|
|
|
return treeElement.selectable;
|
|
}
|
|
|
|
objectForSelection(treeElement)
|
|
{
|
|
return treeElement.representedObject;
|
|
}
|
|
|
|
selectTreeElementInternal(treeElement, suppressNotification = false, selectedByUser = false)
|
|
{
|
|
if (this._processingSelectionChange)
|
|
return;
|
|
|
|
this._itemWasSelectedByUser = selectedByUser;
|
|
this._suppressNextSelectionDidChangeEvent = suppressNotification;
|
|
|
|
if (this.allowsRepeatSelection && this.selectedTreeElement === treeElement) {
|
|
this._dispatchSelectionDidChangeEvent();
|
|
return;
|
|
}
|
|
|
|
this.selectedTreeElement = treeElement;
|
|
}
|
|
|
|
treeElementFromEvent(event)
|
|
{
|
|
// We can't take event.pageX to be our X coordinate, since the TreeElement
|
|
// could be indented, in which case we can't rely on its DOM element to be
|
|
// under the mouse.
|
|
// We choose this X coordinate based on the knowledge that our list
|
|
// items extend at least to the trailing edge of the outer <ol> container.
|
|
// In the no-word-wrap mode the outer <ol> may be wider than the tree container
|
|
// (and partially hidden), in which case we use the edge of its container.
|
|
|
|
let scrollContainer = this.element.parentElement;
|
|
if (scrollContainer.offsetWidth > this.element.offsetWidth)
|
|
scrollContainer = this.element;
|
|
|
|
// This adjustment is useful in order to find the inner-most tree element that
|
|
// lines up horizontally with the location of the event. If the mouse event
|
|
// happened in the space preceding a nested tree element (in the leading indentated
|
|
// space) we use this adjustment to get the nested tree element and not a tree element
|
|
// from a parent / outer tree outline / tree element.
|
|
//
|
|
// NOTE: This can fail if there is floating content over the trailing edge of
|
|
// the <li> content, since the element from point could hit that.
|
|
let isRTL = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL;
|
|
let trailingEdgeOffset = isRTL ? 36 : (scrollContainer.offsetWidth - 36);
|
|
let x = scrollContainer.totalOffsetLeft + trailingEdgeOffset;
|
|
let y = event.pageY;
|
|
|
|
// Our list items have 1-pixel cracks between them vertically. We avoid
|
|
// the cracks by checking slightly above and slightly below the mouse
|
|
// and seeing if we hit the same element each time.
|
|
let elementUnderMouse = this.treeElementFromPoint(x, y);
|
|
let elementAboveMouse = this.treeElementFromPoint(x, y - 2);
|
|
let element = null;
|
|
if (elementUnderMouse === elementAboveMouse)
|
|
element = elementUnderMouse;
|
|
else
|
|
element = this.treeElementFromPoint(x, y + 2);
|
|
|
|
return element;
|
|
}
|
|
|
|
populateContextMenu(contextMenu, event, treeElement)
|
|
{
|
|
treeElement.populateContextMenu(contextMenu, event);
|
|
}
|
|
|
|
// Private
|
|
|
|
static _generateStyleRulesIfNeeded()
|
|
{
|
|
if (WI.TreeOutline._styleElement)
|
|
return;
|
|
|
|
WI.TreeOutline._styleElement = document.createElement("style");
|
|
|
|
let maximumTreeDepth = 32;
|
|
let depthPadding = 10;
|
|
|
|
let styleText = "";
|
|
let childrenSubstring = "";
|
|
for (let i = 1; i <= maximumTreeDepth; ++i) {
|
|
// Keep all the elements at the same depth once the maximum is reached.
|
|
childrenSubstring += i === maximumTreeDepth ? " .children" : " > .children";
|
|
styleText += `.${WI.TreeOutline.ElementStyleClassName}:not(.${WI.TreeOutline.CustomIndentStyleClassName})${childrenSubstring} > .item { `;
|
|
styleText += `padding-inline-start: calc(var(--tree-outline-item-padding) + ${depthPadding * i}px);`;
|
|
styleText += ` }\n`;
|
|
}
|
|
|
|
WI.TreeOutline._styleElement.textContent = styleText;
|
|
|
|
document.head.appendChild(WI.TreeOutline._styleElement);
|
|
}
|
|
|
|
_updateVirtualizedElements(focusedTreeElement)
|
|
{
|
|
console.assert(this.virtualized);
|
|
|
|
this._virtualizedDebouncer.cancel();
|
|
|
|
function walk(parent, callback, count = 0) {
|
|
let shouldReturn = false;
|
|
for (let child of parent.children) {
|
|
if (!child.revealed(false))
|
|
continue;
|
|
|
|
shouldReturn = callback(child, count);
|
|
if (shouldReturn)
|
|
break;
|
|
|
|
++count;
|
|
if (child.expanded) {
|
|
let result = walk(child, callback, count);
|
|
count = result.count;
|
|
if (result.shouldReturn)
|
|
break;
|
|
}
|
|
}
|
|
return {count, shouldReturn};
|
|
}
|
|
|
|
function calculateOffsetFromContainer(node, target) {
|
|
let top = 0;
|
|
while (node !== target) {
|
|
top += node.offsetTop;
|
|
node = node.offsetParent;
|
|
if (!node)
|
|
return 0;
|
|
}
|
|
return top;
|
|
}
|
|
|
|
let offsetFromContainer = calculateOffsetFromContainer(this._virtualizedTopSpacer.parentNode ? this._virtualizedTopSpacer : this.element, this._virtualizedScrollContainer);
|
|
let numberVisible = Math.ceil(Math.max(0, this._virtualizedScrollContainer.offsetHeight - offsetFromContainer) / this._virtualizedTreeItemHeight);
|
|
let extraRows = Math.max(numberVisible * 5, 50);
|
|
let firstItem = Math.floor((this._virtualizedScrollContainer.scrollTop - offsetFromContainer) / this._virtualizedTreeItemHeight) - extraRows;
|
|
let lastItem = firstItem + numberVisible + (extraRows * 2);
|
|
|
|
let shouldScroll = false;
|
|
if (focusedTreeElement && focusedTreeElement.revealed(false)) {
|
|
let index = walk(this, (treeElement) => treeElement === focusedTreeElement).count;
|
|
if (index < firstItem) {
|
|
firstItem = index - extraRows;
|
|
lastItem = index + numberVisible + extraRows;
|
|
} else if (index > lastItem) {
|
|
firstItem = index - numberVisible - extraRows;
|
|
lastItem = index + extraRows;
|
|
}
|
|
|
|
// Only scroll if the `focusedTreeElement` is outside the visible items, not including
|
|
// the added buffer `extraRows`.
|
|
shouldScroll = (index < firstItem + extraRows) || (index > lastItem - extraRows);
|
|
}
|
|
|
|
console.assert(firstItem < lastItem);
|
|
|
|
let visibleTreeElements = new Set;
|
|
let treeElementsToAttach = new Set;
|
|
let treeElementsToDetach = new Set;
|
|
let totalItems = walk(this, (treeElement, count) => {
|
|
if (count >= firstItem && count <= lastItem) {
|
|
treeElementsToAttach.add(treeElement);
|
|
if (count >= firstItem + extraRows && count <= lastItem - extraRows)
|
|
visibleTreeElements.add(treeElement);
|
|
} else if (treeElement._listItemNode.parentNode)
|
|
treeElementsToDetach.add(treeElement);
|
|
|
|
return false;
|
|
}).count;
|
|
|
|
// Redraw if we are about to scroll.
|
|
if (!shouldScroll) {
|
|
// Redraw if there are a different number of items to show.
|
|
if (visibleTreeElements.size === this._virtualizedVisibleTreeElements.size) {
|
|
// Redraw if all of the previously centered `WI.TreeElement` are no longer centered.
|
|
if (visibleTreeElements.intersects(this._virtualizedVisibleTreeElements)) {
|
|
// Redraw if there is a `WI.TreeElement` that should be shown that isn't attached.
|
|
if (visibleTreeElements.isSubsetOf(this._virtualizedAttachedTreeElements))
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
this._virtualizedVisibleTreeElements = visibleTreeElements;
|
|
this._virtualizedAttachedTreeElements = treeElementsToAttach;
|
|
|
|
for (let treeElement of treeElementsToDetach)
|
|
treeElement._listItemNode.remove();
|
|
|
|
for (let treeElement of treeElementsToAttach) {
|
|
treeElement.parent._childrenListNode.appendChild(treeElement._listItemNode);
|
|
if (treeElement._childrenListNode)
|
|
treeElement.parent._childrenListNode.appendChild(treeElement._childrenListNode);
|
|
}
|
|
|
|
this._virtualizedTopSpacer.style.height = (Number.constrain(firstItem, 0, totalItems) * this._virtualizedTreeItemHeight) + "px";
|
|
if (this.element.previousElementSibling !== this._virtualizedTopSpacer)
|
|
this.element.parentNode.insertBefore(this._virtualizedTopSpacer, this.element);
|
|
|
|
this._virtualizedBottomSpacer.style.height = (Number.constrain(totalItems - lastItem, 0, totalItems) * this._virtualizedTreeItemHeight) + "px";
|
|
if (this.element.nextElementSibling !== this._virtualizedBottomSpacer)
|
|
this.element.parentNode.insertBefore(this._virtualizedBottomSpacer, this.element.nextElementSibling);
|
|
|
|
if (shouldScroll)
|
|
this._virtualizedScrollContainer.scrollTop = offsetFromContainer + ((firstItem + extraRows) * this._virtualizedTreeItemHeight);
|
|
}
|
|
|
|
_handleContextmenu(event)
|
|
{
|
|
let treeElement = this.treeElementFromEvent(event);
|
|
if (!treeElement)
|
|
return;
|
|
|
|
let contextMenu = WI.ContextMenu.createFromEvent(event);
|
|
this.populateContextMenu(contextMenu, event, treeElement);
|
|
}
|
|
|
|
_handleMouseDown(event)
|
|
{
|
|
let treeElement = this.treeElementFromEvent(event);
|
|
if (!treeElement || !treeElement.selectable)
|
|
return;
|
|
|
|
if (treeElement.isEventWithinDisclosureTriangle(event)) {
|
|
event.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (!treeElement.canSelectOnMouseDown(event))
|
|
return;
|
|
|
|
if (this.allowsRepeatSelection && treeElement.selected && this._selectionController.selectedItems.size === 1) {
|
|
// Special case for dispatching a selection event for an already selected
|
|
// item in single-selection mode.
|
|
this._itemWasSelectedByUser = true;
|
|
this._dispatchSelectionDidChangeEvent();
|
|
return;
|
|
}
|
|
|
|
this._itemWasSelectedByUser = true;
|
|
this._selectionController.handleItemMouseDown(this.objectForSelection(treeElement), event);
|
|
this._itemWasSelectedByUser = false;
|
|
|
|
treeElement.focus();
|
|
}
|
|
|
|
_dispatchSelectionDidChangeEvent()
|
|
{
|
|
let selectedByUser = this._itemWasSelectedByUser;
|
|
this._itemWasSelectedByUser = false;
|
|
|
|
if (this._suppressNextSelectionDidChangeEvent) {
|
|
this._suppressNextSelectionDidChangeEvent = false;
|
|
return;
|
|
}
|
|
|
|
this.dispatchEventToListeners(WI.TreeOutline.Event.SelectionDidChange, {selectedByUser});
|
|
}
|
|
};
|
|
|
|
WI.TreeOutline._styleElement = null;
|
|
|
|
WI.TreeOutline.ElementStyleClassName = "tree-outline";
|
|
WI.TreeOutline.CustomIndentStyleClassName = "custom-indent";
|
|
|
|
WI.TreeOutline.Event = {
|
|
ElementAdded: "element-added",
|
|
ElementDidChange: "element-did-change",
|
|
ElementRemoved: "element-removed",
|
|
ElementRevealed: "element-revealed",
|
|
ElementClicked: "element-clicked",
|
|
ElementDisclosureDidChanged: "element-disclosure-did-change",
|
|
ElementVisibilityDidChange: "element-visbility-did-change",
|
|
SelectionDidChange: "selection-did-change",
|
|
};
|
|
|
|
WI.TreeOutline._knownTreeElementNextIdentifier = 1;
|
|
|
|
/* Debug/DOMManager.js */
|
|
|
|
/*
|
|
* Copyright (C) 2022 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
Object.defineProperty(WI.DOMManager.prototype, "boundNodesDebugDescription", {
|
|
get() {
|
|
let debugDescription = "";
|
|
function appendLine(line) {
|
|
debugDescription += line + "\n";
|
|
}
|
|
|
|
const singleLevelSpacePrefix = " ";
|
|
function appendChildren(parent, level) {
|
|
let spacePrefix = singleLevelSpacePrefix.repeat(level + 1);
|
|
|
|
for (let child of parent.children) {
|
|
appendLine(`${spacePrefix}id(${child.id}) displayName(${child.displayName})`);
|
|
|
|
for (let pseudoElement of child.pseudoElements().values())
|
|
appendLine(`${spacePrefix}${singleLevelSpacePrefix}id(${pseudoElement.id}) displayName(${pseudoElement.displayName})`);
|
|
|
|
if (child.children)
|
|
appendChildren(child, level + 1);
|
|
}
|
|
}
|
|
|
|
let rootNodes = Object.values(this._idToDOMNode).filter((value) => !value.parentNode);
|
|
for (let rootNode of rootNodes) {
|
|
appendLine(`<${rootNode === this._document ? "Document" : "Detached"} Root> id(${rootNode.id}) displayName(${rootNode.displayName})`);
|
|
if (rootNode.children)
|
|
appendChildren(rootNode, 0);
|
|
}
|
|
|
|
return debugDescription;
|
|
}
|
|
});/* NonMinified/DefaultAudits.js */
|
|
|
|
/*
|
|
* Copyright (C) 2021 Apple Inc. All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
|
|
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
|
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
|
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
|
|
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
|
|
* THE POSSIBILITY OF SUCH DAMAGE.
|
|
*/
|
|
|
|
WI.DefaultAudits = {};
|
|
|
|
WI.DefaultAudits.levelPass = function() {
|
|
return {level: "pass"};
|
|
};
|
|
|
|
WI.DefaultAudits.levelWarn = function() {
|
|
return {level: "warn"};
|
|
};
|
|
|
|
WI.DefaultAudits.levelFail = function() {
|
|
return {level: "fail"};
|
|
};
|
|
|
|
WI.DefaultAudits.levelError = function() {
|
|
return {level: "error"};
|
|
};
|
|
|
|
WI.DefaultAudits.levelUnsupported = function() {
|
|
return {level: "unsupported"};
|
|
};
|
|
|
|
WI.DefaultAudits.dataDOMNodes = function() {
|
|
return {level: "pass", domNodes: [document.body]};
|
|
};
|
|
|
|
WI.DefaultAudits.dataDOMAttributes = function() {
|
|
return {level: "pass", domNodes: Array.from(document.querySelectorAll("[id]")), domAttributes: ["id"]};
|
|
};
|
|
|
|
WI.DefaultAudits.dataErrors = function() {
|
|
throw Error("this error was thrown from inside the audit test code.");
|
|
};
|
|
|
|
WI.DefaultAudits.dataCustom = function() {
|
|
return {level: "pass", a: 1, b: [2], c: {key: 3}};
|
|
};
|
|
|
|
WI.DefaultAudits.getElementsByComputedRole = function() {
|
|
return {level: "pass", domNodes: WebInspectorAudit.Accessibility.getElementsByComputedRole("link"), domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.getActiveDescendant = function() {
|
|
let result = {level: "pass"};
|
|
let activeDescendant = WebInspectorAudit.Accessibility.getActiveDescendant(document.body);
|
|
if (activeDescendant)
|
|
result.domNodes = [activeDescendant];
|
|
return result;
|
|
};
|
|
|
|
WI.DefaultAudits.getChildNodes = function() {
|
|
let childNodes = WebInspectorAudit.Accessibility.getChildNodes(document.body);
|
|
return {level: "pass", domNodes: childNodes || []};
|
|
};
|
|
|
|
WI.DefaultAudits.getComputedProperties = function() {
|
|
let domAttributes = ["alt", "aria-atomic", "aria-busy", "aria-checked", "aria-current", "aria-disabled", "aria-expanded", "aria-haspopup", "aria-hidden", "aria-invalid", "aria-label", "aria-labelledby", "aria-level", "aria-live", "aria-pressed", "aria-readonly", "aria-relevant", "aria-required", "aria-selected", "role", "title"].filter((attribute) => document.body.hasAttribute(attribute));
|
|
let computedProperties = WebInspectorAudit.Accessibility.getComputedProperties(document.body);
|
|
return {level: "pass", domNodes: [document.body], domAttributes, ...(computedProperties || {})};
|
|
};
|
|
|
|
WI.DefaultAudits.getControlledNodes = function() {
|
|
let controlledNodes = WebInspectorAudit.Accessibility.getControlledNodes(document.body);
|
|
return {level: "pass", domNodes: controlledNodes || []};
|
|
};
|
|
|
|
WI.DefaultAudits.getFlowedNodes = function() {
|
|
let flowedNodes = WebInspectorAudit.Accessibility.getFlowedNodes(document.body);
|
|
return {level: "pass", domNodes: flowedNodes || []};
|
|
};
|
|
|
|
WI.DefaultAudits.getMouseEventNode = function() {
|
|
let result = {level: "pass"};
|
|
let mouseEventNode = WebInspectorAudit.Accessibility.getMouseEventNode(document.body);
|
|
if (mouseEventNode)
|
|
result.domNodes = [mouseEventNode];
|
|
return result;
|
|
};
|
|
|
|
WI.DefaultAudits.getOwnedNodes = function() {
|
|
let ownedNodes = WebInspectorAudit.Accessibility.getOwnedNodes(document.body);
|
|
return {level: "pass", domNodes: ownedNodes || []};
|
|
};
|
|
|
|
WI.DefaultAudits.getParentNode = function() {
|
|
let result = {level: "pass"};
|
|
let parentNode = WebInspectorAudit.Accessibility.getParentNode(document.body);
|
|
if (parentNode)
|
|
result.domNodes = [parentNode];
|
|
return result;
|
|
};
|
|
|
|
WI.DefaultAudits.getSelectedChildNodes = function() {
|
|
let selectedChildNodes = WebInspectorAudit.Accessibility.getSelectedChildNodes(document.body);
|
|
return {level: "pass", domNodes: selectedChildNodes || []};
|
|
};
|
|
|
|
WI.DefaultAudits.hasEventListeners = function() {
|
|
let domAttributes = Array.from(document.body.attributes).filter((attribute) => attribute.name.startsWith("on"));
|
|
return {level: "pass", domNodes: [document.body], domAttributes, hasEventListeners: WebInspectorAudit.DOM.hasEventListeners(document.body)};
|
|
};
|
|
|
|
WI.DefaultAudits.hasEventListenersClick = function() {
|
|
let domAttributes = ["onclick"].filter((attribute) => document.body.hasAttribute(attribute));
|
|
return {level: "pass", domNodes: [document.body], domAttributes, hasClickEventListener: WebInspectorAudit.DOM.hasEventListeners(document.body, "click")};
|
|
};
|
|
|
|
WI.DefaultAudits.getResources = function() {
|
|
return {level: "pass", resources: WebInspectorAudit.Resources.getResources()};
|
|
};
|
|
|
|
WI.DefaultAudits.getResourceContent = function() {
|
|
let resources = WebInspectorAudit.Resources.getResources();
|
|
let mainResource = resources.find((resource) => resource.url === window.location.href);
|
|
return {level: "pass", mainResource, resourceContent: WebInspectorAudit.Resources.getResourceContent(mainResource.id)};
|
|
};
|
|
|
|
WI.DefaultAudits.unsupported = function() {
|
|
throw Error("this test should not be supported.");
|
|
};
|
|
|
|
WI.DefaultAudits.testMenuRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
menu: ["menuitem", "menuitemcheckbox", "menuitemradio"],
|
|
menubar: ["menuitem", "menuitemcheckbox", "menuitemradio"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testGridRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
grid: ["row", "rowgroup"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForAriaLabelledBySpelling = function() {
|
|
let domNodes = Array.from(document.querySelectorAll("[aria-labeledby]"));
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-labeledby"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForMultipleBanners = function() {
|
|
let domNodes = [];
|
|
let banners = WebInspectorAudit.Accessibility.getElementsByComputedRole("banner");
|
|
if (banners.length > 1)
|
|
domNodes = banners;
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForLinkLabels = function() {
|
|
let links = WebInspectorAudit.Accessibility.getElementsByComputedRole("link");
|
|
let domNodes = links.filter((link) => {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(link);
|
|
return !properties.label && !properties.hidden;
|
|
});
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testRowGroupRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
rowgroup: ["row"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testTableRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
table: ["row", "rowgroup"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForMultipleLiveRegions = function() {
|
|
const liveRegionRoles = ["alert", "log", "status", "marquee", "timer"];
|
|
let domNodes = [];
|
|
let liveRegions = liveRegionRoles.reduce((a, b) => {
|
|
return a.concat(WebInspectorAudit.Accessibility.getElementsByComputedRole(b));
|
|
}, []);
|
|
liveRegions = liveRegions.concat(Array.from(document.querySelectorAll(`[aria-live="polite"], [aria-live="assertive"]`)));
|
|
if (liveRegions.length > 1)
|
|
domNodes = liveRegions;
|
|
return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-live"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testListBoxRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
listbox: ["option"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testImageLabels = function() {
|
|
let images = WebInspectorAudit.Accessibility.getElementsByComputedRole("img");
|
|
let domNodes = images.filter((image) => !WebInspectorAudit.Accessibility.getComputedProperties(image).label);
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title", "alt"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForAriaHiddenFalse = function() {
|
|
let domNodes = Array.from(document.querySelectorAll(`[aria-hidden="false"]`));
|
|
return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["aria-hidden"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testTreeRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
tree: ["treeitem", "group"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testRadioGroupRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
radiogroup: ["radio"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testFeedRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
feed: ["article"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testTabListRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
tablist: ["tab"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testButtonLabels = function() {
|
|
let buttons = WebInspectorAudit.Accessibility.getElementsByComputedRole("button");
|
|
let domNodes = buttons.filter((button) => !WebInspectorAudit.Accessibility.getComputedProperties(button).label);
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testRowRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
row: ["cell", "gridcell", "columnheader", "rowheader"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testListRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
list: ["listitem", "group"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "warn" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testComboBoxRoleForRequiredChildren = function() {
|
|
const relationships = {
|
|
combobox: ["textbox", "listbox", "tree", "grid", "dialog"],
|
|
};
|
|
let domNodes = [];
|
|
let visitedParents = new Set;
|
|
function hasChildWithRole(node, expectedRoles) {
|
|
let childNode = node;
|
|
if (!childNode)
|
|
return false;
|
|
|
|
if (childNode.parentNode)
|
|
visitedParents.add(childNode.parentNode);
|
|
|
|
while (childNode) {
|
|
let properties = WebInspectorAudit.Accessibility.getComputedProperties(childNode);
|
|
if (childNode.nodeType === Node.ELEMENT_NODE && properties) {
|
|
if (expectedRoles.includes(properties.role))
|
|
return true;
|
|
|
|
if (childNode.hasChildNodes() && hasChildWithRole(childNode.firstChild, expectedRoles))
|
|
return true;
|
|
}
|
|
childNode = childNode.nextSibling;
|
|
}
|
|
return false;
|
|
}
|
|
for (let role in relationships) {
|
|
for (let parentNode of WebInspectorAudit.Accessibility.getElementsByComputedRole(role)) {
|
|
if (visitedParents.has(parentNode))
|
|
continue;
|
|
|
|
if (!hasChildWithRole(parentNode.firstChild, relationships[role]))
|
|
domNodes.push(parentNode);
|
|
}
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForMultipleMainContentSections = function() {
|
|
let domNodes = [];
|
|
let mainContentElements = WebInspectorAudit.Accessibility.getElementsByComputedRole("main");
|
|
if (mainContentElements.length > 1) {
|
|
domNodes = mainContentElements;
|
|
}
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["role"]};
|
|
};
|
|
WI.DefaultAudits.testDialogsForLabels = function() {
|
|
let dialogs = WebInspectorAudit.Accessibility.getElementsByComputedRole("dialog");
|
|
let domNodes = dialogs.filter((dialog) => !WebInspectorAudit.Accessibility.getComputedProperties(dialog).label);
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-label", "aria-labelledby", "title"]};
|
|
};
|
|
|
|
WI.DefaultAudits.testForInvalidAriaHiddenValue = function() {
|
|
let domNodes = Array.from(document.querySelectorAll(`[aria-hidden]:not([aria-hidden="true"], [aria-hidden="false"])`));
|
|
return {level: domNodes.length ? "fail" : "pass", domNodes, domAttributes: ["aria-hidden"]};
|
|
};
|
|
|
|
WI.DefaultAudits.newAuditPlaceholder = function() {
|
|
let result = {
|
|
level: "pass",
|
|
};
|
|
return result;
|
|
};
|