diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..753d3c6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,14 @@ +cmake_minimum_required(VERSION 3.15.0) + +project(Ultralight-SDK + LANGUAGES C CXX + VERSION 1.4.0) + +get_filename_component(UL_SDK_PATH "${CMAKE_CURRENT_SOURCE_DIR}" REALPATH) + +list(APPEND CMAKE_MODULE_PATH ${UL_SDK_PATH}/cmake) + +set(CMAKE_INSTALL_PREFIX "${CMAKE_CURRENT_BINARY_DIR}/out" CACHE PATH "install prefix" FORCE) + +add_subdirectory(samples) +add_subdirectory(tools) \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json new file mode 100644 index 0000000..00a5574 --- /dev/null +++ b/CMakePresets.json @@ -0,0 +1,42 @@ +{ + "version": 3, + "cmakeMinimumRequired": { + "major": 3, + "minor": 15, + "patch": 0 + }, + "configurePresets": [ + { + "name": "Release", + "displayName": "Release", + "description": "Release build configuration", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/out", + "CMAKE_BUILD_TYPE": "Release" + } + }, + { + "name": "Debug", + "displayName": "Debug", + "description": "Debug build configuration", + "binaryDir": "${sourceDir}/build", + "cacheVariables": { + "CMAKE_INSTALL_PREFIX": "${sourceDir}/build/out", + "CMAKE_BUILD_TYPE": "Debug" + } + } + ], + "buildPresets": [ + { + "name": "Release", + "configurePreset": "Release", + "targets": ["install"] + }, + { + "name": "Debug", + "configurePreset": "Debug", + "targets": ["install"] + } + ] +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..406a7ae --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# Ultralight SDK + +Ultralight is a lightweight, universal web renderer for C++. + +Please visit our [website](https://ultralig.ht) for licensing information. + +## Prerequisites + +Before building the Ultralight SDK, ensure you have the following: + + * CMake (version 3.5 or later) + * A C++17 compatible compiler + +### Windows-specific Requirements + +If building on Windows without VS 2022, you'll also need: + + * VS 2022 Redistributable: https://aka.ms/vs/17/release/vc_redist.x64.exe + +## Building Samples and Tools + +The SDK includes a number of samples and tools that can be built using CMake. + +### Building via CMake + +Run the following command in the root of the Ultralight SDK directory to build using the default generator: + +``` +cmake -B build && cmake --build build --config Release && cmake --install build --config Release +``` + +Build output can be found in the `build/out` directory. + +### Building with Visual Studio Code + +The SDK includes Visual Studio Code integration for convenient development: + +1. Open the SDK folder in VS Code +2. Install the recommended extensions when prompted (C/C++ and CMake Tools) +3. Select your preferred build preset (Release or Debug) when prompted +4. Build the project using one of these methods: + - Click the "Build" button in the CMake status bar + - Run the "Build (Release)" or "Build (Debug)" task from the Terminal menu + - Press F7 to build with the default configuration + +#### Running Samples in VS Code + +After building, you can run and debug any sample: +- Open the "Run and Debug" view (Ctrl+Shift+D) +- Select the sample you want to run from the dropdown +- Press F5 to start debugging or Ctrl+F5 to run without debugging + +Each sample is pre-configured with the correct launch settings for Windows, macOS, and Linux. + +## Using the Library + +For information using the library in your application, please visit our [online docs](https://docs.ultralig.ht). + +## Useful Links + +| Link | URL | +| -------------------------- | ------------------------------------- | +| __Website__ | | +| __Join our Discord!__ | | +| __Documentation__ | | \ No newline at end of file diff --git a/VERSION.txt b/VERSION.txt new file mode 100644 index 0000000..37fedd4 --- /dev/null +++ b/VERSION.txt @@ -0,0 +1 @@ +1.4.0.1b4b800 diff --git a/bin/libAppCore.dylib b/bin/libAppCore.dylib new file mode 100644 index 0000000..8fef93c Binary files /dev/null and b/bin/libAppCore.dylib differ diff --git a/bin/libUltralight.dylib b/bin/libUltralight.dylib new file mode 100644 index 0000000..b77a66d Binary files /dev/null and b/bin/libUltralight.dylib differ diff --git a/bin/libUltralightCore.dylib b/bin/libUltralightCore.dylib new file mode 100644 index 0000000..6f69d5b Binary files /dev/null and b/bin/libUltralightCore.dylib differ diff --git a/bin/libWebCore.dylib b/bin/libWebCore.dylib new file mode 100644 index 0000000..f07bcab Binary files /dev/null and b/bin/libWebCore.dylib differ diff --git a/include/AppCore/App.h b/include/AppCore/App.h new file mode 100644 index 0000000..c49eb40 --- /dev/null +++ b/include/AppCore/App.h @@ -0,0 +1,228 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include "Defines.h" +#include +#include +#include + +namespace ultralight { + +class Monitor; +class Window; + +/// +/// Interface for all App-related events. @see App::set_listener +/// +class AExport AppListener { +public: + virtual ~AppListener() {} + + /// + /// Called whenever the App updates. You should update all app logic here. + /// + /// @note This event is fired right before the run loop calls + /// Renderer::Update and Renderer::Render. + /// + virtual void OnUpdate() {} +}; + +/// +/// App-specific settings. +/// +struct AExport Settings { + /// + /// The name of the developer of this app. + /// + /// This is used to generate a unique path to store local application data + /// on the user's machine. + /// + String developer_name = "MyCompany"; + + /// + /// The name of this app. + /// + /// This is used to generate a unique path to store local application data + /// on the user's machine. + /// + String app_name = "MyApp"; + + /// + /// The root file path for our file system. You should set this to the + /// relative path where all of your app data is. + /// + /// This will be used to resolve all file URLs, eg file:///page.html + /// + /// @note This relative path is resolved using the following logic: + /// - Windows: relative to the executable path + /// - Linux: relative to the executable path + /// - macOS: relative to YourApp.app/Contents/Resources/ + /// + String file_system_path = "./assets/"; + + /// + /// Whether or not we should load and compile shaders from the file system + /// (eg, from the /shaders/ path, relative to file_system_path). + /// + /// If this is false (the default), we will instead load pre-compiled shaders + /// from memory which speeds up application startup time. + /// + bool load_shaders_from_file_system = false; + + /// + /// We try to use the GPU renderer when a compatible GPU is detected. + /// + /// Set this to true to force the engine to always use the CPU renderer. + /// + bool force_cpu_renderer = false; +}; + +/// +/// Main application singleton (use this if you want to let the library manage window creation). +/// +/// This convenience class sets up everything you need to display web-based content in a +/// desktop application. +/// +/// The App class initializes the Platform singleton with OS-specific defaults, creates a Renderer, +/// and automatically manages window creation, run loop, input events, and painting. +/// +/// ## Creating the App +/// +/// Call App::Create() to initialize the library and create the App singleton. +/// +/// ``` +/// auto app = App::Create(); +/// ``` +/// +/// ## Creating a Window +/// +/// Call Window::Create() to create one or more windows during the lifetime of your app. +/// +/// ``` +/// auto window = Window::Create(app->main_monitor(), 1024, 768, false, +/// kWindowFlags_Titled | kWindowFlags_Resizable); +/// ``` +/// +/// ### Creating an Overlay in a Window +/// +/// Each Window can have one or more Overlay instances. Overlays are used to display web-based +/// content in a portion of the window. +/// +/// Call Overlay::Create() to create an overlay in a window. +/// +/// ``` +/// auto overlay = Overlay::Create(window, 1024, 768, 0, 0); +/// ``` +/// +/// Each Overlay has a View instance that you can use to load web content into. +/// +/// ``` +/// overlay->view()->LoadURL("https://google.com"); +/// ``` +/// +/// ## Running the App +/// +/// Call App::Run() to start the main run loop. +/// +/// ``` +/// #include +/// +/// using namespace ultralight; +/// +/// int main() { +/// // Initialize app, window, overlay, etc. here... +/// +/// app->Run(); +/// +/// return 0; +/// } +/// ``` +/// +/// ## Shutting Down the App +/// +/// Call App::Quit() to stop the main run loop and shut down the app. +/// +/// ``` +/// app->Quit(); +/// ``` + +/// @note This is optional, you can use the Renderer class directly if you want to manage your +/// own windows and run loop. +/// +class AExport App : public RefCounted { +public: + /// + /// Create the App singleton. + /// + /// @param settings Settings to customize App runtime behavior. + /// + /// @param config Config options for the Ultralight renderer. + /// + /// @return Returns a ref-pointer to the created App instance. + /// + /// @note You should only create one of these per application lifetime. + /// + /// @note Certain Config options may be overridden during App creation, + /// most commonly Config::face_winding and Config::cache_path. + /// + static RefPtr Create(Settings settings = Settings(), Config config = Config()); + + /// + /// Get the App singleton. + /// + static App* instance(); + + /// + /// Get the settings this App was created with. + /// + virtual const Settings& settings() const = 0; + + /// + /// Set an AppListener to receive callbacks for app-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_listener(AppListener* listener) = 0; + + /// + /// Get the AppListener, if any. + /// + virtual AppListener* listener() = 0; + + /// + /// Whether or not the App is running. + /// + virtual bool is_running() const = 0; + + /// + /// Get the main monitor (this is never NULL). + /// + /// @note We'll add monitor enumeration later. + /// + virtual Monitor* main_monitor() = 0; + + /// + /// Get the underlying Renderer instance. + /// + virtual RefPtr renderer() = 0; + + /// + /// Run the main loop. + /// + virtual void Run() = 0; + + /// + /// Quit the application. + /// + virtual void Quit() = 0; + +protected: + virtual ~App(); +}; + +} // namespace ultralight diff --git a/include/AppCore/AppCore.h b/include/AppCore/AppCore.h new file mode 100644 index 0000000..2636171 --- /dev/null +++ b/include/AppCore/AppCore.h @@ -0,0 +1,14 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#include +#include +#include +#include +#include +#include +#include \ No newline at end of file diff --git a/include/AppCore/CAPI.h b/include/AppCore/CAPI.h new file mode 100644 index 0000000..c7b8b2b --- /dev/null +++ b/include/AppCore/CAPI.h @@ -0,0 +1,548 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file AppCore/CAPI.h +/// +/// AppCore API interface. +/// +/// `#include ` +/// +/// AppCore is a convenient windowing system for desktop platforms built on top of the +/// Ultralight renderer. +/// +/// It automatically sets up the Renderer, creates a run loop, and handles all window creation, +/// painting, and platform-specific operations for you. +/// +/// ## Creating the App +/// +/// Call ulCreateApp() to initialize the library and create the App singleton: +/// +/// ``` +/// // Create the App using default config and settings +/// ULApp app = ulCreateApp(NULL, NULL); +/// ``` +/// +/// ## Creating a Window +/// +/// Call ulCreateWindow() to create one or more windows during the lifetime of your app: +/// +/// ``` +/// ULWindow window = ulCreateWindow(ulAppGetMainMonitor(app), 1024, 768, false, +/// kWindowFlags_Titled | kWindowFlags_Resizable); +/// ``` +/// +/// ## Creating an Overlay in a Window +/// +/// Each Window can have one or more Overlay instances. Overlays are used to display web-based +/// content in a portion of the window. +/// +/// Call ulCreateOverlay() to create an overlay in a window: +/// +/// ``` +/// ULOverlay overlay = ulCreateOverlay(window, 1024, 768, 0, 0); +/// ``` +/// +/// Each Overlay has a View instance that you can use to load web content into: +/// +/// ``` +/// ULString url = ulCreateString("https://google.com"); +/// ULView view = ulOverlayGetView(overlay); +/// ulViewLoadURL(view, url); +/// ulDestroyString(url); +/// ``` +/// +/// ## Running the App +/// +/// Call ulAppRun() to start the main run loop: +/// +/// ``` +/// #include +/// +/// int main() { +/// // Initialize app, window, overlay, etc. here... +/// +/// ulAppRun(app); +/// +/// ulDestroyApp(app); +/// return 0; +/// } +/// ``` +/// +#ifndef APPCORE_CAPI_H +#define APPCORE_CAPI_H + +#include + +#if defined(ULTRALIGHT_STATIC_BUILD) +# define ACExport +#else +# if defined(__WIN32__) || defined(_WIN32) +# if defined(APPCORE_IMPLEMENTATION) +# define ACExport __declspec(dllexport) +# else +# define ACExport __declspec(dllimport) +# endif +# else +# define ACExport __attribute__((visibility("default"))) +# endif +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct C_Settings* ULSettings; +typedef struct C_App* ULApp; +typedef struct C_Window* ULWindow; +typedef struct C_Monitor* ULMonitor; +typedef struct C_Overlay* ULOverlay; + +/// +/// Window creation flags. @see Window::Create +/// +typedef enum { + kWindowFlags_Borderless = 1 << 0, + kWindowFlags_Titled = 1 << 1, + kWindowFlags_Resizable = 1 << 2, + kWindowFlags_Maximizable = 1 << 3, + kWindowFlags_Hidden = 1 << 4, +} ULWindowFlags; + +/// +/// Create settings with default values (see ). +/// +ACExport ULSettings ulCreateSettings(); + +/// +/// Destroy settings. +/// +ACExport void ulDestroySettings(ULSettings settings); + +/// +/// Set the name of the developer of this app. +/// +/// This is used to generate a unique path to store local application data +/// on the user's machine. +/// +/// Default is "MyCompany" +/// +ACExport void ulSettingsSetDeveloperName(ULSettings settings, ULString name); + +/// +/// Set the name of this app. +/// +/// This is used to generate a unique path to store local application data +/// on the user's machine. +/// +/// Default is "MyApp" +/// +ACExport void ulSettingsSetAppName(ULSettings settings, ULString name); + +/// +/// Set the root file path for our file system, you should set this to the +/// relative path where all of your app data is. +/// +/// This will be used to resolve all file URLs, eg file:///page.html +/// +/// @note The default path is "./assets/" +/// +/// This relative path is resolved using the following logic: +/// - Windows: relative to the executable path +/// - Linux: relative to the executable path +/// - macOS: relative to YourApp.app/Contents/Resources/ +/// +ACExport void ulSettingsSetFileSystemPath(ULSettings settings, ULString path); + +/// +/// Set whether or not we should load and compile shaders from the file system +/// (eg, from the /shaders/ path, relative to file_system_path). +/// +/// If this is false (the default), we will instead load pre-compiled shaders +/// from memory which speeds up application startup time. +/// +ACExport void ulSettingsSetLoadShadersFromFileSystem(ULSettings settings, + bool enabled); + +/// +/// We try to use the GPU renderer when a compatible GPU is detected. +/// +/// Set this to true to force the engine to always use the CPU renderer. +/// +ACExport void ulSettingsSetForceCPURenderer(ULSettings settings, + bool force_cpu); +/// +/// Create the App singleton. +/// +/// @param settings Settings to customize App runtime behavior. You can pass +/// NULL for this parameter to use default settings. +/// +/// @param config Config options for the Ultralight renderer. You can pass +/// NULL for this parameter to use default config. +/// +/// @note You should only create one of these per application lifetime. +/// +/// @note Certain Config options may be overridden during App creation, +/// most commonly Config::face_winding and Config::device_scale_hint. +/// +ACExport ULApp ulCreateApp(ULSettings settings, ULConfig config); + +/// +/// Destroy the App instance. +/// +ACExport void ulDestroyApp(ULApp app); + +typedef void +(*ULUpdateCallback) (void* user_data); + +/// +/// Set a callback for whenever the App updates. You should update all app +/// logic here. +/// +/// @note This event is fired right before the run loop calls +/// Renderer::Update and Renderer::Render. +/// +ACExport void ulAppSetUpdateCallback(ULApp app, ULUpdateCallback callback, + void* user_data); + +/// +/// Whether or not the App is running. +/// +ACExport bool ulAppIsRunning(ULApp app); + +/// +/// Get the main monitor (this is never NULL). +/// +/// @note We'll add monitor enumeration later. +/// +ACExport ULMonitor ulAppGetMainMonitor(ULApp app); + +/// +/// Get the underlying Renderer instance. +/// +ACExport ULRenderer ulAppGetRenderer(ULApp app); + +/// +/// Run the main loop. +/// +ACExport void ulAppRun(ULApp app); + +/// +/// Quit the application. +/// +ACExport void ulAppQuit(ULApp app); + +/// +/// Get the monitor's DPI scale (1.0 = 100%). +/// +ACExport double ulMonitorGetScale(ULMonitor monitor); + +/// +/// Get the width of the monitor (in pixels). +/// +ACExport unsigned int ulMonitorGetWidth(ULMonitor monitor); + +/// +/// Get the height of the monitor (in pixels). +/// +ACExport unsigned int ulMonitorGetHeight(ULMonitor monitor); + +/// +/// Create a new Window. +/// +/// @param monitor The monitor to create the Window on. +/// +/// @param width The width (in screen coordinates). +/// +/// @param height The height (in screen coordinates). +/// +/// @param fullscreen Whether or not the window is fullscreen. +/// +/// @param window_flags Various window flags. +/// +ACExport ULWindow ulCreateWindow(ULMonitor monitor, unsigned int width, + unsigned int height, bool fullscreen, + unsigned int window_flags); + +/// +/// Destroy a Window. +/// +ACExport void ulDestroyWindow(ULWindow window); + +typedef void +(*ULCloseCallback) (void* user_data, ULWindow window); + +/// +/// Set a callback to be notified when a window closes. +/// +ACExport void ulWindowSetCloseCallback(ULWindow window, + ULCloseCallback callback, + void* user_data); + +typedef void +(*ULResizeCallback) (void* user_data, ULWindow window, unsigned int width, unsigned int height); + +/// +/// Set a callback to be notified when a window resizes +/// (parameters are passed back in pixels). +/// +ACExport void ulWindowSetResizeCallback(ULWindow window, + ULResizeCallback callback, + void* user_data); + +/// +/// Get window width (in screen coordinates). +/// +ACExport unsigned int ulWindowGetScreenWidth(ULWindow window); + +/// +/// Get window width (in pixels). +/// +ACExport unsigned int ulWindowGetWidth(ULWindow window); + +/// +/// Get window height (in screen coordinates). +/// +ACExport unsigned int ulWindowGetScreenHeight(ULWindow window); + +/// +/// Get window height (in pixels). +/// +ACExport unsigned int ulWindowGetHeight(ULWindow window); + +/// +/// Move the window to a new position (in screen coordinates) relative to the top-left of the +/// monitor area. +/// +ACExport void ulWindowMoveTo(ULWindow window, int x, int y); + +/// +/// Move the window to the center of the monitor. +/// +ACExport void ulWindowMoveToCenter(ULWindow); + +/// +/// Get the x-position of the window (in screen coordinates) relative to the top-left of the +/// monitor area. +/// +ACExport int ulWindowGetPositionX(ULWindow window); + +/// +/// Get the y-position of the window (in screen coordinates) relative to the top-left of the +/// monitor area. +/// +ACExport int ulWindowGetPositionY(ULWindow window); + +/// +/// Get whether or not a window is fullscreen. +/// +ACExport bool ulWindowIsFullscreen(ULWindow window); + +/// +/// Get the DPI scale of a window. +/// +ACExport double ulWindowGetScale(ULWindow window); + +/// +/// Set the window title. +/// +ACExport void ulWindowSetTitle(ULWindow window, const char* title); + +/// +/// Set the cursor for a window. +/// +ACExport void ulWindowSetCursor(ULWindow window, ULCursor cursor); + +/// +/// Show the window (if it was previously hidden). +/// +ACExport void ulWindowShow(ULWindow window); + +/// +/// Hide the window. +/// +ACExport void ulWindowHide(ULWindow window); + +/// +/// Whether or not the window is currently visible (not hidden). +/// +ACExport bool ulWindowIsVisible(ULWindow window); + +/// +/// Close a window. +/// +ACExport void ulWindowClose(ULWindow window); + +/// +/// Convert screen coordinates to pixels using the current DPI scale. +/// +ACExport int ulWindowScreenToPixels(ULWindow window, int val); + +/// +/// Convert pixels to screen coordinates using the current DPI scale. +/// +ACExport int ulWindowPixelsToScreen(ULWindow window, int val); + +/// +/// Get the underlying native window handle. +/// +/// @note This is: - HWND on Windows +/// - NSWindow* on macOS +/// - GLFWwindow* on Linux +/// +ACExport void* ulWindowGetNativeHandle(ULWindow window); + +/// +/// Create a new Overlay. +/// +/// @param window The window to create the Overlay in. +/// +/// @param width The width in pixels. +/// +/// @param height The height in pixels. +/// +/// @param x The x-position (offset from the left of the Window), in +/// pixels. +/// +/// @param y The y-position (offset from the top of the Window), in +/// pixels. +/// +/// @note Each Overlay is essentially a View and an on-screen quad. You should +/// create the Overlay then load content into the underlying View. +/// +ACExport ULOverlay ulCreateOverlay(ULWindow window, unsigned int width, + unsigned int height, int x, int y); + +/// +/// Create a new Overlay, wrapping an existing View. +/// +/// @param window The window to create the Overlay in. (we currently only +/// support one window per application) +/// +/// @param view The View to wrap (will use its width and height). +/// +/// @param x The x-position (offset from the left of the Window), in +/// pixels. +/// +/// @param y The y-position (offset from the top of the Window), in +/// pixels. +/// +/// @note Each Overlay is essentially a View and an on-screen quad. You should +/// create the Overlay then load content into the underlying View. +/// +ACExport ULOverlay ulCreateOverlayWithView(ULWindow window, ULView view, + int x, int y); + +/// +/// Destroy an overlay. +/// +ACExport void ulDestroyOverlay(ULOverlay overlay); + +/// +/// Get the underlying View. +/// +ACExport ULView ulOverlayGetView(ULOverlay overlay); + +/// +/// Get the width (in pixels). +/// +ACExport unsigned int ulOverlayGetWidth(ULOverlay overlay); + +/// +/// Get the height (in pixels). +/// +ACExport unsigned int ulOverlayGetHeight(ULOverlay overlay); + +/// +/// Get the x-position (offset from the left of the Window), in pixels. +/// +ACExport int ulOverlayGetX(ULOverlay overlay); + +/// +/// Get the y-position (offset from the top of the Window), in pixels. +/// +ACExport int ulOverlayGetY(ULOverlay overlay); + +/// +/// Move the overlay to a new position (in pixels). +/// +ACExport void ulOverlayMoveTo(ULOverlay overlay, int x, int y); + +/// +/// Resize the overlay (and underlying View), dimensions should be +/// specified in pixels. +/// +ACExport void ulOverlayResize(ULOverlay overlay, unsigned int width, + unsigned int height); + +/// +/// Whether or not the overlay is hidden (not drawn). +/// +ACExport bool ulOverlayIsHidden(ULOverlay overlay); + +/// +/// Hide the overlay (will no longer be drawn). +/// +ACExport void ulOverlayHide(ULOverlay overlay); + +/// +/// Show the overlay. +/// +ACExport void ulOverlayShow(ULOverlay overlay); + +/// +/// Whether or not an overlay has keyboard focus. +/// +ACExport bool ulOverlayHasFocus(ULOverlay overlay); + +/// +/// Grant this overlay exclusive keyboard focus. +/// +ACExport void ulOverlayFocus(ULOverlay overlay); + +/// +/// Remove keyboard focus. +/// +ACExport void ulOverlayUnfocus(ULOverlay overlay); + +/****************************************************************************** + * Platform + *****************************************************************************/ + +/// +/// This is only needed if you are not calling ulCreateApp(). +/// +/// Initializes the platform font loader and sets it as the current FontLoader. +/// +ACExport void ulEnablePlatformFontLoader(); + +/// +/// This is only needed if you are not calling ulCreateApp(). +/// +/// Initializes the platform file system (needed for loading file:/// URLs) and +/// sets it as the current FileSystem. +/// +/// You can specify a base directory path to resolve relative paths against. +/// +ACExport void ulEnablePlatformFileSystem(ULString base_dir); + +/// +/// This is only needed if you are not calling ulCreateApp(). +/// +/// Initializes the default logger (writes the log to a file). +/// +/// You should specify a writable log path to write the log to +/// for example "./ultralight.log". +/// +ACExport void ulEnableDefaultLogger(ULString log_path); + +#ifdef __cplusplus +} +#endif + +#endif // APPCORE_CAPI_H diff --git a/include/AppCore/Defines.h b/include/AppCore/Defines.h new file mode 100644 index 0000000..a6d27fd --- /dev/null +++ b/include/AppCore/Defines.h @@ -0,0 +1,55 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once + +// Needed for limit defines, like INTMAX_MAX, which is used by the std C++ library +#ifndef __STDC_LIMIT_MACROS +#define __STDC_LIMIT_MACROS +#endif + +#include +#include +#include + +#ifdef SWIG +#define AExport +#else + +// Require C++11 Support +#if defined(_MSC_VER) +# if _MSC_VER < 1800 +# error This project needs at least Visual Studio 2013 to build +# endif +#elif __cplusplus <= 199711L +# error This project can only be compiled with a compiler that supports C++11 +#endif + +#if defined(ULTRALIGHT_STATIC_BUILD) +# define AExport +#else +# if defined(__WIN32__) || defined(_WIN32) +# if defined(APPCORE_IMPLEMENTATION) +# define AExport __declspec(dllexport) +# else +# define AExport __declspec(dllimport) +# endif +# else +# define AExport __attribute__((visibility("default"))) +# endif +#endif + +#if defined(__WIN32__) || defined(_WIN32) +# define _thread_local __declspec(thread) +# ifndef _NATIVE_WCHAR_T_DEFINED +# define DISABLE_NATIVE_WCHAR_T +# endif +#else +# define _thread_local __thread +#endif + +#endif diff --git a/include/AppCore/Dialogs.h b/include/AppCore/Dialogs.h new file mode 100644 index 0000000..283ef94 --- /dev/null +++ b/include/AppCore/Dialogs.h @@ -0,0 +1,59 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +#pragma once + +#include "Defines.h" +#include + +namespace ultralight { + +/// +/// Enum representing the icon to be displayed in a dialog. +/// +enum class DialogIcon { + Info, ///< Information icon + Warning, ///< Warning icon + Error, ///< Error icon + Question ///< Question icon +}; + +/// +/// Enum representing the button types in a dialog. +/// +enum class ButtonType { + OK, ///< Single "OK" button + OKCancel, ///< "OK" and "Cancel" buttons + YesNo ///< "Yes" and "No" buttons +}; + +/// +/// Enum representing the result of a dialog button press. +/// +enum class ButtonResult { + OK, ///< "OK" button was pressed + Cancel, ///< "Cancel" button was pressed + Yes, ///< "Yes" button was pressed + No ///< "No" button was pressed +}; + +/// +/// Shows a modal message box with the specified title, message, icon, and buttons. +/// +/// @param title The title of the message box. +/// @param message The message to display in the message box. +/// @param icon The icon to display in the message box (default: DialogIcon::Info). +/// @param buttons The button type to display in the message box (default: ButtonType::OK). +/// +/// @return The result of the button press. +/// +AExport ButtonResult ShowMessageBox(const String& title, const String& message, + DialogIcon icon = DialogIcon::Info, + ButtonType buttons = ButtonType::OK); + +} // namespace ultralight \ No newline at end of file diff --git a/include/AppCore/JSHelpers.h b/include/AppCore/JSHelpers.h new file mode 100644 index 0000000..2a50c93 --- /dev/null +++ b/include/AppCore/JSHelpers.h @@ -0,0 +1,560 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace ultralight { + +/// +/// Set the current JSContext. +/// +/// Most JavaScriptCore C API calls require an active JavaScript execution +/// context (JSContextRef). You can get the JSContextRef for a page via +/// `View::LockJSContext()`. This context changes with each page navigation. +/// +/// @note You MUST set a JSContext before using most of the C++ API below. +/// +void AExport SetJSContext(JSContextRef ctx); + +/// +/// Get the current JSContext. +/// +JSContextRef AExport GetJSContext(); + +/// +/// JavaScript String wrapper that automatically manages JSStringRef lifetime +/// and provides helpful conversions. +/// +class AExport JSString { +public: + /// Create empty string + JSString(); + + /// Create from C-string + JSString(const char* str); + + /// Create from Ultralight String + JSString(const String& str); + + /// Create from existing JSStringRef + JSString(JSStringRef str); + + /// Copy constructor + JSString(const JSString& other); + + /// Destructor + ~JSString(); + + /// Assignment operator + JSString& operator=(const JSString& other); + + /// Cast to String + operator String(); + + /// Cast to underlying JSStringRef + operator JSStringRef() const { return instance_; } + +protected: + JSStringRef instance_; +}; + +class JSArray; +class JSObject; +class JSFunction; + +/// Tag type used with the JSValue constructor to create "Null" types +struct AExport JSValueNullTag {}; + +/// Tag type used with the JSValue constructor to create "Undefined" types +struct AExport JSValueUndefinedTag {}; + +/// +/// JavaScript variant value wrapper that automatically manages JSValueRef +/// lifetime and provides helpful conversions. +/// +class AExport JSValue { +public: + /// Create null (empty) JSValue + JSValue(); + + /// Create null JSValue explicitly + JSValue(JSValueNullTag); + + /// Create undefined JSValue + JSValue(JSValueUndefinedTag); + + /// Create boolean JSValue + JSValue(bool val); + + /// Create unsigned integer JSValue (aka, Number) [will be cast to double] + JSValue(uint32_t val); + + /// Create integer JSValue (aka, Number) [will be cast to double] + JSValue(int32_t val); + + /// Create unsigned integer JSValue (aka, Number) [will be cast to double] + JSValue(uint64_t val); + + /// Create integer JSValue (aka, Number) [will be cast to double] + JSValue(int64_t val); + + /// Create double JSValue (aka, Number) + JSValue(double val); + + /// Create string JSValue + JSValue(const char* val); + + /// Create string JSValue + JSValue(const String& val); + + /// Create string JSValue + JSValue(JSString val); + + /// Create from existing JSValueRef + JSValue(JSValueRef val); + + /// Create object JSValue + JSValue(JSObjectRef obj); + + /// Copy constructor, a shallow copy is made, the constructed JSValue will + /// point to the same JSValueRef. + JSValue(const JSValue& other); + + /// Destructor + virtual ~JSValue(); + + /// A shallow copy is made, this JSValue will point to the same JSValueRef + virtual JSValue& operator=(const JSValue& other); + + /// Whether or not the value is a JavaScript Null type. + bool IsNull() const; + + /// Whether or not the value is a JavaScript Undefined type. + bool IsUndefined() const; + + /// Whether or not the value is a JavaScript Boolean type. + bool IsBoolean() const; + + /// Whether or not the value is a JavaScript Number type. + bool IsNumber() const; + + /// Whether or not the value is a JavaScript String type. + bool IsString() const; + + /// Whether or not the value is a JavaScript Object type. + bool IsObject() const; + + /// Whether or not the value is a JavaScript Array type. + bool IsArray() const; + + /// Whether or not the value is a JavaScript Function type. + bool IsFunction() const; + + /// Get the value as a Boolean + bool ToBoolean() const; + + /// Get the value as a Number (Double) + double ToNumber() const; + + /// Get the value as a Number (Integer) + int64_t ToInteger() const { return static_cast(ToNumber()); } + + /// Get the value as a String + JSString ToString() const; + + /// Get the value as an Object (will debug assert if not an Object) + JSObject ToObject() const; + + /// Get the value as an Array (will debug asset if not an Array) + JSArray ToArray() const; + + /// Get the value as a Function (will debug asset if not a Function) + JSFunction ToFunction() const; + + operator bool() const { return ToBoolean(); } + + operator double() const { return ToNumber(); } + + operator uint32_t() const { return static_cast(ToNumber()); } + + operator int32_t() const { return static_cast(ToNumber()); } + + operator uint64_t() const { return static_cast(ToNumber()); } + + operator int64_t() const { return ToInteger(); } + + operator String() const { return ToString(); } + + operator JSString() const { return ToString(); } + + operator JSObject() const; + + operator JSObjectRef() const; + + operator JSArray() const; + + operator JSFunction() const; + + /// Get the underlying JSValueRef + operator JSValueRef() const { return instance(); } + + /// + /// Get the bound context for this JSValue (it is cached at creation). + /// + JSContextRef context() const { return ctx_; } + + /// + /// Set the JSContext for this JSValue. + /// + /// @note + /// JSValues created from within a JSCallback have a temporary JSContext + /// that is destroyed when the callback returns. You will need to "move" + /// any JSValues created within these callbacks to the View's main context + /// (call set_context() with the main context) before using them outside + /// the callback. + /// + void set_context(JSContextRef context) { ctx_ = context; } + +protected: + JSValue(JSContextRef ctx); + JSValue(JSContextRef ctx, JSValueRef val); + virtual JSValueRef instance() const; + + JSContextRef ctx_; + JSValueRef instance_ = nullptr; + friend class JSFunction; +}; + +/// +/// A vector of JSValues, used for passing around arguments in JSCallback. +/// +class AExport JSArgs { +public: + /// Create an empty list of JavaScript arguments + JSArgs(); + + /// Create a list of JavaScript arguments using a C++ initializer list + JSArgs(const std::initializer_list& values); + + /// Copy-constructor + JSArgs(const JSArgs& other); + + /// Destructor + ~JSArgs(); + + /// Assignment operator + JSArgs& operator=(const JSArgs& other); + + /// + /// Access an element of the argument list by index. + /// + /// @note + /// All JSValues are actually wrappers of JSValueRef instances so even + /// though this function doesn't return a JSValue& you are still operating + /// directly on the underlying JavaScript value instance. + /// + JSValue operator[](size_t pos); + + /// + /// Access an element of the argument list by index. (const overload) + /// + /// @note + /// All JSValues are actually wrappers of JSValueRef instances so even + /// though this function doesn't return a JSValue& you are still operating + /// directly on the underlying JavaScript value instance. + /// + const JSValue operator[](size_t pos) const; + + /// Whether or not the argument list is empty. + bool empty() const; + + /// The number of elements in the argument list. + size_t size() const; + + /// Clear the argument list. + void clear(); + + /// Add a new argument to the end of the list. + void push_back(const JSValue& val); + + /// Remove the last item from the end of the list. + void pop_back(); + + /// Get the argument list as a C-array of JSValues + JSValue* data(); + + /// Get the argument list as a C-array of JSValues (const overload) + const JSValue* data() const; +protected: + void* instance_; +}; + +/// +/// JSCallback typedef used for binding C++ callbacks to JavaScript functions. +/// +/// Takes two arguments (const JSObject& thisObj, const JSArgs& args) and +/// returns nothing (void). +/// +typedef std::function JSCallback; + +/// +/// JSCallbackWithRetval typedef used for binding C++ callbacks to JavaScript +/// functions with an optional return value. +/// +/// Takes two arguments (const JSObject& thisObj, const JSArgs& args) and +/// returns a JSValue back to JavaScript. +/// +typedef std::function JSCallbackWithRetval; + +/// +/// Macro to help bind C++ member functions to a JSCallback +/// +/// Usage: JSCallback callback = BindJSCallback(&MyClass::MyMemberFunction); +/// +/// @note Expected to run from within an instance of 'MyClass', note the 'this' keyword in the +/// macro. +/// +#define BindJSCallback(fn) (JSCallback)std::bind(fn, this, std::placeholders::_1, std::placeholders::_2) + +/// +/// Macro to help bind C++ member functions to a JSCallbackWithRetval +/// +/// Usage: JSCallback callback = BindJSCallback(&MyClass::MyMemberFunction); +/// +/// @note Expected to run from within an instance of 'MyClass', note the 'this' keyword in the +/// macro. +/// +#define BindJSCallbackWithRetval(fn) (JSCallbackWithRetval)std::bind(fn, this, std::placeholders::_1, std::placeholders::_2) + +/// +/// Wrapper for JSObject property value (JSValue subclass). Allows new value assignment +/// to object property, binding C++ callbacks to object properties via function objects, +/// as well as value query via the JSValue interface. +/// +class AExport JSPropertyValue : public JSValue { +public: + virtual ~JSPropertyValue(); + + /// Assign a new value to the property (internally calls JSObjectSetProperty) + virtual JSPropertyValue& operator=(const JSValue& value); + + /// Bind to native C++ callback (creates a Function object that can be called from JS) + JSPropertyValue& operator=(const JSCallback& callback); + + /// Bind to native C++ callback with return value (creates a Function object that can be called from JS) + JSPropertyValue& operator=(const JSCallbackWithRetval& callback); + +protected: + virtual JSValueRef instance() const; + JSPropertyValue(JSContextRef ctx, JSObjectRef proxy_obj, unsigned idx); + JSPropertyValue(JSContextRef ctx, JSObjectRef proxy_obj, JSString idx); + JSPropertyValue(const JSPropertyValue&) = default; + JSPropertyValue& operator=(const JSPropertyValue&) = delete; + + JSObject* proxyObj_; + bool using_numeric_idx_; + unsigned numeric_idx_; + JSString string_idx_; + friend class JSArray; + friend class JSObject; +}; + +/// +/// JSArray wrapper that automatically manages lifetime and provides +/// convenient access to indices and Array functions. +/// +class AExport JSArray { +public: + /// Create empty Array + JSArray(); + + /// Create Array from list of JSValues + JSArray(const std::initializer_list& values); + + /// Create Array from existing JSObjectRef (JavaScriptCore C API) + JSArray(JSObjectRef array_obj); + + /// Copy constructor (shallow copy, will point to same instance) + JSArray(const JSArray& other); + + ~JSArray(); + + /// Assignment (shallow assignment, will point to same instance) + JSArray& operator=(const JSArray& other); + + /// Get number of elements in the Array + unsigned length(); + + /// Push an element to back of Array + void push(const JSValue& val); + + /// Find the index (location) of a certain value, will return -1 if not found + int indexOf(const JSValue& val, int start = 0) const; + + /// Get a property by array index (numbering starts at 0) + JSPropertyValue operator[](unsigned idx) const; + + /// Get the underlying JSObjectRef (JavaScriptCore C API) + operator JSObjectRef() const { return instance_; } + + /// + /// Get the bound context for this JSArray (it is cached at creation). + /// + JSContextRef context() const { return ctx_; } + + /// + /// Set the JSContext for this JSArray. + /// + /// @note + /// JSArrays created from within a JSCallback have a temporary JSContext + /// that is destroyed when the callback returns. You will need to "move" + /// any JSArrays created within these callbacks to the View's main context + /// (call set_context() with the main context) before using them outside + /// the callback. + /// + void set_context(JSContextRef context) { ctx_ = context; } + +protected: + JSArray(JSContextRef ctx, JSValueRef val); + + JSContextRef ctx_; + JSObjectRef instance_; + friend class JSValue; +}; + +/// +/// JSObject wrapper that automatically manages lifetime and provides +/// convenient access to properties. +/// +class AExport JSObject { +public: + /// Create empty Object + JSObject(); + + /// Create from existing JSObjectRef from JavaScriptCore C API + JSObject(JSObjectRef obj); + + /// Copy constructor (shallow copy, will point to same instance) + JSObject(const JSObject& other); + + ~JSObject(); + + /// Assignment (shallow assignment, will point to same instance) + JSObject& operator=(const JSObject& other); + + /// Get a property by name + JSPropertyValue operator[](JSString propertyName) const; + + /// Check if a property exists + bool HasProperty(JSString propertyName) const; + + /// Remove a property + bool DeleteProperty(JSString propertyName); + + /// Get the underlying JSObjectRef (JavaScriptCore C API) + operator JSObjectRef() const { return instance_; } + + /// + /// Get the bound context for this JSObject (it is cached at creation). + /// + JSContextRef context() const { return ctx_; } + + /// + /// Set the JSContext for this JSObject. + /// + /// @note: + /// JSObjects created from within a JSCallback have a temporary JSContext + /// that is destroyed when the callback returns. You will need to "move" + /// any JSObjects created within these callbacks to the View's main context + /// (call set_context() with the main context) before using them outside + /// the callback. + /// + void set_context(JSContextRef context) { ctx_ = context; } + +protected: + JSObject(JSContextRef ctx, JSValueRef val); + JSObject(JSContextRef ctx, JSObjectRef obj); + + JSContextRef ctx_; + JSObjectRef instance_; + friend class JSValue; + friend class JSPropertyValue; +}; + +/// +/// JSFunction wrapper that automatically manages lifetime and provides +/// convenient function invocation operators. +/// +class AExport JSFunction { +public: + /// Create an empty Function. + /// NOTE: It is OKAY to create this without calling SetJSContext() first. + JSFunction(); + + /// Copy constructor (shallow copy, will point to same instance) + JSFunction(const JSFunction& other); + + ~JSFunction(); + + /// Assignment (shallow assignment, will point to same instance) + JSFunction& operator=(const JSFunction& other); + + /// Whether or not this is a valid, callable Function object. + bool IsValid() const; + + /// Call function (using Global Object for 'this') and return the result. + JSValue operator()(const JSArgs& args); + + /// Call function (with explicit object for 'this') and return the result + JSValue operator()(const JSObject& thisObject, const JSArgs& args); + + /// Get the underlying JSObjectRef (JavaScriptCore C API) + operator JSObjectRef() const { return instance_; } + + /// + /// Get the bound context for this JSFunction (it is cached at creation). + /// + JSContextRef context() const { return ctx_; } + + /// + /// Set the JSContext for this JSFunction. + /// + /// @note + /// JSFunctions created from within a JSCallback have a temporary JSContext + /// that is destroyed when the callback returns. You will need to "move" + /// any JSFunctions created within these callbacks to the View's main context + /// (call set_context() with the main context) before using them outside + /// the callback. + /// + void set_context(JSContextRef context) { ctx_ = context; } + +protected: + JSFunction(JSContextRef ctx, JSValueRef val); + + JSContextRef ctx_; + JSObjectRef instance_; + friend class JSValue; +}; + +/// +/// Get the Global Object for the current JSContext. +/// In JavaScript, this would be equivalent to the "window" object. +/// +JSObject AExport JSGlobalObject(); + +/// +/// Evaluate a string of JavaScript and return a result. +/// +JSValue AExport JSEval(const JSString& str); + +} // namespace ultralight diff --git a/include/AppCore/Monitor.h b/include/AppCore/Monitor.h new file mode 100644 index 0000000..72e07d1 --- /dev/null +++ b/include/AppCore/Monitor.h @@ -0,0 +1,51 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include "Defines.h" + +namespace ultralight { + +/// +/// A platform-specific monitor. +/// +class AExport Monitor { +public: + virtual ~Monitor() {} + + /// + /// Get the unique display ID of the monitor. + /// + /// The renderer uses this ID to identify which monitor a View is on (ViewConfig::display_id). + /// + /// AppCore internally tracks the display's hardware refresh event and automatically calls + /// Renderer::RefreshDisplay(id) to drive animation in all corresponding Views. + /// + virtual uint32_t display_id() const = 0; + + /// + /// Get the DPI scale (1.0 = 100%) + /// + virtual double scale() const = 0; + + /// + /// Get the width of the monitor (in pixels). + /// + virtual uint32_t width() const = 0; + + /// + /// Get the height of the monitor (in pixels). + /// + virtual uint32_t height() const = 0; + + /// + /// Get the refresh rate of the monitor (in Hz). + /// + virtual uint32_t refresh_rate() const = 0; +}; + +} // namespace ultralight diff --git a/include/AppCore/Overlay.h b/include/AppCore/Overlay.h new file mode 100644 index 0000000..518f156 --- /dev/null +++ b/include/AppCore/Overlay.h @@ -0,0 +1,150 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include "Window.h" +#include + +namespace ultralight { + +/// +/// Web-content overlay, displays a web-page within a portion of a Window. +/// +/// Overlays are used to display web-based content in a portion of a window. They automatically +/// forward input events to the underlying View instance and handle rendering. +/// +/// ## Creating an Overlay +/// +/// Call Overlay::Create() to create an overlay in a window. +/// +/// ``` +/// auto overlay = Overlay::Create(window, 1024, 768, 0, 0); +/// ``` +/// +/// ## Loading Content into an Overlay +/// +/// Each Overlay has a View instance that you can use to load web content into. +/// +/// ``` +/// overlay->view()->LoadURL("https://google.com"); +/// ``` +/// +class AExport Overlay : public RefCounted { +public: + /// + /// Create a new Overlay. + /// + /// @param window The window to create the Overlay in. + /// + /// @param width The width in pixels. + /// + /// @param height The height in pixels. + /// + /// @param x The x-position (offset from the left of the Window), in + /// pixels. + /// + /// @param y The y-position (offset from the top of the Window), in + /// pixels. + /// + static RefPtr Create(RefPtr window, uint32_t width, + uint32_t height, int x, int y); + + /// + /// Create a new Overlay, wrapping an existing View. + /// + /// @param window The window to create the Overlay in. + /// + /// @param view The View to wrap (will use its width and height). + /// + /// @param x The x-position (offset from the left of the Window), in + /// pixels. + /// + /// @param y The y-position (offset from the top of the Window), in + /// pixels. + /// + static RefPtr Create(RefPtr window, RefPtr view, int x, int y); + + /// + /// Get the underlying View. + /// + virtual ultralight::RefPtr view() = 0; + + /// + /// Get the width (in pixels). + /// + virtual uint32_t width() const = 0; + + /// + /// Get the height (in pixels). + /// + virtual uint32_t height() const = 0; + + /// + /// Get the x-position (offset from the left of the Window), in pixels. + /// + virtual int x() const = 0; + + /// + /// Get the y-position (offset from the top of the Window), in pixels. + /// + virtual int y() const = 0; + + /// + /// Whether or not the overlay is hidden (not drawn). + /// + virtual bool is_hidden() const = 0; + + /// + /// Hide the overlay (will no longer be drawn) + /// + virtual void Hide() = 0; + + /// + /// Show the overlay. + /// + virtual void Show() = 0; + + /// + /// Whether or not this overlay has keyboard focus. + /// + virtual bool has_focus() const = 0; + + /// + /// Grant this overlay exclusive keyboard focus. + /// + virtual void Focus() = 0; + + /// + /// Remove keyboard focus. + /// + virtual void Unfocus() = 0; + + /// + /// Move the overlay to a new position (in pixels). + /// + virtual void MoveTo(int x, int y) = 0; + + /// + /// Resize the overlay (and underlying View), dimensions should be + /// specified in pixels. + /// + virtual void Resize(uint32_t width, uint32_t height) = 0; + + /// + /// Whether or not this Overlay needs repaint (either it has moved, resized, + /// or the internal View needs repaint). + /// + virtual bool NeedsRepaint() = 0; + +protected: + virtual ~Overlay(); + virtual void Render() = 0; + virtual void Paint() = 0; + friend class OverlayManager; +}; + +} // namespace framework diff --git a/include/AppCore/Platform.h b/include/AppCore/Platform.h new file mode 100644 index 0000000..e3b164b --- /dev/null +++ b/include/AppCore/Platform.h @@ -0,0 +1,47 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include "Defines.h" +#include +#include +#include +#include + +namespace ultralight { + +/// +/// Get the native font loader for the current platform. +/// +/// @note This singleton is owned by the library, do not destroy it. +/// +AExport FontLoader* GetPlatformFontLoader(); + +/// +/// Get the native file system for the current platform, creating it if it +/// doesn't exist using the base directory provided. +/// +/// This is used to load data for file:/// URLs. +/// +/// @param baseDir An base file path that will be used to resolve relative +/// file paths. You can optionally specify "@resource_path" +/// on macOS to use the app bundle's resource path. +/// +/// @note This singleton is owned by the library, do not destroy it. +/// +AExport FileSystem* GetPlatformFileSystem(const String& baseDir); + +/// +/// Get the default logger (writes the log to a file on disk). +/// +/// @param logPath A file path to write the log to. +/// +/// @note This singleton is owned by the library, do not destroy it. +/// +AExport Logger* GetDefaultLogger(const String& logPath); + +} // namespace ultralight diff --git a/include/AppCore/Window.h b/include/AppCore/Window.h new file mode 100644 index 0000000..0625c77 --- /dev/null +++ b/include/AppCore/Window.h @@ -0,0 +1,306 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include "Defines.h" +#include +#include +#include +#include +#include +#include + +namespace ultralight { + +class Monitor; +class OverlayManager; +class Surface; +class Window; + +/// +/// Interface for all Window-related events. @see Window::set_listener +/// +class WindowListener { +public: + virtual ~WindowListener() {} + + /// + /// Called when the Window is closed. + /// + virtual void OnClose(ultralight::Window* window) { } + + /// + /// Called when the Window is resized. + /// + /// @param width The new width (in pixels). + /// + /// @param height The new height (in pixels). + /// + virtual void OnResize(ultralight::Window* window, uint32_t width_px, uint32_t height_px) { } + + /// + /// Called when a keyboard event is fired. + /// + /// @param evt Details for the event. + /// + /// @return Return false to consume the event and prevent it from propagating further. + /// + virtual bool OnKeyEvent(const ultralight::KeyEvent& evt) { return true; } + + /// + /// Called when a mouse event is fired. + /// + /// @param evt Details for the event. + /// + /// @return Return false to consume the event and prevent it from propagating further. + /// + virtual bool OnMouseEvent(const ultralight::MouseEvent& evt) { return true; } + + /// + /// Called when a scroll event is fired. + /// + /// @param evt Details for the event. + /// + /// @return Return false to consume the event and prevent it from propagating further. + /// + virtual bool OnScrollEvent(const ultralight::ScrollEvent& evt) { return true; } +}; + +/// +/// Window creation flags. @see Window::Create +/// +enum WindowFlags : uint8_t { + kWindowFlags_Borderless = 1 << 0, + kWindowFlags_Titled = 1 << 1, + kWindowFlags_Resizable = 1 << 2, + kWindowFlags_Maximizable = 1 << 3, + kWindowFlags_Hidden = 1 << 4, +}; + +/// +/// A platform-specific window. +/// +/// This class describes a platform-specific window and provides methods for managing it. +/// +/// ## Creating a Window +/// +/// To create a new Window, use the static Window::Create method: +/// +/// ``` +/// auto window = Window::Create(monitor, 1024, 768, false, kWindowFlags_Titled); +/// ``` +/// +/// ## Setting a WindowListener +/// +/// To receive callbacks for window-related events, set a WindowListener: +/// +/// ``` +/// class MyWindowListener : public WindowListener { +/// virtual void OnClose(Window* window) override { +/// printf("Window closed!\n"); +/// } +/// }; +/// +/// auto listener = new MyWindowListener(); +/// window->set_listener(listener); +/// ``` +/// +/// ## Coordinate Systems +/// +/// Monitors and may windows may have a device scale applied by the OS (for example, a Retina +/// display on macOS may have a 2x or 3x DPI scale). +/// +/// To convert between screen coordinates and pixel coordinates, use the following equation: +/// +/// ``` +/// pixel_coordinate = round(screen_coordinate * scale) +/// ``` +/// +class AExport Window : public RefCounted { +public: + /// + /// Create a new Window. + /// + /// @param monitor The monitor to create the Window on. + /// + /// @param width The width (in screen coordinates). + /// + /// @param height The height (in screen coordinates). + /// + /// @param fullscreen Whether or not the window is fullscreen. + /// + /// @param window_flags Various window flags. + /// + /// @note + /// \parblock + /// + /// Windows are immediately shown by default unless kWindowFlags_Hidden is set in the + /// window_flags parameter. (They can be shown later via Window::Show()) + /// + /// \endparblock + /// + /// @note + /// \parblock + /// + /// Screen coordinates are device-scale-independent and have the following relationship + /// to pixel coordinates: + /// + /// \code + /// pixel_coordinate = round(screen_coordinate * scale) + /// \endcode + /// + /// \endparblock + /// + static RefPtr Create(Monitor* monitor, uint32_t width, uint32_t height, + bool fullscreen, unsigned int window_flags); + + /// + /// Set a WindowListener to receive callbacks for window-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_listener(WindowListener* listener) = 0; + + /// + /// Get the WindowListener, if any. + /// + virtual WindowListener* listener() = 0; + + /// + /// Get the window width (in screen coordinates). + /// + virtual uint32_t screen_width() const = 0; + + /// + /// Get the window width (in pixels). + /// + virtual uint32_t width() const = 0; + + /// + /// Get the window height (in screen coordinates). + /// + virtual uint32_t screen_height() const = 0; + + /// + /// Get the window height (in pixels). + /// + virtual uint32_t height() const = 0; + + /// + /// Move the window to a new position (in screen coordinates) relative to the top-left of the + /// monitor area. + /// + virtual void MoveTo(int x, int y) = 0; + + /// + /// Move the window to the center of the monitor. + /// + virtual void MoveToCenter() = 0; + + /// + /// Get the x-position of the window (in screen coordinates) relative to the top-left of the + /// monitor area. + /// + virtual int x() const = 0; + + /// + /// Get the y-position of the window (in screen coordinates) relative to the top-left of the + /// monitor area. + /// + virtual int y() const = 0; + + /// + /// Whether or not the window is fullscreen. + /// + virtual bool is_fullscreen() const = 0; + + /// + /// Whether or not the window is GPU accelerated. + /// + virtual bool is_accelerated() const = 0; + + /// + /// The render buffer id of the the window's backing texture. + /// (This will be 0 if the window is not accelerated). + /// + virtual uint32_t render_buffer_id() const = 0; + + /// + /// The DPI scale of the window. + /// + virtual double scale() const = 0; + + /// + /// Set the window title. + /// + virtual void SetTitle(const char* title) = 0; + + /// + /// Set the cursor. + /// + virtual void SetCursor(ultralight::Cursor cursor) = 0; + + /// + /// Show the window (if it was previously hidden). + /// + virtual void Show() = 0; + + /// + /// Hide the window. + /// + virtual void Hide() = 0; + + /// + /// Whether or not the window is currently visible (not hidden). + /// + virtual bool is_visible() const = 0; + + /// + /// Close the window. + /// + virtual void Close() = 0; + + /// + /// Convert screen coordinates to pixels using the current DPI scale. + /// + virtual int ScreenToPixels(int val) const = 0; + + /// + /// Convert pixels to screen coordinates using the current DPI scale. + /// + virtual int PixelsToScreen(int val) const = 0; + + /// + /// Draw a surface directly to window, used only by CPU renderer + /// + virtual void DrawSurface(int x, int y, Surface* surface) {} + + /// + /// Get the underlying native window handle. + /// + /// @note + /// This is: + /// - HWND on Windows + /// - NSWindow* on macOS + /// - GLFWwindow* on Linux + /// + virtual void* native_handle() const = 0; + + /// + /// Display frame statistics (FPS, frame times, etc.) in the titlebar. + /// + virtual void EnableFrameStatistics() {} + +protected: + virtual ~Window(); + virtual bool platform_always_uses_cpu_renderer() const = 0; + virtual OverlayManager* overlay_manager() const = 0; + + friend class OverlayImpl; +}; + +} // namespace ultralight diff --git a/include/JavaScriptCore/JSBase.h b/include/JavaScriptCore/JSBase.h new file mode 100644 index 0000000..dbe11e4 --- /dev/null +++ b/include/JavaScriptCore/JSBase.h @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2006 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. + */ + +#ifndef JSBase_h +#define JSBase_h + +#ifndef __cplusplus +#include +#endif + +#ifdef __OBJC__ +#import +#endif + +/* JavaScript engine interface */ + +/*! @typedef JSContextGroupRef A group that associates JavaScript contexts with one another. Contexts in the same group may share and exchange JavaScript objects. */ +typedef const struct OpaqueJSContextGroup* JSContextGroupRef; + +/*! @typedef JSContextRef A JavaScript execution context. Holds the global object and other execution state. */ +typedef const struct OpaqueJSContext* JSContextRef; + +/*! @typedef JSGlobalContextRef A global JavaScript execution context. A JSGlobalContext is a JSContext. */ +typedef struct OpaqueJSContext* JSGlobalContextRef; + +/*! @typedef JSStringRef A UTF16 character buffer. The fundamental string representation in JavaScript. */ +typedef struct OpaqueJSString* JSStringRef; + +/*! @typedef JSClassRef A JavaScript class. Used with JSObjectMake to construct objects with custom behavior. */ +typedef struct OpaqueJSClass* JSClassRef; + +/*! @typedef JSPropertyNameArrayRef An array of JavaScript property names. */ +typedef struct OpaqueJSPropertyNameArray* JSPropertyNameArrayRef; + +/*! @typedef JSPropertyNameAccumulatorRef An ordered set used to collect the names of a JavaScript object's properties. */ +typedef struct OpaqueJSPropertyNameAccumulator* JSPropertyNameAccumulatorRef; + +/*! @typedef JSTypedArrayBytesDeallocator A function used to deallocate bytes passed to a Typed Array constructor. The function should take two arguments. The first is a pointer to the bytes that were originally passed to the Typed Array constructor. The second is a pointer to additional information desired at the time the bytes are to be freed. */ +typedef void (*JSTypedArrayBytesDeallocator)(void* bytes, void* deallocatorContext); + +/* JavaScript data types */ + +/*! @typedef JSValueRef A JavaScript value. The base type for all JavaScript values, and polymorphic functions on them. */ +typedef const struct OpaqueJSValue* JSValueRef; + +/*! @typedef JSObjectRef A JavaScript object. A JSObject is a JSValue. */ +typedef struct OpaqueJSValue* JSObjectRef; + +/* Clang's __has_declspec_attribute emulation */ +/* https://clang.llvm.org/docs/LanguageExtensions.html#has-declspec-attribute */ + +#ifndef __has_declspec_attribute +#define __has_declspec_attribute(x) 0 +#endif + +/* JavaScript symbol exports */ +/* These rules should stay the same as in WebKit/Shared/API/c/WKDeclarationSpecifiers.h */ + +#undef JS_EXPORT +#if defined(JS_NO_EXPORT) +#define JS_EXPORT +#elif defined(WIN32) || defined(_WIN32) || defined(__CC_ARM) || defined(__ARMCC__) || (__has_declspec_attribute(dllimport) && __has_declspec_attribute(dllexport)) +#if defined(BUILDING_JavaScriptCore) || defined(STATICALLY_LINKED_WITH_JavaScriptCore) +#define JS_EXPORT __declspec(dllexport) +#else +#define JS_EXPORT __declspec(dllimport) +#endif +#elif defined(__GNUC__) +#define JS_EXPORT __attribute__((visibility("default"))) +#else /* !defined(JS_NO_EXPORT) */ +#define JS_EXPORT +#endif /* defined(JS_NO_EXPORT) */ + +#ifdef __cplusplus +extern "C" { +#endif + +/* Script Evaluation */ + +/*! +@function JSEvaluateScript +@abstract Evaluates a string of JavaScript. +@param ctx The execution context to use. +@param script A JSString containing the script to evaluate. +@param thisObject The object to use as "this," or NULL to use the global object as "this." +@param sourceURL A JSString containing a URL for the script's source file. This is used by debuggers and when reporting exceptions. Pass NULL if you do not care to include source file information. +@param startingLineNumber An integer value specifying the script's starting line number in the file located at sourceURL. This is only used when reporting exceptions. The value is one-based, so the first line is line 1 and invalid values are clamped to 1. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The JSValue that results from evaluating script, or NULL if an exception is thrown. +*/ +JS_EXPORT JSValueRef JSEvaluateScript(JSContextRef ctx, JSStringRef script, JSObjectRef thisObject, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception); + +/*! +@function JSCheckScriptSyntax +@abstract Checks for syntax errors in a string of JavaScript. +@param ctx The execution context to use. +@param script A JSString containing the script to check for syntax errors. +@param sourceURL A JSString containing a URL for the script's source file. This is only used when reporting exceptions. Pass NULL if you do not care to include source file information in exceptions. +@param startingLineNumber An integer value specifying the script's starting line number in the file located at sourceURL. This is only used when reporting exceptions. The value is one-based, so the first line is line 1 and invalid values are clamped to 1. +@param exception A pointer to a JSValueRef in which to store a syntax error exception, if any. Pass NULL if you do not care to store a syntax error exception. +@result true if the script is syntactically correct, otherwise false. +*/ +JS_EXPORT bool JSCheckScriptSyntax(JSContextRef ctx, JSStringRef script, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception); + +/*! +@function JSGarbageCollect +@abstract Performs a JavaScript garbage collection. +@param ctx The execution context to use. +@discussion JavaScript values that are on the machine stack, in a register, + protected by JSValueProtect, set as the global object of an execution context, + or reachable from any such value will not be collected. + + During JavaScript execution, you are not required to call this function; the + JavaScript engine will garbage collect as needed. JavaScript values created + within a context group are automatically destroyed when the last reference + to the context group is released. +*/ +JS_EXPORT void JSGarbageCollect(JSContextRef ctx); + +#ifdef __cplusplus +} +#endif + +/* Enable the Objective-C API for platforms with a modern runtime. NOTE: This is duplicated in VM.h. */ +#if !defined(JSC_OBJC_API_ENABLED) +#if (defined(__clang__) && defined(__APPLE__) && (defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || (defined(TARGET_OS_IPHONE) && TARGET_OS_IPHONE))) +#define JSC_OBJC_API_ENABLED 1 +#else +#define JSC_OBJC_API_ENABLED 0 +#endif +#endif + +#endif /* JSBase_h */ diff --git a/include/JavaScriptCore/JSContextRef.h b/include/JavaScriptCore/JSContextRef.h new file mode 100644 index 0000000..fb47015 --- /dev/null +++ b/include/JavaScriptCore/JSContextRef.h @@ -0,0 +1,178 @@ +/* + * Copyright (C) 2006 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. + */ + +#ifndef JSContextRef_h +#define JSContextRef_h + +#include +#include +#include + +#ifndef __cplusplus +#include +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +/*! +@function +@abstract Creates a JavaScript context group. +@discussion A JSContextGroup associates JavaScript contexts with one another. + Contexts in the same group may share and exchange JavaScript objects. Sharing and/or exchanging + JavaScript objects between contexts in different groups will produce undefined behavior. + When objects from the same context group are used in multiple threads, explicit + synchronization is required. + + A JSContextGroup may need to run deferred tasks on a run loop, such as garbage collection + or resolving WebAssembly compilations. By default, calling JSContextGroupCreate will use + the run loop of the thread it was called on. Currently, there is no API to change a + JSContextGroup's run loop once it has been created. +@result The created JSContextGroup. +*/ +JS_EXPORT JSContextGroupRef JSContextGroupCreate(void) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! +@function +@abstract Retains a JavaScript context group. +@param group The JSContextGroup to retain. +@result A JSContextGroup that is the same as group. +*/ +JS_EXPORT JSContextGroupRef JSContextGroupRetain(JSContextGroupRef group) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! +@function +@abstract Releases a JavaScript context group. +@param group The JSContextGroup to release. +*/ +JS_EXPORT void JSContextGroupRelease(JSContextGroupRef group) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! +@function +@abstract Creates a global JavaScript execution context. +@discussion JSGlobalContextCreate allocates a global object and populates it with all the + built-in JavaScript objects, such as Object, Function, String, and Array. + + In WebKit version 4.0 and later, the context is created in a unique context group. + Therefore, scripts may execute in it concurrently with scripts executing in other contexts. + However, you may not use values created in the context in other contexts. +@param globalObjectClass The class to use when creating the global object. Pass + NULL to use the default object class. +@result A JSGlobalContext with a global object of class globalObjectClass. +*/ +JS_EXPORT JSGlobalContextRef JSGlobalContextCreate(JSClassRef globalObjectClass) JSC_API_AVAILABLE(macos(10.5), ios(7.0)); + +/*! +@function +@abstract Creates a global JavaScript execution context in the context group provided. +@discussion JSGlobalContextCreateInGroup allocates a global object and populates it with + all the built-in JavaScript objects, such as Object, Function, String, and Array. +@param globalObjectClass The class to use when creating the global object. Pass + NULL to use the default object class. +@param group The context group to use. The created global context retains the group. + Pass NULL to create a unique group for the context. +@result A JSGlobalContext with a global object of class globalObjectClass and a context + group equal to group. +*/ +JS_EXPORT JSGlobalContextRef JSGlobalContextCreateInGroup(JSContextGroupRef group, JSClassRef globalObjectClass) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! +@function +@abstract Retains a global JavaScript execution context. +@param ctx The JSGlobalContext to retain. +@result A JSGlobalContext that is the same as ctx. +*/ +JS_EXPORT JSGlobalContextRef JSGlobalContextRetain(JSGlobalContextRef ctx); + +/*! +@function +@abstract Releases a global JavaScript execution context. +@param ctx The JSGlobalContext to release. +*/ +JS_EXPORT void JSGlobalContextRelease(JSGlobalContextRef ctx); + +/*! +@function +@abstract Gets the global object of a JavaScript execution context. +@param ctx The JSContext whose global object you want to get. +@result ctx's global object. +*/ +JS_EXPORT JSObjectRef JSContextGetGlobalObject(JSContextRef ctx); + +/*! +@function +@abstract Gets the context group to which a JavaScript execution context belongs. +@param ctx The JSContext whose group you want to get. +@result ctx's group. +*/ +JS_EXPORT JSContextGroupRef JSContextGetGroup(JSContextRef ctx) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! +@function +@abstract Gets the global context of a JavaScript execution context. +@param ctx The JSContext whose global context you want to get. +@result ctx's global context. +*/ +JS_EXPORT JSGlobalContextRef JSContextGetGlobalContext(JSContextRef ctx) JSC_API_AVAILABLE(macos(10.7), ios(7.0)); + +/*! +@function +@abstract Gets a copy of the name of a context. +@param ctx The JSGlobalContext whose name you want to get. +@result The name for ctx. +@discussion A JSGlobalContext's name is exposed when inspecting the context to make it easier to identify the context you would like to inspect. +*/ +JS_EXPORT JSStringRef JSGlobalContextCopyName(JSGlobalContextRef ctx) JSC_API_AVAILABLE(macos(10.10), ios(8.0)); + +/*! +@function +@abstract Sets the name exposed when inspecting a context. +@param ctx The JSGlobalContext that you want to name. +@param name The name to set on the context. +*/ +JS_EXPORT void JSGlobalContextSetName(JSGlobalContextRef ctx, JSStringRef name) JSC_API_AVAILABLE(macos(10.10), ios(8.0)); + +/*! +@function +@abstract Gets whether the context is inspectable in Web Inspector. +@param ctx The JSGlobalContext that you want to change the inspectability of. +@result Whether the context is inspectable in Web Inspector. +*/ +JS_EXPORT bool JSGlobalContextIsInspectable(JSGlobalContextRef ctx) JSC_API_AVAILABLE(macos(JSC_MAC_TBA), ios(JSC_IOS_TBA)); + +/*! +@function +@abstract Sets whether the context is inspectable in Web Inspector. Default value is NO. +@param ctx The JSGlobalContext that you want to change the inspectability of. +@param inspectable YES to allow Web Inspector to connect to the context, otherwise NO. +*/ +JS_EXPORT void JSGlobalContextSetInspectable(JSGlobalContextRef ctx, bool inspectable) JSC_API_AVAILABLE(macos(JSC_MAC_TBA), ios(JSC_IOS_TBA)); + +#ifdef __cplusplus +} +#endif + +#endif /* JSContextRef_h */ diff --git a/include/JavaScriptCore/JSObjectRef.h b/include/JavaScriptCore/JSObjectRef.h new file mode 100644 index 0000000..6f1a7ba --- /dev/null +++ b/include/JavaScriptCore/JSObjectRef.h @@ -0,0 +1,755 @@ +/* + * Copyright (C) 2006-2019 Apple Inc. All rights reserved. + * Copyright (C) 2008 Kelvin W Sherlock (ksherlock@gmail.com) + * + * 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. + */ + +#ifndef JSObjectRef_h +#define JSObjectRef_h + +#include +#include +#include + +#ifndef __cplusplus +#include +#endif +#include /* for size_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +/*! +@enum JSPropertyAttribute +@constant kJSPropertyAttributeNone Specifies that a property has no special attributes. +@constant kJSPropertyAttributeReadOnly Specifies that a property is read-only. +@constant kJSPropertyAttributeDontEnum Specifies that a property should not be enumerated by JSPropertyEnumerators and JavaScript for...in loops. +@constant kJSPropertyAttributeDontDelete Specifies that the delete operation should fail on a property. +*/ +enum { + kJSPropertyAttributeNone = 0, + kJSPropertyAttributeReadOnly = 1 << 1, + kJSPropertyAttributeDontEnum = 1 << 2, + kJSPropertyAttributeDontDelete = 1 << 3 +}; + +/*! +@typedef JSPropertyAttributes +@abstract A set of JSPropertyAttributes. Combine multiple attributes by logically ORing them together. +*/ +typedef unsigned JSPropertyAttributes; + +/*! +@enum JSClassAttribute +@constant kJSClassAttributeNone Specifies that a class has no special attributes. +@constant kJSClassAttributeNoAutomaticPrototype Specifies that a class should not automatically generate a shared prototype for its instance objects. Use kJSClassAttributeNoAutomaticPrototype in combination with JSObjectSetPrototype to manage prototypes manually. +*/ +enum { + kJSClassAttributeNone = 0, + kJSClassAttributeNoAutomaticPrototype = 1 << 1 +}; + +/*! +@typedef JSClassAttributes +@abstract A set of JSClassAttributes. Combine multiple attributes by logically ORing them together. +*/ +typedef unsigned JSClassAttributes; + +/*! +@typedef JSObjectInitializeCallback +@abstract The callback invoked when an object is first created. +@param ctx The execution context to use. +@param object The JSObject being created. +@discussion If you named your function Initialize, you would declare it like this: + +void Initialize(JSContextRef ctx, JSObjectRef object); + +Unlike the other object callbacks, the initialize callback is called on the least +derived class (the parent class) first, and the most derived class last. +*/ +typedef void +(*JSObjectInitializeCallback) (JSContextRef ctx, JSObjectRef object); + +/*! +@typedef JSObjectFinalizeCallback +@abstract The callback invoked when an object is finalized (prepared for garbage collection). An object may be finalized on any thread. +@param object The JSObject being finalized. +@discussion If you named your function Finalize, you would declare it like this: + +void Finalize(JSObjectRef object); + +The finalize callback is called on the most derived class first, and the least +derived class (the parent class) last. + +You must not call any function that may cause a garbage collection or an allocation +of a garbage collected object from within a JSObjectFinalizeCallback. This includes +all functions that have a JSContextRef parameter. +*/ +typedef void +(*JSObjectFinalizeCallback) (JSObjectRef object); + +/*! +@typedef JSObjectHasPropertyCallback +@abstract The callback invoked when determining whether an object has a property. +@param ctx The execution context to use. +@param object The JSObject to search for the property. +@param propertyName A JSString containing the name of the property look up. +@result true if object has the property, otherwise false. +@discussion If you named your function HasProperty, you would declare it like this: + +bool HasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); + +If this function returns false, the hasProperty request forwards to object's statically declared properties, then its parent class chain (which includes the default object class), then its prototype chain. + +This callback enables optimization in cases where only a property's existence needs to be known, not its value, and computing its value would be expensive. + +If this callback is NULL, the getProperty callback will be used to service hasProperty requests. +*/ +typedef bool +(*JSObjectHasPropertyCallback) (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); + +/*! +@typedef JSObjectGetPropertyCallback +@abstract The callback invoked when getting a property's value. +@param ctx The execution context to use. +@param object The JSObject to search for the property. +@param propertyName A JSString containing the name of the property to get. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result The property's value if object has the property, otherwise NULL. +@discussion If you named your function GetProperty, you would declare it like this: + +JSValueRef GetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +If this function returns NULL, the get request forwards to object's statically declared properties, then its parent class chain (which includes the default object class), then its prototype chain. +*/ +typedef JSValueRef +(*JSObjectGetPropertyCallback) (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +/*! +@typedef JSObjectSetPropertyCallback +@abstract The callback invoked when setting a property's value. +@param ctx The execution context to use. +@param object The JSObject on which to set the property's value. +@param propertyName A JSString containing the name of the property to set. +@param value A JSValue to use as the property's value. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result true if the property was set, otherwise false. +@discussion If you named your function SetProperty, you would declare it like this: + +bool SetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception); + +If this function returns false, the set request forwards to object's statically declared properties, then its parent class chain (which includes the default object class). +*/ +typedef bool +(*JSObjectSetPropertyCallback) (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSValueRef* exception); + +/*! +@typedef JSObjectDeletePropertyCallback +@abstract The callback invoked when deleting a property. +@param ctx The execution context to use. +@param object The JSObject in which to delete the property. +@param propertyName A JSString containing the name of the property to delete. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result true if propertyName was successfully deleted, otherwise false. +@discussion If you named your function DeleteProperty, you would declare it like this: + +bool DeleteProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +If this function returns false, the delete request forwards to object's statically declared properties, then its parent class chain (which includes the default object class). +*/ +typedef bool +(*JSObjectDeletePropertyCallback) (JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +/*! +@typedef JSObjectGetPropertyNamesCallback +@abstract The callback invoked when collecting the names of an object's properties. +@param ctx The execution context to use. +@param object The JSObject whose property names are being collected. +@param propertyNames A JavaScript property name accumulator in which to accumulate the names of object's properties. +@discussion If you named your function GetPropertyNames, you would declare it like this: + +void GetPropertyNames(JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames); + +Property name accumulators are used by JSObjectCopyPropertyNames and JavaScript for...in loops. + +Use JSPropertyNameAccumulatorAddName to add property names to accumulator. A class's getPropertyNames callback only needs to provide the names of properties that the class vends through a custom getProperty or setProperty callback. Other properties, including statically declared properties, properties vended by other classes, and properties belonging to object's prototype, are added independently. +*/ +typedef void +(*JSObjectGetPropertyNamesCallback) (JSContextRef ctx, JSObjectRef object, JSPropertyNameAccumulatorRef propertyNames); + +/*! +@typedef JSObjectCallAsFunctionCallback +@abstract The callback invoked when an object is called as a function. +@param ctx The execution context to use. +@param function A JSObject that is the function being called. +@param thisObject A JSObject that is the 'this' variable in the function's scope. +@param argumentCount An integer count of the number of arguments in arguments. +@param arguments A JSValue array of the arguments passed to the function. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result A JSValue that is the function's return value. +@discussion If you named your function CallAsFunction, you would declare it like this: + +JSValueRef CallAsFunction(JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +If your callback were invoked by the JavaScript expression 'myObject.myFunction()', function would be set to myFunction, and thisObject would be set to myObject. + +If this callback is NULL, calling your object as a function will throw an exception. +*/ +typedef JSValueRef +(*JSObjectCallAsFunctionCallback) (JSContextRef ctx, JSObjectRef function, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +/*! +@typedef JSObjectCallAsConstructorCallback +@abstract The callback invoked when an object is used as a constructor in a 'new' expression. +@param ctx The execution context to use. +@param constructor A JSObject that is the constructor being called. +@param argumentCount An integer count of the number of arguments in arguments. +@param arguments A JSValue array of the arguments passed to the function. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result A JSObject that is the constructor's return value. +@discussion If you named your function CallAsConstructor, you would declare it like this: + +JSObjectRef CallAsConstructor(JSContextRef ctx, JSObjectRef constructor, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +If your callback were invoked by the JavaScript expression 'new myConstructor()', constructor would be set to myConstructor. + +If this callback is NULL, using your object as a constructor in a 'new' expression will throw an exception. +*/ +typedef JSObjectRef +(*JSObjectCallAsConstructorCallback) (JSContextRef ctx, JSObjectRef constructor, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +/*! +@typedef JSObjectHasInstanceCallback +@abstract hasInstance The callback invoked when an object is used as the target of an 'instanceof' expression. +@param ctx The execution context to use. +@param constructor The JSObject that is the target of the 'instanceof' expression. +@param possibleInstance The JSValue being tested to determine if it is an instance of constructor. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result true if possibleInstance is an instance of constructor, otherwise false. +@discussion If you named your function HasInstance, you would declare it like this: + +bool HasInstance(JSContextRef ctx, JSObjectRef constructor, JSValueRef possibleInstance, JSValueRef* exception); + +If your callback were invoked by the JavaScript expression 'someValue instanceof myObject', constructor would be set to myObject and possibleInstance would be set to someValue. + +If this callback is NULL, 'instanceof' expressions that target your object will return false. + +Standard JavaScript practice calls for objects that implement the callAsConstructor callback to implement the hasInstance callback as well. +*/ +typedef bool +(*JSObjectHasInstanceCallback) (JSContextRef ctx, JSObjectRef constructor, JSValueRef possibleInstance, JSValueRef* exception); + +/*! +@typedef JSObjectConvertToTypeCallback +@abstract The callback invoked when converting an object to a particular JavaScript type. +@param ctx The execution context to use. +@param object The JSObject to convert. +@param type A JSType specifying the JavaScript type to convert to. +@param exception A pointer to a JSValueRef in which to return an exception, if any. +@result The objects's converted value, or NULL if the object was not converted. +@discussion If you named your function ConvertToType, you would declare it like this: + +JSValueRef ConvertToType(JSContextRef ctx, JSObjectRef object, JSType type, JSValueRef* exception); + +If this function returns false, the conversion request forwards to object's parent class chain (which includes the default object class). + +This function is only invoked when converting an object to number or string. An object converted to boolean is 'true.' An object converted to object is itself. +*/ +typedef JSValueRef +(*JSObjectConvertToTypeCallback) (JSContextRef ctx, JSObjectRef object, JSType type, JSValueRef* exception); + +/*! +@struct JSStaticValue +@abstract This structure describes a statically declared value property. +@field name A null-terminated UTF8 string containing the property's name. +@field getProperty A JSObjectGetPropertyCallback to invoke when getting the property's value. +@field setProperty A JSObjectSetPropertyCallback to invoke when setting the property's value. May be NULL if the ReadOnly attribute is set. +@field attributes A logically ORed set of JSPropertyAttributes to give to the property. +*/ +typedef struct { + const char* name; + JSObjectGetPropertyCallback getProperty; + JSObjectSetPropertyCallback setProperty; + JSPropertyAttributes attributes; +} JSStaticValue; + +/*! +@struct JSStaticFunction +@abstract This structure describes a statically declared function property. +@field name A null-terminated UTF8 string containing the property's name. +@field callAsFunction A JSObjectCallAsFunctionCallback to invoke when the property is called as a function. +@field attributes A logically ORed set of JSPropertyAttributes to give to the property. +*/ +typedef struct { + const char* name; + JSObjectCallAsFunctionCallback callAsFunction; + JSPropertyAttributes attributes; +} JSStaticFunction; + +/*! +@struct JSClassDefinition +@abstract This structure contains properties and callbacks that define a type of object. All fields other than the version field are optional. Any pointer may be NULL. +@field version The version number of this structure. The current version is 0. +@field attributes A logically ORed set of JSClassAttributes to give to the class. +@field className A null-terminated UTF8 string containing the class's name. +@field parentClass A JSClass to set as the class's parent class. Pass NULL use the default object class. +@field staticValues A JSStaticValue array containing the class's statically declared value properties. Pass NULL to specify no statically declared value properties. The array must be terminated by a JSStaticValue whose name field is NULL. +@field staticFunctions A JSStaticFunction array containing the class's statically declared function properties. Pass NULL to specify no statically declared function properties. The array must be terminated by a JSStaticFunction whose name field is NULL. +@field initialize The callback invoked when an object is first created. Use this callback to initialize the object. +@field finalize The callback invoked when an object is finalized (prepared for garbage collection). Use this callback to release resources allocated for the object, and perform other cleanup. +@field hasProperty The callback invoked when determining whether an object has a property. If this field is NULL, getProperty is called instead. The hasProperty callback enables optimization in cases where only a property's existence needs to be known, not its value, and computing its value is expensive. +@field getProperty The callback invoked when getting a property's value. +@field setProperty The callback invoked when setting a property's value. +@field deleteProperty The callback invoked when deleting a property. +@field getPropertyNames The callback invoked when collecting the names of an object's properties. +@field callAsFunction The callback invoked when an object is called as a function. +@field hasInstance The callback invoked when an object is used as the target of an 'instanceof' expression. +@field callAsConstructor The callback invoked when an object is used as a constructor in a 'new' expression. +@field convertToType The callback invoked when converting an object to a particular JavaScript type. +@discussion The staticValues and staticFunctions arrays are the simplest and most efficient means for vending custom properties. Statically declared properties autmatically service requests like getProperty, setProperty, and getPropertyNames. Property access callbacks are required only to implement unusual properties, like array indexes, whose names are not known at compile-time. + +If you named your getter function "GetX" and your setter function "SetX", you would declare a JSStaticValue array containing "X" like this: + +JSStaticValue StaticValueArray[] = { + { "X", GetX, SetX, kJSPropertyAttributeNone }, + { 0, 0, 0, 0 } +}; + +Standard JavaScript practice calls for storing function objects in prototypes, so they can be shared. The default JSClass created by JSClassCreate follows this idiom, instantiating objects with a shared, automatically generating prototype containing the class's function objects. The kJSClassAttributeNoAutomaticPrototype attribute specifies that a JSClass should not automatically generate such a prototype. The resulting JSClass instantiates objects with the default object prototype, and gives each instance object its own copy of the class's function objects. + +A NULL callback specifies that the default object callback should substitute, except in the case of hasProperty, where it specifies that getProperty should substitute. + +It is not possible to use JS subclassing with objects created from a class definition that sets callAsConstructor by default. Subclassing is supported via the JSObjectMakeConstructor function, however. +*/ +typedef struct { + int version; /* current (and only) version is 0 */ + JSClassAttributes attributes; + + const char* className; + JSClassRef parentClass; + + const JSStaticValue* staticValues; + const JSStaticFunction* staticFunctions; + + JSObjectInitializeCallback initialize; + JSObjectFinalizeCallback finalize; + JSObjectHasPropertyCallback hasProperty; + JSObjectGetPropertyCallback getProperty; + JSObjectSetPropertyCallback setProperty; + JSObjectDeletePropertyCallback deleteProperty; + JSObjectGetPropertyNamesCallback getPropertyNames; + JSObjectCallAsFunctionCallback callAsFunction; + JSObjectCallAsConstructorCallback callAsConstructor; + JSObjectHasInstanceCallback hasInstance; + JSObjectConvertToTypeCallback convertToType; +} JSClassDefinition; + +/*! +@const kJSClassDefinitionEmpty +@abstract A JSClassDefinition structure of the current version, filled with NULL pointers and having no attributes. +@discussion Use this constant as a convenience when creating class definitions. For example, to create a class definition with only a finalize method: + +JSClassDefinition definition = kJSClassDefinitionEmpty; +definition.finalize = Finalize; +*/ +JS_EXPORT extern const JSClassDefinition kJSClassDefinitionEmpty; + +/*! +@function +@abstract Creates a JavaScript class suitable for use with JSObjectMake. +@param definition A JSClassDefinition that defines the class. +@result A JSClass with the given definition. Ownership follows the Create Rule. +*/ +JS_EXPORT JSClassRef JSClassCreate(const JSClassDefinition* definition); + +/*! +@function +@abstract Retains a JavaScript class. +@param jsClass The JSClass to retain. +@result A JSClass that is the same as jsClass. +*/ +JS_EXPORT JSClassRef JSClassRetain(JSClassRef jsClass); + +/*! +@function +@abstract Releases a JavaScript class. +@param jsClass The JSClass to release. +*/ +JS_EXPORT void JSClassRelease(JSClassRef jsClass); + +/*! +@function +@abstract Creates a JavaScript object. +@param ctx The execution context to use. +@param jsClass The JSClass to assign to the object. Pass NULL to use the default object class. +@param data A void* to set as the object's private data. Pass NULL to specify no private data. +@result A JSObject with the given class and private data. +@discussion The default object class does not allocate storage for private data, so you must provide a non-NULL jsClass to JSObjectMake if you want your object to be able to store private data. + +data is set on the created object before the intialize methods in its class chain are called. This enables the initialize methods to retrieve and manipulate data through JSObjectGetPrivate. +*/ +JS_EXPORT JSObjectRef JSObjectMake(JSContextRef ctx, JSClassRef jsClass, void* data); + +/*! +@function +@abstract Convenience method for creating a JavaScript function with a given callback as its implementation. +@param ctx The execution context to use. +@param name A JSString containing the function's name. This will be used when converting the function to string. Pass NULL to create an anonymous function. +@param callAsFunction The JSObjectCallAsFunctionCallback to invoke when the function is called. +@result A JSObject that is a function. The object's prototype will be the default function prototype. +*/ +JS_EXPORT JSObjectRef JSObjectMakeFunctionWithCallback(JSContextRef ctx, JSStringRef name, JSObjectCallAsFunctionCallback callAsFunction); + +/*! +@function +@abstract Convenience method for creating a JavaScript constructor. +@param ctx The execution context to use. +@param jsClass A JSClass that is the class your constructor will assign to the objects its constructs. jsClass will be used to set the constructor's .prototype property, and to evaluate 'instanceof' expressions. Pass NULL to use the default object class. +@param callAsConstructor A JSObjectCallAsConstructorCallback to invoke when your constructor is used in a 'new' expression. Pass NULL to use the default object constructor. +@result A JSObject that is a constructor. The object's prototype will be the default object prototype. +@discussion The default object constructor takes no arguments and constructs an object of class jsClass with no private data. If the constructor is inherited via JS subclassing and the value returned from callAsConstructor was created with jsClass, then the returned object will have it's prototype overridden to the derived class's prototype. +*/ +JS_EXPORT JSObjectRef JSObjectMakeConstructor(JSContextRef ctx, JSClassRef jsClass, JSObjectCallAsConstructorCallback callAsConstructor); + +/*! + @function + @abstract Creates a JavaScript Array object. + @param ctx The execution context to use. + @param argumentCount An integer count of the number of arguments in arguments. + @param arguments A JSValue array of data to populate the Array with. Pass NULL if argumentCount is 0. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObject that is an Array. + @discussion The behavior of this function does not exactly match the behavior of the built-in Array constructor. Specifically, if one argument + is supplied, this function returns an array with one element. + */ +JS_EXPORT JSObjectRef JSObjectMakeArray(JSContextRef ctx, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! + @function + @abstract Creates a JavaScript Date object, as if by invoking the built-in Date constructor. + @param ctx The execution context to use. + @param argumentCount An integer count of the number of arguments in arguments. + @param arguments A JSValue array of arguments to pass to the Date Constructor. Pass NULL if argumentCount is 0. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObject that is a Date. + */ +JS_EXPORT JSObjectRef JSObjectMakeDate(JSContextRef ctx, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! + @function + @abstract Creates a JavaScript Error object, as if by invoking the built-in Error constructor. + @param ctx The execution context to use. + @param argumentCount An integer count of the number of arguments in arguments. + @param arguments A JSValue array of arguments to pass to the Error Constructor. Pass NULL if argumentCount is 0. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObject that is an Error. + */ +JS_EXPORT JSObjectRef JSObjectMakeError(JSContextRef ctx, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! + @function + @abstract Creates a JavaScript RegExp object, as if by invoking the built-in RegExp constructor. + @param ctx The execution context to use. + @param argumentCount An integer count of the number of arguments in arguments. + @param arguments A JSValue array of arguments to pass to the RegExp Constructor. Pass NULL if argumentCount is 0. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObject that is a RegExp. + */ +JS_EXPORT JSObjectRef JSObjectMakeRegExp(JSContextRef ctx, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception) JSC_API_AVAILABLE(macos(10.6), ios(7.0)); + +/*! + @function + @abstract Creates a JavaScript promise object by invoking the provided executor. + @param ctx The execution context to use. + @param resolve A pointer to a JSObjectRef in which to store the resolve function for the new promise. Pass NULL if you do not care to store the resolve callback. + @param reject A pointer to a JSObjectRef in which to store the reject function for the new promise. Pass NULL if you do not care to store the reject callback. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObject that is a promise or NULL if an exception occurred. + */ +JS_EXPORT JSObjectRef JSObjectMakeDeferredPromise(JSContextRef ctx, JSObjectRef* resolve, JSObjectRef* reject, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! +@function +@abstract Creates a function with a given script as its body. +@param ctx The execution context to use. +@param name A JSString containing the function's name. This will be used when converting the function to string. Pass NULL to create an anonymous function. +@param parameterCount An integer count of the number of parameter names in parameterNames. +@param parameterNames A JSString array containing the names of the function's parameters. Pass NULL if parameterCount is 0. +@param body A JSString containing the script to use as the function's body. +@param sourceURL A JSString containing a URL for the script's source file. This is only used when reporting exceptions. Pass NULL if you do not care to include source file information in exceptions. +@param startingLineNumber An integer value specifying the script's starting line number in the file located at sourceURL. This is only used when reporting exceptions. The value is one-based, so the first line is line 1 and invalid values are clamped to 1. +@param exception A pointer to a JSValueRef in which to store a syntax error exception, if any. Pass NULL if you do not care to store a syntax error exception. +@result A JSObject that is a function, or NULL if either body or parameterNames contains a syntax error. The object's prototype will be the default function prototype. +@discussion Use this method when you want to execute a script repeatedly, to avoid the cost of re-parsing the script before each execution. +*/ +JS_EXPORT JSObjectRef JSObjectMakeFunction(JSContextRef ctx, JSStringRef name, unsigned parameterCount, const JSStringRef parameterNames[], JSStringRef body, JSStringRef sourceURL, int startingLineNumber, JSValueRef* exception); + +/*! +@function +@abstract Gets an object's prototype. +@param ctx The execution context to use. +@param object A JSObject whose prototype you want to get. +@result A JSValue that is the object's prototype. +*/ +JS_EXPORT JSValueRef JSObjectGetPrototype(JSContextRef ctx, JSObjectRef object); + +/*! +@function +@abstract Sets an object's prototype. +@param ctx The execution context to use. +@param object The JSObject whose prototype you want to set. +@param value A JSValue to set as the object's prototype. +*/ +JS_EXPORT void JSObjectSetPrototype(JSContextRef ctx, JSObjectRef object, JSValueRef value); + +/*! +@function +@abstract Tests whether an object has a given property. +@param object The JSObject to test. +@param propertyName A JSString containing the property's name. +@result true if the object has a property whose name matches propertyName, otherwise false. +*/ +JS_EXPORT bool JSObjectHasProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); + +/*! +@function +@abstract Gets a property from an object. +@param ctx The execution context to use. +@param object The JSObject whose property you want to get. +@param propertyName A JSString containing the property's name. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The property's value if object has the property, otherwise the undefined value. +*/ +JS_EXPORT JSValueRef JSObjectGetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +/*! +@function +@abstract Sets a property on an object. +@param ctx The execution context to use. +@param object The JSObject whose property you want to set. +@param propertyName A JSString containing the property's name. +@param value A JSValueRef to use as the property's value. +@param attributes A logically ORed set of JSPropertyAttributes to give to the property. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +*/ +JS_EXPORT void JSObjectSetProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value, JSPropertyAttributes attributes, JSValueRef* exception); + +/*! +@function +@abstract Deletes a property from an object. +@param ctx The execution context to use. +@param object The JSObject whose property you want to delete. +@param propertyName A JSString containing the property's name. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result true if the delete operation succeeds, otherwise false (for example, if the property has the kJSPropertyAttributeDontDelete attribute set). +*/ +JS_EXPORT bool JSObjectDeleteProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef* exception); + +/*! + @function + @abstract Tests whether an object has a given property using a JSValueRef as the property key. + @param object The JSObject to test. + @param propertyKey A JSValueRef containing the property key to use when looking up the property. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result true if the object has a property whose name matches propertyKey, otherwise false. + @discussion This function is the same as performing "propertyKey in object" from JavaScript. + */ +JS_EXPORT bool JSObjectHasPropertyForKey(JSContextRef ctx, JSObjectRef object, JSValueRef propertyKey, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! + @function + @abstract Gets a property from an object using a JSValueRef as the property key. + @param ctx The execution context to use. + @param object The JSObject whose property you want to get. + @param propertyKey A JSValueRef containing the property key to use when looking up the property. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result The property's value if object has the property key, otherwise the undefined value. + @discussion This function is the same as performing "object[propertyKey]" from JavaScript. + */ +JS_EXPORT JSValueRef JSObjectGetPropertyForKey(JSContextRef ctx, JSObjectRef object, JSValueRef propertyKey, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! + @function + @abstract Sets a property on an object using a JSValueRef as the property key. + @param ctx The execution context to use. + @param object The JSObject whose property you want to set. + @param propertyKey A JSValueRef containing the property key to use when looking up the property. + @param value A JSValueRef to use as the property's value. + @param attributes A logically ORed set of JSPropertyAttributes to give to the property. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @discussion This function is the same as performing "object[propertyKey] = value" from JavaScript. + */ +JS_EXPORT void JSObjectSetPropertyForKey(JSContextRef ctx, JSObjectRef object, JSValueRef propertyKey, JSValueRef value, JSPropertyAttributes attributes, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! + @function + @abstract Deletes a property from an object using a JSValueRef as the property key. + @param ctx The execution context to use. + @param object The JSObject whose property you want to delete. + @param propertyKey A JSValueRef containing the property key to use when looking up the property. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result true if the delete operation succeeds, otherwise false (for example, if the property has the kJSPropertyAttributeDontDelete attribute set). + @discussion This function is the same as performing "delete object[propertyKey]" from JavaScript. + */ +JS_EXPORT bool JSObjectDeletePropertyForKey(JSContextRef ctx, JSObjectRef object, JSValueRef propertyKey, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! +@function +@abstract Gets a property from an object by numeric index. +@param ctx The execution context to use. +@param object The JSObject whose property you want to get. +@param propertyIndex An integer value that is the property's name. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The property's value if object has the property, otherwise the undefined value. +@discussion Calling JSObjectGetPropertyAtIndex is equivalent to calling JSObjectGetProperty with a string containing propertyIndex, but JSObjectGetPropertyAtIndex provides optimized access to numeric properties. +*/ +JS_EXPORT JSValueRef JSObjectGetPropertyAtIndex(JSContextRef ctx, JSObjectRef object, unsigned propertyIndex, JSValueRef* exception); + +/*! +@function +@abstract Sets a property on an object by numeric index. +@param ctx The execution context to use. +@param object The JSObject whose property you want to set. +@param propertyIndex The property's name as a number. +@param value A JSValue to use as the property's value. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@discussion Calling JSObjectSetPropertyAtIndex is equivalent to calling JSObjectSetProperty with a string containing propertyIndex, but JSObjectSetPropertyAtIndex provides optimized access to numeric properties. +*/ +JS_EXPORT void JSObjectSetPropertyAtIndex(JSContextRef ctx, JSObjectRef object, unsigned propertyIndex, JSValueRef value, JSValueRef* exception); + +/*! +@function +@abstract Gets an object's private data. +@param object A JSObject whose private data you want to get. +@result A void* that is the object's private data, if the object has private data, otherwise NULL. +*/ +JS_EXPORT void* JSObjectGetPrivate(JSObjectRef object); + +/*! +@function +@abstract Sets a pointer to private data on an object. +@param object The JSObject whose private data you want to set. +@param data A void* to set as the object's private data. +@result true if object can store private data, otherwise false. +@discussion The default object class does not allocate storage for private data. Only objects created with a non-NULL JSClass can store private data. +*/ +JS_EXPORT bool JSObjectSetPrivate(JSObjectRef object, void* data); + +/*! +@function +@abstract Tests whether an object can be called as a function. +@param ctx The execution context to use. +@param object The JSObject to test. +@result true if the object can be called as a function, otherwise false. +*/ +JS_EXPORT bool JSObjectIsFunction(JSContextRef ctx, JSObjectRef object); + +/*! +@function +@abstract Calls an object as a function. +@param ctx The execution context to use. +@param object The JSObject to call as a function. +@param thisObject The object to use as "this," or NULL to use the global object as "this." +@param argumentCount An integer count of the number of arguments in arguments. +@param arguments A JSValue array of arguments to pass to the function. Pass NULL if argumentCount is 0. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The JSValue that results from calling object as a function, or NULL if an exception is thrown or object is not a function. +*/ +JS_EXPORT JSValueRef JSObjectCallAsFunction(JSContextRef ctx, JSObjectRef object, JSObjectRef thisObject, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +/*! +@function +@abstract Tests whether an object can be called as a constructor. +@param ctx The execution context to use. +@param object The JSObject to test. +@result true if the object can be called as a constructor, otherwise false. +*/ +JS_EXPORT bool JSObjectIsConstructor(JSContextRef ctx, JSObjectRef object); + +/*! +@function +@abstract Calls an object as a constructor. +@param ctx The execution context to use. +@param object The JSObject to call as a constructor. +@param argumentCount An integer count of the number of arguments in arguments. +@param arguments A JSValue array of arguments to pass to the constructor. Pass NULL if argumentCount is 0. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The JSObject that results from calling object as a constructor, or NULL if an exception is thrown or object is not a constructor. +*/ +JS_EXPORT JSObjectRef JSObjectCallAsConstructor(JSContextRef ctx, JSObjectRef object, size_t argumentCount, const JSValueRef arguments[], JSValueRef* exception); + +/*! +@function +@abstract Gets the names of an object's enumerable properties. +@param ctx The execution context to use. +@param object The object whose property names you want to get. +@result A JSPropertyNameArray containing the names object's enumerable properties. Ownership follows the Create Rule. +*/ +JS_EXPORT JSPropertyNameArrayRef JSObjectCopyPropertyNames(JSContextRef ctx, JSObjectRef object); + +/*! +@function +@abstract Retains a JavaScript property name array. +@param array The JSPropertyNameArray to retain. +@result A JSPropertyNameArray that is the same as array. +*/ +JS_EXPORT JSPropertyNameArrayRef JSPropertyNameArrayRetain(JSPropertyNameArrayRef array); + +/*! +@function +@abstract Releases a JavaScript property name array. +@param array The JSPropetyNameArray to release. +*/ +JS_EXPORT void JSPropertyNameArrayRelease(JSPropertyNameArrayRef array); + +/*! +@function +@abstract Gets a count of the number of items in a JavaScript property name array. +@param array The array from which to retrieve the count. +@result An integer count of the number of names in array. +*/ +JS_EXPORT size_t JSPropertyNameArrayGetCount(JSPropertyNameArrayRef array); + +/*! +@function +@abstract Gets a property name at a given index in a JavaScript property name array. +@param array The array from which to retrieve the property name. +@param index The index of the property name to retrieve. +@result A JSStringRef containing the property name. +*/ +JS_EXPORT JSStringRef JSPropertyNameArrayGetNameAtIndex(JSPropertyNameArrayRef array, size_t index); + +/*! +@function +@abstract Adds a property name to a JavaScript property name accumulator. +@param accumulator The accumulator object to which to add the property name. +@param propertyName The property name to add. +*/ +JS_EXPORT void JSPropertyNameAccumulatorAddName(JSPropertyNameAccumulatorRef accumulator, JSStringRef propertyName); + +#ifdef __cplusplus +} +#endif + +#endif /* JSObjectRef_h */ diff --git a/include/JavaScriptCore/JSObjectRefPrivate.h b/include/JavaScriptCore/JSObjectRefPrivate.h new file mode 100644 index 0000000..6e32612 --- /dev/null +++ b/include/JavaScriptCore/JSObjectRefPrivate.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010-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. + */ + +#ifndef JSObjectRefPrivate_h +#define JSObjectRefPrivate_h + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/*! + @function + @abstract Sets a private property on an object. This private property cannot be accessed from within JavaScript. + @param ctx The execution context to use. + @param object The JSObject whose private property you want to set. + @param propertyName A JSString containing the property's name. + @param value A JSValue to use as the property's value. This may be NULL. + @result true if object can store private data, otherwise false. + @discussion This API allows you to store JS values directly an object in a way that will be ensure that they are kept alive without exposing them to JavaScript code and without introducing the reference cycles that may occur when using JSValueProtect. + + The default object class does not allocate storage for private data. Only objects created with a non-NULL JSClass can store private properties. + */ +JS_EXPORT bool JSObjectSetPrivateProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName, JSValueRef value); + +/*! + @function + @abstract Gets a private property from an object. + @param ctx The execution context to use. + @param object The JSObject whose private property you want to get. + @param propertyName A JSString containing the property's name. + @result The property's value if object has the property, otherwise NULL. + */ +JS_EXPORT JSValueRef JSObjectGetPrivateProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); + +/*! + @function + @abstract Deletes a private property from an object. + @param ctx The execution context to use. + @param object The JSObject whose private property you want to delete. + @param propertyName A JSString containing the property's name. + @result true if object can store private data, otherwise false. + @discussion The default object class does not allocate storage for private data. Only objects created with a non-NULL JSClass can store private data. + */ +JS_EXPORT bool JSObjectDeletePrivateProperty(JSContextRef ctx, JSObjectRef object, JSStringRef propertyName); + +JS_EXPORT JSObjectRef JSObjectGetProxyTarget(JSObjectRef); + +JS_EXPORT JSGlobalContextRef JSObjectGetGlobalContext(JSObjectRef object); + +#ifdef __cplusplus +} +#endif + +#endif // JSObjectRefPrivate_h diff --git a/include/JavaScriptCore/JSRetainPtr.h b/include/JavaScriptCore/JSRetainPtr.h new file mode 100644 index 0000000..be4d4d2 --- /dev/null +++ b/include/JavaScriptCore/JSRetainPtr.h @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2005-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. + */ + +#pragma once + +#include +#include +#include +#include +#include + +#if !defined(WARN_UNUSED_RETURN) +#define WARN_UNUSED_RETURN +#endif + +inline void JSRetain(JSClassRef context) { JSClassRetain(context); } +inline void JSRelease(JSClassRef context) { JSClassRelease(context); } +inline void JSRetain(JSGlobalContextRef context) { JSGlobalContextRetain(context); } +inline void JSRelease(JSGlobalContextRef context) { JSGlobalContextRelease(context); } +inline void JSRetain(JSStringRef string) { JSStringRetain(string); } +inline void JSRelease(JSStringRef string) { JSStringRelease(string); } + +enum AdoptTag { Adopt }; + +template class JSRetainPtr { +public: + JSRetainPtr() = default; + JSRetainPtr(T ptr) : m_ptr(ptr) { if (ptr) JSRetain(ptr); } + JSRetainPtr(const JSRetainPtr&); + JSRetainPtr(JSRetainPtr&&); + ~JSRetainPtr(); + + T get() const { return m_ptr; } + + void clear(); + T leakRef() WARN_UNUSED_RETURN; + + T operator->() const { return m_ptr; } + + bool operator!() const { return !m_ptr; } + explicit operator bool() const { return m_ptr; } + + JSRetainPtr& operator=(const JSRetainPtr&); + JSRetainPtr& operator=(JSRetainPtr&&); + JSRetainPtr& operator=(T); + + void swap(JSRetainPtr&); + + friend JSRetainPtr adopt(JSStringRef); + friend JSRetainPtr adopt(JSGlobalContextRef); + + // FIXME: Make this private once Apple's internal code is updated to not rely on it. + // https://bugs.webkit.org/show_bug.cgi?id=189644 + JSRetainPtr(AdoptTag, T); + +private: + T m_ptr { nullptr }; +}; + +JSRetainPtr adopt(JSClassRef); +JSRetainPtr adopt(JSStringRef); +JSRetainPtr adopt(JSGlobalContextRef); + +template inline JSRetainPtr::JSRetainPtr(AdoptTag, T ptr) + : m_ptr(ptr) +{ +} + +inline JSRetainPtr adopt(JSClassRef o) +{ + return JSRetainPtr(Adopt, o); +} + +inline JSRetainPtr adopt(JSStringRef o) +{ + return JSRetainPtr(Adopt, o); +} + +inline JSRetainPtr adopt(JSGlobalContextRef o) +{ + return JSRetainPtr(Adopt, o); +} + +template inline JSRetainPtr::JSRetainPtr(const JSRetainPtr& o) + : m_ptr(o.m_ptr) +{ + if (m_ptr) + JSRetain(m_ptr); +} + +template inline JSRetainPtr::JSRetainPtr(JSRetainPtr&& o) + : m_ptr(o.leakRef()) +{ +} + +template inline JSRetainPtr::~JSRetainPtr() +{ + if (m_ptr) + JSRelease(m_ptr); +} + +template inline void JSRetainPtr::clear() +{ + if (T ptr = leakRef()) + JSRelease(ptr); +} + +template inline T JSRetainPtr::leakRef() +{ + return std::exchange(m_ptr, nullptr); +} + +template inline JSRetainPtr& JSRetainPtr::operator=(const JSRetainPtr& o) +{ + return operator=(o.get()); +} + +template inline JSRetainPtr& JSRetainPtr::operator=(JSRetainPtr&& o) +{ + if (T ptr = std::exchange(m_ptr, o.leakRef())) + JSRelease(ptr); + return *this; +} + +template inline JSRetainPtr& JSRetainPtr::operator=(T optr) +{ + if (optr) + JSRetain(optr); + if (T ptr = std::exchange(m_ptr, optr)) + JSRelease(ptr); + return *this; +} + +template inline void JSRetainPtr::swap(JSRetainPtr& o) +{ + std::swap(m_ptr, o.m_ptr); +} + +template inline void swap(JSRetainPtr& a, JSRetainPtr& b) +{ + a.swap(b); +} + +template inline bool operator==(const JSRetainPtr& a, const JSRetainPtr& b) +{ + return a.get() == b.get(); +} + +template inline bool operator==(const JSRetainPtr& a, U* b) +{ + return a.get() == b; +} + +template inline bool operator==(T* a, const JSRetainPtr& b) +{ + return a == b.get(); +} + +template inline bool operator!=(const JSRetainPtr& a, const JSRetainPtr& b) +{ + return a.get() != b.get(); +} + +template inline bool operator!=(const JSRetainPtr& a, U* b) +{ + return a.get() != b; +} + +template inline bool operator!=(T* a, const JSRetainPtr& b) +{ + return a != b.get(); +} diff --git a/include/JavaScriptCore/JSStringRef.h b/include/JavaScriptCore/JSStringRef.h new file mode 100644 index 0000000..bc03ed7 --- /dev/null +++ b/include/JavaScriptCore/JSStringRef.h @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2006 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. + */ + +#ifndef JSStringRef_h +#define JSStringRef_h + +#include + +#ifndef __cplusplus +#include +#endif +#include /* for size_t */ + +#ifdef __cplusplus +extern "C" { +#endif + +#if !defined(_NATIVE_WCHAR_T_DEFINED) /* MSVC */ \ + && (!defined(__WCHAR_MAX__) || (__WCHAR_MAX__ > 0xffffU)) /* ISO C/C++ */ \ + && (!defined(WCHAR_MAX) || (WCHAR_MAX > 0xffffU)) /* RVCT */ +/*! +@typedef JSChar +@abstract A UTF-16 code unit. One, or a sequence of two, can encode any Unicode + character. As with all scalar types, endianness depends on the underlying + architecture. +*/ + typedef unsigned short JSChar; +#else + typedef wchar_t JSChar; +#endif + +/*! +@function +@abstract Creates a JavaScript string from a buffer of Unicode characters. +@param chars The buffer of Unicode characters to copy into the new JSString. +@param numChars The number of characters to copy from the buffer pointed to by chars. +@result A JSString containing chars. Ownership follows the Create Rule. +*/ +JS_EXPORT JSStringRef JSStringCreateWithCharacters(const JSChar* chars, size_t numChars); +/*! +@function +@abstract Creates a JavaScript string from a null-terminated UTF8 string. +@param string The null-terminated UTF8 string to copy into the new JSString. +@result A JSString containing string. Ownership follows the Create Rule. +*/ +JS_EXPORT JSStringRef JSStringCreateWithUTF8CString(const char* string); + +/*! +@function +@abstract Retains a JavaScript string. +@param string The JSString to retain. +@result A JSString that is the same as string. +*/ +JS_EXPORT JSStringRef JSStringRetain(JSStringRef string); +/*! +@function +@abstract Releases a JavaScript string. +@param string The JSString to release. +*/ +JS_EXPORT void JSStringRelease(JSStringRef string); + +/*! +@function +@abstract Returns the number of Unicode characters in a JavaScript string. +@param string The JSString whose length (in Unicode characters) you want to know. +@result The number of Unicode characters stored in string. +*/ +JS_EXPORT size_t JSStringGetLength(JSStringRef string); +/*! +@function +@abstract Returns a pointer to the Unicode character buffer that + serves as the backing store for a JavaScript string. +@param string The JSString whose backing store you want to access. +@result A pointer to the Unicode character buffer that serves as string's + backing store, which will be deallocated when string is deallocated. +*/ +JS_EXPORT const JSChar* JSStringGetCharactersPtr(JSStringRef string); + +/*! +@function +@abstract Returns the maximum number of bytes a JavaScript string will + take up if converted into a null-terminated UTF8 string. +@param string The JSString whose maximum converted size (in bytes) you + want to know. +@result The maximum number of bytes that could be required to convert string into a + null-terminated UTF8 string. The number of bytes that the conversion actually ends + up requiring could be less than this, but never more. +*/ +JS_EXPORT size_t JSStringGetMaximumUTF8CStringSize(JSStringRef string); +/*! +@function +@abstract Converts a JavaScript string into a null-terminated UTF8 string, + and copies the result into an external byte buffer. +@param string The source JSString. +@param buffer The destination byte buffer into which to copy a null-terminated + UTF8 representation of string. On return, buffer contains a UTF8 string + representation of string. If bufferSize is too small, buffer will contain only + partial results. If buffer is not at least bufferSize bytes in size, + behavior is undefined. +@param bufferSize The size of the external buffer in bytes. +@result The number of bytes written into buffer (including the null-terminator byte). +*/ +JS_EXPORT size_t JSStringGetUTF8CString(JSStringRef string, char* buffer, size_t bufferSize); + +/*! +@function +@abstract Tests whether two JavaScript strings match. +@param a The first JSString to test. +@param b The second JSString to test. +@result true if the two strings match, otherwise false. +*/ +JS_EXPORT bool JSStringIsEqual(JSStringRef a, JSStringRef b); +/*! +@function +@abstract Tests whether a JavaScript string matches a null-terminated UTF8 string. +@param a The JSString to test. +@param b The null-terminated UTF8 string to test. +@result true if the two strings match, otherwise false. +*/ +JS_EXPORT bool JSStringIsEqualToUTF8CString(JSStringRef a, const char* b); + +#ifdef __cplusplus +} +#endif + +#endif /* JSStringRef_h */ diff --git a/include/JavaScriptCore/JSTypedArray.h b/include/JavaScriptCore/JSTypedArray.h new file mode 100644 index 0000000..7eaf76c --- /dev/null +++ b/include/JavaScriptCore/JSTypedArray.h @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2015 Dominic Szablewski (dominic@phoboslab.org) + * 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 COMPUTER, 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 COMPUTER, 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. + */ + +#ifndef JSTypedArray_h +#define JSTypedArray_h + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// ------------- Typed Array functions -------------- + +/*! + @function + @abstract Creates a JavaScript Typed Array object with the given number of elements. + @param ctx The execution context to use. + @param arrayType A value identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. + @param length The number of elements to be in the new Typed Array. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef that is a Typed Array with all elements set to zero or NULL if there was an error. + */ +JS_EXPORT JSObjectRef JSObjectMakeTypedArray(JSContextRef ctx, JSTypedArrayType arrayType, size_t length, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Creates a JavaScript Typed Array object from an existing pointer. + @param ctx The execution context to use. + @param arrayType A value identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. + @param bytes A pointer to the byte buffer to be used as the backing store of the Typed Array object. + @param byteLength The number of bytes pointed to by the parameter bytes. + @param bytesDeallocator The allocator to use to deallocate the external buffer when the JSTypedArrayData object is deallocated. + @param deallocatorContext A pointer to pass back to the deallocator. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef Typed Array whose backing store is the same as the one pointed to by bytes or NULL if there was an error. + @discussion If an exception is thrown during this function the bytesDeallocator will always be called. + */ +JS_EXPORT JSObjectRef JSObjectMakeTypedArrayWithBytesNoCopy(JSContextRef ctx, JSTypedArrayType arrayType, void* bytes, size_t byteLength, JSTypedArrayBytesDeallocator bytesDeallocator, void* deallocatorContext, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Creates a JavaScript Typed Array object from an existing JavaScript Array Buffer object. + @param ctx The execution context to use. + @param arrayType A value identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. + @param buffer An Array Buffer object that should be used as the backing store for the created JavaScript Typed Array object. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef that is a Typed Array or NULL if there was an error. The backing store of the Typed Array will be buffer. + */ +JS_EXPORT JSObjectRef JSObjectMakeTypedArrayWithArrayBuffer(JSContextRef ctx, JSTypedArrayType arrayType, JSObjectRef buffer, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Creates a JavaScript Typed Array object from an existing JavaScript Array Buffer object with the given offset and length. + @param ctx The execution context to use. + @param arrayType A value identifying the type of array to create. If arrayType is kJSTypedArrayTypeNone or kJSTypedArrayTypeArrayBuffer then NULL will be returned. + @param buffer An Array Buffer object that should be used as the backing store for the created JavaScript Typed Array object. + @param byteOffset The byte offset for the created Typed Array. byteOffset should aligned with the element size of arrayType. + @param length The number of elements to include in the Typed Array. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef that is a Typed Array or NULL if there was an error. The backing store of the Typed Array will be buffer. + */ +JS_EXPORT JSObjectRef JSObjectMakeTypedArrayWithArrayBufferAndOffset(JSContextRef ctx, JSTypedArrayType arrayType, JSObjectRef buffer, size_t byteOffset, size_t length, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns a temporary pointer to the backing store of a JavaScript Typed Array object. + @param ctx The execution context to use. + @param object The Typed Array object whose backing store pointer to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A pointer to the raw data buffer that serves as object's backing store or NULL if object is not a Typed Array object. + @discussion The pointer returned by this function is temporary and is not guaranteed to remain valid across JavaScriptCore API calls. + */ +JS_EXPORT void* JSObjectGetTypedArrayBytesPtr(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns the length of a JavaScript Typed Array object. + @param ctx The execution context to use. + @param object The Typed Array object whose length to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result The length of the Typed Array object or 0 if the object is not a Typed Array object. + */ +JS_EXPORT size_t JSObjectGetTypedArrayLength(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns the byte length of a JavaScript Typed Array object. + @param ctx The execution context to use. + @param object The Typed Array object whose byte length to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result The byte length of the Typed Array object or 0 if the object is not a Typed Array object. + */ +JS_EXPORT size_t JSObjectGetTypedArrayByteLength(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns the byte offset of a JavaScript Typed Array object. + @param ctx The execution context to use. + @param object The Typed Array object whose byte offset to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result The byte offset of the Typed Array object or 0 if the object is not a Typed Array object. + */ +JS_EXPORT size_t JSObjectGetTypedArrayByteOffset(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns the JavaScript Array Buffer object that is used as the backing of a JavaScript Typed Array object. + @param ctx The execution context to use. + @param object The JSObjectRef whose Typed Array type data pointer to obtain. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef with a JSTypedArrayType of kJSTypedArrayTypeArrayBuffer or NULL if object is not a Typed Array. + */ +JS_EXPORT JSObjectRef JSObjectGetTypedArrayBuffer(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +// ------------- Array Buffer functions ------------- + +/*! + @function + @abstract Creates a JavaScript Array Buffer object from an existing pointer. + @param ctx The execution context to use. + @param bytes A pointer to the byte buffer to be used as the backing store of the Typed Array object. + @param byteLength The number of bytes pointed to by the parameter bytes. + @param bytesDeallocator The allocator to use to deallocate the external buffer when the Typed Array data object is deallocated. + @param deallocatorContext A pointer to pass back to the deallocator. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSObjectRef Array Buffer whose backing store is the same as the one pointed to by bytes or NULL if there was an error. + @discussion If an exception is thrown during this function the bytesDeallocator will always be called. + */ +JS_EXPORT JSObjectRef JSObjectMakeArrayBufferWithBytesNoCopy(JSContextRef ctx, void* bytes, size_t byteLength, JSTypedArrayBytesDeallocator bytesDeallocator, void* deallocatorContext, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns a pointer to the data buffer that serves as the backing store for a JavaScript Typed Array object. + @param object The Array Buffer object whose internal backing store pointer to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A pointer to the raw data buffer that serves as object's backing store or NULL if object is not an Array Buffer object. + @discussion The pointer returned by this function is temporary and is not guaranteed to remain valid across JavaScriptCore API calls. + */ +JS_EXPORT void* JSObjectGetArrayBufferBytesPtr(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/*! + @function + @abstract Returns the number of bytes in a JavaScript data object. + @param ctx The execution context to use. + @param object The JS Arary Buffer object whose length in bytes to return. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result The number of bytes stored in the data object. + */ +JS_EXPORT size_t JSObjectGetArrayBufferByteLength(JSContextRef ctx, JSObjectRef object, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +#ifdef __cplusplus +} +#endif + +#endif /* JSTypedArray_h */ diff --git a/include/JavaScriptCore/JSValueRef.h b/include/JavaScriptCore/JSValueRef.h new file mode 100644 index 0000000..2a9079a --- /dev/null +++ b/include/JavaScriptCore/JSValueRef.h @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2006-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. ``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. + */ + +#ifndef JSValueRef_h +#define JSValueRef_h + +#include +#include + +#ifndef __cplusplus +#include +#endif + +/*! +@enum JSType +@abstract A constant identifying the type of a JSValue. +@constant kJSTypeUndefined The unique undefined value. +@constant kJSTypeNull The unique null value. +@constant kJSTypeBoolean A primitive boolean value, one of true or false. +@constant kJSTypeNumber A primitive number value. +@constant kJSTypeString A primitive string value. +@constant kJSTypeObject An object value (meaning that this JSValueRef is a JSObjectRef). +@constant kJSTypeSymbol A primitive symbol value. +*/ +typedef enum { + kJSTypeUndefined, + kJSTypeNull, + kJSTypeBoolean, + kJSTypeNumber, + kJSTypeString, + kJSTypeObject, + kJSTypeSymbol JSC_API_AVAILABLE(macos(10.15), ios(13.0)) +} JSType; + +/*! + @enum JSTypedArrayType + @abstract A constant identifying the Typed Array type of a JSObjectRef. + @constant kJSTypedArrayTypeInt8Array Int8Array + @constant kJSTypedArrayTypeInt16Array Int16Array + @constant kJSTypedArrayTypeInt32Array Int32Array + @constant kJSTypedArrayTypeUint8Array Uint8Array + @constant kJSTypedArrayTypeUint8ClampedArray Uint8ClampedArray + @constant kJSTypedArrayTypeUint16Array Uint16Array + @constant kJSTypedArrayTypeUint32Array Uint32Array + @constant kJSTypedArrayTypeFloat32Array Float32Array + @constant kJSTypedArrayTypeFloat64Array Float64Array + @constant kJSTypedArrayTypeBigInt64Array BigInt64Array + @constant kJSTypedArrayTypeBigUint64Array BigUint64Array + @constant kJSTypedArrayTypeArrayBuffer ArrayBuffer + @constant kJSTypedArrayTypeNone Not a Typed Array + + */ +typedef enum { + kJSTypedArrayTypeInt8Array, + kJSTypedArrayTypeInt16Array, + kJSTypedArrayTypeInt32Array, + kJSTypedArrayTypeUint8Array, + kJSTypedArrayTypeUint8ClampedArray, + kJSTypedArrayTypeUint16Array, + kJSTypedArrayTypeUint32Array, + kJSTypedArrayTypeFloat32Array, + kJSTypedArrayTypeFloat64Array, + kJSTypedArrayTypeArrayBuffer, + kJSTypedArrayTypeNone, + kJSTypedArrayTypeBigInt64Array, + kJSTypedArrayTypeBigUint64Array, +} JSTypedArrayType JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +#ifdef __cplusplus +extern "C" { +#endif + +/*! +@function +@abstract Returns a JavaScript value's type. +@param ctx The execution context to use. +@param value The JSValue whose type you want to obtain. +@result A value of type JSType that identifies value's type. +*/ +JS_EXPORT JSType JSValueGetType(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the undefined type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the undefined type, otherwise false. +*/ +JS_EXPORT bool JSValueIsUndefined(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the null type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the null type, otherwise false. +*/ +JS_EXPORT bool JSValueIsNull(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the boolean type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the boolean type, otherwise false. +*/ +JS_EXPORT bool JSValueIsBoolean(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the number type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the number type, otherwise false. +*/ +JS_EXPORT bool JSValueIsNumber(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the string type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the string type, otherwise false. +*/ +JS_EXPORT bool JSValueIsString(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the symbol type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the symbol type, otherwise false. +*/ +JS_EXPORT bool JSValueIsSymbol(JSContextRef ctx, JSValueRef value) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/*! +@function +@abstract Tests whether a JavaScript value's type is the object type. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value's type is the object type, otherwise false. +*/ +JS_EXPORT bool JSValueIsObject(JSContextRef ctx, JSValueRef value); + + +/*! +@function +@abstract Tests whether a JavaScript value is an object with a given class in its class chain. +@param ctx The execution context to use. +@param value The JSValue to test. +@param jsClass The JSClass to test against. +@result true if value is an object and has jsClass in its class chain, otherwise false. +*/ +JS_EXPORT bool JSValueIsObjectOfClass(JSContextRef ctx, JSValueRef value, JSClassRef jsClass); + +/*! +@function +@abstract Tests whether a JavaScript value is an array. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value is an array, otherwise false. +*/ +JS_EXPORT bool JSValueIsArray(JSContextRef ctx, JSValueRef value) JSC_API_AVAILABLE(macos(10.11), ios(9.0)); + +/*! +@function +@abstract Tests whether a JavaScript value is a date. +@param ctx The execution context to use. +@param value The JSValue to test. +@result true if value is a date, otherwise false. +*/ +JS_EXPORT bool JSValueIsDate(JSContextRef ctx, JSValueRef value) JSC_API_AVAILABLE(macos(10.11), ios(9.0)); + +/*! +@function +@abstract Returns a JavaScript value's Typed Array type. +@param ctx The execution context to use. +@param value The JSValue whose Typed Array type to return. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result A value of type JSTypedArrayType that identifies value's Typed Array type, or kJSTypedArrayTypeNone if the value is not a Typed Array object. + */ +JS_EXPORT JSTypedArrayType JSValueGetTypedArrayType(JSContextRef ctx, JSValueRef value, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.12), ios(10.0)); + +/* Comparing values */ + +/*! +@function +@abstract Tests whether two JavaScript values are equal, as compared by the JS == operator. +@param ctx The execution context to use. +@param a The first value to test. +@param b The second value to test. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result true if the two values are equal, false if they are not equal or an exception is thrown. +*/ +JS_EXPORT bool JSValueIsEqual(JSContextRef ctx, JSValueRef a, JSValueRef b, JSValueRef* exception); + +/*! +@function +@abstract Tests whether two JavaScript values are strict equal, as compared by the JS === operator. +@param ctx The execution context to use. +@param a The first value to test. +@param b The second value to test. +@result true if the two values are strict equal, otherwise false. +*/ +JS_EXPORT bool JSValueIsStrictEqual(JSContextRef ctx, JSValueRef a, JSValueRef b); + +/*! +@function +@abstract Tests whether a JavaScript value is an object constructed by a given constructor, as compared by the JS instanceof operator. +@param ctx The execution context to use. +@param value The JSValue to test. +@param constructor The constructor to test against. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result true if value is an object constructed by constructor, as compared by the JS instanceof operator, otherwise false. +*/ +JS_EXPORT bool JSValueIsInstanceOfConstructor(JSContextRef ctx, JSValueRef value, JSObjectRef constructor, JSValueRef* exception); + +/* Creating values */ + +/*! +@function +@abstract Creates a JavaScript value of the undefined type. +@param ctx The execution context to use. +@result The unique undefined value. +*/ +JS_EXPORT JSValueRef JSValueMakeUndefined(JSContextRef ctx); + +/*! +@function +@abstract Creates a JavaScript value of the null type. +@param ctx The execution context to use. +@result The unique null value. +*/ +JS_EXPORT JSValueRef JSValueMakeNull(JSContextRef ctx); + +/*! +@function +@abstract Creates a JavaScript value of the boolean type. +@param ctx The execution context to use. +@param boolean The bool to assign to the newly created JSValue. +@result A JSValue of the boolean type, representing the value of boolean. +*/ +JS_EXPORT JSValueRef JSValueMakeBoolean(JSContextRef ctx, bool boolean); + +/*! +@function +@abstract Creates a JavaScript value of the number type. +@param ctx The execution context to use. +@param number The double to assign to the newly created JSValue. +@result A JSValue of the number type, representing the value of number. +*/ +JS_EXPORT JSValueRef JSValueMakeNumber(JSContextRef ctx, double number); + +/*! +@function +@abstract Creates a JavaScript value of the string type. +@param ctx The execution context to use. +@param string The JSString to assign to the newly created JSValue. The + newly created JSValue retains string, and releases it upon garbage collection. +@result A JSValue of the string type, representing the value of string. +*/ +JS_EXPORT JSValueRef JSValueMakeString(JSContextRef ctx, JSStringRef string); + +/*! + @function + @abstract Creates a JavaScript value of the symbol type. + @param ctx The execution context to use. + @param description A description of the newly created symbol value. + @result A unique JSValue of the symbol type, whose description matches the one provided. + */ +JS_EXPORT JSValueRef JSValueMakeSymbol(JSContextRef ctx, JSStringRef description) JSC_API_AVAILABLE(macos(10.15), ios(13.0)); + +/* Converting to and from JSON formatted strings */ + +/*! + @function + @abstract Creates a JavaScript value from a JSON formatted string. + @param ctx The execution context to use. + @param string The JSString containing the JSON string to be parsed. + @result A JSValue containing the parsed value, or NULL if the input is invalid. + */ +JS_EXPORT JSValueRef JSValueMakeFromJSONString(JSContextRef ctx, JSStringRef string) JSC_API_AVAILABLE(macos(10.7), ios(7.0)); + +/*! + @function + @abstract Creates a JavaScript string containing the JSON serialized representation of a JS value. + @param ctx The execution context to use. + @param value The value to serialize. + @param indent The number of spaces to indent when nesting. If 0, the resulting JSON will not contains newlines. The size of the indent is clamped to 10 spaces. + @param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. + @result A JSString with the result of serialization, or NULL if an exception is thrown. + */ +JS_EXPORT JSStringRef JSValueCreateJSONString(JSContextRef ctx, JSValueRef value, unsigned indent, JSValueRef* exception) JSC_API_AVAILABLE(macos(10.7), ios(7.0)); + +/* Converting to primitive values */ + +/*! +@function +@abstract Converts a JavaScript value to boolean and returns the resulting boolean. +@param ctx The execution context to use. +@param value The JSValue to convert. +@result The boolean result of conversion. +*/ +JS_EXPORT bool JSValueToBoolean(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Converts a JavaScript value to number and returns the resulting number. +@param ctx The execution context to use. +@param value The JSValue to convert. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The numeric result of conversion, or NaN if an exception is thrown. +*/ +JS_EXPORT double JSValueToNumber(JSContextRef ctx, JSValueRef value, JSValueRef* exception); + +/*! +@function +@abstract Converts a JavaScript value to string and copies the result into a JavaScript string. +@param ctx The execution context to use. +@param value The JSValue to convert. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result A JSString with the result of conversion, or NULL if an exception is thrown. Ownership follows the Create Rule. +*/ +JS_EXPORT JSStringRef JSValueToStringCopy(JSContextRef ctx, JSValueRef value, JSValueRef* exception); + +/*! +@function +@abstract Converts a JavaScript value to object and returns the resulting object. +@param ctx The execution context to use. +@param value The JSValue to convert. +@param exception A pointer to a JSValueRef in which to store an exception, if any. Pass NULL if you do not care to store an exception. +@result The JSObject result of conversion, or NULL if an exception is thrown. +*/ +JS_EXPORT JSObjectRef JSValueToObject(JSContextRef ctx, JSValueRef value, JSValueRef* exception); + +/* Garbage collection */ +/*! +@function +@abstract Protects a JavaScript value from garbage collection. +@param ctx The execution context to use. +@param value The JSValue to protect. +@discussion Use this method when you want to store a JSValue in a global or on the heap, where the garbage collector will not be able to discover your reference to it. + +A value may be protected multiple times and must be unprotected an equal number of times before becoming eligible for garbage collection. +*/ +JS_EXPORT void JSValueProtect(JSContextRef ctx, JSValueRef value); + +/*! +@function +@abstract Unprotects a JavaScript value from garbage collection. +@param ctx The execution context to use. +@param value The JSValue to unprotect. +@discussion A value may be protected multiple times and must be unprotected an + equal number of times before becoming eligible for garbage collection. +*/ +JS_EXPORT void JSValueUnprotect(JSContextRef ctx, JSValueRef value); + +#ifdef __cplusplus +} +#endif + +#endif /* JSValueRef_h */ diff --git a/include/JavaScriptCore/JavaScript.h b/include/JavaScriptCore/JavaScript.h new file mode 100644 index 0000000..251e393 --- /dev/null +++ b/include/JavaScriptCore/JavaScript.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2006 Apple Inc. All rights reserved. + * Copyright (C) 2008 Alp Toker + * + * 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. + */ + +#ifndef JavaScript_h +#define JavaScript_h + +#include +#include +#include +#include +#include +#include + +#endif /* JavaScript_h */ diff --git a/include/JavaScriptCore/WebKitAvailability.h b/include/JavaScriptCore/WebKitAvailability.h new file mode 100644 index 0000000..3b02398 --- /dev/null +++ b/include/JavaScriptCore/WebKitAvailability.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008, 2009, 2010, 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. ``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. + */ + +#ifndef __WebKitAvailability__ +#define __WebKitAvailability__ + +#if defined(__APPLE__) && defined(DEFINE_AVAILABILITY_MACROS) + +#include +#include + +#if defined(BUILDING_GTK__) || defined(BUILDING_JSCONLY__) +#undef JSC_API_AVAILABLE +#define JSC_API_AVAILABLE(...) + +#undef JSC_API_DEPRECATED +#define JSC_API_DEPRECATED(...) + +#undef JSC_API_DEPRECATED_WITH_REPLACEMENT +#define JSC_API_DEPRECATED_WITH_REPLACEMENT(...) +#endif + +#else +#define JSC_API_AVAILABLE(...) +#define JSC_API_DEPRECATED(...) +#define JSC_API_DEPRECATED_WITH_REPLACEMENT(...) +#endif + +#endif /* __WebKitAvailability__ */ diff --git a/include/Ultralight/Bitmap.h b/include/Ultralight/Bitmap.h new file mode 100644 index 0000000..081adcd --- /dev/null +++ b/include/Ultralight/Bitmap.h @@ -0,0 +1,436 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +/// +/// The various Bitmap formats. +/// +enum class BitmapFormat : uint8_t { + /// + /// Alpha channel only, 8-bits per pixel. + /// + /// Encoding: 8-bits per channel, unsigned normalized. + /// + /// Color-space: Linear (no gamma), alpha-coverage only. + /// + A8_UNORM, + + /// + /// Blue Green Red Alpha channels, 32-bits per pixel. + /// + /// Encoding: 8-bits per channel, unsigned normalized. + /// + /// Color-space: sRGB gamma with premultiplied linear alpha channel. + /// + BGRA8_UNORM_SRGB, +}; + +/// +/// Macro to get the bytes per pixel from a BitmapFormat +/// +#define GetBytesPerPixel(x) (x == BitmapFormat::A8_UNORM ? 1 : 4) + +/// +/// Forward declaration for the LockedPixels class. +/// +template +class LockedPixels; + +/// +/// Function signature for a user-defined destruction callback to be optionally called when the +/// Bitmap is destroyed. +/// +/// @param user_data Pointer to user-defined user-data (this will be the same value as what was +/// passed to Bitmap::Create, if any) +/// +/// @param data Pointer to raw Bitmap pixel data. +/// +typedef void (*DestroyBitmapCallback)(void* user_data, void* data); + +/// +/// A thread-safe container for pixel data. +/// +/// The bitmap class is used to store pixel data in a variety of formats. It intelligently manages +/// the lifetime of the pixel buffer and provides thread-safe access to the pixel data. +/// +/// ## Accessing Pixel Data +/// +/// You can access the underlying pixel data by using the LockPixelsSafe() method. An example +/// follows: +/// +/// ``` +/// auto bitmap = Bitmap::Create(100, 100, BitmapFormat::BGRA8_UNORM_SRGB); +/// auto pixels = bitmap->LockPixelsSafe(); +/// if (pixels && pixels.data()) { +/// // Zero out the pixel buffer by setting every byte to 0. +/// memset(pixels.data(), 0, pixels.size()); +/// } +/// +/// // 'pixels' is automatically unlocked when it goes out of scope. +/// ``` +class UExport Bitmap : public RefCounted { + public: + /// + /// Create an empty Bitmap. No pixels will be allocated. + /// + static RefPtr Create(); + + /// + /// Create a Bitmap with a certain configuration. Pixels will be allocated but not initialized. + /// + /// @param width The width in pixels. + /// + /// @param height The height in pixels. + /// + /// @param format The pixel format to use. + /// + /// @return A ref-pointer to a new Bitmap instance. + /// + static RefPtr Create(uint32_t width, uint32_t height, BitmapFormat format); + + /// + /// Create an aligned Bitmap with a certain configuration. Pixels will be allocated but not + /// initialized. Row bytes will be padded to reach the specified alignment. + /// + /// @param width The width in pixels. + /// + /// @param height The height in pixels. + /// + /// @param format The pixel format to use. + /// + /// @param alignment The alignment (in bytes) to use. Row bytes will be padded to reach a + /// multiple of this value and the underlying storage will be allocated with + /// this alignment. + /// + /// @return A ref-pointer to a new Bitmap instance. + /// + static RefPtr Create(uint32_t width, uint32_t height, BitmapFormat format, + uint32_t alignment); + + /// + /// Create a Bitmap with existing pixel data, a copy will be made unless should_copy is false. + /// + /// @param width The width in pixels. + /// + /// @param height The height in pixels. + /// + /// @param format The pixel format to use. + /// + /// @param row_bytes The number of bytes between each row (note that this value should be >= + /// width * bytes_per_pixel). + /// + /// @param pixels Pointer to raw pixel buffer. + /// + /// @param size Size of the raw pixel buffer. + /// + /// @param should_copy Whether or not a copy should be made of the pixels. If this is false + /// the returned Bitmap will use the raw pixels passed in as its own, but + /// you are still responsible for destroying your buffer afterwards. + /// + /// @return A ref-pointer to a new Bitmap instance. + /// + static RefPtr Create(uint32_t width, uint32_t height, BitmapFormat format, + uint32_t row_bytes, const void* pixels, size_t size, + bool should_copy = true); + + /// + /// Create a Bitmap that wraps existing pixel data, a user-defined destruction callback will be + /// called when the Bitmap wants to destroy the data. + /// + /// @param width The width in pixels. + /// + /// @param height The height in pixels. + /// + /// @param format The pixel format to use. + /// + /// @param row_bytes The number of bytes between each row (note that this value should be >= + /// width * bytes_per_pixel). + /// + /// @param pixels Pointer to raw pixel buffer. + /// + /// @param size Size of the raw pixel buffer. + /// + /// @param user_data Optional user data that will be passed to destruction_callback when the + /// Bitmap wants to destroy the pixel data. (Pass nullptr to ignore) + /// + /// @param destruction_callback Callback that will be called upon destruction. + /// + /// @return A ref-pointer to a new Bitmap instance. + /// + static RefPtr Create(uint32_t width, uint32_t height, BitmapFormat format, + uint32_t row_bytes, const void* pixels, size_t size, + void* user_data, DestroyBitmapCallback destruction_callback); + + /// + /// Create a bitmap from a deep copy of another Bitmap. + /// + static RefPtr Create(const Bitmap& bitmap); + + /// + /// Get the width in pixels. + /// + virtual uint32_t width() const = 0; + + /// + /// Get the height in pixels. + /// + virtual uint32_t height() const = 0; + + /// + /// Get the bounds as an IntRect + /// + virtual IntRect bounds() const = 0; + + /// + /// Get the pixel format. + /// + virtual BitmapFormat format() const = 0; + + /// + /// Get the number of bytes per pixel. + /// + virtual uint32_t bpp() const = 0; + + /// + /// Get the number of bytes between each row of pixels. + /// + /// @note This value is usually calculated as width * bytes_per_pixel (bpp) but it may be larger + /// due to alignment rules in the allocator. + /// + virtual uint32_t row_bytes() const = 0; + + /// + /// Get the size in bytes of the pixel buffer. + /// + /// @note Size is calculated as row_bytes() * height(). + /// + virtual size_t size() const = 0; + + /// + /// Whether or not this Bitmap owns the pixel buffer and will destroy it at the end of its + /// lifetime. + /// + virtual bool owns_pixels() const = 0; + + /// + /// Lock the pixel buffer for reading/writing (safe version, automatically unlocks). + /// + /// @return A managed container that can be used to access the pixels (LockedPixels::data()). + /// This container will automatically unlock the pixels when it goes out of scope. + /// + virtual LockedPixels> LockPixelsSafe() const = 0; + + /// + /// Lock the pixel buffer for reading/writing. + /// + /// @return A pointer to the pixel buffer. + /// + virtual void* LockPixels() = 0; + + /// + /// Unlock the pixel buffer. + /// + virtual void UnlockPixels() = 0; + + /// + /// Lock the pixel buffer for reading/writing. (const) + /// + /// @return A const pointer to the pixel buffer. + /// + virtual const void* LockPixels() const = 0; + + /// + /// Unlock the pixel buffer. (const) + /// + virtual void UnlockPixels() const = 0; + + /// + /// Get the raw pixel buffer. + /// + /// @note You should only call this if pixels are already locked. + /// + virtual void* raw_pixels() = 0; + + /// + /// Whether or not this Bitmap is empty (no pixels allocated). + /// + virtual bool IsEmpty() const = 0; + + /// + /// Erase the Bitmap (set all pixels to 0). + /// + virtual void Erase() = 0; + + /// + /// Assign another bitmap to this one. + /// + /// @param bitmap The bitmap to copy from. + /// + virtual void Set(RefPtr bitmap) = 0; + + /// + /// Draw another bitmap to this bitmap. + /// + /// @note Formats do not need to match. Bitmap formats will be converted to one another + /// automatically. Note that when converting from BGRA8 to A8, only the Blue channel will + /// be used. + /// + /// @param src_rect The source rectangle, relative to src bitmap. + /// + /// @param dest_rect The destination rectangle, relative to this bitmap. + /// + /// @param src The source bitmap. + /// + /// @param pad_repeat Whether or not we should pad the drawn bitmap by one pixel of repeated + /// edge pixels from the source bitmap. + /// + /// @return Whether or not the operation succeeded (this can fail if the src_rect and/or + /// dest_rect are invalid). + /// + virtual bool DrawBitmap(IntRect src_rect, IntRect dest_rect, RefPtr src, bool pad_repeat) + = 0; + + /// + /// Encode this Bitmap as a PNG image and store the encoded bytes in a Buffer. + /// + /// @param convert_to_rgba The PNG format expects RGBA format but our bitmap is stored as BGRA, + /// set this to true to perform the conversion automatically. + /// + /// @param convert_to_straight_alpha The PNG format expects semi-transparent values to be + /// stored as straight alpha instead of premultiplied alpha, + /// set this to true to perform the conversion automatically. + /// + /// @return On success, a buffer containing the encoded bytes, otherwise a null RefPtr. + /// + virtual RefPtr EncodePNG(bool convert_to_rgba = true, + bool convert_to_straight_alpha = true) const = 0; + + /// + /// Write this Bitmap out to a PNG image. + /// + /// @param path The filepath to write to (opened with fopen()) + /// + /// @param convert_to_rgba The PNG format expects RGBA format but our bitmap is stored as BGRA, + /// set this to true to perform the conversion automatically. + /// + /// @param convert_to_straight_alpha The PNG format expects semi-transparent values to be + /// stored as straight alpha instead of premultiplied alpha, + /// set this to true to perform the conversion automatically. + /// + /// @return Whether or not the operation succeeded. + /// + virtual bool WritePNG(const char* path, bool convert_to_rgba = true, + bool convert_to_straight_alpha = true) const = 0; + + /// + /// Make a resized copy of this bitmap by writing to a pre-allocated destination bitmap. + /// + /// @param destination The bitmap to store the result in, the width and height of the + /// destination will be used. + /// + /// @param high_quality Whether or not a high quality resampling will be used during the + /// resize. (Otherwise, just uses fast nearest-neighbor sampling) + /// + /// @return Whether or not the operation succeeded. This operation is only valid if both formats + /// are BitmapFormat::BGRA8_UNORM_SRGB and the source and destination are non-empty. + /// + virtual bool Resample(RefPtr destination, bool high_quality) = 0; + + /// + /// Convert a BGRA bitmap to RGBA bitmap and vice-versa by swapping the red and blue channels. + /// + /// @note Only valid if the format is BitmapFormat::BGRA8_UNORM_SRGB + /// + virtual void SwapRedBlueChannels() = 0; + + /// + /// Convert a BGRA bitmap from premultiplied alpha (the default) to straight alpha. + /// + /// @note Only valid if the format is BitmapFormat::BGRA8_UNORM_SRGB + /// + virtual void ConvertToStraightAlpha() = 0; + + /// + /// Convert a BGRA bitmap from straight alpha to premultiplied alpha. + /// + /// @note Only valid if the format is BitmapFormat::BGRA8_UNORM_SRGB + /// + virtual void ConvertToPremultipliedAlpha() = 0; + + protected: + Bitmap(); + virtual ~Bitmap(); + Bitmap(const Bitmap&); + void operator=(const Bitmap&); +}; + +template +class LockedPixels { + public: + LockedPixels(const LockedPixels&) = delete; + LockedPixels& operator=(const LockedPixels&) = delete; + LockedPixels(int) = delete; + explicit LockedPixels(T& lockable) : lockable_(lockable), data_(nullptr), size_(0) { lock(); } + + ~LockedPixels() { + if (lockable_) + lockable_->UnlockPixels(); + } + + /// + /// Access the locked pixel data. + /// + void* data() { return data_; } + + /// + /// Access the size of the locked pixel data. + /// + size_t size() { return size_; } + + explicit operator bool() const { return !!lockable_; } + + LockedPixels(LockedPixels&& other) : lockable_(other.lockable_), data_(other.data_), + size_(other.size_) { + other.lockable_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; + } + + LockedPixels& operator=(LockedPixels&& other) { + if (lockable_) + lockable_->UnlockPixels(); + lockable_ = other.lockable_; + data_ = other.data_; + size_ = other.size_; + other.lockable_ = nullptr; + other.data_ = nullptr; + other.size_ = 0; + return *this; + } + + private: + void lock() { + if (lockable_) { + data_ = lockable_->LockPixels(); + size_ = lockable_->size(); + } + } + + T lockable_; + void* data_; + size_t size_; +}; + + +} // namespace ultralight diff --git a/include/Ultralight/Buffer.h b/include/Ultralight/Buffer.h new file mode 100644 index 0000000..135adad --- /dev/null +++ b/include/Ultralight/Buffer.h @@ -0,0 +1,88 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +/// +/// Function signature for a user-defined destruction callback to optionally be called when Buffer +/// is destroyed. Users can use this to deallocate any data associated with the Buffer. +/// +/// @param user_data Pointer to user-defined user-data (this will be the same value as what was +/// passed to Buffer::Create, if any) +/// +/// @param data Pointer to raw Buffer data. +/// +typedef void (*DestroyBufferCallback)(void* user_data, void* data); + +/// +/// A fixed-size container for raw byte data. +/// +/// This class is used to represent raw data buffers in Ultralight. It intelligently manages the +/// lifetime of the data and can optionally call a user-supplied callback to deallocate the data +/// when the Buffer is destroyed. +/// +class UExport Buffer : public RefCounted { + public: + /// + /// Create a Buffer from existing, user-owned data without any copies. An optional, user-supplied + /// callback will be called to deallocate data upon destruction. + /// + /// @param data A pointer to the data. + /// + /// @param size Size of the data in bytes. + /// + /// @param user_data Optional user data that will be passed to destruction_callback + /// when the returned Buffer is destroyed. + /// + /// @param destruction_callback Optional callback that will be called upon destruction. Pass a + /// null pointer if you don't want to be informed of destruction. + /// + /// + /// @return A ref-counted Buffer object that wraps the existing data. + /// + static RefPtr Create(void* data, size_t size, void* user_data, + DestroyBufferCallback destruction_callback); + + /// + /// Create a Buffer from existing data, a deep copy of data will be made. + /// + static RefPtr CreateFromCopy(const void* data, size_t size); + + /// + /// Get a pointer to the raw byte data. + /// + virtual void* data() = 0; + + /// + /// Get the size in bytes. + /// + virtual size_t size() const = 0; + + /// + /// Get the user data associated with this Buffer, if any. + /// + virtual void* user_data() = 0; + + /// + /// Check whether this Buffer owns its own data (Buffer was created via CreateFromCopy). + /// If this is false, Buffer will call the user-supplied destruction callback to deallocate data + /// when this Buffer instance is destroyed. + /// + virtual bool owns_data() const = 0; + + protected: + Buffer(); + virtual ~Buffer(); + Buffer(const Buffer&); + void operator=(const Buffer&); +}; + +} // namespace ultralight diff --git a/include/Ultralight/CAPI.h b/include/Ultralight/CAPI.h new file mode 100644 index 0000000..79d0997 --- /dev/null +++ b/include/Ultralight/CAPI.h @@ -0,0 +1,41 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#ifndef ULTRALIGHT_CAPI_H +#define ULTRALIGHT_CAPI_H + +/************************************************************************************************** + * API Note: + * + * You should only destroy objects that you explicitly create. Do not destroy any objects returned + * from the API or callbacks unless otherwise noted. + **************************************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#endif // ULTRALIGHT_CAPI_H diff --git a/include/Ultralight/CAPI/CAPI_Bitmap.h b/include/Ultralight/CAPI/CAPI_Bitmap.h new file mode 100644 index 0000000..cc061a8 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Bitmap.h @@ -0,0 +1,150 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Bitmap.h +/// +/// A thread-safe container for pixel data. +/// +/// `#include ` +/// +/// The bitmap class is used to store pixel data in a variety of formats. It intelligently manages +/// the lifetime of the pixel buffer and provides thread-safe access to the pixel data. +/// +/// ## Accessing Pixel Data +/// +/// To access the pixel data, you must first lock the pixels using ulBitmapLockPixels(). This will +/// return a pointer to the pixel buffer. An example follows: +/// +/// ``` +/// void* pixels = ulBitmapLockPixels(bitmap); +/// if (pixels) { +/// // Zero out the pixel buffer +/// memset(pixels, 0, ulBitmapGetSize(bitmap)); +/// } +/// +/// // Unlock the pixels when you're done. +/// ulBitmapUnlockPixels(bitmap); +/// ``` +/// +#ifndef ULTRALIGHT_CAPI_BITMAP_H +#define ULTRALIGHT_CAPI_BITMAP_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Bitmap + *****************************************************************************/ + +/// +/// Create empty bitmap. +/// +ULExport ULBitmap ulCreateEmptyBitmap(); + +/// +/// Create bitmap with certain dimensions and pixel format. +/// +ULExport ULBitmap ulCreateBitmap(unsigned int width, unsigned int height, ULBitmapFormat format); + +/// +/// Create bitmap from existing pixel buffer. @see Bitmap for help using this function. +/// +ULExport ULBitmap ulCreateBitmapFromPixels(unsigned int width, unsigned int height, + ULBitmapFormat format, unsigned int row_bytes, + const void* pixels, size_t size, bool should_copy); + +/// +/// Create bitmap from copy. +/// +ULExport ULBitmap ulCreateBitmapFromCopy(ULBitmap existing_bitmap); + +/// +/// Destroy a bitmap (you should only destroy Bitmaps you have explicitly created via one of the +/// creation functions above. +/// +ULExport void ulDestroyBitmap(ULBitmap bitmap); + +/// +/// Get the width in pixels. +/// +ULExport unsigned int ulBitmapGetWidth(ULBitmap bitmap); + +/// +/// Get the height in pixels. +/// +ULExport unsigned int ulBitmapGetHeight(ULBitmap bitmap); + +/// +/// Get the pixel format. +/// +ULExport ULBitmapFormat ulBitmapGetFormat(ULBitmap bitmap); + +/// +/// Get the bytes per pixel. +/// +ULExport unsigned int ulBitmapGetBpp(ULBitmap bitmap); + +/// +/// Get the number of bytes per row. +/// +ULExport unsigned int ulBitmapGetRowBytes(ULBitmap bitmap); + +/// +/// Get the size in bytes of the underlying pixel buffer. +/// +ULExport size_t ulBitmapGetSize(ULBitmap bitmap); + +/// +/// Whether or not this bitmap owns its own pixel buffer. +/// +ULExport bool ulBitmapOwnsPixels(ULBitmap bitmap); + +/// +/// Lock pixels for reading/writing, returns pointer to pixel buffer. +/// +ULExport void* ulBitmapLockPixels(ULBitmap bitmap); + +/// +/// Unlock pixels after locking. +/// +ULExport void ulBitmapUnlockPixels(ULBitmap bitmap); + +/// +/// Get raw pixel buffer-- you should only call this if Bitmap is already locked. +/// +ULExport void* ulBitmapRawPixels(ULBitmap bitmap); + +/// +/// Whether or not this bitmap is empty. +/// +ULExport bool ulBitmapIsEmpty(ULBitmap bitmap); + +/// +/// Reset bitmap pixels to 0. +/// +ULExport void ulBitmapErase(ULBitmap bitmap); + +/// +/// Write bitmap to a PNG on disk. +/// +ULExport bool ulBitmapWritePNG(ULBitmap bitmap, const char* path); + +/// +/// This converts a BGRA bitmap to RGBA bitmap and vice-versa by swapping the red and blue channels. +/// +ULExport void ulBitmapSwapRedBlueChannels(ULBitmap bitmap); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_BITMAP_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Buffer.h b/include/Ultralight/CAPI/CAPI_Buffer.h new file mode 100644 index 0000000..fedd268 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Buffer.h @@ -0,0 +1,84 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Buffer.h +/// +/// A fixed-size container for raw byte data. +/// +/// `#include ` +/// +/// This class is used to represent raw data buffers in Ultralight. It intelligently manages the +/// lifetime of the data and can optionally call a user-supplied callback to deallocate the data +/// when the Buffer is destroyed. +/// +#ifndef ULTRALIGHT_CAPI_BUFFER_H +#define ULTRALIGHT_CAPI_BUFFER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void (*ulDestroyBufferCallback)(void* user_data, void* data); + +/// +/// Create a Buffer from existing, user-owned data without any copies. An optional, user-supplied +/// callback will be called to deallocate data upon destruction. +/// +/// @param data A pointer to the data. +/// +/// @param size Size of the data in bytes. +/// +/// @param user_data Optional user data that will be passed to destruction_callback +/// when the returned Buffer is destroyed. +/// +/// @param destruction_callback Optional callback that will be called upon destruction. Pass a +/// null pointer if you don't want to be informed of destruction. +/// +ULExport ULBuffer ulCreateBuffer(void* data, size_t size, void* user_data, + ulDestroyBufferCallback destruction_callback); + +/// +/// Create a Buffer from existing data, a deep copy of data will be made. +/// +ULExport ULBuffer ulCreateBufferFromCopy(const void* data, size_t size); + +/// +/// Destroy buffer (you should destroy any buffers you explicitly Create). +/// +ULExport void ulDestroyBuffer(ULBuffer buffer); + +/// +/// Get a pointer to the raw byte data. +/// +ULExport void* ulBufferGetData(ULBuffer buffer); + +/// +/// Get the size in bytes. +/// +ULExport size_t ulBufferGetSize(ULBuffer buffer); + +/// +/// Get the user data associated with this Buffer, if any. +/// +ULExport void* ulBufferGetUserData(ULBuffer buffer); + +/// +/// Check whether this Buffer owns its own data (Buffer was created via ulCreateBufferFromCopy). +/// If this is false, Buffer will call the user-supplied destruction callback to deallocate data +/// when this Buffer instance is destroyed. +/// +ULExport bool ulBufferOwnsData(ULBuffer buffer); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_BUFFER_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Clipboard.h b/include/Ultralight/CAPI/CAPI_Clipboard.h new file mode 100644 index 0000000..c9183ca --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Clipboard.h @@ -0,0 +1,66 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Clipboard.h +/// +/// User-defined clipboard interface. +/// +/// `#include ` +/// +/// The library uses this to read and write data to the system's clipboard. +/// +/// @see ulPlatformSetClipboard() +/// +#ifndef ULTRALIGHT_CAPI_CLIPBOARD_H +#define ULTRALIGHT_CAPI_CLIPBOARD_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Clipboard + *****************************************************************************/ + +/// +/// The callback invoked when the library wants to clear the system's clipboard. +/// +typedef void (*ULClipboardClearCallback)(); + +/// +/// The callback invoked when the library wants to read from the system's clipboard. +/// +/// You should store the result (if any) in 'result'. +/// +typedef void (*ULClipboardReadPlainTextCallback)(ULString result); + +/// +/// The callback invoked when the library wants to write to the system's clipboard. +/// +typedef void (*ULClipboardWritePlainTextCallback)(ULString text); + +/// +/// User-defined clipboard interface. +/// +/// You should implement each of these callbacks, then pass an instance of this struct containing +/// your callbacks to ulPlatformSetClipboard(). +/// +typedef struct { + ULClipboardClearCallback clear; + ULClipboardReadPlainTextCallback read_plain_text; + ULClipboardWritePlainTextCallback write_plain_text; +} ULClipboard; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_CLIPBOARD_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Config.h b/include/Ultralight/CAPI/CAPI_Config.h new file mode 100644 index 0000000..0b50abd --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Config.h @@ -0,0 +1,235 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Config.h +/// +/// Core configuration for the renderer. +/// +/// `#include ` +/// +/// These are various configuration options that can be used to customize the behavior of the +/// library. These options can only be set once before creating the Renderer. +/// +#ifndef ULTRALIGHT_CAPI_CONFIG_H +#define ULTRALIGHT_CAPI_CONFIG_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Config + *****************************************************************************/ + +/// +/// Create config with default values (see ). +/// +ULExport ULConfig ulCreateConfig(); + +/// +/// Destroy config. +/// +ULExport void ulDestroyConfig(ULConfig config); + +/// +/// A writable OS file path to store persistent Session data in. +/// +/// This data may include cookies, cached network resources, indexed DB, etc. +/// +/// @note Files are only written to the path when using a persistent Session. +/// +ULExport void ulConfigSetCachePath(ULConfig config, ULString cache_path); + +/// +/// The relative path to the resources folder (loaded via the FileSystem API). +/// +/// The library loads certain resources (SSL certs, ICU data, etc.) from the FileSystem API +/// during runtime (eg, `file:///resources/cacert.pem`). +/// +/// You can customize the relative file path to the resources folder by modifying this setting. +/// +/// (Default = "resources/") +/// +ULExport void ulConfigSetResourcePathPrefix(ULConfig config, ULString resource_path_prefix); + +/// +/// The winding order for front-facing triangles. +/// +/// @pre Only used when GPU rendering is enabled for the View. +/// +/// (Default = kFaceWinding_CounterClockwise) +/// +ULExport void ulConfigSetFaceWinding(ULConfig config, ULFaceWinding winding); + +/// +/// The hinting algorithm to use when rendering fonts. (Default = kFontHinting_Normal) +/// +/// @see ULFontHinting +/// +ULExport void ulConfigSetFontHinting(ULConfig config, ULFontHinting font_hinting); + +/// +/// The gamma to use when compositing font glyphs, change this value to adjust contrast (Adobe and +/// Apple prefer 1.8, others may prefer 2.2). (Default = 1.8) +/// +ULExport void ulConfigSetFontGamma(ULConfig config, double font_gamma); + +/// +/// Global user-defined CSS string (included before any CSS on the page). +/// +/// You can use this to override default styles for various elements on the page. +/// +/// @note This is an actual string of CSS, not a file path. +/// +ULExport void ulConfigSetUserStylesheet(ULConfig config, ULString css_string); + +/// +/// Whether or not to continuously repaint any Views, regardless if they are dirty. +/// +/// This is mainly used to diagnose painting/shader issues and profile performance. +/// +/// (Default = False) +/// +ULExport void ulConfigSetForceRepaint(ULConfig config, bool enabled); + +/// +/// The delay (in seconds) between every tick of a CSS animation. +/// +/// (Default = 1.0 / 60.0) +/// +ULExport void ulConfigSetAnimationTimerDelay(ULConfig config, double delay); + +/// +/// The delay (in seconds) between every tick of a smooth scroll animation. +/// +/// (Default = 1.0 / 60.0) +/// +ULExport void ulConfigSetScrollTimerDelay(ULConfig config, double delay); + +/// +/// The delay (in seconds) between every call to the recycler. +/// +/// The library attempts to reclaim excess memory during calls to the internal recycler. You can +/// change how often this is run by modifying this value. +/// +/// (Default = 4.0) +/// +ULExport void ulConfigSetRecycleDelay(ULConfig config, double delay); + +/// +/// The size of WebCore's memory cache in bytes. +/// +/// @note You should increase this if you anticipate handling pages with large resources, Safari +/// typically uses 128+ MiB for its cache. +/// +/// (Default = 64 * 1024 * 1024) +/// +ULExport void ulConfigSetMemoryCacheSize(ULConfig config, unsigned int size); + +/// +/// The number of pages to keep in the cache. (Default: 0, none) +/// +/// @note +/// \parblock +/// +/// Safari typically caches about 5 pages and maintains an on-disk cache to support typical +/// web-browsing activities. +/// +/// If you increase this, you should probably increase the memory cache size as well. +/// +/// \endparblock +/// +/// (Default = 0) +/// +ULExport void ulConfigSetPageCacheSize(ULConfig config, unsigned int size); + +/// +/// The system's physical RAM size in bytes. +/// +/// JavaScriptCore tries to detect the system's physical RAM size to set reasonable allocation +/// limits. Set this to anything other than 0 to override the detected value. Size is in bytes. +/// +/// This can be used to force JavaScriptCore to be more conservative with its allocation strategy +/// (at the cost of some performance). +/// +ULExport void ulConfigSetOverrideRAMSize(ULConfig config, unsigned int size); + +/// +/// The minimum size of large VM heaps in JavaScriptCore. +/// +/// Set this to a lower value to make these heaps start with a smaller initial value. +/// +/// (Default = 32 * 1024 * 1024) +/// +ULExport void ulConfigSetMinLargeHeapSize(ULConfig config, unsigned int size); + +/// +/// The minimum size of small VM heaps in JavaScriptCore. +/// +/// Set this to a lower value to make these heaps start with a smaller initial value. +/// +/// (Default = 1 * 1024 * 1024) +/// +ULExport void ulConfigSetMinSmallHeapSize(ULConfig config, unsigned int size); + +/// +/// The number of threads to use in the Renderer (for parallel painting on the CPU, etc.). +/// +/// You can set this to a certain number to limit the number of threads to spawn. +/// +/// @note +/// \parblock +/// +/// If this value is 0, the number of threads will be determined at runtime using the following +/// formula: +/// +/// ``` +/// max(PhysicalProcessorCount() - 1, 1) +/// ``` +/// +/// \endparblock +/// +ULExport void ulConfigSetNumRendererThreads(ULConfig config, unsigned int num_renderer_threads); + +/// +/// The max amount of time (in seconds) to allow repeating timers to run during each call to +/// Renderer::Update. +/// +/// The library will attempt to throttle timers if this time budget is exceeded. +/// +/// (Default = 1.0 / 200.0) +/// +ULExport void ulConfigSetMaxUpdateTime(ULConfig config, double max_update_time); + +/// +/// The alignment (in bytes) of the BitmapSurface when using the CPU renderer. +/// +/// The underlying bitmap associated with each BitmapSurface will have row_bytes padded to reach +/// this alignment. +/// +/// Aligning the bitmap helps improve performance when using the CPU renderer. Determining the +/// proper value to use depends on the CPU architecture and max SIMD instruction set used. +/// +/// We generally target the 128-bit SSE2 instruction set across most PC platforms so '16' is a safe +/// value to use. +/// +/// You can set this to '0' to perform no padding (row_bytes will always be width * 4) at a slight +/// cost to performance. +/// +/// (Default = 16) +/// +ULExport void ulConfigSetBitmapAlignment(ULConfig config, unsigned int bitmap_alignment); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_CONFIG_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Defines.h b/include/Ultralight/CAPI/CAPI_Defines.h new file mode 100644 index 0000000..38e1337 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Defines.h @@ -0,0 +1,318 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Defines.h +/// +/// Various defines and utility functions for the C API. +/// +/// `#include ` +/// +/// This file contains various defines, structures, and utility functions for the C API. +/// +#ifndef ULTRALIGHT_CAPI_DEFINES_H +#define ULTRALIGHT_CAPI_DEFINES_H + +#include +#include +#include +#ifdef __OBJC__ +#import +#endif + +#if defined(ULTRALIGHT_STATIC_BUILD) +#define ULExport +#else +#if defined(__WIN32__) || defined(_WIN32) +#if defined(ULTRALIGHT_IMPLEMENTATION) +#define ULExport __declspec(dllexport) +#else +#define ULExport __declspec(dllimport) +#endif +#else +#define ULExport __attribute__((visibility("default"))) +#endif +#endif + +#if defined(__WIN32__) || defined(_WIN32) +#define _thread_local __declspec(thread) +#ifndef _NATIVE_WCHAR_T_DEFINED +#define DISABLE_NATIVE_WCHAR_T +typedef unsigned short ULChar16; +#else +typedef wchar_t ULChar16; +#endif +#else +#define _thread_local __thread +typedef unsigned short ULChar16; +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct C_Config* ULConfig; +typedef struct C_Renderer* ULRenderer; +typedef struct C_Session* ULSession; +typedef struct C_ViewConfig* ULViewConfig; +typedef struct C_View* ULView; +typedef struct C_Bitmap* ULBitmap; +typedef struct C_String* ULString; +typedef struct C_Buffer* ULBuffer; +typedef struct C_KeyEvent* ULKeyEvent; +typedef struct C_MouseEvent* ULMouseEvent; +typedef struct C_ScrollEvent* ULScrollEvent; +typedef struct C_GamepadEvent* ULGamepadEvent; +typedef struct C_GamepadAxisEvent* ULGamepadAxisEvent; +typedef struct C_GamepadButtonEvent* ULGamepadButtonEvent; +typedef struct C_Surface* ULSurface; +typedef struct C_Surface* ULBitmapSurface; +typedef struct C_FontFile* ULFontFile; +typedef struct C_ImageSource* ULImageSource; + +typedef enum { + kMessageSource_XML = 0, + kMessageSource_JS, + kMessageSource_Network, + kMessageSource_ConsoleAPI, + kMessageSource_Storage, + kMessageSource_AppCache, + kMessageSource_Rendering, + kMessageSource_CSS, + kMessageSource_Security, + kMessageSource_ContentBlocker, + kMessageSource_Media, + kMessageSource_MediaSource, + kMessageSource_WebRTC, + kMessageSource_ITPDebug, + kMessageSource_PrivateClickMeasurement, + kMessageSource_PaymentRequest, + kMessageSource_Other, +} ULMessageSource; + +typedef enum { + kMessageLevel_Log = 0, + kMessageLevel_Warning, + kMessageLevel_Error, + kMessageLevel_Debug, + kMessageLevel_Info, +} ULMessageLevel; + +typedef enum { + kCursor_Pointer = 0, + kCursor_Cross, + kCursor_Hand, + kCursor_IBeam, + kCursor_Wait, + kCursor_Help, + kCursor_EastResize, + kCursor_NorthResize, + kCursor_NorthEastResize, + kCursor_NorthWestResize, + kCursor_SouthResize, + kCursor_SouthEastResize, + kCursor_SouthWestResize, + kCursor_WestResize, + kCursor_NorthSouthResize, + kCursor_EastWestResize, + kCursor_NorthEastSouthWestResize, + kCursor_NorthWestSouthEastResize, + kCursor_ColumnResize, + kCursor_RowResize, + kCursor_MiddlePanning, + kCursor_EastPanning, + kCursor_NorthPanning, + kCursor_NorthEastPanning, + kCursor_NorthWestPanning, + kCursor_SouthPanning, + kCursor_SouthEastPanning, + kCursor_SouthWestPanning, + kCursor_WestPanning, + kCursor_Move, + kCursor_VerticalText, + kCursor_Cell, + kCursor_ContextMenu, + kCursor_Alias, + kCursor_Progress, + kCursor_NoDrop, + kCursor_Copy, + kCursor_None, + kCursor_NotAllowed, + kCursor_ZoomIn, + kCursor_ZoomOut, + kCursor_Grab, + kCursor_Grabbing, + kCursor_Custom +} ULCursor; + +typedef enum { + /// + /// Alpha channel only, 8-bits per pixel. + /// + /// Encoding: 8-bits per channel, unsigned normalized. + /// + /// Color-space: Linear (no gamma), alpha-coverage only. + /// + kBitmapFormat_A8_UNORM, + + /// + /// Blue Green Red Alpha channels, 32-bits per pixel. + /// + /// Encoding: 8-bits per channel, unsigned normalized. + /// + /// Color-space: sRGB gamma with premultiplied linear alpha channel. + /// + kBitmapFormat_BGRA8_UNORM_SRGB +} ULBitmapFormat; + +typedef enum { + /// + /// Key-Down event type. This type does **not** trigger accelerator commands in WebCore (eg, + /// Ctrl+C for copy is an accelerator command). + /// + /// @warning You should probably use kKeyEventType_RawKeyDown instead. This type is only here for + /// historic compatibility with WebCore's key event types. + /// + kKeyEventType_KeyDown, + + /// + /// Key-Up event type. Use this when a physical key is released. + /// + kKeyEventType_KeyUp, + + /// + /// Raw Key-Down type. Use this when a physical key is pressed. + /// + kKeyEventType_RawKeyDown, + + /// + /// Character input event type. Use this when the OS generates text from + /// a physical key being pressed (eg, WM_CHAR on Windows). + /// + kKeyEventType_Char, +} ULKeyEventType; + +typedef enum { + kMouseEventType_MouseMoved, + kMouseEventType_MouseDown, + kMouseEventType_MouseUp, +} ULMouseEventType; + +typedef enum { + kMouseButton_None = 0, + kMouseButton_Left, + kMouseButton_Middle, + kMouseButton_Right, +} ULMouseButton; + +typedef enum { + kScrollEventType_ScrollByPixel, + kScrollEventType_ScrollByPage, +} ULScrollEventType; + +typedef enum { + kGamepadEventType_Connected, + kGamepadEventType_Disconnected, +} ULGamepadEventType; + +typedef enum { + kFaceWinding_Clockwise, + kFaceWinding_CounterClockwise, +} ULFaceWinding; + +typedef enum { + /// + /// Lighter hinting algorithm-- glyphs are slightly fuzzier but better + /// resemble their original shape. This is achieved by snapping glyphs to the + /// pixel grid only vertically which better preserves inter-glyph spacing. + /// + kFontHinting_Smooth, + + /// + /// Default hinting algorithm-- offers a good balance between sharpness and + /// shape at smaller font sizes. + /// + kFontHinting_Normal, + + /// + /// Strongest hinting algorithm-- outputs only black/white glyphs. The result + /// is usually unpleasant if the underlying TTF does not contain hints for + /// this type of rendering. + /// + kFontHinting_Monochrome, +} ULFontHinting; + +typedef struct { + float left; + float top; + float right; + float bottom; +} ULRect; + +typedef struct { + int left; + int top; + int right; + int bottom; +} ULIntRect; + +/// +/// Offscreen render target, used when rendering Views via the GPU renderer. +/// +/// When a View is rendered via the GPU renderer (see ulViewIsAccelerated()), it will be rendered to +/// an offscreen render target (ulViewGetRenderTarget()) that you can display in your application. +/// +/// This is intended to be used with a custom ULGPUDriver implementation in a game or similar +/// application (ulPlatformSetGPUDriver()). +/// +typedef struct { + bool is_empty; + unsigned int width; + unsigned int height; + unsigned int texture_id; + unsigned int texture_width; + unsigned int texture_height; + ULBitmapFormat texture_format; + ULRect uv_coords; + unsigned int render_buffer_id; +} ULRenderTarget; + +/****************************************************************************** + * Version + *****************************************************************************/ + +/// +/// Get the version string of the library in MAJOR.MINOR.PATCH format. +/// +ULExport const char* ulVersionString(); + +/// +/// Get the numeric major version of the library. +/// +ULExport unsigned int ulVersionMajor(); + +/// +/// Get the numeric minor version of the library. +/// +ULExport unsigned int ulVersionMinor(); + +/// +/// Get the numeric patch version of the library. +/// +ULExport unsigned int ulVersionPatch(); + +/// +/// Get the full WebKit version string. +/// +ULExport const char* ulWebKitVersionString(); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_DEFINES_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_FileSystem.h b/include/Ultralight/CAPI/CAPI_FileSystem.h new file mode 100644 index 0000000..aca8640 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_FileSystem.h @@ -0,0 +1,94 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_FileSystem.h +/// +/// User-defined file system interface. +/// +/// `#include ` +/// +/// The library uses this to load file data (ie, raw file bytes) for a given file URL +/// (eg, `file:///page.html`) . +/// +/// You can provide the library with your own FileSystem implementation (ULFileSystem) so that file +/// data is provided directly by your application (eg, from memory, from a virtual file system, +/// etc). +/// +/// @see ulPlatformSetFileSystem +/// +#ifndef ULTRALIGHT_CAPI_FILESYSTEM_H +#define ULTRALIGHT_CAPI_FILESYSTEM_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * File System + *****************************************************************************/ + +/// +/// The callback invoked when the FileSystem wants to check if a file path exists, return true if it +/// exists. +/// +typedef bool (*ULFileSystemFileExistsCallback)(ULString path); + +/// +/// Get the mime-type of the file (eg "text/html"). +/// +/// This is usually determined by analyzing the file extension. +/// +/// If a mime-type cannot be determined, you should return "application/unknown" for this value. +/// +/// The library will consume the result and call ulDestroyString() after this call returns. +/// +typedef ULString (*ULFileSystemGetFileMimeTypeCallback)(ULString path); + +/// +/// Get the charset / encoding of the file (eg "utf-8"). +/// +/// This is only important for text-based files and is usually determined by analyzing the +/// contents of the file. +/// +/// If a charset cannot be determined, it's usually safe to return "utf-8" for this value. +/// +/// The library will consume the result and call ulDestroyString() after this call returns. +/// +typedef ULString (*ULFileSystemGetFileCharsetCallback)(ULString path); + +/// +/// Open file for reading and map it to a Buffer. +/// +/// To minimize copies, you should map the requested file into memory and use ulCreateBuffer() +/// to wrap the data pointer (unmapping should be performed in the destruction callback). +/// +/// If the file was unable to be opened, you should return NULL for this value. +/// +typedef ULBuffer (*ULFileSystemOpenFileCallback)(ULString path); + +/// +/// User-defined file system interface. +/// +/// You should implement each of these callbacks, then pass an instance of this struct containing +/// your callbacks to ulPlatformSetFileSystem(). +/// +typedef struct { + ULFileSystemFileExistsCallback file_exists; + ULFileSystemGetFileMimeTypeCallback get_file_mime_type; + ULFileSystemGetFileCharsetCallback get_file_charset; + ULFileSystemOpenFileCallback open_file; +} ULFileSystem; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_FILESYSTEM_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_FontFile.h b/include/Ultralight/CAPI/CAPI_FontFile.h new file mode 100644 index 0000000..504d53a --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_FontFile.h @@ -0,0 +1,52 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_FontFile.h +/// +/// Font file interface. +/// +/// `#include ` +/// +/// The font file interface represents a font file: either on-disk path or in-memory file contents. +/// +/// @see ULFontLoader +/// +#ifndef ULTRALIGHT_CAPI_FONTFILE_H +#define ULTRALIGHT_CAPI_FONTFILE_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// +/// Create a font file from an on-disk file path. +/// +/// @note The file path should already exist. +/// +ULExport ULFontFile ulFontFileCreateFromFilePath(ULString file_path); + +/// +/// Create a font file from an in-memory buffer. +/// +ULExport ULFontFile ulFontFileCreateFromBuffer(ULBuffer buffer); + +/// +/// Destroy font file +/// +ULExport void ulDestroyFontFile(ULFontFile font_file); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_FONTFILE_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_FontLoader.h b/include/Ultralight/CAPI/CAPI_FontLoader.h new file mode 100644 index 0000000..f949af5 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_FontLoader.h @@ -0,0 +1,99 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_FontLoader.h +/// +/// User-defined font loader interface. +/// +/// `#include ` +/// +/// The library uses this to load a font file (eg, `Arial.ttf`) for a given font description (eg, +/// `font-family: Arial;`). +/// +/// Every OS has its own library of installed system fonts. The FontLoader interface is used to +/// lookup these fonts and fetch the actual font data (raw TTF/OTF file data) for a given font +/// description. +/// +/// You can provide the library with your own font loader implementation so that you can bundle +/// fonts with your application rather than relying on the system's installed fonts. +/// +/// @see ulPlatformSetFontLoader +/// +#ifndef ULTRALIGHT_CAPI_FONTLOADER_H +#define ULTRALIGHT_CAPI_FONTLOADER_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Font Loader + *****************************************************************************/ + +/// +/// Fallback font family name. Will be used if all other fonts fail to load. +/// +/// @note This font should be guaranteed to exist (eg, ULFontLoader::load should not fail when +/// when passed this font family name). +/// +/// @note The returned ULString instance will be consumed (ulDestroyString will be called on it). +/// +typedef ULString (*ULFontLoaderGetFallbackFont)(); + +/// +/// Fallback font family name that can render the specified characters. This is mainly used to +/// support CJK (Chinese, Japanese, Korean) text display. +/// +/// @param characters One or more UTF-16 characters. This is almost always a single character. +/// +/// @param weight Font weight. +/// +/// @param italic Whether or not italic is requested. +/// +/// @return Should return a font family name that can render the text. The returned ULString +/// instance will be consumed (ulDestroyString will be called on it). +/// +typedef ULString (*ULFontLoaderGetFallbackFontForCharacters)(ULString characters, int weight, + bool italic); + +/// +/// Get the actual font file data (TTF/OTF) for a given font description. +/// +/// @param family Font family name. +/// +/// @param weight Font weight. +/// +/// @param italic Whether or not italic is requested. +/// +/// @return A font file matching the given description (either an on-disk font filepath or an +/// in-memory file buffer). You can return NULL here and the loader will fallback to +/// another font. +/// +typedef ULFontFile (*ULFontLoaderLoad)(ULString family, int weight, bool italic); + +/// +/// User-defined font loader interface. +/// +/// You should implement each of these callbacks, then pass an instance of this struct containing +/// your callbacks to ulPlatformSetFontLoader(). +/// +typedef struct { + ULFontLoaderGetFallbackFont get_fallback_font; + ULFontLoaderGetFallbackFontForCharacters get_fallback_font_for_characters; + ULFontLoaderLoad load; +} ULFontLoader; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_FONTLOADER_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_GPUDriver.h b/include/Ultralight/CAPI/CAPI_GPUDriver.h new file mode 100644 index 0000000..0c9d195 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_GPUDriver.h @@ -0,0 +1,478 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +// clang-format off + +/// +/// @file CAPI_GPUDriver.h +/// +/// User-defined GPU driver interface. +/// +/// `#include ` +/// +/// The library uses this to optionally render Views on the GPU (see ulViewIsAccelerated()). +/// +/// You can provide the library with your own GPU driver implementation so that all rendering is +/// performed using an existing GPU context (useful for game engines). +/// +/// When a View is rendered on the GPU, you can retrieve the backing texture ID via +/// ulViewGetRenderTarget(). +/// +/// @see ulPlatformSetGPUDriver() +/// +#ifndef ULTRALIGHT_CAPI_GPUDRIVER_H +#define ULTRALIGHT_CAPI_GPUDRIVER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * GPUDriver + *****************************************************************************/ + +/// +/// Render buffer description. +/// +/// This structure describes a render buffer that can be used as a target for drawing commands. +/// +typedef struct { + unsigned int texture_id; ///< The backing texture for this RenderBuffer + unsigned int width; ///< The width of the RenderBuffer texture + unsigned int height; ///< The height of the RenderBuffer texture + bool has_stencil_buffer; ///< Currently unused, always false. + bool has_depth_buffer; ///< Currently unsued, always false. +} ULRenderBuffer; + +/// \cond ignore +/// This pragma pack(push, 1) command is important! +/// GPU structs should not be padded with any bytes. +/// \endcond +#pragma pack(push, 1) + +/// +/// Vertex layout for path vertices. +/// +/// This struct is the in-memory layout for each path vertex (useful for synthesizing or modifying +/// your own vertex data). +/// +typedef struct { + float pos[2]; + unsigned char color[4]; + float obj[2]; +} ULVertex_2f_4ub_2f; + +/// +/// Vertex layout for quad vertices. +/// +/// This struct is the in-memory layout for each quad vertex (useful for synthesizing or modifying +/// your own vertex data). +/// +typedef struct { + float pos[2]; + unsigned char color[4]; + float tex[2]; + float obj[2]; + float data0[4]; + float data1[4]; + float data2[4]; + float data3[4]; + float data4[4]; + float data5[4]; + float data6[4]; +} ULVertex_2f_4ub_2f_2f_28f; + +/// +/// End single-byte alignment. +/// +#pragma pack(pop) + +/// +/// Vertex buffer formats. +/// +/// This enumeration describes the format of a vertex buffer. +/// +typedef enum { + kVertexBufferFormat_2f_4ub_2f, ///< Vertex_2f_4ub_2f (used for path rendering) + kVertexBufferFormat_2f_4ub_2f_2f_28f, ///< Vertex_2f_4ub_2f_2f_28f (used for quad rendering) +} ULVertexBufferFormat; + +/// +/// Vertex buffer description. +/// +/// @see ULGPUDriver::create_geometry +/// +typedef struct { + ULVertexBufferFormat format; ///< The format of the vertex buffer. + unsigned int size; ///< The size of the vertex buffer in bytes. + unsigned char* data; ///< The raw vertex buffer data. +} ULVertexBuffer; + +/// +/// Vertex index type. +/// +typedef unsigned int ULIndexType; + +/// +/// Index buffer description. +/// +/// This structure describes an index buffer that can be used to index into a vertex buffer. +/// +/// @note The index buffer is a simple array of IndexType values. +/// +typedef struct { + unsigned int size; ///< The size of the index buffer in bytes. + unsigned char* data; ///< The raw index buffer data. +} ULIndexBuffer; + +/// +/// Shader program types, used with ULGPUState::shader_type +/// +/// Each of these correspond to a vertex/pixel shader pair. You can find stock shader code for these +/// in the `shaders` folder of the AppCore repo. +/// +typedef enum { + kShaderType_Fill, ///< Shader program for filling quad geometry. + kShaderType_FillPath, ///< Shader program for filling tesselated path geometry. +} ULShaderType; + +/// +/// Raw 4x4 matrix as an array of floats in column-major order. +/// +typedef struct { + float data[16]; +} ULMatrix4x4; + +/// +/// 4-component float vector +/// +typedef struct { + float value[4]; +} ULvec4; + +/// +/// The state of the GPU for a given draw command. +/// +/// This structure describes the current state of the GPU for a given draw command. +/// +typedef struct { + /// Viewport width in pixels + unsigned int viewport_width; + + /// Viewport height in pixels + unsigned int viewport_height; + + /// Transform matrix-- you should multiply this with the screen-space orthographic projection + /// matrix then pass to the vertex shader. + ULMatrix4x4 transform; + + /// Whether or not we should enable texturing for the current draw command. + bool enable_texturing; + + /// Whether or not we should enable blending for the current draw command. If blending is + /// disabled, any drawn pixels should overwrite existing. Mainly used so we can modify alpha + /// values of the RenderBuffer during scissored clears. + bool enable_blend; + + /// The vertex/pixel shader program pair to use for the current draw command. You should cast this + /// to ShaderType to get the corresponding enum. + unsigned char shader_type; + + /// The render buffer to use for the current draw command. + unsigned int render_buffer_id; + + /// The texture id to bind to slot #1. (Will be 0 if none) + unsigned int texture_1_id; + + /// The texture id to bind to slot #2. (Will be 0 if none) + unsigned int texture_2_id; + + /// The texture id to bind to slot #3. (Will be 0 if none) + unsigned int texture_3_id; + + /// The uniform scalars (passed to the pixel shader via uniforms). + float uniform_scalar[8]; + + /// The uniform vectors (passed to the pixel shader via uniforms). + ULvec4 uniform_vector[8]; + + /// The clip size (passed to the pixel shader via uniforms). + unsigned char clip_size; + + /// The clip stack (passed to the pixel shader via uniforms). + ULMatrix4x4 clip[8]; + + /// Whether or not scissor testing should be used for the current draw command. + bool enable_scissor; + + /// The scissor rect to use for scissor testing (units in pixels) + ULIntRect scissor_rect; +} ULGPUState; + +/// +/// The types of commands. +/// +/// This enumeration describes the type of command to execute on the GPU. Used with +/// ULCommand::command_type +/// +typedef enum { + kCommandType_ClearRenderBuffer, ///< Clear the specified render buffer. + kCommandType_DrawGeometry, ///< Draw the specified geometry to the specified render buffer. +} ULCommandType; + +/// +/// A command to execute on the GPU. +/// +/// This structure describes a command to be executed on the GPU. +/// +/// Commands are dispatched to the GPU driver asynchronously via ULGPUDriver::update_command_list, +/// the GPU driver should consume these commands and execute them at an appropriate time. +/// +/// @see ULCommandList +/// +typedef struct { + unsigned char command_type; ///< The type of command to dispatch. + ULGPUState gpu_state; ///< The current GPU state. + unsigned int geometry_id; ///< The geometry ID to bind. (used with kCommandType_DrawGeometry) + unsigned int indices_count; ///< The number of indices. (used with kCommandType_DrawGeometry) + unsigned int indices_offset; ///< The index to start from. (used with kCommandType_DrawGeometry) +} ULCommand; + +/// +/// List of commands to execute on the GPU. +/// +/// @see ULGPUDriver::update_command_list +/// +typedef struct { + unsigned int size; ///< The number of commands in the list. + ULCommand* commands; ///< The raw command list data. +} ULCommandList; + +/// +/// Callback for users to implement ULGPUDriver::begin_synchronize. +/// +/// Called before any state (eg, create_texture(), update_texture(), destroy_texture(), etc.) is +/// updated during a call to ulRender(). +/// +/// This is a good time to prepare the GPU for any state updates. +/// +typedef void (*ULGPUDriverBeginSynchronizeCallback)(); + +/// +/// Callback for users to implement ULGPUDriver::end_synchronize. +/// +/// Called after all state has been updated during a call to ulRender(). +/// +typedef void (*ULGPUDriverEndSynchronizeCallback)(); + +/// +/// Callback for users to implement ULGPUDriver::next_texture_id. +/// +/// Get the next available texture ID. +/// +/// This is used to generate a unique texture ID for each texture created by the library. The +/// GPU driver implementation is responsible for mapping these IDs to a native ID. +/// +/// @note Numbering should start at 1, 0 is reserved for "no texture". +/// +/// @return Returns the next available texture ID. +/// +typedef unsigned int (*ULGPUDriverNextTextureIdCallback)(); + +/// +/// Callback for users to implement ULGPUDriver::create_texture. +/// +/// Create a texture with a certain ID and optional bitmap. +/// +/// @param texture_id The texture ID to use for the new texture. +/// +/// @param bitmap The bitmap to initialize the texture with (can be empty). +/// +/// @note If the Bitmap is empty (ulBitmapIsEmpty()), then a RTT Texture should be created instead. +/// This will be used as a backing texture for a new RenderBuffer. +/// +/// @warning A deep copy of the bitmap data should be made if you are uploading it to the GPU +/// asynchronously, it will not persist beyond this call. +/// +typedef void (*ULGPUDriverCreateTextureCallback)(unsigned int texture_id, ULBitmap bitmap); + +/// +/// Callback for users to implement ULGPUDriver::update_texture. +/// +/// Update an existing non-RTT texture with new bitmap data. +/// +/// @param texture_id The texture to update. +/// +/// @param bitmap The new bitmap data. +/// +/// @warning A deep copy of the bitmap data should be made if you are uploading it to the GPU +/// asynchronously, it will not persist beyond this call. +/// +typedef void (*ULGPUDriverUpdateTextureCallback)(unsigned int texture_id, ULBitmap bitmap); + +/// +/// Callback for users to implement ULGPUDriver::destroy_texture. +/// +/// Destroy a texture. +/// +/// @param texture_id The texture to destroy. +/// +typedef void (*ULGPUDriverDestroyTextureCallback)(unsigned int texture_id); + +/// +/// Callback for users to implement ULGPUDriver::next_render_buffer_id. +/// +/// Get the next available render buffer ID. +/// +/// This is used to generate a unique render buffer ID for each render buffer created by the +/// library. The GPU driver implementation is responsible for mapping these IDs to a native ID. +/// +/// @note Numbering should start at 1, 0 is reserved for "no render buffer". +/// +/// @return Returns the next available render buffer ID. +/// +typedef unsigned int (*ULGPUDriverNextRenderBufferIdCallback)(); + +/// +/// Callback for users to implement ULGPUDriver::create_render_buffer. +/// +/// Create a render buffer with certain ID and buffer description. +/// +/// @param render_buffer_id The render buffer ID to use for the new render buffer. +/// +/// @param buffer The render buffer description. +/// +typedef void (*ULGPUDriverCreateRenderBufferCallback)(unsigned int render_buffer_id, + ULRenderBuffer buffer); + +/// +/// Callback for users to implement ULGPUDriver::destroy_render_buffer. +/// +/// Destroy a render buffer. +/// +/// @param render_buffer_id The render buffer to destroy. +/// +typedef void (*ULGPUDriverDestroyRenderBufferCallback)(unsigned int render_buffer_id); + +/// +/// Callback for users to implement ULGPUDriver::next_geometry_id. +/// +/// Get the next available geometry ID. +/// +/// This is used to generate a unique geometry ID for each geometry created by the library. The +/// GPU driver implementation is responsible for mapping these IDs to a native ID. +/// +/// @note Numbering should start at 1, 0 is reserved for "no geometry". +/// +/// @return Returns the next available geometry ID. +/// +typedef unsigned int (*ULGPUDriverNextGeometryIdCallback)(); + +/// +/// Callback for users to implement ULGPUDriver::create_geometry. +/// +/// Create geometry with certain ID and vertex/index data. +/// +/// @param geometry_id The geometry ID to use for the new geometry. +/// +/// @param vertices The vertex buffer data. +/// +/// @param indices The index buffer data. +/// +/// @warning A deep copy of the vertex/index data should be made if you are uploading it to the +/// GPU asynchronously, it will not persist beyond this call. +/// +typedef void (*ULGPUDriverCreateGeometryCallback)(unsigned int geometry_id, ULVertexBuffer vertices, + ULIndexBuffer indices); + +/// +/// Callback for users to implement ULGPUDriver::update_geometry. +/// +/// Update existing geometry with new vertex/index data. +/// +/// @param geometry_id The geometry to update. +/// +/// @param vertices The new vertex buffer data. +/// +/// @param indices The new index buffer data. +/// +/// @warning A deep copy of the vertex/index data should be made if you are uploading it to the +/// GPU asynchronously, it will not persist beyond this call. +/// +typedef void (*ULGPUDriverUpdateGeometryCallback)(unsigned int geometry_id, ULVertexBuffer vertices, + ULIndexBuffer indices); + +/// +/// Callback for users to implement ULGPUDriver::destroy_geometry. +/// +/// Destroy geometry. +/// +/// @param geometry_id The geometry to destroy. +/// +typedef void (*ULGPUDriverDestroyGeometryCallback)(unsigned int geometry_id); + +/// +/// Callback for users to implement ULGPUDriver::update_command_list. +/// +/// Update the pending command list with commands to execute on the GPU. +/// +/// Commands are dispatched to the GPU driver asynchronously via this method. The GPU driver +/// implementation should consume these commands and execute them at an appropriate time. +/// +/// @param list The list of commands to execute. +/// +/// @warning Implementations should make a deep copy of the command list, it will not persist +/// beyond this call. +/// +typedef void (*ULGPUDriverUpdateCommandListCallback)(ULCommandList list); + +/// +/// User-defined GPU driver interface. +/// +/// You should implement each of these callbacks, then pass an instance of this struct containing +/// your callbacks to ulPlatformSetGPUDriver(). +/// +typedef struct { + ULGPUDriverBeginSynchronizeCallback begin_synchronize; + ULGPUDriverEndSynchronizeCallback end_synchronize; + ULGPUDriverNextTextureIdCallback next_texture_id; + ULGPUDriverCreateTextureCallback create_texture; + ULGPUDriverUpdateTextureCallback update_texture; + ULGPUDriverDestroyTextureCallback destroy_texture; + ULGPUDriverNextRenderBufferIdCallback next_render_buffer_id; + ULGPUDriverCreateRenderBufferCallback create_render_buffer; + ULGPUDriverDestroyRenderBufferCallback destroy_render_buffer; + ULGPUDriverNextGeometryIdCallback next_geometry_id; + ULGPUDriverCreateGeometryCallback create_geometry; + ULGPUDriverUpdateGeometryCallback update_geometry; + ULGPUDriverDestroyGeometryCallback destroy_geometry; + ULGPUDriverUpdateCommandListCallback update_command_list; +} ULGPUDriver; + +/// +/// Sets up an orthographic projection matrix with a certain viewport width and height, multiplies +/// it by 'transform', and returns the result. +/// +/// This should be used to calculate the model-view projection matrix for the vertex shaders using +/// the current ULGPUState. +/// +/// The 'flip_y' can be optionally used to flip the Y coordinate-space. (Usually flip_y == true for +/// OpenGL) +/// +ULExport ULMatrix4x4 ulApplyProjection(ULMatrix4x4 transform, float viewport_width, + float viewport_height, bool flip_y); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_GPUDRIVER_H + +// clang-format on \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_GamepadEvent.h b/include/Ultralight/CAPI/CAPI_GamepadEvent.h new file mode 100644 index 0000000..a5fd1ef --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_GamepadEvent.h @@ -0,0 +1,77 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_GamepadEvent.h +/// +/// Gamepad event interface. +/// +/// `#include ` +/// +/// This file defines the C API for gamepad events. +/// +/// @see ulFireGamepadEvent +/// +#ifndef ULTRALIGHT_CAPI_GAMEPADEVENT_H +#define ULTRALIGHT_CAPI_GAMEPADEVENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Gamepad Event + *****************************************************************************/ + +/// +/// Create a gamepad event, see GamepadEvent for help using this function. +/// +ULExport ULGamepadEvent ulCreateGamepadEvent(unsigned int index, ULGamepadEventType type); + +/// +/// Destroy a gamepad event. +/// +ULExport void ulDestroyGamepadEvent(ULGamepadEvent evt); + +/****************************************************************************** + * Gamepad Axis Event + *****************************************************************************/ + +/// +/// Create a gamepad axis event, see GamepadAxisEvent for help using this function. +/// +ULExport ULGamepadAxisEvent ulCreateGamepadAxisEvent(unsigned int index, unsigned int axis_index, + double value); + +/// +/// Destroy a gamepad axis event. +/// +ULExport void ulDestroyGamepadAxisEvent(ULGamepadAxisEvent evt); + +/****************************************************************************** + * Gamepad Button Event + *****************************************************************************/ + +/// +/// Create a gamepad button event, see GamepadButtonEvent for help using this function. +/// +ULExport ULGamepadButtonEvent ulCreateGamepadButtonEvent(unsigned int index, + unsigned int button_index, double value); + +/// +/// Destroy a gamepad button event. +/// +ULExport void ulDestroyGamepadButtonEvent(ULGamepadButtonEvent evt); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_GAMEPADEVENT_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Geometry.h b/include/Ultralight/CAPI/CAPI_Geometry.h new file mode 100644 index 0000000..99c7c62 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Geometry.h @@ -0,0 +1,59 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Geometry.h +/// +/// Geometry utilities. +/// +/// `#include ` +/// +/// This file defines the C API for various geometry utilities. +/// +#ifndef ULTRALIGHT_CAPI_GEOMETRY_H +#define ULTRALIGHT_CAPI_GEOMETRY_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Rect + *****************************************************************************/ + +/// +/// Whether or not a ULRect is empty (all members equal to 0) +/// +ULExport bool ulRectIsEmpty(ULRect rect); + +/// +/// Create an empty ULRect (all members equal to 0) +/// +ULExport ULRect ulRectMakeEmpty(); + +/****************************************************************************** + * IntRect + *****************************************************************************/ + +/// +/// Whether or not a ULIntRect is empty (all members equal to 0) +/// +ULExport bool ulIntRectIsEmpty(ULIntRect rect); + +/// +/// Create an empty ULIntRect (all members equal to 0) +/// +ULExport ULIntRect ulIntRectMakeEmpty(); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_GEOMETRY_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_ImageSource.h b/include/Ultralight/CAPI/CAPI_ImageSource.h new file mode 100644 index 0000000..834a4d5 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_ImageSource.h @@ -0,0 +1,147 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_ImageSource.h +/// +/// User-defined image source to display custom images on a web-page. +/// +/// `#include ` +/// +/// This API allows you to composite your own images into a web-page. This is useful for displaying +/// in-game textures, external image assets, or other custom content. +/// +/// ## ImageSource File Format +/// +/// To use an ImageSource, you must first create an `.imgsrc` file containing a string identifying +/// the image source. This string will be used to lookup the ImageSource from ImageSourceProvider +/// when it is loaded on a web-page. +/// +/// The file format is as follows: +/// +/// ``` +/// IMGSRC-V1 +/// +/// ``` +/// +/// You can use the `.imgsrc` file anywhere in your web-page that typically accepts an image URL. +/// For example: +/// +/// ```html +/// +/// ``` +/// +/// ## Creating from a GPU Texture +/// +/// To composite your own GPU texture on a web-page, you should first reserve a texture ID from +/// ULGPUDriver::next_texture_id and then create an ImageSource from that texture ID. Next, you +/// should register the ImageSource with ImageSourceProvider using the identifier from the `.imgsrc` +/// file. +/// +/// When the image element is drawn on the web-page, the library will draw geometry using the +/// specified texture ID and UV coordinates. You should bind your own texture when the specified +/// texture ID is used. +/// +/// If the GPU renderer is not enabled for the View or pixel data is needed for other purposes, the +/// library will sample the backing bitmap instead. +/// +/// ## Creating from a Bitmap +/// +/// To composite your own bitmap on a web-page, you should create an ImageSource from a Bitmap. +/// Next, you should register the ImageSource with ImageSourceProvider using the identifier from +/// the `.imgsrc` file. +/// +/// When the image element is drawn on the web-page, the library will sample this bitmap directly. +/// +/// ## Invalidating Images +/// +/// If you modify the texture or bitmap after creating the ImageSource, you should call +/// ulImageSourceInvalidate() to notify the library that the image should be redrawn. +/// +#ifndef ULTRALIGHT_CAPI_IMAGESOURCE_H +#define ULTRALIGHT_CAPI_IMAGESOURCE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * ImageSource + *****************************************************************************/ + +/// +/// Create an image source from a GPU texture with optional backing bitmap. +/// +/// @param width The width of the image in pixels (used for layout). +/// +/// @param height The height of the image in pixels (used for layout). +/// +/// @param texture_id The GPU texture identifier to bind when drawing the quad for this image. +/// This should be non-zero and obtained from ULGPUDriver::next_texture_id. +/// +/// @param texture_uv The UV coordinates of the texture. +/// +/// @param bitmap Optional backing bitmap for this image source. This is used when drawing +/// the image using the CPU renderer or when pixel data is needed for other +/// purposes. You should update this bitmap when the texture changes. +/// +/// @return A new image source instance. +/// +ULExport ULImageSource ulCreateImageSourceFromTexture(unsigned int width, unsigned int height, + unsigned int texture_id, ULRect texture_uv, + ULBitmap bitmap); + +/// +/// Create an image source from a bitmap. +/// +/// @param bitmap The backing bitmap for this image source. +/// +/// @return A new image source instance. +/// +ULExport ULImageSource ulCreateImageSourceFromBitmap(ULBitmap bitmap); + +/// +/// Destroy an image source. +/// +/// @param image_source The image source to destroy. +/// +ULExport void ulDestroyImageSource(ULImageSource image_source); + +/// +/// Invalidate the image source, notifying the library that the image has changed +/// and should be redrawn. +/// +ULExport void ulImageSourceInvalidate(ULImageSource image_source); + +/****************************************************************************** + * ImageSourceProvider + *****************************************************************************/ + +/// +/// Add an image source to the provider. +/// +/// @param id The identifier of the image source. +/// +/// @param image_source The image source to add. +/// +ULExport void ulImageSourceProviderAddImageSource(ULString id, ULImageSource image_source); + +/// +/// Remove an image source from the provider. +/// +/// @param id The identifier of the image source. +/// +ULExport void ulImageSourceProviderRemoveImageSource(ULString id); + +#ifdef __cplusplus +} +#endif + +#endif // ULTRALIGHT_CAPI_IMAGESOURCE_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_KeyEvent.h b/include/Ultralight/CAPI/CAPI_KeyEvent.h new file mode 100644 index 0000000..980340a --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_KeyEvent.h @@ -0,0 +1,63 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_KeyEvent.h +/// +/// Key event interface. +/// +/// `#include ` +/// +/// This file defines the C API for various key events. +/// +#ifndef ULTRALIGHT_CAPI_KEYEVENT_H +#define ULTRALIGHT_CAPI_KEYEVENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Key Event + ******************************************************************************/ + +/// +/// Create a key event, see KeyEvent in the C++ API for help with the parameters. +/// +ULExport ULKeyEvent ulCreateKeyEvent(ULKeyEventType type, unsigned int modifiers, + int virtual_key_code, int native_key_code, ULString text, + ULString unmodified_text, bool is_keypad, bool is_auto_repeat, + bool is_system_key); + +#ifdef _WIN32 +/// +/// Create a key event from native Windows event. +/// +ULExport ULKeyEvent ulCreateKeyEventWindows(ULKeyEventType type, uintptr_t wparam, intptr_t lparam, + bool is_system_key); +#endif + +#ifdef __OBJC__ +/// +/// Create a key event from native macOS event. +/// +ULExport ULKeyEvent ulCreateKeyEventMacOS(NSEvent* evt); +#endif + +/// +/// Destroy a key event. +/// +ULExport void ulDestroyKeyEvent(ULKeyEvent evt); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_KEYEVENT_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Logger.h b/include/Ultralight/CAPI/CAPI_Logger.h new file mode 100644 index 0000000..6d555ba --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Logger.h @@ -0,0 +1,50 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Logger.h +/// +/// User-defined logging interface. +/// +/// `#include ` +/// +/// The library uses this to display log messages for debugging during development. +/// +/// This is intended to be implemented by users and defined before creating the Renderer. +/// +/// @see ulPlatformSetLogger() +/// +#ifndef ULTRALIGHT_CAPI_LOGGER_H +#define ULTRALIGHT_CAPI_LOGGER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Logger + *****************************************************************************/ + +typedef enum { kLogLevel_Error = 0, kLogLevel_Warning, kLogLevel_Info } ULLogLevel; + +/// +/// The callback invoked when the library wants to print a message to the log. +/// +typedef void (*ULLoggerLogMessageCallback)(ULLogLevel log_level, ULString message); + +typedef struct { + ULLoggerLogMessageCallback log_message; +} ULLogger; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_LOGGER_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_MouseEvent.h b/include/Ultralight/CAPI/CAPI_MouseEvent.h new file mode 100644 index 0000000..93a6d48 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_MouseEvent.h @@ -0,0 +1,45 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_MouseEvent.h +/// +/// Mouse event interface. +/// +/// `#include ` +/// +/// This file defines the C API for mouse events. +/// +#ifndef ULTRALIGHT_CAPI_MOUSEEVENT_H +#define ULTRALIGHT_CAPI_MOUSEEVENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Mouse Event + *****************************************************************************/ + +/// +/// Create a mouse event, see MouseEvent in the C++ API for help using this function. +/// +ULExport ULMouseEvent ulCreateMouseEvent(ULMouseEventType type, int x, int y, ULMouseButton button); + +/// +/// Destroy a mouse event. +/// +ULExport void ulDestroyMouseEvent(ULMouseEvent evt); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_MOUSEEVENT_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Platform.h b/include/Ultralight/CAPI/CAPI_Platform.h new file mode 100644 index 0000000..eee3e00 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Platform.h @@ -0,0 +1,168 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Platform.h +/// +/// Global platform singleton, manages user-defined platform handlers.. +/// +/// `#include ` +/// +/// The library uses the Platform API for most platform-specific operations (eg, file access, +/// clipboard, font loading, GPU access, pixel buffer transport, etc.). +/// +/// ## Motivation +/// +/// Ultralight is designed to work in as many platforms and environments as possible. To achieve +/// this, we've factored out most platform-specific code into a set of interfaces that you can +/// implement and set on the Platform singleton. +/// +/// ## Default Implementations +/// +/// We provide a number of default implementations for desktop platforms (eg, Windows, macOS, Linux) +/// for you when you call ulCreateApp(). These implementations are defined in the +/// [AppCore repository](https://github.com/ultralight-ux/AppCore/tree/master/src), we recommend +/// using their source code as a starting point for your own implementations. +/// +/// ## Required Handlers +/// +/// When using ulCreateRenderer() directly, you'll need to provide your own implementations for +/// ULFileSystem and ULFontLoader at a minimum. +/// +/// @par Overview of which platform handlers are required / optional / provided: +/// +/// | | ulCreateRenderer() | ulCreateApp() | +/// |---------------------|--------------------|---------------| +/// | ULFileSystem | **Required** | *Provided* | +/// | ULFontLoader | **Required** | *Provided* | +/// | ULClipboard | *Optional* | *Provided* | +/// | ULGPUDriver | *Optional* | *Provided* | +/// | ULLogger | *Optional* | *Provided* | +/// | ULSurfaceDefinition | *Provided* | *Provided* | +/// +/// @note This singleton should be set up before creating the Renderer or App. +/// +#ifndef ULTRALIGHT_CAPI_PLATFORM_H +#define ULTRALIGHT_CAPI_PLATFORM_H + +#include +#include +#include +#include +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Platform + *****************************************************************************/ + +/// +/// Set a custom Logger implementation. +/// +/// This is used to log debug messages to the console or to a log file. +/// +/// You should call this before ulCreateRenderer() or ulCreateApp(). +/// +/// @note ulCreateApp() will use the default logger if you never call this. +/// +/// @note If you're not using ulCreateApp(), (eg, using ulCreateRenderer()) you can still use the +/// default logger by calling ulEnableDefaultLogger() (@see ) +/// +ULExport void ulPlatformSetLogger(ULLogger logger); + +/// +/// Set a custom FileSystem implementation. +/// +/// The library uses this to load all file URLs (eg, ). +/// +/// You can provide the library with your own FileSystem implementation so that file assets are +/// loaded from your own pipeline. +/// +/// You should call this before ulCreateRenderer() or ulCreateApp(). +/// +/// @warning This is required to be defined before calling ulCreateRenderer() +/// +/// @note ulCreateApp() will use the default platform file system if you never call this. +/// +/// @note If you're not using ulCreateApp(), (eg, using ulCreateRenderer()) you can still use the +/// default platform file system by calling ulEnablePlatformFileSystem()' +/// (@see ) +/// +ULExport void ulPlatformSetFileSystem(ULFileSystem file_system); + +/// +/// Set a custom FontLoader implementation. +/// +/// The library uses this to load all system fonts. +/// +/// Every operating system has its own library of installed system fonts. The FontLoader interface +/// is used to lookup these fonts and fetch the actual font data (raw TTF/OTF file data) for a given +/// given font description. +/// +/// You should call this before ulCreateRenderer() or ulCreateApp(). +/// +/// @warning This is required to be defined before calling ulCreateRenderer() +/// +/// @note ulCreateApp() will use the default platform font loader if you never call this. +/// +/// @note If you're not using ulCreateApp(), (eg, using ulCreateRenderer()) you can still use the +/// default platform font loader by calling ulEnablePlatformFontLoader()' +/// (@see ) +/// +ULExport void ulPlatformSetFontLoader(ULFontLoader font_loader); + +/// +/// Set a custom Surface implementation. +/// +/// This can be used to wrap a platform-specific GPU texture, Windows DIB, macOS CGImage, or any +/// other pixel buffer target for display on screen. +/// +/// By default, the library uses a bitmap surface for all surfaces but you can override this by +/// providing your own surface definition here. +/// +/// You should call this before ulCreateRenderer() or ulCreateApp(). +/// +ULExport void ulPlatformSetSurfaceDefinition(ULSurfaceDefinition surface_definition); + +/// +/// Set a custom GPUDriver implementation. +/// +/// This should be used if you have enabled the GPU renderer in the Config and are using +/// ulCreateRenderer() (which does not provide its own GPUDriver implementation). +/// +/// The GPUDriver interface is used by the library to dispatch GPU calls to your native GPU context +/// (eg, D3D11, Metal, OpenGL, Vulkan, etc.) There are reference implementations for this interface +/// in the AppCore repo. +/// +/// You should call this before ulCreateRenderer(). +/// +ULExport void ulPlatformSetGPUDriver(ULGPUDriver gpu_driver); + +/// +/// Set a custom Clipboard implementation. +/// +/// This should be used if you are using ulCreateRenderer() (which does not provide its own +/// clipboard implementation). +/// +/// The Clipboard interface is used by the library to make calls to the system's native clipboard +/// (eg, cut, copy, paste). +/// +/// You should call this before ulCreateRenderer(). +/// +ULExport void ulPlatformSetClipboard(ULClipboard clipboard); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_PLATFORM_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Renderer.h b/include/Ultralight/CAPI/CAPI_Renderer.h new file mode 100644 index 0000000..b8f208f --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Renderer.h @@ -0,0 +1,278 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Renderer.h +/// +/// Core renderer singleton for the library, coordinates all library functions. +/// +/// `#include ` +/// +/// The Renderer class is responsible for creating and painting Views, managing Sessions, as well +/// as coordinating network requests, events, JavaScript execution, and more. +/// +/// ## Creating the Renderer +/// +/// @note A Renderer will be created for you automatically when you call ulCreateApp (access it +/// via ulAppGetRenderer()). +/// +/// @note ulCreateApp() is part of the AppCore API and automatically manages window creation, run +/// loop, input, painting, and most platform-specific functionality. (Available on desktop +/// platforms only) +/// \endparblock +/// +/// ### Defining Platform Handlers +/// +/// Before creating the Renderer, you should define your platform handlers via the Platform +/// singleton (see CAPI_Platform.h). This can be used to customize file loading, font loading, +/// clipboard access, and other functionality typically provided by the OS. +/// +/// Default implementations for most platform handlers are available in the +/// [AppCore repo](https://github.com/ultralight-ux/AppCore/tree/master/src). You can use these +/// stock implementations by copying the code into your project, or you can write your own. +/// +/// At a minimum, you should provide a ULFileSystem and ULFontLoader otherwise Renderer creation will +/// fail. +/// +/// ### Creating the Renderer +/// +/// Once you've set up the Platform handlers you can create the Renderer by calling +/// `ulCreateRenderer()`. +/// +/// @par Example creation code +/// ``` +/// // Setup our config. +/// ULConfig config = ulCreateConfig(); +/// +/// // Use AppCore's font loader. +/// ulEnablePlatformFontLoader(); +/// +/// // Use AppCore's file system to load file:/// URLs from the OS. +/// ULString base_dir = ulCreateString("./assets/"); +/// ulEnablePlatformFileSystem(base_dir); +/// ulDestroyString(base_dir); +/// +/// // Create the renderer. +/// ULRenderer renderer = ulCreateRenderer(config); +/// +/// // Destroy the config. +/// ulDestroyConfig(config); +/// +/// // Set up Views here... +/// ``` +/// +/// ## Updating Renderer Logic +/// +/// You should call ulUpdate() from your main update loop as often as possible to give the +/// library an opportunity to dispatch events and timers: +/// +/// @par Example update code +/// ``` +/// void mainLoop() +/// { +/// while(true) +/// { +/// // Update program logic here +/// ulUpdate(renderer); +/// } +/// } +/// ``` +/// +/// ## Rendering Each Frame +/// +/// When your program is ready to display a new frame (usually in synchrony with the monitor +/// refresh rate), you should call `ulRefreshDisplay()` and `ulRender` so the +/// library can render all active Views as needed. +/// +/// @par Example per-frame render code +/// ``` +/// void displayFrame() +/// { +/// // Notify the renderer that the main display has refreshed. This will update animations, +/// // smooth scroll, and window.requestAnimationFrame() for all Views matching the display id. +/// ulRefreshDisplay(renderer, 0); +/// +/// // Render all Views as needed +/// ulRender(renderer); +/// +/// // Each View will render to a +/// // - Pixel-Buffer Surface (ulViewGetSurface()) +/// // or +/// // - GPU texture (ulViewGetRenderTarget()) +/// // based on whether CPU or GPU rendering is used. +/// // +/// // You will need to display the image data here as needed. +/// } +/// } +/// ``` +/// +#ifndef ULTRALIGHT_CAPI_RENDERER_H +#define ULTRALIGHT_CAPI_RENDERER_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// +/// Create the core renderer singleton for the library. +/// +/// You should set up the Platform singleton (see CAPI_Platform.h) before calling this function. +/// +/// @note You do not need to the call this if you're using ulCreateApp() from AppCore. +/// +/// \parblock +/// @warning You'll need to define a ULFontLoader and ULFileSystem within the Platform singleton +/// or else this call will fail. +/// \endparblock +/// +/// \parblock +/// @warning You should only create one Renderer during the lifetime of your program. +/// \endparblock +/// +/// @param config The configuration to use for the renderer. +/// +/// @return Returns the new renderer instance. +/// +ULExport ULRenderer ulCreateRenderer(ULConfig config); + +/// +/// Destroy the renderer. +/// +/// @param renderer The renderer instance to destroy. +/// +ULExport void ulDestroyRenderer(ULRenderer renderer); + +/// +/// Update timers and dispatch internal callbacks (JavaScript and network). +/// +/// @param renderer The active renderer instance. +/// +ULExport void ulUpdate(ULRenderer renderer); + +/// +/// Notify the renderer that a display has refreshed (you should call this after vsync). +/// +/// This updates animations, smooth scroll, and window.requestAnimationFrame() for all Views +/// matching the display id. +/// +/// @param renderer The active renderer instance. +/// +/// @param display_id The display ID to refresh (0 by default). +/// +ULExport void ulRefreshDisplay(ULRenderer renderer, unsigned int display_id); + +/// +/// Render all active Views to their respective surfaces and render targets. +/// +/// @param renderer The active renderer instance. +/// +ULExport void ulRender(ULRenderer renderer); + +/// +/// Attempt to release as much memory as possible. Don't call this from any callbacks or driver +/// code. +/// +/// @param renderer The active renderer instance. +/// +ULExport void ulPurgeMemory(ULRenderer renderer); + +/// +/// Print detailed memory usage statistics to the log. (@see ulPlatformSetLogger) +/// +/// @param renderer The active renderer instance. +/// +ULExport void ulLogMemoryUsage(ULRenderer renderer); + +/// +/// Start the remote inspector server. +/// +/// While the remote inspector is active, Views that are loaded into this renderer +/// will be able to be remotely inspected from another Ultralight instance either locally +/// (another app on same machine) or remotely (over the network) by navigating a View to: +/// +/// \code +/// inspector://
: +/// \endcode +/// +/// @param renderer The active renderer instance. +/// +/// @param address The address for the server to listen on (eg, "127.0.0.1") +/// +/// @param port The port for the server to listen on (eg, 9222) +/// +/// @return Returns whether the server started successfully or not. +/// +ULExport bool ulStartRemoteInspectorServer(ULRenderer renderer, const char* address, + unsigned short port); + +/// +/// Describe the details of a gamepad, to be used with ulFireGamepadEvent and related +/// events below. This can be called multiple times with the same index if the details change. +/// +/// @param renderer The active renderer instance. +/// +/// @param index The unique index (or "connection slot") of the gamepad. For example, +/// controller #1 would be "1", controller #2 would be "2" and so on. +/// +/// @param id A string ID representing the device, this will be made available +/// in JavaScript as gamepad.id +/// +/// @param axis_count The number of axes on the device. +/// +/// @param button_count The number of buttons on the device. +/// +ULExport void ulSetGamepadDetails(ULRenderer renderer, unsigned int index, ULString id, + unsigned int axis_count, unsigned int button_count); + +/// +/// Fire a gamepad event (connection / disconnection). +/// +/// @note The gamepad should first be described via ulSetGamepadDetails before calling this +/// function. +/// +/// @param renderer The active renderer instance. +/// +/// @param evt The event to fire. +/// +/// @see +/// +ULExport void ulFireGamepadEvent(ULRenderer renderer, ULGamepadEvent evt); + +/// +/// Fire a gamepad axis event (to be called when an axis value is changed). +/// +/// @note The gamepad should be connected via a previous call to ulFireGamepadEvent. +/// +/// @param renderer The active renderer instance. +/// +/// @param evt The event to fire. +/// +/// @see +/// +ULExport void ulFireGamepadAxisEvent(ULRenderer renderer, ULGamepadAxisEvent evt); + +/// +/// Fire a gamepad button event (to be called when a button value is changed). +/// +/// @note The gamepad should be connected via a previous call to ulFireGamepadEvent. +/// +/// @param renderer The active renderer instance. +/// +/// @param evt The event to fire. +/// +/// @see +/// +ULExport void ulFireGamepadButtonEvent(ULRenderer renderer, ULGamepadButtonEvent evt); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_RENDERER_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_ScrollEvent.h b/include/Ultralight/CAPI/CAPI_ScrollEvent.h new file mode 100644 index 0000000..7bc94ef --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_ScrollEvent.h @@ -0,0 +1,45 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_ScrollEvent.h +/// +/// Scroll event interface. +/// +/// `#include ` +/// +/// This file defines the C API for scroll events. +/// +#ifndef ULTRALIGHT_CAPI_SCROLLEVENT_H +#define ULTRALIGHT_CAPI_SCROLLEVENT_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Scroll Event + *****************************************************************************/ + +/// +/// Create a scroll event, see ScrollEvent in the C++ API for help using this function. +/// +ULExport ULScrollEvent ulCreateScrollEvent(ULScrollEventType type, int delta_x, int delta_y); + +/// +/// Destroy a scroll event. +/// +ULExport void ulDestroyScrollEvent(ULScrollEvent evt); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_SCROLLEVENT_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Session.h b/include/Ultralight/CAPI/CAPI_Session.h new file mode 100644 index 0000000..e70d493 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Session.h @@ -0,0 +1,84 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Session.h +/// +/// Storage for a browsing session (cookies, local storage, etc.). +/// +/// `#include ` +/// +/// This class stores data for a unique browsing session (cookies, local storage, application cache, +/// indexed db. etc.). You can create multiple sessions to isolate data between different browsing +/// contexts. +/// +/// ## Default Session +/// +/// The library has a default session named "default" that is used if no session is specified when +/// when creating a View. +/// +/// ## Session Lifetime +/// +/// Sessions can be either temporary (in-memory only) or persistent (backed to disk). +/// +#ifndef ULTRALIGHT_CAPI_SESSION_H +#define ULTRALIGHT_CAPI_SESSION_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Session + *****************************************************************************/ + +/// +/// Create a Session to store local data in (such as cookies, local storage, application cache, +/// indexed db, etc). +/// +ULExport ULSession ulCreateSession(ULRenderer renderer, bool is_persistent, ULString name); + +/// +/// Destroy a Session. +/// +ULExport void ulDestroySession(ULSession session); + +/// +/// Get the default session (persistent session named "default"). +/// +/// @note This session is owned by the Renderer, you shouldn't destroy it. +/// +ULExport ULSession ulDefaultSession(ULRenderer renderer); + +/// +/// Whether or not is persistent (backed to disk). +/// +ULExport bool ulSessionIsPersistent(ULSession session); + +/// +/// Unique name identifying the session (used for unique disk path). +/// +ULExport ULString ulSessionGetName(ULSession session); + +/// +/// Unique numeric Id for the session. +/// +ULExport unsigned long long ulSessionGetId(ULSession session); + +/// +/// The disk path to write to (used by persistent sessions only). +/// +ULExport ULString ulSessionGetDiskPath(ULSession session); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_SESSION_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_String.h b/include/Ultralight/CAPI/CAPI_String.h new file mode 100644 index 0000000..cc4ec29 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_String.h @@ -0,0 +1,86 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_String.h +/// +/// Unicode string container (natively UTF-8). +/// +/// `#include ` +/// +/// This class is used to represent strings in Ultralight. It can be created from a variety of +/// string types (ASCII, UTF-8, UTF-16) and accessed as a null-terminated UTF-8 buffer. +/// +#ifndef ULTRALIGHT_CAPI_STRING_H +#define ULTRALIGHT_CAPI_STRING_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * String + *****************************************************************************/ + +/// +/// Create string from null-terminated ASCII C-string. +/// +ULExport ULString ulCreateString(const char* str); + +/// +/// Create string from UTF-8 buffer. +/// +ULExport ULString ulCreateStringUTF8(const char* str, size_t len); + +/// +/// Create string from UTF-16 buffer. +/// +ULExport ULString ulCreateStringUTF16(ULChar16* str, size_t len); + +/// +/// Create string from copy of existing string. +/// +ULExport ULString ulCreateStringFromCopy(ULString str); + +/// +/// Destroy string (you should destroy any strings you explicitly Create). +/// +ULExport void ulDestroyString(ULString str); + +/// +/// Get native UTF-8 buffer data (always null-terminated). +/// +ULExport char* ulStringGetData(ULString str); + +/// +/// Get length (in bytes) of the UTF-8 buffer data, not including null terminator. +/// +ULExport size_t ulStringGetLength(ULString str); + +/// +/// Whether this string is empty or not. +/// +ULExport bool ulStringIsEmpty(ULString str); + +/// +/// Replaces the contents of 'str' with the contents of 'new_str' +/// +ULExport void ulStringAssignString(ULString str, ULString new_str); + +/// +/// Replaces the contents of 'str' with the contents of a C-string. +/// +ULExport void ulStringAssignCString(ULString str, const char* c_str); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_STRING_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_Surface.h b/include/Ultralight/CAPI/CAPI_Surface.h new file mode 100644 index 0000000..21d45b5 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_Surface.h @@ -0,0 +1,250 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_Surface.h +/// +/// User-defined pixel buffer surface. +/// +/// `#include ` +/// +/// The library uses this to store pixel data when rendering Views on the CPU (see +/// ulViewIsAccelerated()). +/// +/// You can provide the library with your own Surface implementation to reduce the latency of +/// displaying pixels in your application (Views will be drawn directly to a block of memory +/// controlled by you). +/// +/// When a View is rendered on the CPU, you can retrieve the backing Surface via ulViewGetSurface(). +/// +/// @pre This is automatically managed for you when using ulCreateApp(), if you want to override +/// ULSurfaceDefinition, you'll need to use ulCreateRenderer() instead. +/// +/// ## Default Implementation +/// +/// A default Surface implementation, BitmapSurface, is automatically provided by the library when +/// you call ulCreateRenderer() without defining a custom ULSurfaceDefinition. +/// +/// You should cast the ULSurface to a ULBitmapSurface and call ulBitmapSurfaceGetBitmap() to access +/// the underlying Bitmap. +/// +/// ## Setting the Surface Implementation +/// +/// To define your own implementation, you should implement the ULSurfaceDefinition callbacks, +/// and then pass an instance of ULSurfaceDefinition containing your callbacks to +/// ulPlatformSetSurfaceDefinition() before calling ulCreateRenderer(). +/// +#ifndef ULTRALIGHT_CAPI_SURFACE_H +#define ULTRALIGHT_CAPI_SURFACE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * Surface + *****************************************************************************/ + +/// +/// Width (in pixels). +/// +ULExport unsigned int ulSurfaceGetWidth(ULSurface surface); + +/// +/// Height (in pixels). +/// +ULExport unsigned int ulSurfaceGetHeight(ULSurface surface); + +/// +/// Number of bytes between rows (usually width * 4) +/// +ULExport unsigned int ulSurfaceGetRowBytes(ULSurface surface); + +/// +/// Size in bytes. +/// +ULExport size_t ulSurfaceGetSize(ULSurface surface); + +/// +/// Lock the pixel buffer and get a pointer to the beginning of the data for reading/writing. +/// +/// Native pixel format is premultiplied BGRA 32-bit (8 bits per channel). +/// +ULExport void* ulSurfaceLockPixels(ULSurface surface); + +/// +/// Unlock the pixel buffer. +/// +ULExport void ulSurfaceUnlockPixels(ULSurface surface); + +/// +/// Resize the pixel buffer to a certain width and height (both in pixels). +/// +/// This should never be called while pixels are locked. +/// +ULExport void ulSurfaceResize(ULSurface surface, unsigned int width, unsigned int height); + +/// +/// Set the dirty bounds to a certain value. +/// +/// This is called after the Renderer paints to an area of the pixel buffer. (The new value will be +/// joined with the existing dirty_bounds()) +/// +ULExport void ulSurfaceSetDirtyBounds(ULSurface surface, ULIntRect bounds); + +/// +/// Get the dirty bounds. +/// +/// This value can be used to determine which portion of the pixel buffer has been updated since the +/// last call to ulSurfaceClearDirtyBounds(). +/// +/// The general algorithm to determine if a Surface needs display is: +///
+///   if (!ulIntRectIsEmpty(ulSurfaceGetDirtyBounds(surface))) {
+///       // Surface pixels are dirty and needs display.
+///       // Cast Surface to native Surface and use it here (pseudo code)
+///       DisplaySurface(surface);
+///
+///       // Once you're done, clear the dirty bounds:
+///       ulSurfaceClearDirtyBounds(surface);
+///  }
+///  
+/// +ULExport ULIntRect ulSurfaceGetDirtyBounds(ULSurface surface); + +/// +/// Clear the dirty bounds. +/// +/// You should call this after you're done displaying the Surface. +/// +ULExport void ulSurfaceClearDirtyBounds(ULSurface surface); + +/// +/// Get the underlying user data pointer (this is only valid if you have set a custom surface +/// implementation via ulPlatformSetSurfaceDefinition). +/// +/// This will return nullptr if this surface is the default ULBitmapSurface. +/// +ULExport void* ulSurfaceGetUserData(ULSurface surface); + +/****************************************************************************** + * BitmapSurface + *****************************************************************************/ + +/// +/// Get the underlying Bitmap from the default Surface. +/// +/// @note Do not call ulDestroyBitmap() on the returned value, it is owned by the surface. +/// +ULExport ULBitmap ulBitmapSurfaceGetBitmap(ULBitmapSurface surface); + +/****************************************************************************** + * Surface Definition + *****************************************************************************/ + +/// +/// The callback invoked when a Surface is created. +/// +/// @param width The width in pixels. +/// @param height The height in pixels. +/// +/// @return This callback should return a pointer to user-defined data for the instance. This user +/// data pointer will be passed to all other callbacks when operating on the instance. +/// +typedef void* (*ULSurfaceDefinitionCreateCallback)(unsigned int width, unsigned int height); + +/// +/// The callback invoked when a Surface is destroyed. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef void (*ULSurfaceDefinitionDestroyCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's width (in pixels) is requested. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef unsigned int (*ULSurfaceDefinitionGetWidthCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's height (in pixels) is requested. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef unsigned int (*ULSurfaceDefinitionGetHeightCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's row bytes is requested. +/// +/// @note This value is also known as "stride". Usually width * 4. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef unsigned int (*ULSurfaceDefinitionGetRowBytesCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's size (in bytes) is requested. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef size_t (*ULSurfaceDefinitionGetSizeCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's pixel buffer is requested to be locked for reading/writing +/// (should return a pointer to locked bytes). +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef void* (*ULSurfaceDefinitionLockPixelsCallback)(void* user_data); + +/// +/// The callback invoked when a Surface's pixel buffer is requested to be unlocked after previously +/// being locked. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +typedef void (*ULSurfaceDefinitionUnlockPixelsCallback)(void* user_data); + +/// +/// The callback invoked when a Surface is requested to be resized to a certain width/height. +/// +/// @param user_data User data pointer uniquely identifying the surface. +/// +/// @param width Width in pixels. +/// +/// @param height Height in pixels. +/// +typedef void (*ULSurfaceDefinitionResizeCallback)(void* user_data, unsigned int width, + unsigned int height); + +/// +/// User-defined surface interface. +/// +/// You should implement each of these callbacks, then pass an instance of this struct containing +/// your callbacks to ulPlatformSetSurfaceDefinition(). +/// +typedef struct { + ULSurfaceDefinitionCreateCallback create; + ULSurfaceDefinitionDestroyCallback destroy; + ULSurfaceDefinitionGetWidthCallback get_width; + ULSurfaceDefinitionGetHeightCallback get_height; + ULSurfaceDefinitionGetRowBytesCallback get_row_bytes; + ULSurfaceDefinitionGetSizeCallback get_size; + ULSurfaceDefinitionLockPixelsCallback lock_pixels; + ULSurfaceDefinitionUnlockPixelsCallback unlock_pixels; + ULSurfaceDefinitionResizeCallback resize; +} ULSurfaceDefinition; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_SURFACE_H \ No newline at end of file diff --git a/include/Ultralight/CAPI/CAPI_View.h b/include/Ultralight/CAPI/CAPI_View.h new file mode 100644 index 0000000..794d550 --- /dev/null +++ b/include/Ultralight/CAPI/CAPI_View.h @@ -0,0 +1,554 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +/// +/// @file CAPI_View.h +/// +/// Web-page container rendered to an offscreen surface. +/// +/// `#include ` +/// +/// The View class is responsible for loading and rendering web-pages to an offscreen surface. It +/// is completely isolated from the OS windowing system, you must forward all input events to it +/// from your application. +/// +/// ## Creating a View +/// +/// You can create a View by calling ulCreateView(): +/// +/// ``` +/// // Create a ULViewConfig with default values +/// ULViewConfig view_config = ulCreateViewConfig(); +/// +/// // Create a View, 500 by 500 pixels in size, using the default Session +/// ULView view = ulCreateView(renderer, 500, 500, view_config, NULL); +/// +/// // Clean up the ULViewConfig +/// ulDestroyViewConfig(view_config); +/// ``` +/// +/// @note When using ulCreateApp(), the library will automatically create a View for you when you +/// call ulCreateOverlay(). +/// +#ifndef ULTRALIGHT_CAPI_VIEW_H +#define ULTRALIGHT_CAPI_VIEW_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/****************************************************************************** + * ViewConfig + *****************************************************************************/ + +/// +/// Create view configuration with default values (see ). +/// +ULExport ULViewConfig ulCreateViewConfig(); + +/// +/// Destroy view configuration. +/// +ULExport void ulDestroyViewConfig(ULViewConfig config); + +/// +/// Set a user-generated id of the display (monitor, TV, or screen) that the View will be shown on. +/// +/// Animations are driven based on the physical refresh rate of the display. Multiple Views can +/// share the same display. +/// +/// +/// @note This is automatically managed for you when ulCreateApp() is used. +/// +/// @see ulRefreshDisplay() +/// +ULExport void ulViewConfigSetDisplayId(ULViewConfig config, unsigned int display_id); + +/// +/// Set whether to render using the GPU renderer (accelerated) or the CPU renderer (unaccelerated). +/// +/// This option is only valid if you're managing the Renderer yourself (eg, you've previously +/// called ulCreateRenderer() instead of ulCreateApp()). +/// +/// When true, the View will be rendered to an offscreen GPU texture using the GPU driver set in +/// ulPlatformSetGPUDriver(). You can fetch details for the texture via ulViewGetRenderTarget(). +/// +/// When false (the default), the View will be rendered to an offscreen pixel buffer using the +/// multithreaded CPU renderer. This pixel buffer can optionally be provided by the user-- +/// for more info see ulViewGetSurface(). +/// +ULExport void ulViewConfigSetIsAccelerated(ULViewConfig config, bool is_accelerated); + +/// +/// Set whether images should be enabled (Default = True). +/// +ULExport void ulViewConfigSetIsTransparent(ULViewConfig config, bool is_transparent); + +/// +/// Set the initial device scale, ie. the amount to scale page units to screen pixels. This should be +/// set to the scaling factor of the device that the View is displayed on. (Default = 1.0) +/// +/// @note 1.0 is equal to 100% zoom (no scaling), 2.0 is equal to 200% zoom (2x scaling) +/// +ULExport void ulViewConfigSetInitialDeviceScale(ULViewConfig config, double initial_device_scale); + +/// +/// Set whether or not the View should initially have input focus. (Default = True) +/// +ULExport void ulViewConfigSetInitialFocus(ULViewConfig config, bool is_focused); + +/// +/// Set whether images should be enabled (Default = True). +/// +ULExport void ulViewConfigSetEnableImages(ULViewConfig config, bool enabled); + +/// +/// Set whether JavaScript should be enabled (Default = True). +/// +ULExport void ulViewConfigSetEnableJavaScript(ULViewConfig config, bool enabled); + +/// +/// Set default font-family to use (Default = Times New Roman). +/// +ULExport void ulViewConfigSetFontFamilyStandard(ULViewConfig config, ULString font_name); + +/// +/// Set default font-family to use for fixed fonts, eg
 and 
+/// (Default = Courier New).
+///
+ULExport void ulViewConfigSetFontFamilyFixed(ULViewConfig config, ULString font_name);
+
+///
+/// Set default font-family to use for serif fonts (Default = Times New Roman).
+///
+ULExport void ulViewConfigSetFontFamilySerif(ULViewConfig config, ULString font_name);
+
+///
+/// Set default font-family to use for sans-serif fonts (Default = Arial).
+///
+ULExport void ulViewConfigSetFontFamilySansSerif(ULViewConfig config, ULString font_name);
+
+///
+/// Set user agent string (See  for the default).
+///
+ULExport void ulViewConfigSetUserAgent(ULViewConfig config, ULString agent_string);
+
+/******************************************************************************
+ * View
+ *****************************************************************************/
+
+///
+/// Create a View with certain size (in pixels).
+///
+/// @note  You can pass null to 'session' to use the default session.
+///
+ULExport ULView ulCreateView(ULRenderer renderer, unsigned int width, unsigned int height,
+                             ULViewConfig view_config, ULSession session);
+
+///
+/// Destroy a View.
+///
+ULExport void ulDestroyView(ULView view);
+
+///
+/// Get current URL.
+///
+/// @note Don't destroy the returned string, it is owned by the View.
+///
+ULExport ULString ulViewGetURL(ULView view);
+
+///
+/// Get current title.
+///
+/// @note Don't destroy the returned string, it is owned by the View.
+///
+ULExport ULString ulViewGetTitle(ULView view);
+
+///
+/// Get the width, in pixels.
+///
+ULExport unsigned int ulViewGetWidth(ULView view);
+
+///
+/// Get the height, in pixels.
+///
+ULExport unsigned int ulViewGetHeight(ULView view);
+
+///
+// Get the display id of the View.
+///
+ULExport unsigned int ulViewGetDisplayId(ULView view);
+
+///
+/// Set the display id of the View.
+/// 
+/// This should be called when the View is moved to another display.
+/// 
+ULExport void ulViewSetDisplayId(ULView view, unsigned int display_id);
+
+///
+/// Get the device scale, ie. the amount to scale page units to screen pixels.
+///
+/// For example, a value of 1.0 is equivalent to 100% zoom. A value of 2.0 is 200% zoom.
+///
+ULExport double ulViewGetDeviceScale(ULView view);
+
+///
+/// Set the device scale.
+///
+ULExport void ulViewSetDeviceScale(ULView view, double scale);
+
+///
+/// Whether or not the View is GPU-accelerated. If this is false, the page will be rendered
+/// via the CPU renderer.
+///
+ULExport bool ulViewIsAccelerated(ULView view);
+
+///
+/// Whether or not the View supports transparent backgrounds.
+///
+ULExport bool ulViewIsTransparent(ULView view);
+
+///
+/// Check if the main frame of the page is currrently loading.
+///
+ULExport bool ulViewIsLoading(ULView view);
+
+///
+/// Get the RenderTarget for the View.
+///
+/// @note  Only valid if this View is GPU accelerated.
+/// 
+///        You can use this with your GPUDriver implementation to bind and display the
+///        corresponding texture in your application.
+///
+ULExport ULRenderTarget ulViewGetRenderTarget(ULView view);
+
+///
+/// Get the Surface for the View (native pixel buffer that the CPU renderer draws into).
+///
+/// @note  This operation is only valid if you're managing the Renderer yourself (eg, you've
+///        previously called ulCreateRenderer() instead of ulCreateApp()).
+///
+///        This function will return NULL if this View is GPU accelerated.
+///
+///        The default Surface is BitmapSurface but you can provide your own Surface implementation
+///        via ulPlatformSetSurfaceDefinition.
+///
+///        When using the default Surface, you can retrieve the underlying bitmap by casting
+///        ULSurface to ULBitmapSurface and calling ulBitmapSurfaceGetBitmap().
+///
+ULExport ULSurface ulViewGetSurface(ULView view);
+
+///
+/// Load a raw string of HTML.
+///
+ULExport void ulViewLoadHTML(ULView view, ULString html_string);
+
+///
+/// Load a URL into main frame.
+///
+ULExport void ulViewLoadURL(ULView view, ULString url_string);
+
+///
+/// Resize view to a certain width and height (in pixels).
+///
+ULExport void ulViewResize(ULView view, unsigned int width, unsigned int height);
+
+///
+/// Acquire the page's JSContext for use with JavaScriptCore API.
+///
+/// @note  This call locks the context for the current thread. You should call
+///        ulViewUnlockJSContext() after using the context so other worker threads can modify
+///        JavaScript state.
+///
+/// @note  The lock is recusive, it's okay to call this multiple times as long as you call
+///        ulViewUnlockJSContext() the same number of times.
+///
+ULExport JSContextRef ulViewLockJSContext(ULView view);
+
+///
+/// Unlock the page's JSContext after a previous call to ulViewLockJSContext().
+///
+ULExport void ulViewUnlockJSContext(ULView view);
+
+///
+/// Evaluate a string of JavaScript and return result.
+///
+/// @param  js_string  The string of JavaScript to evaluate.
+///
+/// @param  exception  The address of a ULString to store a description of the last exception. Pass
+///                    NULL to ignore this. Don't destroy the exception string returned, it's owned
+///                    by the View.
+///
+/// @note Don't destroy the returned string, it's owned by the View. This value is reset with every
+///       call-- if you want to retain it you should copy the result to a new string via
+///       ulCreateStringFromCopy().
+///
+/// @note An example of using this API:
+///       
+///         ULString script = ulCreateString("1 + 1");
+///         ULString exception;
+///         ULString result = ulViewEvaluateScript(view, script, &exception);
+///         /* Use the result ("2") and exception description (if any) here. */
+///         ulDestroyString(script);
+///       
+/// +ULExport ULString ulViewEvaluateScript(ULView view, ULString js_string, ULString* exception); + +/// +/// Check if can navigate backwards in history. +/// +ULExport bool ulViewCanGoBack(ULView view); + +/// +/// Check if can navigate forwards in history. +/// +ULExport bool ulViewCanGoForward(ULView view); + +/// +/// Navigate backwards in history. +/// +ULExport void ulViewGoBack(ULView view); + +/// +/// Navigate forwards in history. +/// +ULExport void ulViewGoForward(ULView view); + +/// +/// Navigate to arbitrary offset in history. +/// +ULExport void ulViewGoToHistoryOffset(ULView view, int offset); + +/// +/// Reload current page. +/// +ULExport void ulViewReload(ULView view); + +/// +/// Stop all page loads. +/// +ULExport void ulViewStop(ULView view); + +/// +/// Give focus to the View. +/// +/// You should call this to give visual indication that the View has input focus (changes active +/// text selection colors, for example). +/// +ULExport void ulViewFocus(ULView view); + +/// +/// Remove focus from the View and unfocus any focused input elements. +/// +/// You should call this to give visual indication that the View has lost input focus. +/// +ULExport void ulViewUnfocus(ULView view); + +/// +/// Whether or not the View has focus. +/// +ULExport bool ulViewHasFocus(ULView view); + +/// +/// Whether or not the View has an input element with visible keyboard focus (indicated by a +/// blinking caret). +/// +/// You can use this to decide whether or not the View should consume keyboard input events (useful +/// in games with mixed UI and key handling). +/// +ULExport bool ulViewHasInputFocus(ULView view); + +/// +/// Fire a keyboard event. +/// +ULExport void ulViewFireKeyEvent(ULView view, ULKeyEvent key_event); + +/// +/// Fire a mouse event. +/// +ULExport void ulViewFireMouseEvent(ULView view, ULMouseEvent mouse_event); + +/// +/// Fire a scroll event. +/// +ULExport void ulViewFireScrollEvent(ULView view, ULScrollEvent scroll_event); + +typedef void (*ULChangeTitleCallback)(void* user_data, ULView caller, ULString title); + +/// +/// Set callback for when the page title changes. +/// +ULExport void ulViewSetChangeTitleCallback(ULView view, ULChangeTitleCallback callback, + void* user_data); + +typedef void (*ULChangeURLCallback)(void* user_data, ULView caller, ULString url); + +/// +/// Set callback for when the page URL changes. +/// +ULExport void ulViewSetChangeURLCallback(ULView view, ULChangeURLCallback callback, + void* user_data); + +typedef void (*ULChangeTooltipCallback)(void* user_data, ULView caller, ULString tooltip); + +/// +/// Set callback for when the tooltip changes (usually result of a mouse hover). +/// +ULExport void ulViewSetChangeTooltipCallback(ULView view, ULChangeTooltipCallback callback, + void* user_data); + +typedef void (*ULChangeCursorCallback)(void* user_data, ULView caller, ULCursor cursor); + +/// +/// Set callback for when the mouse cursor changes. +/// +ULExport void ulViewSetChangeCursorCallback(ULView view, ULChangeCursorCallback callback, + void* user_data); + +typedef void (*ULAddConsoleMessageCallback)(void* user_data, ULView caller, ULMessageSource source, + ULMessageLevel level, ULString message, + unsigned int line_number, unsigned int column_number, + ULString source_id); + +/// +/// Set callback for when a message is added to the console (useful for JavaScript / network errors +/// and debugging). +/// +ULExport void ulViewSetAddConsoleMessageCallback(ULView view, ULAddConsoleMessageCallback callback, + void* user_data); + +typedef ULView (*ULCreateChildViewCallback)(void* user_data, ULView caller, ULString opener_url, + ULString target_url, bool is_popup, + ULIntRect popup_rect); + +/// +/// Set callback for when the page wants to create a new View. +/// +/// This is usually the result of a user clicking a link with target="_blank" or by JavaScript +/// calling window.open(url). +/// +/// To allow creation of these new Views, you should create a new View in this callback, resize it +/// to your container, and return it. You are responsible for displaying the returned View. +/// +/// You should return NULL if you want to block the action. +/// +ULExport void ulViewSetCreateChildViewCallback(ULView view, ULCreateChildViewCallback callback, + void* user_data); + +typedef ULView (*ULCreateInspectorViewCallback)(void* user_data, ULView caller, bool is_local, + ULString inspected_url); + +/// +/// Set callback for when the page wants to create a new View to display the local inspector in. +/// +/// You should create a new View in this callback, resize it to your +/// container, and return it. You are responsible for displaying the returned View. +/// +ULExport void ulViewSetCreateInspectorViewCallback(ULView view, ULCreateInspectorViewCallback callback, + void* user_data); + +typedef void (*ULBeginLoadingCallback)(void* user_data, ULView caller, unsigned long long frame_id, + bool is_main_frame, ULString url); + +/// +/// Set callback for when the page begins loading a new URL into a frame. +/// +ULExport void ulViewSetBeginLoadingCallback(ULView view, ULBeginLoadingCallback callback, + void* user_data); + +typedef void (*ULFinishLoadingCallback)(void* user_data, ULView caller, unsigned long long frame_id, + bool is_main_frame, ULString url); + +/// +/// Set callback for when the page finishes loading a URL into a frame. +/// +ULExport void ulViewSetFinishLoadingCallback(ULView view, ULFinishLoadingCallback callback, + void* user_data); + +typedef void (*ULFailLoadingCallback)(void* user_data, ULView caller, unsigned long long frame_id, + bool is_main_frame, ULString url, ULString description, + ULString error_domain, int error_code); + +/// +/// Set callback for when an error occurs while loading a URL into a frame. +/// +ULExport void ulViewSetFailLoadingCallback(ULView view, ULFailLoadingCallback callback, + void* user_data); + +typedef void (*ULWindowObjectReadyCallback)(void* user_data, ULView caller, + unsigned long long frame_id, bool is_main_frame, + ULString url); + +/// +/// Set callback for when the JavaScript window object is reset for a new page load. +/// +/// This is called before any scripts are executed on the page and is the earliest time to setup any +/// initial JavaScript state or bindings. +/// +/// The document is not guaranteed to be loaded/parsed at this point. If you need to make any +/// JavaScript calls that are dependent on DOM elements or scripts on the page, use DOMReady +/// instead. +/// +/// The window object is lazily initialized (this will not be called on pages with no scripts). +/// +ULExport void ulViewSetWindowObjectReadyCallback(ULView view, ULWindowObjectReadyCallback callback, + void* user_data); + +typedef void (*ULDOMReadyCallback)(void* user_data, ULView caller, unsigned long long frame_id, + bool is_main_frame, ULString url); + +/// +/// Set callback for when all JavaScript has been parsed and the document is ready. +/// +/// This is the best time to make any JavaScript calls that are dependent on DOM elements or scripts +/// on the page. +/// +ULExport void ulViewSetDOMReadyCallback(ULView view, ULDOMReadyCallback callback, void* user_data); + +typedef void (*ULUpdateHistoryCallback)(void* user_data, ULView caller); + +/// +/// Set callback for when the history (back/forward state) is modified. +/// +ULExport void ulViewSetUpdateHistoryCallback(ULView view, ULUpdateHistoryCallback callback, + void* user_data); + +/// +/// Set whether or not a view should be repainted during the next call to ulRender. +/// +/// @note This flag is automatically set whenever the page content changes but you can set it +/// directly in case you need to force a repaint. +/// +ULExport void ulViewSetNeedsPaint(ULView view, bool needs_paint); + +/// +/// Whether or not a view should be painted during the next call to ulRender. +/// +ULExport bool ulViewGetNeedsPaint(ULView view); + +/// +/// Create an Inspector View to inspect / debug this View locally. +/// +/// This will only succeed if you have the inspector assets in your filesystem-- the inspector +/// will look for file:///inspector/Main.html when it first loads. +/// +/// You must handle ulViewSetCreateInspectorViewCallback so that the library has a View to display +/// the inspector in. This function will call the callback only if an inspector view is not +/// currently active. +/// +ULExport void ulViewCreateLocalInspectorView(ULView view); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // ULTRALIGHT_CAPI_VIEW_H \ No newline at end of file diff --git a/include/Ultralight/Config.h b/include/Ultralight/Config.h new file mode 100644 index 0000000..37969f2 --- /dev/null +++ b/include/Ultralight/Config.h @@ -0,0 +1,25 @@ +// Generated by CMake - DO NOT EDIT +#pragma once + +// Current edition level (see UltralightEdition enum below) +#define ULTRALIGHT_EDITION_LEVEL 0 + +// Edition name string +#define ULTRALIGHT_EDITION_NAME "Free" + +// Edition defines +#define ULTRALIGHT_EDITION_FREE 0 +#define ULTRALIGHT_EDITION_PLUS 1 +#define ULTRALIGHT_EDITION_PRO 2 +#define ULTRALIGHT_EDITION_ENTERPRISE 3 +#define ULTRALIGHT_EDITION_UNLIMITED 4 + +// Edition check macros +#define UL_EDITION(x) (ULTRALIGHT_EDITION_LEVEL == ULTRALIGHT_EDITION_##x) +#define UL_EDITION_AT_LEAST(x) (ULTRALIGHT_EDITION_LEVEL >= ULTRALIGHT_EDITION_##x) + +// Maximum FPS each View can render at (0 is uncapped) +#define ULTRALIGHT_EDITION_MAX_FPS 60 + +// Whether or not this is an evaluation build +#define ULTRALIGHT_EVALUATION 0 diff --git a/include/Ultralight/ConsoleMessage.h b/include/Ultralight/ConsoleMessage.h new file mode 100644 index 0000000..e37ef99 --- /dev/null +++ b/include/Ultralight/ConsoleMessage.h @@ -0,0 +1,125 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// MessageSource types +/// +enum MessageSource { + kMessageSource_XML = 0, + kMessageSource_JS, + kMessageSource_Network, + kMessageSource_ConsoleAPI, + kMessageSource_Storage, + kMessageSource_AppCache, + kMessageSource_Rendering, + kMessageSource_CSS, + kMessageSource_Security, + kMessageSource_ContentBlocker, + kMessageSource_Media, + kMessageSource_MediaSource, + kMessageSource_WebRTC, + kMessageSource_ITPDebug, + kMessageSource_PrivateClickMeasurement, + kMessageSource_PaymentRequest, + kMessageSource_Other, +}; + +enum MessageType { + kMessageType_Log = 0, + kMessageType_Dir, + kMessageType_DirXML, + kMessageType_Table, + kMessageType_Trace, + kMessageType_StartGroup, + kMessageType_StartGroupCollapsed, + kMessageType_EndGroup, + kMessageType_Clear, + kMessageType_Assert, + kMessageType_Timing, + kMessageType_Profile, + kMessageType_ProfileEnd, + kMessageType_Image, +}; + +/// +/// MessageLevel types +/// +enum MessageLevel { + kMessageLevel_Log = 0, + kMessageLevel_Warning, + kMessageLevel_Error, + kMessageLevel_Debug, + kMessageLevel_Info, +}; + +/// +/// @brief Interface for console messages. +/// +class UExport ConsoleMessage { + public: + virtual ~ConsoleMessage() = default; + + /// + /// The source of the message. + /// + virtual MessageSource source() const = 0; + + /// + /// The type of content displayed. + /// + virtual MessageType type() const = 0; + + /// + /// The log level for the message. + /// + virtual MessageLevel level() const = 0; + + /// + /// The message as a string-- for multi-argument calls to console.log() this just converts the + /// first parameter to a string. + /// + virtual String message() const = 0; + + /// + /// The line number of the JavaScript associated with this call, if any. + /// + virtual uint32_t line_number() const = 0; + + /// + /// The column number of the JavaScript associated with this call, if any. + /// + virtual uint32_t column_number() const = 0; + + /// + /// The source id (eg, URL) of the page associated with this call, if any. + /// + virtual String source_id() const = 0; + + /// + /// The JavaScript execution context for the arguments, if any. + /// + virtual JSContextRef argument_context() const = 0; + + /// + /// The number of JavaScript arguments passed to console.log(), if any. + /// + virtual uint32_t num_arguments() const = 0; + + /// + /// Get the JavaScript argument at a specific index (numbering starts at 0). + /// + virtual JSValueRef argument_at(uint32_t idx) const = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/Defines.h b/include/Ultralight/Defines.h new file mode 100644 index 0000000..ea82f92 --- /dev/null +++ b/include/Ultralight/Defines.h @@ -0,0 +1,139 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2025 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once + +// Needed for limit defines, like INTMAX_MAX, which is used by the std C++ library +#ifndef __STDC_LIMIT_MACROS +#define __STDC_LIMIT_MACROS +#endif + +#include +#include +#include + +#ifdef SWIG +#define UExport +#else + +#include +#include + +// Require C++11 Support +#if defined(_MSC_VER) +# if _MSC_VER < 1800 +# error This project needs at least Visual Studio 2013 to build +# endif +#elif __cplusplus <= 199711L +# error This project can only be compiled with a compiler that supports C++11 +#endif + +#if INTPTR_MAX == INT32_MAX +# define UL_ARCH_32_BIT +#elif INTPTR_MAX == INT64_MAX +# define UL_ARCH_64_BIT +#else +# error "Unknown CPU architecture: environment not 32 or 64-bit." +#endif + +#if defined(__aarch64__) +# define UL_ARCH_ARM64 +# if defined(__APPLE__) +# define UL_ARCH_ARM64_APPLE_SILICON +# endif +#endif + +#if defined(__WIN32__) || defined(_WIN32) +# define _thread_local __declspec(thread) +# ifndef _NATIVE_WCHAR_T_DEFINED +# define DISABLE_NATIVE_WCHAR_T +# endif +#else +# define _thread_local __thread +#endif + +#ifndef UL_COMPILER_GCC_LIKE +# if defined(__clang__) || defined(__GNUC__) || defined(__GNUG__) +# define UL_COMPILER_GCC_LIKE +# endif +#endif + +#ifndef UL_ALWAYS_INLINE +#if defined(UL_COMPILER_GCC_LIKE) && defined(NDEBUG) +# define UL_ALWAYS_INLINE inline __attribute__((__always_inline__)) +# elif defined(_MSC_VER) && defined(NDEBUG) +# define UL_ALWAYS_INLINE __forceinline +# else +# define UL_ALWAYS_INLINE inline +# endif +#endif + +#ifndef UL_UNLIKELY +# if defined(UL_COMPILER_GCC_LIKE) +# define UL_UNLIKELY(x) __builtin_expect(!!(x), 0) +# else +# define UL_UNLIKELY(x) (x) +# endif +#endif + +#ifndef UL_ALIGN + #if defined(UL_COMPILER_GCC_LIKE) + #define UL_ALIGN(x) __attribute__((aligned(x))) + #elif defined(__cplusplus) && __cplusplus >= 201103L + #define UL_ALIGN(x) alignas(x) + #elif defined(_MSC_VER) + #define UL_ALIGN(x) __declspec(align(x)) + #else + #define UL_ALIGN(x) + #endif +#endif + +#endif // #ifdef SWIG + +#define ULTRALIGHT_VERSION "1.4.0" +#define ULTRALIGHT_VERSION_MAJOR 1 +#define ULTRALIGHT_VERSION_MINOR 4 +#define ULTRALIGHT_VERSION_PATCH 0 + +#define WEBKIT_VERSION "615.1.18.100.1" +#define WEBKIT_VERSION_MAJOR 615 +#define WEBKIT_VERSION_MINOR 1 +#define WEBKIT_VERSION_TINY 18 +#define WEBKIT_VERSION_MICRO 100 +#define WEBKIT_VERSION_NANO 1 + +#define ULTRALIGHT_USER_AGENT "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " \ + "AppleWebKit/615.1.18.100.1 (KHTML, like Gecko) " \ + "Ultralight/1.4.0 Version/16.4.1 Safari/615.1.18.100.1" + +#ifdef __cplusplus +extern "C" { +#endif + +/// +/// Full library version string (corresponds to ULTRALIGHT_VERSION) +/// +UExport const char* UltralightVersionString(); + +UExport uint32_t UltralightVersionMajor(); +UExport uint32_t UltralightVersionMinor(); +UExport uint32_t UltralightVersionPatch(); + +/// +/// Full WebKit version string (corresponds to WEBKIT_VERSION) +/// +UExport const char* WebKitVersionString(); + +UExport uint32_t WebKitVersionMajor(); +UExport uint32_t WebKitVersionMinor(); +UExport uint32_t WebKitVersionTiny(); +UExport uint32_t WebKitVersionMicro(); +UExport uint32_t WebKitVersionNano(); + +#ifdef __cplusplus +} +#endif \ No newline at end of file diff --git a/include/Ultralight/Exports.h b/include/Ultralight/Exports.h new file mode 100644 index 0000000..f026760 --- /dev/null +++ b/include/Ultralight/Exports.h @@ -0,0 +1,48 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#ifndef __ULTRALIGHT_EXPORTS_H__ +#define __ULTRALIGHT_EXPORTS_H__ + +#if defined(__WIN32__) || defined(_WIN32) +# if defined(ULTRALIGHT_STATIC_BUILD) +# define UExport +# else +# if defined(ULTRALIGHT_IMPLEMENTATION) +# define UExport __declspec(dllexport) +# else +# define UExport __declspec(dllimport) +# endif +# endif +#else +# if defined(ULTRALIGHT_STATIC_BUILD) +# define UExport +# else +# define UExport __attribute__((visibility("default"))) +# endif +#endif + +// UCExport is for symbols that are exported strictly by UltralightCore +#if defined(__WIN32__) || defined(_WIN32) +# if defined(ULTRALIGHT_STATIC_BUILD) +# define UCExport +# else +# if defined(ULTRALIGHT_IMPLEMENTATION) && defined(ULTRALIGHT_MODULE_ULTRALIGHTCORE) +# define UCExport __declspec(dllexport) +# else +# define UCExport __declspec(dllimport) +# endif +# endif +#else +# if defined(ULTRALIGHT_STATIC_BUILD) +# define UCExport +# else +# define UCExport __attribute__((visibility("default"))) +# endif +#endif + +#endif // __ULTRALIGHT_EXPORTS_H__ \ No newline at end of file diff --git a/include/Ultralight/GamepadEvent.h b/include/Ultralight/GamepadEvent.h new file mode 100644 index 0000000..8ceb790 --- /dev/null +++ b/include/Ultralight/GamepadEvent.h @@ -0,0 +1,109 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +/// +/// Event representing a change in gamepad connection state. +/// +/// @see Renderer::FireGamepadEvent +/// +class UExport GamepadEvent { + public: + /// + /// The various GamepadEvent types. + /// + enum Type { + /// + /// This event type should be fired when a gamepad is connected. + /// + /// @note You will need to previously declare the gamepad, its index, and details about + /// its axis and button layout via Renderer::SetGamepadDetails prior to calling + /// Renderer::FireGamepadEvent. + /// + kType_GamepadConnected, + + /// + /// This event type should be fired when a gamepad is disconnected. + /// + kType_GamepadDisconnected, + }; + + /// + // The type of this GamepadEvent + /// + Type type; + + /// + /// The index of the gamepad, this should match the value previously set in + /// Renderer::SetGamepadDetails. + /// + uint32_t index; +}; + +/// +/// Event representing a change in gamepad axis state (eg, pressing a stick in a certain direction). +/// +/// @see Renderer::FireGamepadAxisEvent +/// +class UExport GamepadAxisEvent { + public: + /// + /// The index of the gamepad, this should match the value previously set in + /// Renderer::SetGamepadDetails. + /// + uint32_t index; + + /// + /// The index of the axis whose value has changed. + /// + /// This value should be in the range previously set in Renderer::SetGamepadDetails. + /// + uint32_t axis_index; + + /// + /// The new value of the axis. + /// + /// This value should be normalized to the range [-1.0, 1.0]. + /// + double value; +}; + +/// +/// Event representing a change in gamepad button state (eg, pressing a button on a gamepad). +/// +/// @see Renderer::FireGamepadButtonEvent +/// +class UExport GamepadButtonEvent { + public: + /// + /// The index of the gamepad, this should match the value previously set in + /// Renderer::SetGamepadDetails. + /// + uint32_t index; + + /// + /// The index of the button whose value has changed. + /// + /// This value should be in the range previously set in Renderer::SetGamepadDetails. + /// + uint32_t button_index; + + /// + /// The new value of the button. + /// + /// This value should be normalized to the range [-1.0, 1.0], with any value greater than + /// 0.0 to be considered "pressed". + /// + double value; +}; + +} // namespace ultralight diff --git a/include/Ultralight/Geometry.h b/include/Ultralight/Geometry.h new file mode 100644 index 0000000..da010d1 --- /dev/null +++ b/include/Ultralight/Geometry.h @@ -0,0 +1,660 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include + +namespace ultralight { + +/// +/// 2D Vector Helper +/// +struct UExport vec2 { + float x, y; + + inline vec2() : x(0.0f), y(0.0f) { } + + inline vec2(float x, float y) : x(x), y(y) {} + + inline vec2(float x) : x(x), y(x) {} + + inline vec2 yx() const { return { y, x }; } + + inline vec2 xx() const { return { x, x }; } + + inline vec2 yy() const { return { y, y }; } + + inline friend vec2 operator+(vec2 lhs, const vec2& rhs) { lhs += rhs; return lhs; } + + inline friend vec2 operator-(vec2 lhs, const vec2& rhs) { lhs -= rhs; return lhs; } + + inline friend vec2 operator*(vec2 lhs, const vec2& rhs) { lhs *= rhs; return lhs; } + + inline friend vec2 operator/(vec2 lhs, const vec2& rhs) { lhs /= rhs; return lhs; } + + inline friend vec2 operator+(vec2 lhs, float rhs) { lhs += rhs; return lhs; } + + inline friend vec2 operator-(vec2 lhs, float rhs) { lhs -= rhs; return lhs; } + + inline friend vec2 operator*(vec2 lhs, float rhs) { lhs *= rhs; return lhs; } + + inline friend vec2 operator/(vec2 lhs, float rhs) { lhs /= rhs; return lhs; } + + inline vec2& operator+=(const vec2& rhs) { + x += rhs.x; + y += rhs.y; + return *this; + } + + inline vec2& operator-=(const vec2& rhs) { + x -= rhs.x; + y -= rhs.y; + return *this; + } + + inline vec2& operator*=(const vec2& rhs) { + x *= rhs.x; + y *= rhs.y; + return *this; + } + + inline vec2& operator/=(const vec2& rhs) { + x /= rhs.x; + y /= rhs.y; + return *this; + } + + inline vec2& operator+=(float rhs) { + x += rhs; + y += rhs; + return *this; + } + + inline vec2& operator-=(float rhs) { + x -= rhs; + y -= rhs; + return *this; + } + + inline vec2& operator*=(float rhs) { + x *= rhs; + y *= rhs; + return *this; + } + + inline vec2& operator/=(float rhs) { + x /= rhs; + y /= rhs; + return *this; + } + + inline friend bool operator==(const vec2& a, const vec2& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + inline friend bool operator!=(const vec2& a, const vec2& b) { + return !(a == b); + } + + inline friend vec2 min_(const vec2& a, const vec2& b) { + return{ (b.x < a.x) ? b.x : a.x, + (b.y < a.y) ? b.y : a.y }; + } + + inline friend vec2 max_(const vec2& a, const vec2& b) { + return{ (a.x < b.x) ? b.x : a.x, + (a.y < b.y) ? b.y : a.y }; + } + + inline friend vec2 clamp(const vec2& x, const vec2& minVal, const vec2& maxVal) { + return min_(max_(x, minVal), maxVal); + } + + inline friend vec2 mix(const vec2& a, const vec2& b, float t) { + return a * (1.0f - t) + b * t; + } + + inline friend float length(const vec2& a) { + return sqrtf(a.x * a.x + a.y * a.y); + } + + // squared length + inline friend float length2(const vec2& a) { + return dot(a, a); + } + + inline friend float distance(const vec2& a, const vec2& b) { + return length(a - b); + } + + // squared distance + inline friend float distance2(const vec2& a, const vec2& b) { + return length2(a - b); + } + + inline friend vec2 normalize(const vec2& a) { + return a / length(a); + } + + inline friend float dot(const vec2& a, const vec2& b) { + return a.x * b.x + a.y * b.y; + } +}; + +/// +/// 3D Vector Helper +/// +struct UExport vec3 { + float x, y, z; + + inline vec3() : x(0.0f), y(0.0f), z(0.0f) { } + + inline vec3(float x, float y, float z) : x(x), y(y), z(z) {} + + inline vec3(float x) : x(x), y(x), z(x) {} + + inline friend vec3 operator+(vec3 lhs, const vec3& rhs) { lhs += rhs; return lhs; } + + inline friend vec3 operator-(vec3 lhs, const vec3& rhs) { lhs -= rhs; return lhs; } + + inline friend vec3 operator*(vec3 lhs, const vec3& rhs) { lhs *= rhs; return lhs; } + + inline friend vec3 operator/(vec3 lhs, const vec3& rhs) { lhs /= rhs; return lhs; } + + inline friend vec3 operator+(vec3 lhs, float rhs) { lhs += rhs; return lhs; } + + inline friend vec3 operator-(vec3 lhs, float rhs) { lhs -= rhs; return lhs; } + + inline friend vec3 operator*(vec3 lhs, float rhs) { lhs *= rhs; return lhs; } + + inline friend vec3 operator/(vec3 lhs, float rhs) { lhs /= rhs; return lhs; } + + inline vec3& operator+=(const vec3& rhs) { + x += rhs.x; + y += rhs.y; + z += rhs.z; + return *this; + } + + inline vec3& operator-=(const vec3& rhs) { + x -= rhs.x; + y -= rhs.y; + z -= rhs.z; + return *this; + } + + inline vec3& operator*=(const vec3& rhs) { + x *= rhs.x; + y *= rhs.y; + z *= rhs.z; + return *this; + } + + inline vec3& operator/=(const vec3& rhs) { + x /= rhs.x; + y /= rhs.y; + z /= rhs.z; + return *this; + } + + inline vec3& operator+=(float rhs) { + x += rhs; + y += rhs; + z += rhs; + return *this; + } + + inline vec3& operator-=(float rhs) { + x -= rhs; + y -= rhs; + z -= rhs; + return *this; + } + + inline vec3& operator*=(float rhs) { + x *= rhs; + y *= rhs; + z *= rhs; + return *this; + } + + inline vec3& operator/=(float rhs) { + x /= rhs; + y /= rhs; + z /= rhs; + return *this; + } + + inline friend bool operator==(const vec3& a, const vec3& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + inline friend bool operator!=(const vec3& a, const vec3& b) { + return !(a == b); + } + + inline friend vec3 min_(const vec3& a, const vec3& b) { + return{ (b.x < a.x) ? b.x : a.x, + (b.y < a.y) ? b.y : a.y, + (b.z < a.z) ? b.z : a.z }; + } + + inline friend vec3 max_(const vec3& a, const vec3& b) { + return{ (a.x < b.x) ? b.x : a.x, + (a.y < b.y) ? b.y : a.y, + (a.z < b.z) ? b.z : a.z }; + } + inline friend vec3 clamp(const vec3& x, const vec3& minVal, const vec3& maxVal) { + return min_(max_(x, minVal), maxVal); + } + + inline friend vec3 mix(const vec3& a, const vec3& b, float t) { + return a * (1.0f - t) + b * t; + } + + inline friend float length(const vec3& a) { + return sqrtf(a.x * a.x + a.y * a.y + a.z * a.z); + } + + inline friend float distance(const vec3& a, const vec3& b) { + return length(a - b); + } + + inline friend vec3 normalize(const vec3& a) { + return a / length(a); + } + + inline friend float dot(const vec3& a, const vec3& b) { + return a.x * b.x + a.y * b.y + a.z * b.z; + } +}; + +/// +/// 4D Vector Helper +/// +struct UExport vec4 { + float x, y, z, w; + + inline vec4() : x(0.0f), y(0.0f), z(0.0f), w(0.0f) { } + + inline vec4(float x, float y, float z, float w) : x(x), y(y), z(z), w(w) {} + + inline vec4(float x) : x(x), y(x), z(x), w(x) {} + + inline vec4(const float val[4]) : x(val[0]), y(val[1]), z(val[2]), w(val[3]) { } + + inline friend bool operator==(const vec4& a, const vec4& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + inline friend bool operator!=(const vec4& a, const vec4& b) { + return !(a == b); + } + + inline friend vec4 operator+(vec4 lhs, const vec4& rhs) { lhs += rhs; return lhs; } + + inline friend vec4 operator-(vec4 lhs, const vec4& rhs) { lhs -= rhs; return lhs; } + + inline friend vec4 operator*(vec4 lhs, const vec4& rhs) { lhs *= rhs; return lhs; } + + inline friend vec4 operator/(vec4 lhs, const vec4& rhs) { lhs /= rhs; return lhs; } + + inline friend vec4 operator+(vec4 lhs, float rhs) { lhs += rhs; return lhs; } + + inline friend vec4 operator-(vec4 lhs, float rhs) { lhs -= rhs; return lhs; } + + inline friend vec4 operator*(vec4 lhs, float rhs) { lhs *= rhs; return lhs; } + + inline friend vec4 operator/(vec4 lhs, float rhs) { lhs /= rhs; return lhs; } + + inline vec4& operator+=(const vec4& rhs) { + x += rhs.x; + y += rhs.y; + z += rhs.z; + w += rhs.w; + return *this; + } + + inline vec4& operator-=(const vec4& rhs) { + x -= rhs.x; + y -= rhs.y; + z -= rhs.z; + w -= rhs.w; + return *this; + } + + inline vec4& operator*=(const vec4& rhs) { + x *= rhs.x; + y *= rhs.y; + z *= rhs.z; + w *= rhs.w; + return *this; + } + + inline vec4& operator/=(const vec4& rhs) { + x /= rhs.x; + y /= rhs.y; + z /= rhs.z; + w /= rhs.w; + return *this; + } + + inline vec4& operator+=(float rhs) { + x += rhs; + y += rhs; + z += rhs; + w += rhs; + return *this; + } + + inline vec4& operator-=(float rhs) { + x -= rhs; + y -= rhs; + z -= rhs; + w -= rhs; + return *this; + } + + inline vec4& operator*=(float rhs) { + x *= rhs; + y *= rhs; + z *= rhs; + w *= rhs; + return *this; + } + + inline vec4& operator/=(float rhs) { + x /= rhs; + y /= rhs; + z /= rhs; + w /= rhs; + return *this; + } + + inline friend vec4 min_(const vec4& a, const vec4& b) { + return{ (b.x < a.x) ? b.x : a.x, + (b.y < a.y) ? b.y : a.y, + (b.z < a.z) ? b.z : a.z, + (b.w < a.w) ? b.w : a.w }; + } + + inline friend vec4 max_(const vec4& a, const vec4& b) { + return{ (a.x < b.x) ? b.x : a.x, + (a.y < b.y) ? b.y : a.y, + (a.z < b.z) ? b.z : a.z, + (a.w < b.w) ? b.w : a.w }; + } + + inline void load(const float* val) { + x = val[0]; + y = val[1]; + z = val[2]; + w = val[3]; + } + + inline void store(float* val) const { + val[0] = x; + val[1] = y; + val[2] = z; + val[3] = w; + } +}; + +/// +/// Point is typedef'd to a 2D vector +/// +typedef vec2 Point; + +/// +/// Float Rectangle Helper +/// +struct UExport Rect { + float left, top, right, bottom; + + static constexpr Rect MakeEmpty() { return Rect{ 0.0f, 0.0f, 0.0f, 0.0f }; } + + inline float width() const { return right - left; } + inline float height() const { return bottom - top; } + inline float x() const { return left; } + inline float y() const { return top; } + inline float center_x() const { return (left + right) * 0.5f; } + inline float center_y() const { return (top + bottom) * 0.5f; } + + inline Point origin() const { return { left, top }; } + + inline void SetEmpty() { *this = MakeEmpty(); } + + inline bool IsEmpty() const { + return *this == MakeEmpty(); + } + + inline bool IsValid() const { + return width() > 0 && height() > 0; + } + + inline void Inset(float dx, float dy) { + left += dx; + top += dy; + right -= dx; + bottom -= dy; + } + + inline void Outset(float dx, float dy) { + Inset(-dx, -dy); + } + + inline void Move(float dx, float dy) { + left += dx; + top += dy; + right += dx; + bottom += dy; + } + + inline float area() const { + return width() * height(); + } + + inline void Join(const Rect& rhs) { + // if we are empty, just assign + if (IsEmpty()) { + *this = rhs; + } + else { + if (rhs.left < left) left = rhs.left; + if (rhs.top < top) top = rhs.top; + if (rhs.right > right) right = rhs.right; + if (rhs.bottom > bottom) bottom = rhs.bottom; + } + } + + inline void Join(const Point& p) { + // if we are empty, just assign + if (IsEmpty()) { + *this = { p.x, p.y, p.x, p.y }; + } + else { + if (p.x < left) left = p.x; + if (p.y < top) top = p.y; + if (p.x > right) right = p.x; + if (p.y > bottom) bottom = p.y; + } + } + + inline bool Contains(const Point& p) const { + return p.x >= left && p.x <= right && + p.y >= top && p.y <= bottom; + } + + inline bool Contains(const Rect& r) const { + return left <= r.left && top <= r.top && + right >= r.right && bottom >= r.bottom; + } + + inline bool Intersects(const Rect& rhs) const { + return !(rhs.left > right || + rhs.right < left || + rhs.top > bottom || + rhs.bottom < top); + } + + inline Rect Intersect(const Rect& other) const { + return{ (left < other.left) ? other.left : left, + (top < other.top) ? other.top : top, + (other.right < right) ? other.right : right, + (other.bottom < bottom) ? other.bottom : bottom }; + } + + friend inline bool operator==(const Rect& a, const Rect& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + friend inline bool operator!=(const Rect& a, const Rect& b) { + return !(a == b); + } + + inline vec4 ToVec4() { return vec4 { left, top, right, bottom }; } +}; + +/// +/// Integer Rectangle Helper +/// +struct UExport IntRect { + int left, top, right, bottom; + + static constexpr IntRect MakeEmpty() { return IntRect{ 0, 0, 0, 0 }; } + + inline int width() const { return right - left; } + inline int height() const { return bottom - top; } + inline int x() const { return left; } + inline int y() const { return top; } + inline int center_x() const { return (int)std::round((left + right) * 0.5f); } + inline int center_y() const { return (int)std::round((top + bottom) * 0.5f); } + + inline Point origin() const { return Point{ (float)left, (float)top }; } + + inline void SetEmpty() { *this = MakeEmpty(); } + + inline bool IsEmpty() const { + return *this == MakeEmpty(); + } + + inline bool IsValid() const { + return width() > 0 && height() > 0; + } + + inline void Inset(int dx, int dy) { + left += dx; + top += dy; + right -= dx; + bottom -= dy; + } + + inline void Outset(int dx, int dy) { + Inset(-dx, -dy); + } + + inline void Move(int dx, int dy) { + left += dx; + top += dy; + right += dx; + bottom += dy; + } + + inline int area() const { + return width() * height(); + } + + inline void Join(const IntRect& rhs) { + // if we are empty, just assign + if (IsEmpty()) { + *this = rhs; + } + else { + if (rhs.left < left) left = rhs.left; + if (rhs.top < top) top = rhs.top; + if (rhs.right > right) right = rhs.right; + if (rhs.bottom > bottom) bottom = rhs.bottom; + } + } + + inline void Join(const Point& p) { + // if we are empty, just assign + if (IsEmpty()) { + *this = { (int)std::floor(p.x), (int)std::floor(p.y), (int)std::ceil(p.x), (int)std::ceil(p.y) }; + } + else { + if ((int)std::floor(p.x) < left) left = (int)std::floor(p.x); + if ((int)std::floor(p.y) < top) top = (int)std::floor(p.y); + if ((int)std::ceil(p.x) > right) right = (int)std::ceil(p.x); + if ((int)std::ceil(p.y) > bottom) bottom = (int)std::ceil(p.y); + } + } + + inline bool Contains(const Point& p) const { + return p.x >= left && p.x <= right && + p.y >= top && p.y <= bottom; + } + + inline bool Contains(const IntRect& r) const { + return left <= r.left && top <= r.top && + right >= r.right && bottom >= r.bottom; + } + + inline bool Intersects(const IntRect& rhs) const { + // Since this is mostly used for pixel operations, we only count + // intersections that have width and height >= 1. + return !(rhs.left > right - 1 || + rhs.right < left || + rhs.top > bottom - 1 || + rhs.bottom < top); + } + + inline IntRect Intersect(const IntRect& other) const { + return{ (left < other.left) ? other.left : left, + (top < other.top) ? other.top : top, + (other.right < right) ? other.right : right, + (other.bottom < bottom) ? other.bottom : bottom }; + } + + friend inline bool operator==(const IntRect& a, const IntRect& b) { + return !memcmp(&a, &b, sizeof(a)); + } + + friend inline bool operator!=(const IntRect& a, const IntRect& b) { + return !(a == b); + } +}; + +/// +/// Rounded Rectangle Helper +/// +struct UExport RoundedRect { + Rect rect; + float radii_x[4]; + float radii_y[4]; + + void SetEmpty(); + + bool IsRounded() const; + + // Negative is inside, positive is outside. + float GetSignedDistance(const Point& p) const; + + // Returns whether or not intersection is found. Can fail if the resulting + // geometry is not a rounded rectangle. + bool Intersect(const RoundedRect& other, RoundedRect& result) const; + + void SnapToPixels(); + + Rect CalculateInterior() const; +}; + +} // namespace ultralight diff --git a/include/Ultralight/ImageSource.h b/include/Ultralight/ImageSource.h new file mode 100644 index 0000000..1b64fcf --- /dev/null +++ b/include/Ultralight/ImageSource.h @@ -0,0 +1,257 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +class ImageSourceListener; +class ImageSourceProviderListener; + +/// +/// User-defined image source to display custom images on a web-page. +/// +/// This API allows you to composite your own images into a web-page. This is useful for displaying +/// in-game textures, external image assets, or other custom content. +/// +/// ## ImageSource File Format +/// +/// To use an ImageSource, you must first create an `.imgsrc` file containing a string identifying +/// the image source. This string will be used to lookup the ImageSource from ImageSourceProvider +/// when it is loaded on a web-page. +/// +/// The file format is as follows: +/// +/// ``` +/// IMGSRC-V1 +/// +/// ``` +/// +/// You can use the `.imgsrc` file anywhere in your web-page that typically accepts an image URL. +/// For example: +/// +/// ```html +/// +/// ``` +/// +/// ## Creating from a GPU Texture +/// +/// To composite your own GPU texture on a web-page, you should first reserve a texture ID from +/// GPUDriver::NextTextureId() and then create an ImageSource from that texture ID. Next, you should +/// register the ImageSource with ImageSourceProvider using the identifier from the `.imgsrc` file. +/// +/// When the image element is drawn on the web-page, the library will draw geometry using the +/// specified texture ID and UV coordinates. You should bind your own texture when the specified +/// texture ID is used. +/// +//// @note If the GPU renderer is not enabled for the View or pixel data is needed for other +/// purposes, the library will sample the backing bitmap instead. +/// +/// ## Creating from a Bitmap +/// +/// To composite your own bitmap on a web-page, you should create an ImageSource from a Bitmap. +/// Next, you should register the ImageSource with ImageSourceProvider using the identifier from +/// the `.imgsrc` file. +/// +/// When the image element is drawn on the web-page, the library will sample this bitmap directly. +/// +/// ## Invalidating Images +/// +/// If you modify the texture or bitmap pixels after creating the ImageSource, you should call +/// ImageSource::Invalidate() to notify the library that the image should be redrawn. +/// +class UExport ImageSource : public RefCounted { + public: + /// + /// Create an ImageSource from a GPU texture with optional backing bitmap. + /// + /// @param width The width of the image in pixels (used for layout). + /// + /// @param height The height of the image in pixels (used for layout). + /// + /// @param texture_id The GPU texture identifier to bind when drawing the quad for this image. + /// This should be non-zero and obtained from GPUDriver::NextTextureId(). + /// + /// @param texture_uv The UV coordinates of the texture. + /// + /// @param bitmap Optional backing bitmap for this image source. This is used when drawing + /// the image using the CPU renderer or when pixel data is needed for other + /// purposes. You should update this bitmap when the texture changes. + /// + /// @return A new ImageSource instance. + /// + static RefPtr CreateFromTexture(uint32_t width, uint32_t height, uint32_t texture_id, + const Rect& texture_uv, + RefPtr bitmap = nullptr); + + /// + /// Create an ImageSource from a Bitmap. + /// + /// @param bitmap The backing bitmap for this image source. + /// + /// @return A new ImageSource instance. + /// + static RefPtr CreateFromBitmap(RefPtr bitmap); + + /// + /// Get the width of the image in pixels. + /// + virtual uint32_t width() const = 0; + + /// + /// Get the height of the image in pixels. + /// + virtual uint32_t height() const = 0; + + /// + /// Get the GPU texture identifier to bind when drawing the quad for this image. + /// + /// @note This will be zero (0) if the image source was created from a bitmap. + /// + virtual uint32_t texture_id() const = 0; + + /// + /// Get the UV coordinates of the texture. + /// + virtual Rect texture_uv() const = 0; + + /// + /// Get the backing bitmap for this image source. + /// + virtual RefPtr bitmap() = 0; + + /// + /// Invalidate the image. + /// + /// This will notify the library that the image has changed and should be redrawn. + /// + virtual void Invalidate() = 0; + + /// + /// Add a listener to the image source. + /// + /// @param listener The listener to add. + /// + virtual void AddListener(ImageSourceListener* listener) = 0; + + /// + /// Remove a listener from the image source. + /// + /// @param listener The listener to remove. + /// + virtual void RemoveListener(ImageSourceListener* listener) = 0; + + protected: + ImageSource() = default; + virtual ~ImageSource() = default; + ImageSource(const ImageSource&) = delete; + void operator=(const ImageSource&) = delete; +}; + +/// +/// Listener for ImageSource events. +/// +/// This is used to notify listeners when the image source is invalidated. +/// +class UExport ImageSourceListener { + public: + virtual ~ImageSourceListener() = default; + + /// + /// Called when the image source is invalidated. + /// + /// @param image_source The image source that was invalidated. + /// + virtual void OnInvalidateImageSource(ImageSource* image_source) = 0; +}; + +/// +/// Maps image sources to string identifiers. +/// +/// This is used to lookup ImageSource instances when they are requested by a web-page. +/// +class UExport ImageSourceProvider { + public: + /// + /// Get the ImageSourceProvider singleton. + /// + static ImageSourceProvider& instance(); + + /// + /// Get an ImageSource by its identifier. + /// + /// @param id The identifier of the image source. + /// + /// @return The ImageSource instance or nullptr if not found. + /// + virtual RefPtr GetImageSource(const String& id) = 0; + + /// + /// Add an ImageSource to the provider. + /// + /// @param id The identifier of the image source. + /// + /// @param image_source The ImageSource instance. + /// + virtual void AddImageSource(const String& id, RefPtr image_source) = 0; + + /// + /// Remove an ImageSource from the provider. + /// + /// @param id The identifier of the image source. + /// + virtual void RemoveImageSource(const String& id) = 0; + + /// + /// Add a listener to the provider. + /// + /// @param listener The listener to add. + /// + virtual void AddListener(ImageSourceProviderListener* listener) = 0; + + /// + /// Remove a listener from the provider. + /// + /// @param listener The listener to remove. + /// + virtual void RemoveListener(ImageSourceProviderListener* listener) = 0; + + protected: + virtual ~ImageSourceProvider() = default; +}; + +/// +/// Listener for ImageSourceProvider events. +/// +/// This is used to notify listeners when an ImageSource is added or removed from the provider. +/// +class ImageSourceProviderListener { + public: + virtual ~ImageSourceProviderListener() = default; + + /// + /// Called when an ImageSource is added to the provider. + /// + /// @param id The identifier of the image source. + /// + /// @param image_source The ImageSource instance. + /// + virtual void OnAddImageSource(const String& id, RefPtr image_source) = 0; + + /// + /// Called when an ImageSource is removed from the provider. + /// + /// @param id The identifier of the image source. + /// + virtual void OnRemoveImageSource(const String& id) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/JavaScript.h b/include/Ultralight/JavaScript.h new file mode 100644 index 0000000..264c305 --- /dev/null +++ b/include/Ultralight/JavaScript.h @@ -0,0 +1,32 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +/// +/// This class wraps a JSContextRef (a JavaScript execution context for use with JavaScriptCore) +/// and locks the context on the current thread for the duration of its lifetime. +/// +class UExport JSContext : public RefCounted { +public: + /// Get the underlying JSContextRef for use with JavaScriptCore C API + virtual JSContextRef ctx() = 0; + + /// Typecast to a JSContextRef for use with JavaScriptCore C API + operator JSContextRef(); + +protected: + virtual ~JSContext(); +}; + +} // namespace ultralight diff --git a/include/Ultralight/KeyCodes.h b/include/Ultralight/KeyCodes.h new file mode 100644 index 0000000..137f092 --- /dev/null +++ b/include/Ultralight/KeyCodes.h @@ -0,0 +1,532 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once + +namespace ultralight { + +/// +/// Namespace containing all the key-code definitions for KeyboardEvent. +/// Most of these correspond directly to the key-code values on Windows. +/// +namespace KeyCodes { + +// GK_BACK (08) BACKSPACE key +const int GK_BACK = 0x08; + +// GK_TAB (09) TAB key +const int GK_TAB = 0x09; + +// GK_CLEAR (0C) CLEAR key +const int GK_CLEAR = 0x0C; + +// GK_RETURN (0D) +const int GK_RETURN = 0x0D; + +// GK_SHIFT (10) SHIFT key +const int GK_SHIFT = 0x10; + +// GK_CONTROL (11) CTRL key +const int GK_CONTROL = 0x11; + +// GK_MENU (12) ALT key +const int GK_MENU = 0x12; + +// GK_PAUSE (13) PAUSE key +const int GK_PAUSE = 0x13; + +// GK_CAPITAL (14) CAPS LOCK key +const int GK_CAPITAL = 0x14; + +// GK_KANA (15) Input Method Editor (IME) Kana mode +const int GK_KANA = 0x15; + +// GK_HANGUEL (15) IME Hanguel mode (maintained for compatibility; use GK_HANGUL) +// GK_HANGUL (15) IME Hangul mode +const int GK_HANGUL = 0x15; + +// GK_IME_ON (16) IME On +const int GK_IME_ON = 0x16; + +// GK_JUNJA (17) IME Junja mode +const int GK_JUNJA = 0x17; + +// GK_FINAL (18) IME final mode +const int GK_FINAL = 0x18; + +// GK_HANJA (19) IME Hanja mode +const int GK_HANJA = 0x19; + +// GK_KANJI (19) IME Kanji mode +const int GK_KANJI = 0x19; + +// GK_IME_OFF (1A) IME Off +const int GK_IME_OFF = 0x1A; + +// GK_ESCAPE (1B) ESC key +const int GK_ESCAPE = 0x1B; + +// GK_CONVERT (1C) IME convert +const int GK_CONVERT = 0x1C; + +// GK_NONCONVERT (1D) IME nonconvert +const int GK_NONCONVERT = 0x1D; + +// GK_ACCEPT (1E) IME accept +const int GK_ACCEPT = 0x1E; + +// GK_MODECHANGE (1F) IME mode change request +const int GK_MODECHANGE = 0x1F; + +// GK_SPACE (20) SPACEBAR +const int GK_SPACE = 0x20; + +// GK_PRIOR (21) PAGE UP key +const int GK_PRIOR = 0x21; + +// GK_NEXT (22) PAGE DOWN key +const int GK_NEXT = 0x22; + +// GK_END (23) END key +const int GK_END = 0x23; + +// GK_HOME (24) HOME key +const int GK_HOME = 0x24; + +// GK_LEFT (25) LEFT ARROW key +const int GK_LEFT = 0x25; + +// GK_UP (26) UP ARROW key +const int GK_UP = 0x26; + +// GK_RIGHT (27) RIGHT ARROW key +const int GK_RIGHT = 0x27; + +// GK_DOWN (28) DOWN ARROW key +const int GK_DOWN = 0x28; + +// GK_SELECT (29) SELECT key +const int GK_SELECT = 0x29; + +// GK_PRINT (2A) PRINT key +const int GK_PRINT = 0x2A; + +// GK_EXECUTE (2B) EXECUTE key +const int GK_EXECUTE = 0x2B; + +// GK_SNAPSHOT (2C) PRINT SCREEN key +const int GK_SNAPSHOT = 0x2C; + +// GK_INSERT (2D) INS key +const int GK_INSERT = 0x2D; + +// GK_DELETE (2E) DEL key +const int GK_DELETE = 0x2E; + +// GK_HELP (2F) HELP key +const int GK_HELP = 0x2F; + +// (30) 0 key +const int GK_0 = 0x30; + +// (31) 1 key +const int GK_1 = 0x31; + +// (32) 2 key +const int GK_2 = 0x32; + +// (33) 3 key +const int GK_3 = 0x33; + +// (34) 4 key +const int GK_4 = 0x34; + +// (35) 5 key; +const int GK_5 = 0x35; + +// (36) 6 key +const int GK_6 = 0x36; + +// (37) 7 key +const int GK_7 = 0x37; + +// (38) 8 key +const int GK_8 = 0x38; + +// (39) 9 key +const int GK_9 = 0x39; + +// (41) A key +const int GK_A = 0x41; + +// (42) B key +const int GK_B = 0x42; + +// (43) C key +const int GK_C = 0x43; + +// (44) D key +const int GK_D = 0x44; + +// (45) E key +const int GK_E = 0x45; + +// (46) F key +const int GK_F = 0x46; + +// (47) G key +const int GK_G = 0x47; + +// (48) H key +const int GK_H = 0x48; + +// (49) I key +const int GK_I = 0x49; + +// (4A) J key +const int GK_J = 0x4A; + +// (4B) K key +const int GK_K = 0x4B; + +// (4C) L key +const int GK_L = 0x4C; + +// (4D) M key +const int GK_M = 0x4D; + +// (4E) N key +const int GK_N = 0x4E; + +// (4F) O key +const int GK_O = 0x4F; + +// (50) P key +const int GK_P = 0x50; + +// (51) Q key +const int GK_Q = 0x51; + +// (52) R key +const int GK_R = 0x52; + +// (53) S key +const int GK_S = 0x53; + +// (54) T key +const int GK_T = 0x54; + +// (55) U key +const int GK_U = 0x55; + +// (56) V key +const int GK_V = 0x56; + +// (57) W key +const int GK_W = 0x57; + +// (58) X key +const int GK_X = 0x58; + +// (59) Y key +const int GK_Y = 0x59; + +// (5A) Z key +const int GK_Z = 0x5A; + +// GK_LWIN (5B) Left Windows key (Microsoft Natural keyboard) +const int GK_LWIN = 0x5B; + +// GK_RWIN (5C) Right Windows key (Natural keyboard) +const int GK_RWIN = 0x5C; + +// GK_APPS (5D) Applications key (Natural keyboard) +const int GK_APPS = 0x5D; + +// GK_SLEEP (5F) Computer Sleep key +const int GK_SLEEP = 0x5F; + +// GK_NUMPAD0 (60) Numeric keypad 0 key +const int GK_NUMPAD0 = 0x60; + +// GK_NUMPAD1 (61) Numeric keypad 1 key +const int GK_NUMPAD1 = 0x61; + +// GK_NUMPAD2 (62) Numeric keypad 2 key +const int GK_NUMPAD2 = 0x62; + +// GK_NUMPAD3 (63) Numeric keypad 3 key +const int GK_NUMPAD3 = 0x63; + +// GK_NUMPAD4 (64) Numeric keypad 4 key +const int GK_NUMPAD4 = 0x64; + +// GK_NUMPAD5 (65) Numeric keypad 5 key +const int GK_NUMPAD5 = 0x65; + +// GK_NUMPAD6 (66) Numeric keypad 6 key +const int GK_NUMPAD6 = 0x66; + +// GK_NUMPAD7 (67) Numeric keypad 7 key +const int GK_NUMPAD7 = 0x67; + +// GK_NUMPAD8 (68) Numeric keypad 8 key +const int GK_NUMPAD8 = 0x68; + +// GK_NUMPAD9 (69) Numeric keypad 9 key +const int GK_NUMPAD9 = 0x69; + +// GK_MULTIPLY (6A) Multiply key +const int GK_MULTIPLY = 0x6A; + +// GK_ADD (6B) Add key +const int GK_ADD = 0x6B; + +// GK_SEPARATOR (6C) Separator key +const int GK_SEPARATOR = 0x6C; + +// GK_SUBTRACT (6D) Subtract key +const int GK_SUBTRACT = 0x6D; + +// GK_DECIMAL (6E) Decimal key +const int GK_DECIMAL = 0x6E; + +// GK_DIVIDE (6F) Divide key +const int GK_DIVIDE = 0x6F; + +// GK_F1 (70) F1 key +const int GK_F1 = 0x70; + +// GK_F2 (71) F2 key +const int GK_F2 = 0x71; + +// GK_F3 (72) F3 key +const int GK_F3 = 0x72; + +// GK_F4 (73) F4 key +const int GK_F4 = 0x73; + +// GK_F5 (74) F5 key +const int GK_F5 = 0x74; + +// GK_F6 (75) F6 key +const int GK_F6 = 0x75; + +// GK_F7 (76) F7 key +const int GK_F7 = 0x76; + +// GK_F8 (77) F8 key +const int GK_F8 = 0x77; + +// GK_F9 (78) F9 key +const int GK_F9 = 0x78; + +// GK_F10 (79) F10 key +const int GK_F10 = 0x79; + +// GK_F11 (7A) F11 key +const int GK_F11 = 0x7A; + +// GK_F12 (7B) F12 key +const int GK_F12 = 0x7B; + +// GK_F13 (7C) F13 key +const int GK_F13 = 0x7C; + +// GK_F14 (7D) F14 key +const int GK_F14 = 0x7D; + +// GK_F15 (7E) F15 key +const int GK_F15 = 0x7E; + +// GK_F16 (7F) F16 key +const int GK_F16 = 0x7F; + +// GK_F17 (80H) F17 key +const int GK_F17 = 0x80; + +// GK_F18 (81H) F18 key +const int GK_F18 = 0x81; + +// GK_F19 (82H) F19 key +const int GK_F19 = 0x82; + +// GK_F20 (83H) F20 key +const int GK_F20 = 0x83; + +// GK_F21 (84H) F21 key +const int GK_F21 = 0x84; + +// GK_F22 (85H) F22 key +const int GK_F22 = 0x85; + +// GK_F23 (86H) F23 key +const int GK_F23 = 0x86; + +// GK_F24 (87H) F24 key +const int GK_F24 = 0x87; + +// GK_NUMLOCK (90) NUM LOCK key +const int GK_NUMLOCK = 0x90; + +// GK_SCROLL (91) SCROLL LOCK key +const int GK_SCROLL = 0x91; + +// GK_LSHIFT (A0) Left SHIFT key +const int GK_LSHIFT = 0xA0; + +// GK_RSHIFT (A1) Right SHIFT key +const int GK_RSHIFT = 0xA1; + +// GK_LCONTROL (A2) Left CONTROL key +const int GK_LCONTROL = 0xA2; + +// GK_RCONTROL (A3) Right CONTROL key +const int GK_RCONTROL = 0xA3; + +// GK_LMENU (A4) Left MENU key +const int GK_LMENU = 0xA4; + +// GK_RMENU (A5) Right MENU key +const int GK_RMENU = 0xA5; + +// GK_BROWSER_BACK (A6) Windows 2000/XP: Browser Back key +const int GK_BROWSER_BACK = 0xA6; + +// GK_BROWSER_FORWARD (A7) Windows 2000/XP: Browser Forward key +const int GK_BROWSER_FORWARD = 0xA7; + +// GK_BROWSER_REFRESH (A8) Windows 2000/XP: Browser Refresh key +const int GK_BROWSER_REFRESH = 0xA8; + +// GK_BROWSER_STOP (A9) Windows 2000/XP: Browser Stop key +const int GK_BROWSER_STOP = 0xA9; + +// GK_BROWSER_SEARCH (AA) Windows 2000/XP: Browser Search key +const int GK_BROWSER_SEARCH = 0xAA; + +// GK_BROWSER_FAVORITES (AB) Windows 2000/XP: Browser Favorites key +const int GK_BROWSER_FAVORITES = 0xAB; + +// GK_BROWSER_HOME (AC) Windows 2000/XP: Browser Start and Home key +const int GK_BROWSER_HOME = 0xAC; + +// GK_VOLUME_MUTE (AD) Windows 2000/XP: Volume Mute key +const int GK_VOLUME_MUTE = 0xAD; + +// GK_VOLUME_DOWN (AE) Windows 2000/XP: Volume Down key +const int GK_VOLUME_DOWN = 0xAE; + +// GK_VOLUME_UP (AF) Windows 2000/XP: Volume Up key +const int GK_VOLUME_UP = 0xAF; + +// GK_MEDIA_NEXT_TRACK (B0) Windows 2000/XP: Next Track key +const int GK_MEDIA_NEXT_TRACK = 0xB0; + +// GK_MEDIA_PREV_TRACK (B1) Windows 2000/XP: Previous Track key +const int GK_MEDIA_PREV_TRACK = 0xB1; + +// GK_MEDIA_STOP (B2) Windows 2000/XP: Stop Media key +const int GK_MEDIA_STOP = 0xB2; + +// GK_MEDIA_PLAY_PAUSE (B3) Windows 2000/XP: Play/Pause Media key +const int GK_MEDIA_PLAY_PAUSE = 0xB3; + +// GK_LAUNCH_MAIL (B4) Windows 2000/XP: Start Mail key +const int GK_MEDIA_LAUNCH_MAIL = 0xB4; + +// GK_LAUNCH_MEDIA_SELECT (B5) Windows 2000/XP: Select Media key +const int GK_MEDIA_LAUNCH_MEDIA_SELECT = 0xB5; + +// GK_LAUNCH_APP1 (B6) Windows 2000/XP: Start Application 1 key +const int GK_MEDIA_LAUNCH_APP1 = 0xB6; + +// GK_LAUNCH_APP2 (B7) Windows 2000/XP: Start Application 2 key +const int GK_MEDIA_LAUNCH_APP2 = 0xB7; + +// GK_OEM_1 (BA) ';:' for US +const int GK_OEM_1 = 0xBA; + +// GK_OEM_PLUS (BB) '=+' any country +const int GK_OEM_PLUS = 0xBB; + +// GK_OEM_COMMA (BC) ',<' any country +const int GK_OEM_COMMA = 0xBC; + +// GK_OEM_MINUS (BD) '-_' any country +const int GK_OEM_MINUS = 0xBD; + +// GK_OEM_PERIOD (BE) '.>' any country +const int GK_OEM_PERIOD = 0xBE; + +// GK_OEM_2 (BF) '/?' for US +const int GK_OEM_2 = 0xBF; + +// GK_OEM_3 (C0) '`~' for US +const int GK_OEM_3 = 0xC0; + +// GK_OEM_4 (DB) '[{' for US +const int GK_OEM_4 = 0xDB; + +// GK_OEM_5 (DC) '\|' for US +const int GK_OEM_5 = 0xDC; + +// GK_OEM_6 (DD) ']}' for US +const int GK_OEM_6 = 0xDD; + +// GK_OEM_7 (DE) ''"' for US +const int GK_OEM_7 = 0xDE; + +// GK_OEM_8 (DF) Used for miscellaneous characters; it can vary by keyboard. +const int GK_OEM_8 = 0xDF; + +// GK_OEM_102 (E2) Windows 2000/XP: Either the angle bracket key or the backslash key on the RT +// 102-key keyboard +const int GK_OEM_102 = 0xE2; + +// GK_PROCESSKEY (E5) Windows 95/98/Me, Windows NT 4.0, Windows 2000/XP: IME PROCESS key +const int GK_PROCESSKEY = 0xE5; + +// GK_PACKET (E7) Windows 2000/XP: Used to pass Unicode characters as if they were keystrokes. The +// GK_PACKET key is the low word of a 32-bit Virtual Key value used for non-keyboard input methods. +// For more information, see Remark in KEYBDINPUT,SendInput, WM_KEYDOWN, and WM_KEYUP +const int GK_PACKET = 0xE7; + +const int GK_OEM_ATTN = 0xF0; + +// GK_ATTN (F6) Attn key +const int GK_ATTN = 0xF6; + +// GK_CRSEL (F7) CrSel key +const int GK_CRSEL = 0xF7; + +// GK_EXSEL (F8) ExSel key +const int GK_EXSEL = 0xF8; + +// GK_EREOF (F9) Erase EOF key +const int GK_EREOF = 0xF9; + +// GK_PLAY (FA) Play key +const int GK_PLAY = 0xFA; + +// GK_ZOOM (FB) Zoom key +const int GK_ZOOM = 0xFB; + +// GK_NONAME (FC) Reserved for future use +const int GK_NONAME = 0xFC; + +// GK_PA1 (FD) PA1 key +const int GK_PA1 = 0xFD; + +// GK_OEM_CLEAR (FE) Clear key +const int GK_OEM_CLEAR = 0xFE; + +const int GK_UNKNOWN = 0; + +} // namespace KeyCodes + +} // namespace ultralight diff --git a/include/Ultralight/KeyEvent.h b/include/Ultralight/KeyEvent.h new file mode 100644 index 0000000..307b26f --- /dev/null +++ b/include/Ultralight/KeyEvent.h @@ -0,0 +1,180 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#ifdef __OBJC__ +#import +#endif + +namespace ultralight { + +/// +/// Keyboard event representing a change in keyboard state. +/// +/// @see View::FireKeyEvent +/// +class UExport KeyEvent { + public: + /// + /// The various KeyEvent types. + /// + enum Type { + /// + /// Key-Down event type. This type does **not** trigger accelerator commands in WebCore (eg, + /// Ctrl+C for copy is an accelerator command). + /// + /// @warning You should probably use kType_RawKeyDown instead. This type is only here for + /// historic compatibility with WebCore's key event types. + /// + kType_KeyDown, + + /// + /// Key-Up event type. Use this when a physical key is released. + /// + kType_KeyUp, + + /// + /// Raw Key-Down type. Use this when a physical key is pressed. + /// + kType_RawKeyDown, + + /// + /// Character input event type. Use this when the OS generates text from a physical key being + /// pressed (for example, this maps to WM_CHAR on Windows). + /// + kType_Char, + }; + + /// + /// Creates an empty KeyEvent, you will need to initialize its members + /// yourself. This is useful for synthesizing your own keyboard events. + /// + KeyEvent(); + +#ifdef _WIN32 + /// + /// Create a KeyEvent directly from a Windows keyboard event. + /// + KeyEvent(Type type, uintptr_t wparam, intptr_t lparam, bool is_system_key); +#endif + +#ifdef __OBJC__ + /// + /// Create a KeyEvent directly from a macOS NSEvent. + /// + KeyEvent(NSEvent* evt); +#endif + + /// + /// An enumeration of the different keyboard modifiers. + /// + enum Modifiers : uint8_t { + /// Whether or not an ALT key is down + kMod_AltKey = 1 << 0, + + /// Whether or not a Control key is down + kMod_CtrlKey = 1 << 1, + + /// Whether or not a meta key (Command-key on Mac, Windows-key on Win) is down + kMod_MetaKey = 1 << 2, + + /// Whether or not a Shift key is down + kMod_ShiftKey = 1 << 3, + }; + + /// + /// The type of this KeyEvent. + /// + Type type; + + /// + /// The current state of the keyboard. Modifiers may be OR'd together to represent multiple + /// values. + /// + unsigned modifiers; + + /// + /// The virtual key-code associated with this keyboard event. This is either directly from the + /// event (ie, WPARAM on Windows) or via a mapping function. You can see a full list of the + /// possible virtual key-codes in KeyCodes.h + /// + int virtual_key_code; + + /// + /// The actual key-code generated by the platform. The DOM spec primarily uses Windows-equivalent + /// codes (hence virtualKeyCode above) but it helps to also specify the platform-specific + /// key-code as well. + /// + int native_key_code; + + /// + /// This is a string identifying the key that was pressed. This can be generated from the + /// virtual_key_code via the GetKeyIdentifierFromVirtualKeyCode() utility function. You can find + /// the full list of key identifiers at: + /// + /// + String key_identifier; + + /// + /// The actual text generated by this keyboard event. This is usually only a single character. + /// + String text; + + /// + /// The text generated by this keyboard event before all modifiers except shift are applied. This + /// is used internally for working out shortcut keys. This is usually only a single character. + /// + String unmodified_text; + + /// + /// Whether or not this is a keypad event. + /// + bool is_keypad; + + /// + /// Whether or not this was generated as the result of an auto-repeat (eg, holding down a key). + /// + bool is_auto_repeat; + + /// + /// Whether or not the pressed key is a "system key". This is a Windows-only concept and should + /// be "false" for all non-Windows platforms. For more information, see the following link: + /// + /// + bool is_system_key; +}; + +/// +/// Utility function for generating a key identifier string from a virtual +/// key-code. +/// +/// @param virtual_key_code The virtual key-code to generate the key identifier from. +/// +/// @param key_identifier_result The string to store the result in. +/// +void UExport GetKeyIdentifierFromVirtualKeyCode(int virtual_key_code, + String& key_identifier_result); + +/// +/// Utility function for generating a key string from a virtual key-code. +/// +/// @param virtual_key_code The virtual key-code to generate the key string from. +/// +/// @param shift Whether or not the shift key is currently pressed. +/// +/// @param key_result The string to store the result in. +/// +/// @note This function assumes US keyboard layout. +/// +/// @see +/// +void UExport GetKeyFromVirtualKeyCode(int virtual_key_code, bool shift, String& key_result); + +} // namespace ultralight diff --git a/include/Ultralight/Listener.h b/include/Ultralight/Listener.h new file mode 100644 index 0000000..30be4e7 --- /dev/null +++ b/include/Ultralight/Listener.h @@ -0,0 +1,350 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2025 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include +#include +#include + +namespace ultralight { + +class View; + + +/// +/// Cursor types, @see ViewListener::OnChangeCursor +/// +enum Cursor { + kCursor_Pointer = 0, + kCursor_Cross, + kCursor_Hand, + kCursor_IBeam, + kCursor_Wait, + kCursor_Help, + kCursor_EastResize, + kCursor_NorthResize, + kCursor_NorthEastResize, + kCursor_NorthWestResize, + kCursor_SouthResize, + kCursor_SouthEastResize, + kCursor_SouthWestResize, + kCursor_WestResize, + kCursor_NorthSouthResize, + kCursor_EastWestResize, + kCursor_NorthEastSouthWestResize, + kCursor_NorthWestSouthEastResize, + kCursor_ColumnResize, + kCursor_RowResize, + kCursor_MiddlePanning, + kCursor_EastPanning, + kCursor_NorthPanning, + kCursor_NorthEastPanning, + kCursor_NorthWestPanning, + kCursor_SouthPanning, + kCursor_SouthEastPanning, + kCursor_SouthWestPanning, + kCursor_WestPanning, + kCursor_Move, + kCursor_VerticalText, + kCursor_Cell, + kCursor_ContextMenu, + kCursor_Alias, + kCursor_Progress, + kCursor_NoDrop, + kCursor_Copy, + kCursor_None, + kCursor_NotAllowed, + kCursor_ZoomIn, + kCursor_ZoomOut, + kCursor_Grab, + kCursor_Grabbing, + kCursor_Custom +}; + +/// +/// User-defined interface to handle general events for a View. +/// +/// @see View::set_view_listener +/// +class UExport ViewListener { + public: + virtual ~ViewListener() { } + + /// + /// Called when the page title changes + /// + virtual void OnChangeTitle(ultralight::View* caller, const String& title) { } + + /// + /// Called when the page URL changes + /// + virtual void OnChangeURL(ultralight::View* caller, const String& url) { } + + /// + /// Called when the tooltip changes (usually as result of a mouse hover) + /// + virtual void OnChangeTooltip(ultralight::View* caller, const String& tooltip) { } + + /// + /// Called when the mouse cursor changes + /// + virtual void OnChangeCursor(ultralight::View* caller, Cursor cursor) { } + + /// + /// Called when a message is added to the console (useful for errors / debug) + /// + virtual void OnAddConsoleMessage(ultralight::View* caller, + const ultralight::ConsoleMessage& message) { } + + /// + /// Called when the page wants to create a new child View. + /// + /// This is usually the result of a user clicking a link with target="_blank" + /// or by JavaScript calling window.open(url). + /// + /// To allow creation of these new Views, you should create a new View in this callback (eg, + /// Renderer::CreateView()), resize it to your container, and return it. You are responsible for + /// displaying the returned View. + /// + /// @param caller The View that called this event. + /// + /// @param opener_url The URL of the page that initiated this request. + /// + /// @param target_url The URL that the new View will navigate to. + /// + /// @param is_popup Whether or not this was triggered by window.open(). + /// + /// @param popup_rect Popups can optionally request certain dimensions and coordinates via + /// window.open(). You can choose to respect these or not by resizing/moving + /// the View to this rect. + /// + /// @return Returns a RefPtr to a created View to use to satisfy the the request (or return + /// nullptr if you want to block the action). + /// + virtual RefPtr OnCreateChildView(ultralight::View* caller, const String& opener_url, + const String& target_url, bool is_popup, + const IntRect& popup_rect); + + /// + /// Called when the page wants to create a new View to display the local inspector in. + /// + /// You should create a new View in this callback (eg, Renderer::CreateView()), resize it to your + /// container, and return it. You are responsible for displaying the returned View. + /// + /// @return Returns a RefPtr to a created View to use to satisfy the the request (or return + /// nullptr if you want to block the action). + /// + virtual RefPtr OnCreateInspectorView(ultralight::View* caller, bool is_local, + const String& inspected_url); + + /// + /// Called when the page requests to be closed. + /// + virtual void OnRequestClose(ultralight::View* caller) { } + +}; + +/// +/// User-defined interface to handle load-related events for a View. +/// +/// @see View::set_load_listener +/// +class UExport LoadListener { + public: + virtual ~LoadListener() { } + + /// + /// Called when the page begins loading a new URL into a frame. + /// + /// @param frame_id A unique ID for the frame. + /// + /// @param is_main_frame Whether or not this is the main frame. + /// + /// @param url The URL for the load. + /// + /// @note This will be called for each frame on the page. You can filter for the main frame load + /// by checking if `is_main_frame` is `true`. + /// + virtual void OnBeginLoading(ultralight::View* caller, uint64_t frame_id, bool is_main_frame, + const String& url) { } + + /// + /// Called when the page finishes loading a URL into a frame. + /// + /// @param frame_id A unique ID for the frame. + /// + /// @param is_main_frame Whether or not this is the main frame. + /// + /// @param url The URL for the load. + /// + /// @note This will be called for each frame on the page. You can filter for the main frame load + /// by checking if `is_main_frame` is `true`. + /// + virtual void OnFinishLoading(ultralight::View* caller, uint64_t frame_id, bool is_main_frame, + const String& url) { } + + /// + /// Called when an error occurs while loading a URL into a frame. + /// + /// @param frame_id A unique ID for the frame. + /// + /// @param is_main_frame Whether or not this is the main frame. + /// + /// @param url The URL for the load. + /// + /// @param description A human-readable description of the error. + /// + /// @param error_domain The name of the module that triggered the error. + /// + /// @param error_code Internal error code generated by the module. + /// + /// @note This will be called for each frame on the page. You can filter for the main frame load + /// by checking if `is_main_frame` is `true`. + /// + virtual void OnFailLoading(ultralight::View* caller, uint64_t frame_id, bool is_main_frame, + const String& url, const String& description, + const String& error_domain, int error_code) { } + + /// + /// Called when the JavaScript window object is reset for a new page load. + /// + /// This is called before any scripts are executed on the page and is the earliest time to setup + /// any initial JavaScript state or bindings. + /// + /// The document is not guaranteed to be loaded/parsed at this point. If you need to make any + /// JavaScript calls that are dependent on DOM elements or scripts on the page, use OnDOMReady + /// instead. + /// + /// The window object is lazily initialized (this will not be called on pages with no scripts). + /// + /// @param frame_id A unique ID for the frame. + /// + /// @param is_main_frame Whether or not this is the main frame. + /// + /// @param url The URL for the load. + /// + /// @note This will be called for each frame on the page. You can filter for the main frame load + /// by checking if `is_main_frame` is `true`. + /// + virtual void OnWindowObjectReady(ultralight::View* caller, uint64_t frame_id, bool is_main_frame, + const String& url) { } + + /// + /// Called when all JavaScript has been parsed and the document is ready. + /// + /// This is the best time to make any JavaScript calls that are dependent on DOM elements or + /// scripts on the page. + /// + /// @param frame_id A unique ID for the frame. + /// + /// @param is_main_frame Whether or not this is the main frame. + /// + /// @param url The URL for the load. + /// + /// @note This will be called for each frame on the page. You can filter for the main frame load + /// by checking if `is_main_frame` is `true`. + /// + virtual void OnDOMReady(ultralight::View* caller, uint64_t frame_id, bool is_main_frame, + const String& url) { } + + /// + /// Called when the session history (back/forward state) is modified. + /// + virtual void OnUpdateHistory(ultralight::View* caller) { } +}; + +/// +/// A unique identifier representing an active download. +/// +typedef uint32_t DownloadId; + +/// +/// User-defined interface to handle download-related events for a View. +/// +/// You must implement this interface to handle downloads initiated by a View. +/// +/// @see View::set_download_listener +/// +class UExport DownloadListener { + public: + virtual ~DownloadListener() {} + + /// + /// Called when the View wants to generate a unique download id. + /// + /// You should generally return an integer (starting at 0) that is incremented with each call + /// to this callback. + /// + virtual DownloadId NextDownloadId(ultralight::View* caller) = 0; + + /// + /// Called when the View wants to start downloading a resource from the network. + /// + /// You should return true to allow the download, or false to block the download. + /// + virtual bool OnRequestDownload(ultralight::View* caller, DownloadId id, const String& url) = 0; + + /// + /// Called when the View begins downloading a resource from the network. + /// + /// The View will not actually write any data to disk, you should open a file for writing + /// yourself and handle the OnReceiveDataForDownload callback below. + /// + virtual void OnBeginDownload(ultralight::View* caller, DownloadId id, const String& url, + const String& filename, int64_t expected_content_length) = 0; + + /// + /// Called when the View receives data for a certain download from the network. + /// + /// This may be called multiple times for each active download as data is streamed in. + /// + /// You should write the data to the associated file in this callback. + /// + virtual void OnReceiveDataForDownload(ultralight::View* caller, DownloadId id, + RefPtr data) = 0; + + /// + /// Called when the View finishes downloading a resource from the network. + /// + /// You should close the associated file in this callback. + /// + virtual void OnFinishDownload(ultralight::View* caller, DownloadId id) = 0; + + /// + /// Called when the View fails downloading a resource from the network. + /// + /// You should close the associated file and delete it from disk in this callback. + /// + virtual void OnFailDownload(ultralight::View* caller, DownloadId id) = 0; +}; + +/// +/// User-defined interface to handle network-related events for a View. +/// +/// @see View::set_network_listener +/// +class UExport NetworkListener { + public: + virtual ~NetworkListener() { } + + /// + /// Called when the View is about to begin a network request. + /// + /// You can use this to block or modify network requests before they are sent. + /// + /// Return true to allow the request, return false to block it. + /// + /// @pre This feature is only available in Ultralight Pro edition and above. + /// + virtual bool OnNetworkRequest(ultralight::View* caller, NetworkRequest& request) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/Lock.h b/include/Ultralight/Lock.h new file mode 100644 index 0000000..6c6f7ba --- /dev/null +++ b/include/Ultralight/Lock.h @@ -0,0 +1,53 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include // for std::lock_guard<> + +namespace ultralight { + +/// +/// Tiny, efficient spinlock that is optimized for short locking periods but will still +/// intelligently yield the current thread and save CPU if the lock is held longer. +/// +/// Can be used in place of std::mutex since it implements the STL's Lockable interface. +/// +class Lock { + public: + constexpr Lock() = default; + + UL_ALWAYS_INLINE void lock() noexcept { + // Optimistically assume the lock is free on the first try + if (!lock_.exchange(true, std::memory_order_acquire)) + return; + + contended_lock(); + } + + UL_ALWAYS_INLINE bool try_lock() noexcept { + // First do a relaxed load to check if lock is free in order to prevent + // unnecessary cache misses if someone does while(!try_lock()) + return !lock_.load(std::memory_order_relaxed) + && !lock_.exchange(true, std::memory_order_acquire); + } + + UL_ALWAYS_INLINE void unlock() noexcept { lock_.store(false, std::memory_order_release); } + + protected: + Lock(const Lock&) = delete; + Lock& operator=(const Lock&) = delete; + + void contended_lock() noexcept; + + std::atomic lock_ = { 0 }; +}; + +using LockHolder = std::lock_guard; + +} // namespace ultralight diff --git a/include/Ultralight/Matrix.h b/include/Ultralight/Matrix.h new file mode 100644 index 0000000..fdb1b1b --- /dev/null +++ b/include/Ultralight/Matrix.h @@ -0,0 +1,179 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2025 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// 4x4 Matrix Helper +/// +struct UExport Matrix4x4 { + /// + /// Raw 4x4 matrix as an array in column-major order. + /// + float data[16]; + + /// + /// Set to identity matrix. + /// + void SetIdentity(); +}; + +/// +/// Transformation Matrix helper +/// +struct UExport UL_ALIGN(16) Matrix { + typedef double Aligned4x4[4][4]; + + /// + /// Raw matrix data in column-major order (eg, `data[column][row]`) + /// + Aligned4x4 data; + + /// + /// Set to identity matrix. + /// + void SetIdentity(); + + /// + /// Set to an orthographic projection matrix suitable for use with our + /// vertex shaders. Optionally flip the y-coordinate space (eg, for OpenGL). + /// + void SetOrthographicProjection(double screen_width, double screen_height, + bool flip_y); + + /// + /// Set to another matrix. + /// + void Set(const Matrix& other); + + /// + /// Set to another matrix. + /// + void Set(const Matrix4x4& other); + + /// + /// Set from raw affine members. + /// + void Set(double a, double b, double c, double d, double e, double f); + + /// + /// Set from raw 4x4 components. + /// + void Set(double m11, double m12, double m13, double m14, + double m21, double m22, double m23, double m24, + double m31, double m32, double m33, double m34, + double m41, double m42, double m43, double m44); + + inline double m11() const { return data[0][0]; } + inline double m12() const { return data[0][1]; } + inline double m13() const { return data[0][2]; } + inline double m14() const { return data[0][3]; } + inline double m21() const { return data[1][0]; } + inline double m22() const { return data[1][1]; } + inline double m23() const { return data[1][2]; } + inline double m24() const { return data[1][3]; } + inline double m31() const { return data[2][0]; } + inline double m32() const { return data[2][1]; } + inline double m33() const { return data[2][2]; } + inline double m34() const { return data[2][3]; } + inline double m41() const { return data[3][0]; } + inline double m42() const { return data[3][1]; } + inline double m43() const { return data[3][2]; } + inline double m44() const { return data[3][3]; } + + inline double a() const { return data[0][0]; } + inline double b() const { return data[0][1]; } + inline double c() const { return data[1][0]; } + inline double d() const { return data[1][1]; } + inline double e() const { return data[3][0]; } + inline double f() const { return data[3][1]; } + + /// + /// Whether or not this is an identity matrix. + /// + bool IsIdentity() const; + + /// + /// Whether or not this is an identity matrix or translation. + /// + bool IsIdentityOrTranslation() const; + + /// + /// Whether or not this matrix uses only affine transformations. + /// + bool IsAffine() const; + + /// + /// Whether or not this is an identity, translation, or non-negative + /// uniform scale. + /// + bool IsSimple() const; + + /// + /// Translate by x and y. + /// + void Translate(double x, double y); + + /// + /// Scale by x and y. + /// + void Scale(double x, double y); + + /// + /// Rotate matrix by theta (in degrees) + /// + void Rotate(double theta); + + /// + /// Rotate matrix by x and y + /// + void Rotate(double x, double y); + + /// + /// Transform (multiply) by another Matrix + /// + void Transform(const Matrix& other); + + /// + /// Get the inverse of this matrix. May return false if not invertible. + /// + bool GetInverse(Matrix& result) const; + + /// + /// Transform point by this matrix and get the result. + /// + Point Apply(const Point& p) const; + + /// + /// Transform rect by this matrix and get the result as an axis-aligned rect. + /// + Rect Apply(const Rect& r) const; + + /// + /// Get an integer hash of this matrix's members. + /// + uint32_t Hash() const; + + /// + /// Get this matrix as unaligned 4x4 float components (for use passing to + /// GPU driver APIs). + /// + Matrix4x4 GetMatrix4x4() const; +}; + +bool UExport operator==(const Matrix& a, const Matrix& b); +bool UExport operator!=(const Matrix& a, const Matrix& b); + +bool UExport operator==(const Matrix4x4& a, const Matrix4x4& b); +bool UExport operator!=(const Matrix4x4& a, const Matrix4x4& b); + +} // namespace ultralight diff --git a/include/Ultralight/MouseEvent.h b/include/Ultralight/MouseEvent.h new file mode 100644 index 0000000..6bf1ed6 --- /dev/null +++ b/include/Ultralight/MouseEvent.h @@ -0,0 +1,71 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include + +namespace ultralight { + +/// +/// Mouse event representing a change in mouse state. +/// +/// @see View::FireMouseEvent +/// +class MouseEvent { +public: + /// + /// The various MouseEvent types. + /// + enum Type { + /// + /// Mouse moved type + /// + kType_MouseMoved, + + /// + /// Mouse button pressed type + /// + kType_MouseDown, + + /// + /// Mouse button released type + /// + kType_MouseUp, + }; + + /// + /// The various mouse button types. + /// + enum Button { + kButton_None = 0, + kButton_Left, + kButton_Middle, + kButton_Right, + }; + + /// + /// The type of this MouseEvent + /// + Type type; + + /// + /// The current x-position of the mouse, relative to the View + /// + int x; + + /// + /// The current y-position of the mouse, relative to the View + /// + int y; + + /// + /// The mouse button that was pressed/released, if any. + /// + Button button; +}; + +} // namespace ultralight diff --git a/include/Ultralight/NetworkRequest.h b/include/Ultralight/NetworkRequest.h new file mode 100644 index 0000000..818cd40 --- /dev/null +++ b/include/Ultralight/NetworkRequest.h @@ -0,0 +1,63 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include + +namespace ultralight { + +/// +/// @brief Interface for Network requests. +/// +class UExport NetworkRequest { + public: + virtual ~NetworkRequest() { } + + /// + /// URL of the request. + /// + virtual String url() const = 0; + + /// + /// The host portion of the URL. + /// + virtual String urlHost() const = 0; + + /// + /// The protocol of the URL (eg, "http") + /// + virtual String urlProtocol() const = 0; + + /// + /// The HTTP method (eg, "POST" or "GET") + /// + virtual String httpMethod() const = 0; + + /// + /// The origin of the request. + /// + virtual String httpOrigin() const = 0; + + /// + /// The user-agent of the request. + /// + virtual String httpUserAgent() const = 0; + + /// + /// Enforce additional TLS/SSL certificate validation by verifying the + /// server's pinned public key. + /// + /// The public key string can be any number of base64 encoded sha256 hashes + /// preceded by "sha256//" and separated by ";". + /// + /// For more info see the cURL docs: + /// + /// + virtual void EnforcePinnedPublicKey(const String& public_key) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/RefPtr.h b/include/Ultralight/RefPtr.h new file mode 100644 index 0000000..6fbc4f0 --- /dev/null +++ b/include/Ultralight/RefPtr.h @@ -0,0 +1,325 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +/* +* Portions of the below code are derived from 'RefPtr.h' from Apple's WTF, +* with the following license header: +* +* 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. +*/ + +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// @brief Interface for all ref-counted objects that will be managed using +/// the RefPtr<> smart pointer. +/// +class UExport RefCounted { + public: + virtual void AddRef() const = 0; + virtual void Release() const = 0; + virtual int ref_count() const = 0; + protected: + virtual ~RefCounted(); +}; + +template class RefPtr; + +/// +/// @brief Helper for wrapping new objects with the RefPtr smart pointer. +/// +/// All ref-counted object are created with an initial ref-count of '1'. +/// The AdoptRef() helper returns a RefPtr without calling AddRef(). +/// This is used for creating new objects, like so: +/// +/// RefPtr ref = AdoptRef(*new ObjectImpl()); +/// +template +RefPtr AdoptRef(T& reference) +{ + return RefPtr(reference, RefPtr::Adopt); +} + +/// +/// @brief A nullable smart pointer. +/// +/// This smart pointer automatically manages the lifetime of a RefCounted +/// object. The managed instance may be NULL. +/// +template class RefPtr { + public: + /// + /// Construct a NULL ref-pointer. + /// + constexpr RefPtr() + : instance_(nullptr) + { + } + + /// + /// Construct a NULL ref-pointer. + /// + inline RefPtr(std::nullptr_t) + : instance_(nullptr) + { + } + + /// + /// Construct from a pointer. (Will increment ref-count by one) + /// + inline RefPtr(T* other) + : instance_(other) + { + if (instance_) + instance_->AddRef(); + } + + /// + /// Copy constructor. + /// + inline RefPtr(const RefPtr& other) + : instance_(other.instance_) + { + if (instance_) + instance_->AddRef(); + } + + /// + /// Copy constructor with internal type conversion. + /// + template + RefPtr(const RefPtr& other) + : instance_(other.instance_) + { + if (instance_) + instance_->AddRef(); + } + + /// + /// Move constructor. + /// + inline RefPtr(RefPtr&& other) + : instance_(other.LeakRef()) + { + } + + /// + /// Move constructor. + /// + template + RefPtr(RefPtr&& other) + : instance_(other.LeakRef()) + { + } + + /// + /// Destroy RefPtr (wll decrement ref-count by one) + /// + inline ~RefPtr() + { + T* old_value = std::move(instance_); + instance_ = std::forward(nullptr); + if (old_value) + old_value->Release(); + } + + /// + /// Get a pointer to wrapped object. + /// + inline T* get() const { return instance_; } + + T* LeakRef() { + T* result = std::move(instance_); + instance_ = std::forward(nullptr); + return result; + } + + T& operator*() const { assert(instance_); return *instance_; } + inline T* operator->() const { return instance_; } + + bool operator!() const { return !instance_; } + + // This conversion operator allows implicit conversion to bool but not to other integer types. + typedef T* (RefPtr::*UnspecifiedBoolType); + operator UnspecifiedBoolType() const { return instance_ ? &RefPtr::instance_ : nullptr; } + + RefPtr& operator=(const RefPtr&); + RefPtr& operator=(T*); + RefPtr& operator=(std::nullptr_t); + template RefPtr& operator=(const RefPtr&); + RefPtr& operator=(RefPtr&&); + template RefPtr& operator=(RefPtr&&); + + friend inline bool operator==(const RefPtr& a, const RefPtr& b) { + return a.instance_ == b.instance_; + } + + friend inline bool operator!=(const RefPtr& a, const RefPtr& b) { + return a.instance_ != b.instance_; + } + + friend inline bool operator<(const RefPtr& a, const RefPtr& b) { + return a.instance_ < b.instance_; + } + + /// + /// Releases the ownership of the managed object, if any + /// + void reset(); + + /// + /// Replaces the managed object with another. + /// + void reset(T* obj); + + /// + /// Exchanges the stored pointer values and the ownerships of *this and ptr. + /// Reference counts, if any, are not adjusted. + /// + void swap(RefPtr& ptr); + +protected: + friend RefPtr AdoptRef(T&); + + enum AdoptTag { Adopt }; + RefPtr(T& object, AdoptTag) : instance_(&object) { } + +private: + T* instance_; +}; + +template +RefPtr& RefPtr::operator=(const RefPtr& other) +{ + RefPtr ptr = other; + swap(ptr); + return *this; +} + +template +template +RefPtr& RefPtr::operator=(const RefPtr& other) +{ + RefPtr ptr = other; + swap(ptr); + return *this; +} + +template +RefPtr& RefPtr::operator=(T* object) +{ + RefPtr ptr = object; + swap(ptr); + return *this; +} + +template +RefPtr& RefPtr::operator=(std::nullptr_t) +{ + T* old_instance = std::move(instance_); + instance_ = std::forward(nullptr); + if (old_instance) + old_instance->Release(); + return *this; +} + +template +RefPtr& RefPtr::operator=(RefPtr&& other) +{ + RefPtr ptr = std::move(other); + swap(ptr); + return *this; +} + +template +template +RefPtr& RefPtr::operator=(RefPtr&& other) +{ + RefPtr ptr = std::move(other); + swap(ptr); + return *this; +} + +template void RefPtr::reset() { *this = nullptr; } + +template void RefPtr::reset(T* obj) { *this = obj; } + +template +void RefPtr::swap(RefPtr& other) +{ + std::swap(instance_, other.instance_); +} + +template +void swap(RefPtr& a, RefPtr& b) +{ + a.swap(b); +} + +template +bool operator==(const RefPtr& a, const RefPtr& b) +{ + return a.get() == b.get(); +} + +template +bool operator==(const RefPtr& a, const U* b) +{ + return a.get() == b; +} + +template +bool operator==(const T* a, const RefPtr& b) +{ + return a == b.get(); +} + +template +bool operator!=(const RefPtr& a, const RefPtr& b) +{ + return a.get() != b.get(); +} + +template +bool operator!=(const RefPtr& a, const U* b) +{ + return a.get() != b; +} + +template +bool operator!=(const T* a, const RefPtr& b) +{ + return a != b.get(); +} + +} // namespace ultralight diff --git a/include/Ultralight/RenderTarget.h b/include/Ultralight/RenderTarget.h new file mode 100644 index 0000000..41d6b3b --- /dev/null +++ b/include/Ultralight/RenderTarget.h @@ -0,0 +1,86 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +#pragma pack(push, 1) + +/// +/// Offscreen render target, used when rendering Views via the GPU renderer. +/// +/// When a View is rendered via the GPU renderer (see View::is_accelerated), it will be rendered to +/// an offscreen render target (View::render_target) that you can display in your application. +/// +/// This is intended to be used with a custom GPUDriver implementation in a game or similar +/// application. +/// +/// ## Displaying the Render Target +/// +/// To display the View's render target, you should: +/// +/// 1. Retrieve the underlying texture via RenderTarget::texture_id(). +/// 2. Bind the texture using your custom GPUDriver implementation. +/// 3. Draw a textured quad with the provided UV coordinates (RenderTarget::uv_coords()). +/// +struct UExport RenderTarget { + /// + /// Whether this target is empty (null texture) + /// + bool is_empty; + + /// + /// The viewport width (in device coordinates). + /// + uint32_t width; + + /// + /// The viewport height (in device coordinates). + /// + uint32_t height; + + /// + /// The GPUDriver-specific texture ID (you should bind the texture using your custom GPUDriver + /// implementation before drawing a quad). + /// + uint32_t texture_id; + + /// + /// The texture width (in pixels). This may be padded. + /// + uint32_t texture_width; + + /// + /// The texture height (in pixels). This may be padded. + /// + uint32_t texture_height; + + /// + /// The pixel format of the texture. + /// + BitmapFormat texture_format; + + /// + /// UV coordinates of the texture (this is needed because the texture may be padded). + /// + Rect uv_coords; + + /// + /// The GPUDriver-specific render buffer ID. + /// + uint32_t render_buffer_id; + + RenderTarget(); +}; + +#pragma pack(pop) + +} // namespace ultralight diff --git a/include/Ultralight/Renderer.h b/include/Ultralight/Renderer.h new file mode 100644 index 0000000..73aa16a --- /dev/null +++ b/include/Ultralight/Renderer.h @@ -0,0 +1,307 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include + +namespace ultralight { + +/// +/// Core renderer singleton for the library, coordinates all library functions. +/// +/// The Renderer class is responsible for creating and painting View%s, managing Session%s, as well +/// as coordinating network requests, events, JavaScript execution, and more. +/// +/// ## Creating the Renderer +/// +/// @note A Renderer will be created for you automatically when you call App::Create() (access it +/// via App::renderer). +/// +/// @note App::Create() is part of the AppCore API and automatically manages window creation, run +/// loop, input, painting, and most platform-specific functionality. (Available on desktop +/// platforms only) +/// \endparblock +/// +/// ### Defining Platform Handlers +/// +/// Before creating the Renderer, you should define your platform handlers via the Platform +/// singleton. This can be used to customize file loading, font loading, clipboard access, and other +/// functionality typically provided by the OS. +/// +/// Default implementations for most platform handlers are available in the +/// [AppCore repo](https://github.com/ultralight-ux/AppCore/tree/master/src). You can use these +/// stock implementations by copying the code into your project, or you can write your own. +/// +/// At a minimum, you should provide a FileSystem and FontLoader otherwise Renderer creation will +/// fail. +/// +/// ### Setting Up the Config +/// +/// You can configure various library options by creating a Config object and passing it to +/// `Platform::instance().set_config()`. +/// +/// ### Creating the Renderer +/// +/// Once you've set up the Platform handlers and Config, you can create the Renderer by calling +/// `Renderer::Create()`. You should store the result in a RefPtr to keep it alive. +/// +/// @par Example creation code +/// ``` +/// // Get the Platform singleton (maintains global library state) +/// auto& platform = Platform::instance(); +/// +/// // Setup config +/// Config my_config; +/// platform.set_config(my_config); +/// +/// // Create platform handlers (these are the minimum required) +/// // (This is pseudo-code, you will need to define your own) +/// MyFileSystem* file_system = new MyFileSystem(); +/// MyFontLoader* font_loader = new MyFontLoader(); +/// +/// // Setup platform handlers +/// platform.set_file_system(file_system); +/// platform.set_font_loader(font_loader); +/// +/// // Create the Renderer +/// RefPtr renderer = Renderer::Create(); +/// +/// // Create Views here +/// ``` +/// +/// ## Updating Renderer Logic +/// +/// You should call Renderer::Update() from your main update loop as often as possible to give the +/// library an opportunity to dispatch events and timers: +/// +/// @par Example update code +/// ``` +/// void mainLoop() +/// { +/// while(true) +/// { +/// // Update program logic here +/// renderer.Update(); +/// } +/// } +/// ``` +/// +/// ## Rendering Each Frame +/// +/// When your program is ready to display a new frame (usually in synchrony with the monitor +/// refresh rate), you should call `Renderer::RefreshDisplay()` and `Renderer::Render()` so the +/// library can render all active View%s as needed. +/// +/// @par Example per-frame render code +/// ``` +/// void displayFrame() +/// { +/// // Notify the renderer that the main display has refreshed. This will update animations, +/// // smooth scroll, and window.requestAnimationFrame() for all Views matching the display id. +/// renderer.RefreshDisplay(0); +/// +/// // Render all Views as needed +/// renderer.Render(); +/// +/// // Each View will render to a +/// // - Pixel-Buffer Surface (View::surface()) +/// // or +/// // - GPU texture (View::render_target()) +/// // based on whether CPU or GPU rendering is used. +/// // +/// // You will need to display the image data here as needed. +/// } +/// } +/// ``` +/// +class UExport Renderer : public RefCounted { + public: + /// + /// Create the core renderer singleton for the library. + /// + /// You should set up the Platform singleton before calling this function. + /// + /// @note You do not need to the call this if you're using the App class from AppCore. + /// + /// \parblock + /// @warning You'll need to define a FontLoader and FileSystem within the Platform singleton + /// or else this call will fail. + /// \endparblock + /// + /// \parblock + /// @warning You should only create one Renderer during the lifetime of your program. + /// \endparblock + /// + /// @return Renderer is ref-counted. This method returns a ref-pointer to a new instance, you + /// should store it in a RefPtr to keep the instance alive. + /// + static RefPtr Create(); + + /// + /// Create a unique, named Session to store browsing data in (cookies, local storage, + /// application cache, indexed db, etc). + /// + /// @note A default, persistent Session is already created for you. You only need to call this + /// if you want to create private, in-memory session or use a separate session for each + /// View. + /// + /// @param is_persistent Whether or not to store the session on disk. Persistent sessions will + /// be written to the path set in Config::cache_path + /// + /// @param name A unique name for this session, this will be used to generate a unique disk + /// path for persistent sessions. + /// + virtual RefPtr CreateSession(bool is_persistent, const String& name) = 0; + + /// + /// Get the default Session. This session is persistent (backed to disk) and has the name + /// "default". + /// + virtual RefPtr default_session() = 0; + + /// + /// Create a new View to load and display web pages in. + /// + /// Views are similar to a tab in a browser. They have certain dimensions but are rendered to an + /// offscreen surface and must be forwarded all input events. + /// + /// @param width The initial width, in pixels. + /// + /// @param height The initial height, in pixels. + /// + /// @param config Configuration details for the View. + /// + /// @param session The session to store local data in. Pass a nullptr to use the default + /// session. + /// + /// @return Returns a ref-pointer to a new View instance. + /// + virtual RefPtr CreateView(uint32_t width, uint32_t height, const ViewConfig& config, + RefPtr session) + = 0; + + /// + /// Update timers and dispatch callbacks. + /// + /// You should call this as often as you can from your application's run loop. + /// + virtual void Update() = 0; + + /// + /// Notify the renderer that a display has refreshed (you should call this after vsync). + /// + /// This updates animations, smooth scroll, and window.requestAnimationFrame() for all Views + /// matching the display id. + /// + virtual void RefreshDisplay(uint32_t display_id) = 0; + + /// + /// Render all active views to their respective render-targets/surfaces. + /// + /// @note Views are only repainted if they actually need painting. + /// + virtual void Render() = 0; + + /// + /// Render a subset of views to their respective surfaces and render targets. + /// + /// @param view_array A C-array containing a list of View pointers. + /// + /// @param view_array_len The length of the C-array. + /// + virtual void RenderOnly(View** view_array, size_t view_array_len) = 0; + + /// + /// Attempt to release as much memory as possible. + /// + /// @warning Don't call this from any callbacks or driver code. + /// + virtual void PurgeMemory() = 0; + + /// + /// Print detailed memory usage statistics to the log. + /// + /// @see Platform::set_logger + /// + virtual void LogMemoryUsage() = 0; + + /// + /// Start the remote inspector server. + /// + /// @pre This feature is only available in Ultralight Pro edition and above. + /// + /// While the remote inspector is active, Views that are loaded into this renderer + /// will be able to be remotely inspected from another Ultralight instance either locally + /// (another app on same machine) or remotely (over the network) by navigating a View to: + /// + /// \code + /// inspector://
: + /// \endcode + /// + /// @param address The address for the server to listen on (eg, "127.0.0.1") + /// + /// @param port The port for the server to listen on (eg, 9222) + /// + /// @return Returns whether the server started successfully or not. + /// + virtual bool StartRemoteInspectorServer(const char* address, uint16_t port) = 0; + + /// + /// Describe the details of a gamepad, to be used with FireGamepadEvent and related + /// events below. This can be called multiple times with the same index if the details change. + /// + /// @param index The unique index (or "connection slot") of the gamepad. For example, + /// controller #1 would be "1", controller #2 would be "2" and so on. + /// + /// @param id A string ID representing the device, this will be made available + /// in JavaScript as gamepad.id + /// + /// @param axis_count The number of axes on the device. + /// + /// @param button_count The number of buttons on the device. + /// + virtual void SetGamepadDetails(uint32_t index, const String& id, uint32_t axis_count, + uint32_t button_count) + = 0; + + /// + /// Fire a gamepad event (connection / disconnection). + /// + /// @note The gamepad should first be described via SetGamepadDetails before calling this + /// function. + /// + /// @see + /// + virtual void FireGamepadEvent(const GamepadEvent& evt) = 0; + + /// + /// Fire a gamepad axis event (to be called when an axis value is changed). + /// + /// @note The gamepad should be connected via a previous call to FireGamepadEvent. + /// + /// @see + /// + virtual void FireGamepadAxisEvent(const GamepadAxisEvent& evt) = 0; + + /// + /// Fire a gamepad button event (to be called when a button value is changed). + /// + /// @note The gamepad should be connected via a previous call to FireGamepadEvent. + /// + /// @see + /// + virtual void FireGamepadButtonEvent(const GamepadButtonEvent& evt) = 0; + + protected: + virtual ~Renderer(); +}; + +} // namespace ultralight diff --git a/include/Ultralight/ScrollEvent.h b/include/Ultralight/ScrollEvent.h new file mode 100644 index 0000000..f1cd782 --- /dev/null +++ b/include/Ultralight/ScrollEvent.h @@ -0,0 +1,44 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include + +namespace ultralight { + +/// +/// Scroll event representing a change in scroll state. +/// +/// @see View::FireScrollEvent +/// +class ScrollEvent { + public: + /// + /// The scroll event granularity type + /// + enum Type { + kType_ScrollByPixel, ///< The delta value will be interpreted as number of pixels to scroll. + kType_ScrollByPage, ///< The delta value will be interpreted as number of pages to scroll. + }; + + /// + /// Scroll granularity type + /// + Type type; + + /// + /// Horizontal scroll amount + /// + int delta_x; + + /// + /// Vertical scroll amount + /// + int delta_y; +}; + +} // namespace ultralight diff --git a/include/Ultralight/Session.h b/include/Ultralight/Session.h new file mode 100644 index 0000000..77531ca --- /dev/null +++ b/include/Ultralight/Session.h @@ -0,0 +1,59 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// Storage for a browsing session (cookies, local storage, etc.). +/// +/// This class stores data for a unique browsing session (cookies, local storage, application cache, +/// indexed db. etc.). You can create multiple sessions to isolate data between different browsing +/// contexts. +/// +/// ## Default Session +/// +/// The Renderer has a default session named "default" that is used if no session is specified when +/// when creating a View. +/// +/// ## Session Lifetime +/// +/// Sessions can be either temporary (in-memory only) or persistent (backed to disk). +/// +/// @see Renderer::CreateSession +/// +class UExport Session : public RefCounted { + public: + /// + /// Whether or not this session is written to disk. + /// + virtual bool is_persistent() const = 0; + + /// + /// A unique name identifying this session. + /// + virtual String name() const = 0; + + /// + /// A unique numeric ID identifying this session. + /// + virtual uint64_t id() const = 0; + + /// + /// The disk path of this session (only valid for persistent sessions). + /// + virtual String disk_path() const = 0; + + protected: + virtual ~Session(); +}; + +} // namespace ultralight diff --git a/include/Ultralight/String.h b/include/Ultralight/String.h new file mode 100644 index 0000000..cbd755d --- /dev/null +++ b/include/Ultralight/String.h @@ -0,0 +1,158 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +/// +/// Unicode string container with conversions for UTF-8, UTF-16, and UTF-32. +/// +/// This class is used to represent strings in Ultralight. It can be created from a variety of +/// string types and converted to a number of unicode string types. +/// +/// ## Accessing string data +/// +/// Strings are natively stored in a null-terminated UTF-8 format. You can access the UTF-8 bytes +/// using the `utf8()` method: +/// +/// ``` +/// String str("Hello, world!"); +/// +/// // Print the UTF-8 data (guaranteed to be null-terminated) +/// printf("%s\n", str.utf8().data()); +/// ``` +/// +class UExport String { + public: + /// + /// Create empty string + /// + String(); + + /// + /// Create from null-terminated, ASCII C-string + /// + String(const char* str); + + /// + /// Create from raw, UTF-8 string with certain length + /// + String(const char* str, size_t len); + + /// + /// Create from existing String8 (UTF-8). + /// + String(const String8& str); + + /// + /// Create from raw UTF-16 string with certain length + /// + String(const Char16* str, size_t len); + + /// + /// Create from existing String16 (UTF-16) + /// + String(const String16& str); + + /// + /// Create from existing String32 (UTF-32) + /// + String(const String32& str); + + /// + /// Copy constructor + /// + String(const String& other); + + /// + /// Move constructor + /// + String(String&& other); + + /// + /// Destructor + /// + ~String(); + + /// + /// Assign string from another, copy is made + /// + String& operator=(const String& other); + + /// + /// Move assignment operator + /// + String& operator=(String&& other); + + /// + /// Append string with another + /// + String& operator+=(const String& other); + + /// + /// Concatenation operator + /// + inline friend String operator+(String lhs, const String& rhs) { + lhs += rhs; + return lhs; + } + + /// + /// Get native UTF-8 string + /// + String8& utf8() { return str_; } + + /// + /// Get native UTF-8 string + /// + const String8& utf8() const { return str_; } + + /// + /// Convert to UTF-16 string + /// + String16 utf16() const; + + /// + /// Convert to UTF-32 string + /// + String32 utf32() const; + + /// + /// Check if string is empty or not + /// + bool empty() const { return str_.empty(); } + + /// + /// Hash function + /// + size_t Hash() const; + + /// + /// Comparison operator + /// + bool operator<(const String& other) const; + + /// + /// Equality operator + /// + bool operator==(const String& other) const; + + /// + /// Inequality operator + /// + bool operator!=(const String& other) const; + + private: + String8 str_; +}; + +} // namespace ultralight diff --git a/include/Ultralight/String16.h b/include/Ultralight/String16.h new file mode 100644 index 0000000..8d13093 --- /dev/null +++ b/include/Ultralight/String16.h @@ -0,0 +1,152 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +class String8; +class String32; + +namespace detail { + template struct selector; + template<> struct selector<4> { typedef char16_t Char16; }; + template<> struct selector<2> { typedef wchar_t Char16; }; +} + +#ifdef DISABLE_NATIVE_WCHAR_T +// Force Char16 type to use char16_t, used on Windows when native wchar_t support is disabled. +typedef char16_t Char16; +#else +// We use wchar_t if size == 2, otherwise use char16_t +typedef detail::selector::Char16 Char16; +#endif + +/// +/// A null-terminated UTF-16 string container. +/// +class UExport String16 { +public: + // Native character type + typedef Char16 CharType; + + // Make an empty String16 + String16(); + + // Make a String16 from raw UTF-16 buffer with certain length + String16(const Char16* str, size_t len); + + // Make a String16 from raw unsigned short UTF-16 buffer with certain length. Useful on Windows + // when native support for wchar_t is disabled (eg, /Zc:wchar_t-). + String16(const unsigned short* str, size_t len); + + // Make a deep copy of String16 + String16(const String16& other); + + // Move constructor + String16(String16&& other); + + // Destructor + ~String16(); + + // Assign a String16 to this one, deep copy is made + String16& operator=(const String16& other); + + // Move assignment operator + String16& operator=(String16&& other); + + // Append a String16 to this one. + String16& operator+=(const String16& other); + + // Concatenation operator + inline friend String16 operator+(String16 lhs, const String16& rhs) { lhs += rhs; return lhs; } + + // Get raw UTF-16 data + Char16* data() { return data_; } + + // Get raw UTF-16 data (const) + const Char16* data() const { return data_; } + + // Get raw UTF-16 data as unsigned short. This is useful on Windows if you compile without native + // support for wchar_t (eg, /Zc:wchar_t-) + unsigned short* udata() { return reinterpret_cast(data_); } + + // Get raw UTF-16 data as unsigned short (const). + const unsigned short* udata() const { return reinterpret_cast(data_); } + + // Get length in characters. + size_t length() const { return length_; } + + // Get size in characters (synonym for length) + size_t size() const { return length_; } + + // Get size in bytes + size_t sizeBytes() const { return length_ * sizeof(Char16); } + + // Check if string is empty. + bool empty() const { return !data_ || length_ == 0; } + + // Get character at specific position + Char16& operator[](size_t pos) { return data_[pos]; } + + // Get character at specific position (const) + const Char16& operator[](size_t pos) const { return data_[pos]; } + + // Get a UTF-8 copy of this string + String8 utf8() const; + + // Get a UTF-32 copy of this string + String32 utf32() const; + + // Hash function + size_t Hash() const; + + // Comparison operator + bool operator<(const String16& other) const; + + // Equality operator + bool operator==(const String16& other) const; + + // Inequality operator + bool operator!=(const String16& other) const; + +private: + Char16* data_; + size_t length_; +}; + +/// +/// @brief A UTF-16 string vector. +/// +class UExport String16Vector : public RefCounted { +public: + // Create an empty string vector + static RefPtr Create(); + + // Create a string vector from an existing array (a deep copy is made) + static RefPtr Create(const String16* stringArray, size_t len); + + // Add an element to the back of the string vector + virtual void push_back(const String16& val) = 0; + + // Get raw String16 vector array + virtual String16* data() = 0; + + // Get the number of elements in vector + virtual size_t size() const = 0; + +protected: + String16Vector(); + virtual ~String16Vector(); + String16Vector(const String16Vector&); + void operator=(const String16Vector&); +}; + +} // namespace ultralight diff --git a/include/Ultralight/String32.h b/include/Ultralight/String32.h new file mode 100644 index 0000000..e201fc7 --- /dev/null +++ b/include/Ultralight/String32.h @@ -0,0 +1,99 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +class String8; +class String16; + +/// +/// A null-terminated UTF-32 string container. +/// +class UExport String32 { +public: + // Native character type + typedef char32_t CharType; + + // Make an empty String32 + String32(); + + // Make a String32 from raw UTF-32 string with certain length + String32(const char32_t* c_str, size_t len); + + // Make a deep copy of String32 + String32(const String32& other); + + // Move constructor + String32(String32&& other); + + // Destructor + ~String32(); + + // Assign a String32 to this one, deep copy is made + String32& operator=(const String32& other); + + // Move assignment operator + String32& operator=(String32&& other); + + // Append a String32 to this one. + String32& operator+=(const String32& other); + + // Concatenation operator + inline friend String32 operator+(String32 lhs, const String32& rhs) { lhs += rhs; return lhs; } + + // Get raw UTF-32 data + char32_t* data() { return data_; } + + // Get raw UTF-32 data (const) + const char32_t* data() const { return data_; } + + // Get length in characters. + size_t length() const { return length_; } + + // Get size in characters (synonym for length) + size_t size() const { return length_; } + + // Get size in bytes + size_t sizeBytes() const { return length_ * sizeof(char32_t); } + + // Check if string is empty. + bool empty() const { return !data_ || length_ == 0; } + + // Get character at specific position + char32_t& operator[](size_t pos) { return data_[pos]; } + + // Get character at specific position (const) + const char32_t& operator[](size_t pos) const { return data_[pos]; } + + // Get a UTF-8 copy of this string + String8 utf8() const; + + // Get a UTF-16 copy of this string + String16 utf16() const; + + // Hash function + size_t Hash() const; + + // Comparison operator + bool operator<(const String32& other) const; + + // Equality operator + bool operator==(const String32& other) const; + + // Inequality operator + bool operator!=(const String32& other) const; + +private: + char32_t* data_; + size_t length_; +}; + +} // namespace ultralight diff --git a/include/Ultralight/String8.h b/include/Ultralight/String8.h new file mode 100644 index 0000000..06b957c --- /dev/null +++ b/include/Ultralight/String8.h @@ -0,0 +1,102 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +class String16; +class String32; + +/// +/// A null-terminated UTF-8 string container. +// +class UExport String8 { +public: + // Native character type + typedef char CharType; + + // Make an empty String8 + String8(); + + // Make a String8 from raw, null-terminated UTF-8 string + String8(const char* c_str); + + // Make a String8 from raw UTF-8 string with certain length + String8(const char* c_str, size_t len); + + // Make a deep copy of String8 + String8(const String8& other); + + // Move constructor + String8(String8&& other); + + // Destructor + ~String8(); + + // Assign a String8 to this one, deep copy is made + String8& operator=(const String8& other); + + // Move assignment operator + String8& operator=(String8&& other); + + // Append a String8 to this one. + String8& operator+=(const String8& other); + + // Concatenation operator + inline friend String8 operator+(String8 lhs, const String8& rhs) { lhs += rhs; return lhs; } + + // Get raw UTF-8 data + char* data() { return data_; } + + // Get raw UTF-8 data (const) + const char* data() const { return data_; } + + // Get length in characters. + size_t length() const { return length_; } + + // Get size in characters (synonym for length) + size_t size() const { return length_; } + + // Get size in bytes + size_t sizeBytes() const { return length_ * sizeof(char); } + + // Check if string is empty. + bool empty() const { return !data_ || length_ == 0; } + + // Get character at specific position + char& operator[](size_t pos) { return data_[pos]; } + + // Get character at specific position (const) + const char& operator[](size_t pos) const { return data_[pos]; } + + // Get a UTF-16 copy of this string + String16 utf16() const; + + // Get a UTF-32 copy of this string + String32 utf32() const; + + // Hash function + size_t Hash() const; + + // Comparison operator + bool operator<(const String8& other) const; + + // Equality operator + bool operator==(const String8& other) const; + + // Inequality operator + bool operator!=(const String8& other) const; + +private: + char* data_; + size_t length_; +}; + +} // namespace ultralight diff --git a/include/Ultralight/StringSTL.h b/include/Ultralight/StringSTL.h new file mode 100644 index 0000000..a67bed8 --- /dev/null +++ b/include/Ultralight/StringSTL.h @@ -0,0 +1,199 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +/// +/// @file StringSTL.h +/// +/// STL compatibility header for ultralight::String. +/// +/// `#include ` +/// +/// This optional header provides utility functions for converting between ultralight::String, +/// std::string, and std::string_view. It also provides support for using ultralight::String +/// with standard library containers and stream operators. +/// +/// @pre This header requires C++17 or later. +/// +/// ## Example +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// ultralight::String myStr("Hello, world!"); +/// +/// // Convert ultralight::String to std::string +/// std::string stdStr = ultralight::Convert(myStr); +/// +/// // Convert std::string to ultralight::String +/// ultralight::String backToUl = ultralight::Convert(stdStr); +/// +/// // Print ultralight::String to std::cout +/// std::cout << myStr << std::endl; +/// ``` +/// +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +/// +/// Trait to check if a type is a supported string-like type. +/// +template +struct is_string_type : std::false_type {}; + +template <> struct is_string_type : std::true_type {}; +template <> struct is_string_type : std::true_type {}; +template <> struct is_string_type : std::true_type {}; +template <> struct is_string_type : std::true_type {}; + +/// +/// Convert between string types. +/// +/// This function provides efficient conversion between different string types. +/// It supports ultralight::String, std::string, std::string_view, and const char*. +/// +/// The following type conversions are automatically deduced (no template argument needed): +/// - ultralight::String -> std::string +/// - std::string -> ultralight::String +/// - std::string_view -> ultralight::String +/// - const char* -> ultralight::String +/// +/// For explicit conversion to std::string_view, use Convert(). +/// +/// @tparam To The target string type (optional, deduced in common cases) +/// @tparam From The source string type (optional) +/// +/// @param from The string to convert +/// +/// @return The converted string in the target type +/// +/// ## Example +/// +/// ```cpp +/// ultralight::String myStr("Hello, world!"); +/// +/// // ultralight::String -> std::string +/// std::string stdStr = ultralight::Convert(myStr); +/// +/// // ultralight::String -> std::string_view +/// std::string_view svStr = ultralight::Convert(myStr); +/// +/// // std::string -> ultralight::String +/// ultralight::String backToUl = ultralight::Convert(stdStr); +/// +/// // std::string_view -> ultralight::String +/// ultralight::String fromView = ultralight::Convert(std::string_view("View")); +/// ``` +/// +template +auto Convert(const From& from) { + static_assert(is_string_type>>::value, + "Convert only supports String, std::string, std::string_view, and const char*"); + + if constexpr (std::is_same_v) { + // Automatic deduction + if constexpr (std::is_same_v) { + return std::string(from.utf8().data(), from.utf8().length()); + } else if constexpr (std::is_same_v || + std::is_same_v) { + return String(from.data(), from.length()); + } else if constexpr (std::is_same_v) { + return String(from); // String constructor handles null-termination + } else { + // This case should never be reached due to the static_assert + return From{}; + } + } else { + // Explicit conversion + static_assert(is_string_type::value, + "Convert only supports String, std::string, std::string_view, and const char*"); + + if constexpr (std::is_same_v) { + return from; + } else if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return String(from); // String constructor handles null-termination + } else { + return String(from.data(), from.length()); + } + } else if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return std::string(from.utf8().data(), from.utf8().length()); + } else if constexpr (std::is_same_v) { + return std::string(from); // std::string constructor handles null-termination + } else { + return std::string(from); + } + } else if constexpr (std::is_same_v) { + if constexpr (std::is_same_v) { + return std::string_view(from.utf8().data(), from.utf8().length()); + } else if constexpr (std::is_same_v) { + return std::string_view(from); // std::string_view constructor handles null-termination + } else { + return std::string_view(from); + } + } else if constexpr (std::is_same_v) { + static_assert(!std::is_same_v, + "Direct conversion to const char* is not supported due to ownership issues. " + "Convert to String, std::string, or std::string_view instead."); + } else { + // This case should never be reached due to the static_assert + return To{}; + } + } +} + +} // namespace ultralight + +namespace std { + +/// +/// Hash specialization for ultralight::String +/// +template<> +struct hash { + size_t operator()(const ultralight::String& str) const { + return str.Hash(); + } +}; + +} // namespace std + +/// +/// Stream output operator for ultralight::String. +/// +/// @param os The output stream. +// +/// @param str The string to output. +/// +/// @return The output stream. +/// +inline std::ostream& operator<<(std::ostream& os, const ultralight::String& str) { + return os << ultralight::Convert(str); +} + +/// +/// Stream input operator for ultralight::String. +/// +/// @param is The input stream. +/// +/// @param str The string to input into. +/// +/// @return The input stream. +/// +inline std::istream& operator>>(std::istream& is, ultralight::String& str) { + std::string temp; + is >> temp; + str = ultralight::Convert(temp); + return is; +} \ No newline at end of file diff --git a/include/Ultralight/Ultralight.h b/include/Ultralight/Ultralight.h new file mode 100644 index 0000000..423292b --- /dev/null +++ b/include/Ultralight/Ultralight.h @@ -0,0 +1,39 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include diff --git a/include/Ultralight/View.h b/include/Ultralight/View.h new file mode 100644 index 0000000..606c12f --- /dev/null +++ b/include/Ultralight/View.h @@ -0,0 +1,526 @@ +/************************************************************************************************** + * This file is a part of Ultralight, an ultra-portable web-browser engine. * + * * + * See for licensing and more. * + * * + * (C) 2025 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ultralight { + +/// +/// View-specific configuration settings. +/// +/// @see Renderer::CreateView +/// +struct UExport ViewConfig { + + /// + /// A user-generated id for the display (monitor, TV, or screen) that this View will be shown on. + /// + /// Animations are driven based on the physical refresh rate of the display. Multiple Views can + /// share the same display. + /// + /// @note This is automatically managed for you when App::Create() is used. + /// + /// @see Renderer::RefreshDisplay. + /// + uint32_t display_id = 0; + + /// + /// Whether to render using the GPU renderer (accelerated) or the CPU renderer (unaccelerated). + /// + /// When true, the View will be rendered to an offscreen GPU texture using the GPU driver set in + /// Platform::set_gpu_driver. You can fetch details for the texture via View::render_target. + /// + /// When false (the default), the View will be rendered to an offscreen pixel buffer using the + /// multithreaded CPU renderer. This pixel buffer can optionally be provided by the user-- + /// for more info see Platform::set_surface_factory and View::surface. + /// + /// @note This is automatically managed for you when App::Create() is used. + /// + bool is_accelerated = false; + + /// + /// The initial device scale, ie. the amount to scale page units to screen pixels. This should + /// be set to the scaling factor of the device that the View is displayed on. + /// + /// @note 1.0 is equal to 100% zoom (no scaling), 2.0 is equal to 200% zoom (2x scaling) + /// + /// @note This is automatically managed for you when App::Create() is used. + /// + double initial_device_scale = 1.0; + + /// + /// Whether or not this View should support transparency. + /// + /// @note Make sure to also set the following CSS on the page: + /// + /// html, body { background: transparent; } + /// + bool is_transparent = false; + + /// + /// Whether or not the View should initially have input focus, @see View::Focus() + /// + bool initial_focus = true; + + /// + /// Whether or not images should be enabled. + /// + bool enable_images = true; + + /// + /// Whether or not JavaScript should be enabled. + /// + bool enable_javascript = true; + + /// + /// Whether or not compositing should be enabled. + /// + bool enable_compositor = false; + + /// + /// Default font-family to use. + /// + String font_family_standard = "Times New Roman"; + + /// + /// Default font-family to use for fixed fonts. (pre/code) + /// + String font_family_fixed = "Courier New"; + + /// + /// Default font-family to use for serif fonts. + /// + String font_family_serif = "Times New Roman"; + + /// + /// Default font-family to use for sans-serif fonts. + /// + String font_family_sans_serif = "Arial"; + + /// + /// Custom user-agent string. You can use this to override the default user-agent string. + /// + /// @pre This feature is only available in Ultralight Pro edition and above. + /// + String user_agent = ULTRALIGHT_USER_AGENT; +}; + +/// +/// Web-page container rendered to an offscreen surface. +/// +/// The View class is responsible for loading and rendering web-pages to an offscreen surface. It +/// is completely isolated from the OS windowing system, you must forward all input events to it +/// from your application. +/// +/// ## Creating a View +/// +/// You can create a View using Renderer::CreateView. +/// +/// ``` +/// // Create a ViewConfig with the desired settings +/// ViewConfig view_config; +/// +/// // Create a View, 500 by 500 pixels in size, using the default Session +/// RefPtr view = renderer->CreateView(500, 500, view_config, nullptr); +/// ``` +/// +/// @note When using App::Create, the library will automatically create a View for you when you +/// call Overlay::Create. +/// +/// ## Loading Content into a View +/// +/// You can load content asynchronously into a View using View::LoadURL(). +/// +/// ``` +/// // Load a URL into the View +/// view->LoadURL("https://en.wikipedia.org/wiki/Main_Page"); +/// ``` +/// +/// ### Local File URLs +/// +/// Local file URLs (eg, `file:///page.html`) will be loaded via FileSystem. You can provide your +/// own FileSystem implementation so these files can be loaded from your application's resources. +/// +/// ### Displaying Views in Your Application +/// +/// Views are rendered either to a pixel-buffer (View::surface) or a GPU texture +/// (View::render_target) depending on whether CPU or GPU rendering is used (see +/// ViewConfig::is_accelerated). +/// +/// You can use the Surface or RenderTarget to display the View in your application. +/// +/// ``` +/// // Get the Surface for the View (assuming CPU rendering) +/// Surface* surface = view->surface(); +/// +/// // Check if the Surface is dirty (pixels have changed) +/// if (!surface->dirty_bounds().IsEmpty()) { +/// // Cast to the default Surface implementation (BitmapSurface) and get +/// // the underlying Bitmap. +/// RefPtr bitmap = static_cast(surface)->bitmap(); +/// +/// // Use the bitmap pixels here... +/// +/// // Clear the dirty bounds after you're done displaying the pixels +/// surface->ClearDirtyBounds(); +/// } +/// ``` +/// +/// ## Input Events +/// +/// You must forward all input events to the View from your application. This includes keyboard, +/// mouse, and scroll events. +/// +/// ``` +/// // Forward a mouse-move event to the View +/// MouseEvent evt; +/// evt.type = MouseEvent::kType_MouseMoved; +/// evt.x = 100; +/// evt.y = 100; +/// evt.button = MouseEvent::kButton_None; +/// view->FireMouseEvent(evt); +/// ``` +/// +/// @note The View API is not thread-safe, all calls must be made on the same thread that the +/// Renderer or App was created on. +/// +class UExport View : public RefCounted { + public: + /// + /// Get the URL of the current page loaded into this View, if any. + /// + virtual String url() = 0; + + /// + /// Get the title of the current page loaded into this View, if any. + /// + virtual String title() = 0; + + /// + /// Get the width of the View, in pixels. + /// + virtual uint32_t width() const = 0; + + /// + /// Get the height of the View, in pixels. + /// + virtual uint32_t height() const = 0; + + /// + /// Get the display id of the View. + /// + /// @see ViewConfig::display_id + /// + virtual uint32_t display_id() const = 0; + + /// + /// Set the display id of the View. + /// + /// This should be called when the View is moved to another display. + /// + virtual void set_display_id(uint32_t id) = 0; + + /// + /// Get the device scale, ie. the amount to scale page units to screen pixels. + /// + /// For example, a value of 1.0 is equivalent to 100% zoom. A value of 2.0 is 200% zoom. + /// + virtual double device_scale() const = 0; + + /// + /// Set the device scale. + /// + virtual void set_device_scale(double scale) = 0; + + /// + /// Whether or not the View is GPU-accelerated. If this is false, the page will be rendered + /// via the CPU renderer. + /// + virtual bool is_accelerated() const = 0; + + /// + /// Whether or not the View supports transparent backgrounds. + /// + virtual bool is_transparent() const = 0; + + /// + /// Check if the main frame of the page is currently loading. + /// + virtual bool is_loading() = 0; + + /// + /// Get the RenderTarget for the View. + /// + /// @pre Only valid if this View is using the GPU renderer (see ViewConfig::is_accelerated). + /// + /// @note You can use this with your GPUDriver implementation to bind and display the + /// corresponding texture in your application. + /// + virtual RenderTarget render_target() = 0; + + /// + /// Get the Surface for the View (native pixel buffer that the CPU renderer draws into). + /// + /// @pre This operation is only valid if the View is using the CPU renderer, (eg, it is + /// **not** GPU accelerated, see ViewConfig::is_accelerated). This function will return + /// return nullptr if the View is using the GPU renderer. + /// + /// @note The default Surface is BitmapSurface but you can provide your own Surface + /// implementation via Platform::set_surface_factory(). + /// + virtual Surface* surface() = 0; + + /// + /// Load a raw string of HTML, the View will navigate to it as a new page. + /// + /// @param html The raw HTML string to load. + /// + /// @param url An optional URL for this load (to make it appear as if we we loaded this HTML + /// from a certain URL). Can be used for resolving relative URLs and cross-origin + /// rules. + /// + /// @param add_to_history Whether or not this load should be added to the session's history + /// (eg, the back/forward list). + /// + virtual void LoadHTML(const String& html, const String& url = "", bool add_to_history = false) + = 0; + + /// + /// Load a URL, the View will navigate to it as a new page. + /// + /// @note You can use File URLs (eg, file:///page.html) but you must define your own FileSystem + /// implementation if you are not using AppCore. @see Platform::set_file_system + /// + virtual void LoadURL(const String& url) = 0; + + /// + /// Resize View to a certain size. + /// + /// @param width The initial width, in pixels. + /// + /// @param height The initial height, in pixels. + /// + /// + virtual void Resize(uint32_t width, uint32_t height) = 0; + + /// + /// Acquire the page's JSContext for use with the JavaScriptCore API + /// + /// @note You can use the underlying JSContextRef with the JavaScriptCore C API. This allows you + /// to marshall C/C++ objects to/from JavaScript, bind callbacks, and call JS functions + /// directly. + /// + /// @note The JSContextRef gets reset after each page navigation. You should initialize your + /// JavaScript state within the OnWindowObjectReady and OnDOMReady events, + /// @see ViewListener. + /// + /// @note This call locks the internal context for the current thread. It will be unlocked when + /// the returned JSContext's ref-count goes to zero. The lock is recursive, you can call + /// this multiple times. + /// + virtual RefPtr LockJSContext() = 0; + + /// + /// Get a handle to the internal JavaScriptCore VM. + /// + virtual void* JavaScriptVM() = 0; + + /// + /// Helper function to evaluate a raw string of JavaScript and return the result as a String. + /// + /// @param script A string of JavaScript to evaluate in the main frame. + /// + /// @param exception A string to store the exception in, if any. Pass a nullptr if you don't + /// care about exceptions. + /// + /// @return Returns the JavaScript result typecast to a String. + /// + /// + /// @note You do not need to lock the JS context, it is done automatically. + /// + /// @note If you need lower-level access to native JavaScript values, you should instead lock + /// the JS context and call JSEvaluateScript() in the JavaScriptCore C API. + /// @see + /// + virtual String EvaluateScript(const String& script, String* exception = nullptr) = 0; + + /// + /// Whether or not we can navigate backwards in history + /// + virtual bool CanGoBack() = 0; + + /// + /// Whether or not we can navigate forwards in history + /// + virtual bool CanGoForward() = 0; + + /// + /// Navigate backwards in history + /// + virtual void GoBack() = 0; + + /// + /// Navigate forwards in history + /// + virtual void GoForward() = 0; + + /// + /// Navigate to an arbitrary offset in history + /// + virtual void GoToHistoryOffset(int offset) = 0; + + /// + /// Reload current page + /// + virtual void Reload() = 0; + + /// + /// Stop all page loads + /// + virtual void Stop() = 0; + + /// + /// Give focus to the View. + /// + /// You should call this to give visual indication that the View has input focus (changes active + /// text selection colors, for example). + /// + virtual void Focus() = 0; + + /// + /// Remove focus from the View and unfocus any focused input elements. + /// + /// You should call this to give visual indication that the View has lost input focus. + /// + virtual void Unfocus() = 0; + + /// + /// Whether or not the View has focus. + /// + virtual bool HasFocus() = 0; + + /// + /// Whether or not the View has an input element with visible keyboard focus (indicated by a + /// blinking caret). + /// + /// You can use this to decide whether or not the View should consume keyboard input events + /// (useful in games with mixed UI and key handling). + /// + virtual bool HasInputFocus() = 0; + + /// + /// Fire a keyboard event + /// + /// @note Only 'Char' events actually generate text in input fields. + /// + virtual void FireKeyEvent(const KeyEvent& evt) = 0; + + /// + /// Fire a mouse event + /// + virtual void FireMouseEvent(const MouseEvent& evt) = 0; + + /// + /// Fire a scroll event + /// + virtual void FireScrollEvent(const ScrollEvent& evt) = 0; + + /// + /// Set a ViewListener to receive callbacks for View-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_view_listener(ViewListener* listener) = 0; + + /// + /// Get the active ViewListener, if any + /// + virtual ViewListener* view_listener() const = 0; + + /// + /// Set a LoadListener to receive callbacks for Load-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_load_listener(LoadListener* listener) = 0; + + /// + /// Get the active LoadListener, if any + /// + virtual LoadListener* load_listener() const = 0; + + /// + /// Set a DownloadListener to receive callbacks for download-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_download_listener(DownloadListener* listener) = 0; + + /// + /// Get the active DownloadListener, if any + /// + virtual DownloadListener* download_listener() const = 0; + + /// + /// Cancel an active download. + /// + virtual void CancelDownload(DownloadId id) = 0; + + /// + /// Set a NetworkListener to receive callbacks for network-related events. + /// + /// @note Ownership remains with the caller. + /// + virtual void set_network_listener(NetworkListener* listener) = 0; + + /// + /// Get the active NetworkListener, if any + /// + virtual NetworkListener* network_listener() const = 0; + + /// + /// Set whether or not this View should be repainted during the next call to Renderer::Render + /// + /// @note This flag is automatically set whenever the page content changes but you can set it + /// directly in case you need to force a repaint. + /// + virtual void set_needs_paint(bool needs_paint) = 0; + + /// + /// Whether or not this View should be repainted during the next call to + /// Renderer::Render. + /// + virtual bool needs_paint() const = 0; + + /// + /// Create an Inspector View to inspect / debug this View locally. + /// + /// This will only succeed if you have the inspector assets in your filesystem-- the inspector + /// will look for file:///inspector/Main.html when it first loads. + /// + /// You must handle ViewListener::OnCreateInspectorView so that the library has a View to display + /// the inspector in. This function will call this event only if an inspector view is not + /// currently active. + /// + virtual void CreateLocalInspectorView() = 0; + + protected: + virtual ~View(); +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/Allocator.h b/include/Ultralight/platform/Allocator.h new file mode 100644 index 0000000..f8bc40b --- /dev/null +++ b/include/Ultralight/platform/Allocator.h @@ -0,0 +1,106 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#ifndef ULTRALIGHT_ALLOCATOR_H +#define ULTRALIGHT_ALLOCATOR_H + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/// +/// User-defined allocator interface. +/// +/// @pre This API is only available in the Pro edition when the UL_ENABLE_ALLOCATOR_OVERRIDE +/// build option is enabled. +/// +/// The library uses this to allocate memory. You can override the default allocator functions +/// by setting the ulAllocator object with your own functions. +/// +/// This should be done before calling any other library functions. +/// +/// @see ulAllocator +/// +typedef struct { + // + // Allocate a block of memory of at least |bytes| size. + // + void* (*malloc)(size_t bytes); + + // + // Reallocate a block of memory to at least |bytes| size. + // + void* (*realloc)(void* address, size_t bytes); + + // + // Free a block of memory allocated with malloc or realloc. + // + void (*free)(void* address); + + // + // Allocate a block of memory of at least |bytes| size, aligned to |alignment|. + // + void* (*aligned_malloc)(size_t bytes, size_t alignment); + + // + // Reallocate a block of memory to at least |bytes| size, aligned to |alignment|. + // + void* (*aligned_realloc)(void* address, size_t bytes, size_t alignment); + + // + // Free a block of memory allocated with aligned_malloc or aligned_realloc. + // + void (*aligned_free)(void* address); + + // + // Get the size of the memory block that backs the allocation at |address|. The memory + // block size is always at least as large as the allocation it backs, and may be larger. + // * Windows equivalent: _msize + // * POSIX equivalent: malloc_size + // + size_t (*get_size_estimate)(void* address); + +} ULAllocator; + +/// +/// Get the allocator interface object for the library. +/// +/// @pre This API is only available in the Pro edition when the UL_ENABLE_ALLOCATOR_OVERRIDE +/// build option is enabled. +/// +/// The C functions set in this object will be used for allocating memory inside the library +/// when the `UL_ENABLE_ALLOCATOR_OVERRIDE` build option is enabled. +/// +/// Default functions are already set for all of these but you can override them with your own. +/// +/// Platform specific notes: +/// * __Windows__: The default functions use `HeapAlloc` / `HeapReAlloc` / `HeapFree`. +/// +extern UCExport ULAllocator ulAllocator; + +#ifdef _WIN32 +/// +/// Get the handle to the private heap used by the library. +/// +/// This is the handle returned by `HeapCreate()`, you should destroy it after unloading the library +/// by calling `HeapDestroy()`. +/// +/// This is only valid if the UL_ENABLE_ALLOCATOR_OVERRIDE build option is enabled and the default +/// functions are set in the ulAllocator object. +/// +UCExport void* ulGetHeapHandle(); +#endif + +#ifdef __cplusplus +} +#endif + +#endif // ULTRALIGHT_ALLOCATOR_H \ No newline at end of file diff --git a/include/Ultralight/platform/Clipboard.h b/include/Ultralight/platform/Clipboard.h new file mode 100644 index 0000000..a1ea152 --- /dev/null +++ b/include/Ultralight/platform/Clipboard.h @@ -0,0 +1,49 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +/// +/// User-defined clipboard interface. +/// +/// The library uses this to read and write data to the system's clipboard. +/// +/// AppCore automatically provides a platform-specific implementation of this that cuts, copies, +/// and pastes to the OS clipboard when you call App::Create(). +/// +/// If you are using Renderer::Create() instead of App::Create(), you will need to provide your own +/// implementation of this. @see Platform::set_clipboard(). +/// +class UExport Clipboard { + public: + virtual ~Clipboard(); + + /// + /// Clear the clipboard. + /// + virtual void Clear() = 0; + + /// + /// Read plain text from the clipboard + /// + /// This is called when the library wants to read text from the OS clipboard. + /// + virtual String ReadPlainText() = 0; + + /// + /// Write plain text to the clipboard. + /// + /// This is called when the library wants to write text to the OS clipboard. + /// + virtual void WritePlainText(const String& text) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/Config.h b/include/Ultralight/platform/Config.h new file mode 100644 index 0000000..0be3682 --- /dev/null +++ b/include/Ultralight/platform/Config.h @@ -0,0 +1,274 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2025 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +/// +/// The winding order for front-facing triangles. (Only used when the GPU renderer is used) +/// +enum class FaceWinding : uint8_t { + /// + /// Clockwise Winding (Direct3D, etc.) + /// + Clockwise, + + /// + /// Counter-Clockwise Winding (OpenGL, etc.) + /// + CounterClockwise, +}; + +enum class FontHinting : uint8_t { + /// + /// Lighter hinting algorithm-- glyphs are slightly fuzzier but better resemble their original + /// shape. This is achieved by snapping glyphs to the pixel grid only vertically which better + /// preserves inter-glyph spacing. + /// + Smooth, + + /// + /// Default hinting algorithm-- offers a good balance between sharpness and shape at smaller font + /// sizes. + /// + Normal, + + /// + /// Strongest hinting algorithm-- outputs only black/white glyphs. The result is usually + /// unpleasant if the underlying TTF does not contain hints for this type of rendering. + /// + Monochrome, + + /// + /// No hinting is performed-- fonts may be blurry at smaller font sizes. + /// + None, +}; + +enum class EffectQuality : uint8_t { + /// + /// Fastest effect quality-- uses the lowest quality effects (half-resolution, fewer passes, etc.) + /// + Low, + + /// + /// Default effect quality-- strikes a good balance between quality and performance. + /// + Medium, + + /// + /// Highest effect quality-- favors quality over performance. + /// + High, +}; + +/// +/// Core configuration for the renderer. +/// +/// These are various configuration options that can be used to customize the behavior of the +/// library. These options can only be set once before creating the Renderer. +/// +/// ## Setting the Config +/// +/// You should create an instance of the Config struct, set its members, and then call +/// Platform::set_config() before creating the Renderer at the beginning of your +/// application's lifetime. +/// +/// @par Example usage +/// ``` +/// Config config; +/// config.user_stylesheet = "body { background: purple; }"; +/// +/// Platform::instance().set_config(config); +/// // (Setup other Platform interfaces here.) +/// +/// auto renderer = Renderer::Create(); +/// ``` +/// +struct UExport Config { + /// + /// A writable OS file path to store persistent Session data in. + /// + /// This data may include cookies, cached network resources, indexed DB, etc. + /// + /// @note Files are only written to the path when using a persistent Session. + /// + /// @see Renderer::CreateSession() + /// + String cache_path; + + /// + /// The relative path to the resources folder (loaded via the FileSystem API). + /// + /// The library loads certain resources (SSL certs, ICU data, etc.) from the FileSystem API + /// during runtime (eg, `file:///resources/cacert.pem`). + /// + /// You can customize the relative file path to the resources folder by modifying this setting. + /// + /// @see FileSystem + /// + String resource_path_prefix = "resources/"; + + /// + /// The winding order for front-facing triangles. + /// + /// @pre Only used when GPU rendering is enabled for the View. + /// + /// @see FaceWinding + /// + FaceWinding face_winding = FaceWinding::CounterClockwise; + + /// + /// The hinting algorithm to use when rendering fonts. + /// + /// @see FontHinting + /// + FontHinting font_hinting = FontHinting::Normal; + + /// + /// The gamma to use when compositing font glyphs. + /// + /// You can change this value to adjust font contrast (Adobe and Apple prefer 1.8). + /// + double font_gamma = 1.8; + + /// + /// Global user-defined CSS string (included before any CSS on the page). + /// + /// You can use this to override default styles for various elements on the page. + /// + /// @note This is an actual string of CSS, not a file path. + /// + String user_stylesheet; + + /// + /// Whether or not to continuously repaint any Views, regardless if they are dirty. + /// + /// This is mainly used to diagnose painting/shader issues and profile performance. + /// + bool force_repaint = false; + + /// + /// The delay (in seconds) between every tick of a CSS animation. (Default: 60 FPS) + /// + double animation_timer_delay = 1.0 / 60.0; + + /// + /// The delay (in seconds) between every tick of a smooth scroll animation. (Default: 60 FPS) + /// + double scroll_timer_delay = 1.0 / 60.0; + + /// + /// The delay (in seconds) between every call to the recycler. + /// + /// The library attempts to reclaim excess memory during calls to the internal recycler. You can + /// change how often this is run by modifying this value. + /// + double recycle_delay = 4.0; + + /// + /// The size of WebCore's memory cache in bytes. + /// + /// @note You should increase this if you anticipate handling pages with large resources, Safari + /// typically uses 128+ MiB for its cache. + /// + uint32_t memory_cache_size = 64 * 1024 * 1024; + + /// + /// The number of pages to keep in the cache. (Default: 0, none) + /// + /// @note + /// \parblock + /// + /// Safari typically caches about 5 pages and maintains an on-disk cache to support typical + /// web-browsing activities. + /// + /// If you increase this, you should probably increase the memory cache size as well. + /// + /// \endparblock + /// + uint32_t page_cache_size = 0; + + /// + /// The system's physical RAM size in bytes. + /// + /// JavaScriptCore tries to detect the system's physical RAM size to set reasonable allocation + /// limits. Set this to anything other than 0 to override the detected value. Size is in bytes. + /// + /// This can be used to force JavaScriptCore to be more conservative with its allocation strategy + /// (at the cost of some performance). + /// + uint32_t override_ram_size = 0; + + /// + /// The minimum size of large VM heaps in JavaScriptCore. + /// + /// Set this to a lower value to make these heaps start with a smaller initial value. + /// + uint32_t min_large_heap_size = 32 * 1024 * 1024; + + /// + /// The minimum size of small VM heaps in JavaScriptCore. + /// + /// Set this to a lower value to make these heaps start with a smaller initial value. + /// + uint32_t min_small_heap_size = 1 * 1024 * 1024; + + /// + /// The number of threads to use in the Renderer (for parallel painting on the CPU, etc.). + /// + /// You can set this to a certain number to limit the number of threads to spawn. + /// + /// @note + /// \parblock + /// + /// If this value is 0, the number of threads will be determined at runtime using the following + /// formula: + /// + /// ``` + /// max(PhysicalProcessorCount() - 1, 1) + /// ``` + /// + /// \endparblock + /// + uint32_t num_renderer_threads = 0; + + /// + /// The max amount of time (in seconds) to allow repeating timers to run during each call to + /// Renderer::Update. + /// + /// The library will attempt to throttle timers if this time budget is exceeded. + /// + double max_update_time = 1.0 / 200.0; + + /// + /// The alignment (in bytes) of the BitmapSurface when using the CPU renderer. + /// + /// The underlying bitmap associated with each BitmapSurface will have row_bytes padded to reach + /// this alignment. + /// + /// Aligning the bitmap helps improve performance when using the CPU renderer. Determining the + /// proper value to use depends on the CPU architecture and max SIMD instruction set used. + /// + /// We generally target the 128-bit SSE2 instruction set across most PC platforms so '16' is + /// a safe value to use. + /// + /// You can set this to '0' to perform no padding (row_bytes will always be width * 4) at a + /// slight cost to performance. + /// + uint32_t bitmap_alignment = 16; + + /// + /// The quality of effects (blurs, CSS filters, SVG filters, etc.) to use when rendering. + /// + EffectQuality effect_quality = EffectQuality::Medium; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/FileSystem.h b/include/Ultralight/platform/FileSystem.h new file mode 100644 index 0000000..bc5f1c6 --- /dev/null +++ b/include/Ultralight/platform/FileSystem.h @@ -0,0 +1,105 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// User-defined file system interface. +/// +/// The library uses this to load file data (ie, raw file bytes) for a given file URL +/// (eg, `file:///page.html`) . +/// +/// You can provide the library with your own FileSystem implementation so that file data is +/// provided directly by your application (eg, from memory, from a virtual file system, etc). +/// +/// ## Default Implementation +/// +/// A platform-specific implementation of FileSystem is provided for you when you call +/// App::Create(). +/// +/// If you are using Renderer::Create(), you **must** provide your own. You can still use AppCore's +/// implementation however-- see the helper functions defined in . +/// +/// ## Setting the File System +/// +/// To provide your own custom FileSystem implementation, you should inherit from this class, +/// handle the virtual member functions, and then pass an instance of your class to +/// Platform::set_file_system() before calling Renderer::Create() or App::Create(). +/// +class UExport FileSystem { + public: + virtual ~FileSystem(); + + /// + /// Check if a file exists within the file system. + /// + /// @param file_path Relative file path (the string following the file:/// prefix) + /// + /// @return Returns whether or not a file exists at the path specified. + /// + virtual bool FileExists(const String& file_path) = 0; + + /// + /// Get the mime-type of a file (eg "text/html"). + /// + /// This is usually determined by analyzing the file extension. + /// + /// If a mime-type cannot be determined, this should return "application/unknown". + /// + /// @param file_path Relative file path (the string following the file:/// prefix) + /// + /// @return Returns whether or not a file exists at the path specified. + /// + virtual String GetFileMimeType(const String& file_path) = 0; + + /// + /// Get the charset / encoding of a file (eg "utf-8", "iso-8859-1"). + /// + /// @note This is only applicable for text-based files (eg, "text/html", "text/plain") and is + /// usually determined by analyzing the contents of the file. + /// + /// @param file_path Relative file path (the string following the file:/// prefix) + /// + /// @return Returns the charset of the specified file. If a charset cannot be determined, a safe + /// default to return is "utf-8". + /// + virtual String GetFileCharset(const String& file_path) = 0; + + /// + /// Open a file for reading and map it to a Buffer. + /// + /// To minimize copies, you should map the requested file into memory and use Buffer::Create() + /// to wrap the data pointer (unmapping should be performed in the destruction callback). + /// + /// @note + /// \parblock + /// File data addresses returned from this function should generally be aligned to 16-byte + /// boundaries (the default alignment on most operating systems-- if you're using C stdlib or + /// C++ STL functions this is already handled for you). + /// + /// This requirement is currently necessary when loading the ICU data file (eg, icudt67l.dat), + /// and may be relaxed for other files (but you may still see a performance benefit due to cache + /// line alignment). + /// + /// If you can't guarantee alignment or are unsure, you can use Buffer::CreateFromCopy to copy + /// the file data content to an aligned block (at the expense of data duplication). + /// \endparblock + /// + /// @param file_path Relative file path (the string following the file:/// prefix) + /// + /// @return If the file was able to be opened, this returns a Buffer object representing the + /// contents of the file. If the file was unable to be opened, you should return nullptr. + /// + virtual RefPtr OpenFile(const String& file_path) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/FontLoader.h b/include/Ultralight/platform/FontLoader.h new file mode 100644 index 0000000..0fc7f37 --- /dev/null +++ b/include/Ultralight/platform/FontLoader.h @@ -0,0 +1,129 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include + +namespace ultralight { + +/// +/// Represents a font file, either on-disk path or in-memory file contents. +/// +class UExport FontFile : public RefCounted { + public: + /// + /// Create a font file from an on-disk file path. + /// + /// @note The file path should already exist. + /// + static RefPtr Create(const String& filepath); + + /// + /// Create a font file from an in-memory buffer. + /// + static RefPtr Create(RefPtr buffer); + + /// + /// Whether or not this font file was created from an in-memory buffer. + /// + virtual bool is_in_memory() const = 0; + + /// + /// The file path (if any). + /// + virtual String filepath() const = 0; + + /// + /// The in-memory buffer (if any). + /// + virtual RefPtr buffer() const = 0; + + /// + /// Unique hash (if this is a filepath, only the path string is hashed). + /// + virtual uint32_t hash() const = 0; + + protected: + FontFile(); + virtual ~FontFile(); + FontFile(const FontFile&); + void operator=(const FontFile&); +}; + +/// +/// User-defined font loader interface. +/// +/// The library uses this to load a font file (eg, `Arial.ttf`) for a given font description (eg, +/// `font-family: Arial;`). +/// +/// Every OS has its own library of installed system fonts. The FontLoader interface is used to +/// lookup these fonts and fetch the actual font data (raw TTF/OTF file data) for a given font +/// description. +/// +/// You can provide the library with your own font loader implementation so that you can bundle +/// fonts with your application rather than relying on the system's installed fonts. +/// +/// ## Default Implementation +/// +/// A platform-specific implementation of FontLoader is provided for you when you call +/// App::Create(). +/// +/// If you are using Renderer::Create(), you **must** provide your own. You can still use AppCore's +/// implementation however-- see the helper functions defined in . +/// +/// ## Setting the Font Loader +/// +/// To provide your own custom FontLoader implementation, you should inherit from this class, +/// handle the virtual member functions, and then pass an instance of your class to +/// Platform::set_font_loader() before calling Renderer::Create() or App::Create(). +/// +class UExport FontLoader { + public: + virtual ~FontLoader(); + + /// + /// Fallback font family name. Will be used if all other fonts fail to load. + /// + /// @note This font should be guaranteed to exist (eg, FontLoader::Load won't fail when passed + /// this font family name). + /// + virtual String fallback_font() const = 0; + + /// + /// Fallback font family name that can render the specified characters. Mainly used to support + /// CJK (Chinese, Japanese, Korean) text display. + /// + /// @param characters One or more UTF-16 characters. This is almost always a single character. + /// + /// @param weight Font weight. + /// + /// @param italic Whether or not italic is requested. + /// + /// @return Returns a font family name that can render the text. + /// + virtual String fallback_font_for_characters(const String& characters, int weight, + bool italic) const = 0; + + /// + /// Get the actual font file data (TTF/OTF) for a given font description. + /// + /// @param family Font family name. + /// + /// @param weight Font weight. + /// + /// @param italic Whether or not italic is requested. + /// + /// @return A font file matching the given description (either an on-disk font filepath or an + /// in-memory file contents). You can return NULL here and the loader will fallback to + /// another font. + /// + virtual RefPtr Load(const String& family, int weight, bool italic) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/GPUDriver.h b/include/Ultralight/platform/GPUDriver.h new file mode 100644 index 0000000..728f8a3 --- /dev/null +++ b/include/Ultralight/platform/GPUDriver.h @@ -0,0 +1,436 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ + +// clang-format off +#pragma once +#pragma warning(disable : 4251) +#include +#include +#include +#include + +namespace ultralight { + +/// \cond ignore +/// This pragma pack(push, 1) command is important! +/// GPU structs should not be padded with any bytes. +/// \endcond +#pragma pack(push, 1) + +/// +/// Render buffer description. +/// +/// This structure describes a render buffer that can be used as a target for drawing commands. +/// +/// @see GPUDriver::CreateRenderBuffer. +/// +struct UExport RenderBuffer { + uint32_t texture_id; ///< The backing texture for this RenderBuffer + uint32_t width; ///< The width of the RenderBuffer texture + uint32_t height; ///< The height of the RenderBuffer texture + bool has_stencil_buffer; ///< Currently unused, always false. + bool has_depth_buffer; ///< Currently unsued, always false. +}; + +/// +/// Vertex layout for path vertices. +/// +/// This struct is the in-memory layout for each path vertex (useful for synthesizing or modifying +/// your own vertex data). +/// +struct Vertex_2f_4ub_2f { + float pos[2]; + unsigned char color[4]; + float obj[2]; +}; + +/// +/// Vertex layout for quad vertices. +/// +/// This struct is the in-memory layout for each quad vertex (useful for synthesizing or modifying +/// your own vertex data). +/// +struct Vertex_2f_4ub_2f_2f_28f { + float pos[2]; + unsigned char color[4]; + float tex[2]; + float obj[2]; + float data0[4]; + float data1[4]; + float data2[4]; + float data3[4]; + float data4[4]; + float data5[4]; + float data6[4]; +}; + +/// +/// Vertex buffer formats. +/// +/// This enumeration describes the format of a vertex buffer. +/// +/// @note Identifiers start with an underscore due to C++ naming rules. +/// +/// @see VertexBuffer +/// +enum class VertexBufferFormat : uint8_t { + _2f_4ub_2f, ///< Vertex_2f_4ub_2f (used for path rendering) + _2f_4ub_2f_2f_28f, ///< Vertex_2f_4ub_2f_2f_28f (used for quad rendering) +}; + +/// +/// Vertex buffer description. +/// +/// @see GPUDriver::CreateGeometry +/// +struct UExport VertexBuffer { + VertexBufferFormat format; ///< The format of the vertex buffer. + uint32_t size; ///< The size of the vertex buffer in bytes. + uint8_t* data; ///< The raw vertex buffer data. +}; + +/// +/// Vertex index type. +/// +typedef uint32_t IndexType; + +/// +/// Index buffer description. +/// +/// This structure describes an index buffer that can be used to index into a vertex buffer. +/// +/// @note The index buffer is a simple array of IndexType values. +/// +/// @see GPUDriver::CreateGeometry +/// +struct UExport IndexBuffer { + uint32_t size; ///< The size of the index buffer in bytes. + uint8_t* data; ///< The raw index buffer data. +}; + +/// +/// Shader program types. +/// +/// Each of these correspond to a vertex/pixel shader pair. You can find stock shader code for +/// these in the `shaders` folder of the AppCore repo. +/// +/// @see GPUState::shader_type +/// +enum class ShaderType : uint8_t { + Fill, ///< Shader program for filling quad geometry. + FillPath, ///< Shader program for filling tesselated path geometry. +}; + +/// +/// The state of the GPU for a given draw command. +/// +/// This structure describes the current state of the GPU for a given draw command. +/// +/// @see Command::gpu_state +/// +struct UExport GPUState { + /// Viewport width in pixels + uint32_t viewport_width; + + /// Viewport height in pixels + uint32_t viewport_height; + + /// Transform matrix-- you should multiply this with the screen-space orthographic projection + /// matrix then pass to the vertex shader. + Matrix4x4 transform; + + /// Whether or not we should enable texturing for the current draw command. + bool enable_texturing; + + /// Whether or not we should enable blending for the current draw command. If blending is + /// disabled, any drawn pixels should overwrite existing. This is mainly used so we can modify + /// alpha values of the RenderBuffer during scissored clears. + bool enable_blend; + + /// The vertex/pixel shader program pair to use for the current draw command. + ShaderType shader_type; + + /// The render buffer to use for the current draw command. + uint32_t render_buffer_id; + + /// The texture id to bind to slot #1. (Will be 0 if none) + uint32_t texture_1_id; + + /// The texture id to bind to slot #2. (Will be 0 if none) + uint32_t texture_2_id; + + /// The texture id to bind to slot #3. (Will be 0 if none) + uint32_t texture_3_id; + + /// The uniform scalars (passed to the pixel shader via uniforms). + float uniform_scalar[8]; + + /// The uniform vectors (passed to the pixel shader via uniforms). + vec4 uniform_vector[8]; + + /// The clip size (passed to the pixel shader via uniforms). + uint8_t clip_size; + + /// The clip stack (passed to the pixel shader via uniforms). + Matrix4x4 clip[8]; + + /// Whether or not scissor testing should be used for the current draw command. + bool enable_scissor; + + /// The scissor rect to use for scissor testing (units in pixels) + IntRect scissor_rect; +}; + +/// +/// The types of commands. +/// +/// This enumeration describes the type of command to execute on the GPU. +/// +/// @see Command +/// +enum class CommandType : uint8_t { + ClearRenderBuffer, ///< Clear the specified render buffer. + DrawGeometry, ///< Draw the specified geometry to the specified render buffer. +}; + +/// +/// A command to execute on the GPU. +/// +/// This structure describes a command to be executed on the GPU. +/// +/// Commands are dispatched to the GPU driver asynchronously via GPUDriver::UpdateCommandList(), +/// the GPU driver should consume these commands and execute them at an appropriate time. +/// +/// @see CommandList +/// +struct UExport Command { + CommandType command_type; ///< The type of command to dispatch. + GPUState gpu_state; ///< The current GPU state. + uint32_t geometry_id; ///< The geometry ID to bind. (used with CommandType::DrawGeometry) + uint32_t indices_count; ///< The number of indices. (used with CommandType::DrawGeometry) + uint32_t indices_offset; ///< The index to start from. (used with CommandType::DrawGeometry) +}; + +/// +/// List of commands to execute on the GPU. +/// +/// @see GPUDriver::UpdateCommandList +/// +struct UExport CommandList { + uint32_t size; ///< The number of commands in the list. + Command* commands; ///< The raw command list data. +}; + +#pragma pack(pop) + +/// +/// User-defined GPU driver interface. +/// +/// The library uses this to optionally render Views on the GPU (see ViewConfig::is_accelerated). +/// +/// You can provide the library with your own GPU driver implementation so that all rendering is +/// performed using an existing GPU context (useful for game engines). +/// +/// When a View is rendered on the GPU, you can retrieve the backing texture ID via +/// View::render_target(). +/// +/// ## Default Implementation +/// +/// A platform-specific implementation of GPUDriver is provided for you when you call App::Create(), +/// (currently D3D11, Metal, and OpenGL). We recommend using these classes as a starting point for +/// your own implementation (available open-source in the AppCore repository on GitHub). +/// +/// ## Setting the GPU Driver +/// +/// When using Renderer::Create(), you can provide your own implementation of this +/// class via Platform::set_gpu_driver(). +/// +/// ## State Synchronization +/// +/// During each call to Renderer::Render(), the library will update the state of the GPU driver +/// (textures, render buffers, geometry, command lists, etc.) to match the current state of the +/// library. +/// +/// ### Detecting State Changes +/// +/// The library will call BeginSynchronize() before any state is updated and EndSynchronize() after +/// all state is updated. All `Create` / `Update` / `Destroy` calls will be made between these two +/// calls. +/// +/// This allows the GPU driver implementation to prepare the GPU for any state changes. +/// +/// ## Drawing +/// +/// All drawing is done via command lists (UpdateCommandList()) to allow asynchronous execution +/// of commands on the GPU. +/// +/// The library will dispatch a list of commands to the GPU driver during state synchronization. The +/// GPU driver implementation should periodically consume the command list and execute the commands +/// at an appropriate time. +/// +/// @see Platform::set_gpu_driver() +/// +class UExport GPUDriver { + public: + virtual ~GPUDriver(); + + /// + /// Called before any state (eg, CreateTexture(), UpdateTexture(), DestroyTexture(), etc.) is + /// updated during a call to Renderer::Render(). + /// + /// This is a good time to prepare the GPU for any state updates. + /// + virtual void BeginSynchronize() = 0; + + /// + /// Called after all state has been updated during a call to Renderer::Render(). + /// + virtual void EndSynchronize() = 0; + + /// + /// Get the next available texture ID. + /// + /// This is used to generate a unique texture ID for each texture created by the library. The + /// GPU driver implementation is responsible for mapping these IDs to a native ID. + /// + /// @note Numbering should start at 1, 0 is reserved for "no texture". + /// + /// @return Returns the next available texture ID. + /// + virtual uint32_t NextTextureId() = 0; + + /// + /// Create a texture with a certain ID and optional bitmap. + /// + /// @param texture_id The texture ID to use for the new texture. + /// + /// @param bitmap The bitmap to initialize the texture with (can be empty). + /// + /// @note If the Bitmap is empty (Bitmap::IsEmpty), then a RTT Texture should be created instead. + /// This will be used as a backing texture for a new RenderBuffer. + /// + /// @warning A deep copy of the bitmap data should be made if you are uploading it to the GPU + /// asynchronously, it will not persist beyond this call. + /// + virtual void CreateTexture(uint32_t texture_id, RefPtr bitmap) = 0; + + /// + /// Update an existing non-RTT texture with new bitmap data. + /// + /// @param texture_id The texture to update. + /// + /// @param bitmap The new bitmap data. + /// + /// @warning A deep copy of the bitmap data should be made if you are uploading it to the GPU + /// asynchronously, it will not persist beyond this call. + /// + virtual void UpdateTexture(uint32_t texture_id, RefPtr bitmap) = 0; + + /// + /// Destroy a texture. + /// + /// @param texture_id The texture to destroy. + /// + virtual void DestroyTexture(uint32_t texture_id) = 0; + + /// + /// Get the next available render buffer ID. + /// + /// This is used to generate a unique render buffer ID for each render buffer created by the + /// library. The GPU driver implementation is responsible for mapping these IDs to a native ID. + /// + /// @note Numbering should start at 1, 0 is reserved for "no render buffer". + /// + /// @return Returns the next available render buffer ID. + /// + virtual uint32_t NextRenderBufferId() = 0; + + /// + /// Create a render buffer with certain ID and buffer description. + /// + /// @param render_buffer_id The render buffer ID to use for the new render buffer. + /// + /// @param buffer The render buffer description. + /// + virtual void CreateRenderBuffer(uint32_t render_buffer_id, const RenderBuffer& buffer) = 0; + + /// + /// Destroy a render buffer. + /// + /// @param render_buffer_id The render buffer to destroy. + /// + virtual void DestroyRenderBuffer(uint32_t render_buffer_id) = 0; + + /// + /// Get the next available geometry ID. + /// + /// This is used to generate a unique geometry ID for each geometry created by the library. The + /// GPU driver implementation is responsible for mapping these IDs to a native ID. + /// + /// @note Numbering should start at 1, 0 is reserved for "no geometry". + /// + /// @return Returns the next available geometry ID. + /// + virtual uint32_t NextGeometryId() = 0; + + /// + /// Create geometry with certain ID and vertex/index data. + /// + /// @param geometry_id The geometry ID to use for the new geometry. + /// + /// @param vertices The vertex buffer data. + /// + /// @param indices The index buffer data. + /// + /// @warning A deep copy of the vertex/index data should be made if you are uploading it to the + /// GPU asynchronously, it will not persist beyond this call. + /// + virtual void CreateGeometry(uint32_t geometry_id, const VertexBuffer& vertices, + const IndexBuffer& indices) + = 0; + + /// + /// Update existing geometry with new vertex/index data. + /// + /// @param geometry_id The geometry to update. + /// + /// @param vertices The new vertex buffer data. + /// + /// @param indices The new index buffer data. + /// + /// @warning A deep copy of the vertex/index data should be made if you are uploading it to the + /// GPU asynchronously, it will not persist beyond this call. + /// + virtual void UpdateGeometry(uint32_t geometry_id, const VertexBuffer& vertices, + const IndexBuffer& indices) + = 0; + + /// + /// Destroy geometry. + /// + /// @param geometry_id The geometry to destroy. + /// + virtual void DestroyGeometry(uint32_t geometry_id) = 0; + + /// + /// Update the pending command list with commands to execute on the GPU. + /// + /// Commands are dispatched to the GPU driver asynchronously via this method. The GPU driver + /// implementation should consume these commands and execute them at an appropriate time. + /// + /// @param list The list of commands to execute. + /// + /// @warning Implementations should make a deep copy of the command list, it will not persist + /// beyond this call. + /// + virtual void UpdateCommandList(const CommandList& list) = 0; +}; + +} // namespace ultralight + +// clang-format on \ No newline at end of file diff --git a/include/Ultralight/platform/Logger.h b/include/Ultralight/platform/Logger.h new file mode 100644 index 0000000..568184c --- /dev/null +++ b/include/Ultralight/platform/Logger.h @@ -0,0 +1,42 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include + +namespace ultralight { + +/// +/// Log levels, used with Logger::LogMessage +/// +enum class LogLevel : uint8_t { + Error, + Warning, + Info +}; + +/// +/// User-defined logging interface. +/// +/// The library uses this to display log messages for debugging during development. +/// +/// This is intended to be implemented by users and defined before creating the Renderer. +/// +/// @see Platform::set_logger() +/// +class UExport Logger { +public: + virtual ~Logger(); + + /// + /// Called when the library wants to display a log message. + /// + virtual void LogMessage(LogLevel log_level, const String& message) = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/Platform.h b/include/Ultralight/platform/Platform.h new file mode 100644 index 0000000..8d89af2 --- /dev/null +++ b/include/Ultralight/platform/Platform.h @@ -0,0 +1,182 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include + +namespace ultralight { + +struct Config; +class Logger; +class GPUDriver; +class FontLoader; +class FileSystem; +class Clipboard; +class SurfaceFactory; +class ThreadFactory; + +/// +/// Global platform singleton, manages user-defined platform handlers and global config. +/// +/// The library uses the Platform API for most platform-specific operations (eg, file access, +/// clipboard, font loading, GPU access, pixel buffer transport, etc.). +/// +/// ## Motivation +/// +/// Ultralight is designed to work in as many platforms and environments as possible. To achieve +/// this, we've factored out most platform-specific code into a set of interfaces that you can +/// implement and set on the Platform singleton. +/// +/// ## Default Implementations +/// +/// We provide a number of default implementations for desktop platforms (eg, Windows, macOS, Linux) +/// for you when you call App::Create(). These implementations are defined in the +/// [AppCore repository](https://github.com/ultralight-ux/AppCore/tree/master/src), we recommend +/// using their source code as a starting point for your own implementations. +/// +/// ## Required Handlers +/// +/// When using Renderer::Create() directly, you'll need to provide your own implementations for +/// FileSystem and FontLoader at a minimum. +/// +/// @par Overview of which platform handlers are required / optional / provided: +/// +/// | | Renderer::Create() | App::Create() | +/// |----------------|--------------------|---------------| +/// | FileSystem | **Required** | *Provided* | +/// | FontLoader | **Required** | *Provided* | +/// | Clipboard | *Optional* | *Provided* | +/// | GPUDriver | *Optional* | *Provided* | +/// | Logger | *Optional* | *Provided* | +/// | SurfaceFactory | *Provided* | *Provided* | +/// | ThreadFactory | *Optional* | *Optional* | +/// +/// @note This singleton should be set up before creating the Renderer or App. +/// +class UExport Platform { + public: + /// + /// Get the Platform singleton + /// + static Platform& instance(); + + virtual ~Platform(); + + /// + /// Set the Config + /// + virtual void set_config(const Config& config) = 0; + + /// + /// Get the Config + /// + virtual const Config& config() const = 0; + + /// + /// Set the Logger (to handle error messages and debug output). + /// + /// @param logger A user-defined Logger implementation, ownership remains with the caller. + /// + virtual void set_logger(Logger* logger) = 0; + + /// + /// Get the Logger + /// + virtual Logger* logger() const = 0; + + /// + /// Set the GPU Driver (will handle all rendering) + /// + /// @param gpu_driver A user-defined GPUDriver implementation, ownership remains with the + /// caller. + /// + virtual void set_gpu_driver(GPUDriver* gpu_driver) = 0; + + /// + /// Get the GPU Driver + /// + virtual GPUDriver* gpu_driver() const = 0; + + /// + /// Set the Font Loader (will be used to map font families to actual fonts) + /// + /// @param font_loader A user-defined FontLoader implementation, ownership remains with the + /// caller. + /// + virtual void set_font_loader(FontLoader* font_loader) = 0; + + /// + /// Get the Font Loader + /// + virtual FontLoader* font_loader() const = 0; + + /// + /// Set the File System (will be used for all file system operations) + /// + /// @param file_system A user-defined FileSystem implementation, ownership remains with the + /// caller. + /// + virtual void set_file_system(FileSystem* file_system) = 0; + + /// + /// Get the File System + /// + virtual FileSystem* file_system() const = 0; + + /// + /// Set the Clipboard (will be used for all clipboard operations) + /// + /// @param clipboard A user-defined Clipboard implementation, ownership remains with the + /// caller. + /// + virtual void set_clipboard(Clipboard* clipboard) = 0; + + /// + /// Get the Clipboard + /// + virtual Clipboard* clipboard() const = 0; + + /// + /// Set the SurfaceFactory + /// + /// This can be used to provide a platform-specific bitmap surface for View to paint into when + /// the CPU renderer is enabled. See View::surface(). + /// + /// @param surface_factory A user-defined SurfaceFactory implementation, ownership remains with + /// the caller. + /// + /// @note A default BitmapSurfaceFactory is defined if you never call this, View::surface() can + /// be safely cast to BitmapSurface. + /// + virtual void set_surface_factory(SurfaceFactory* surface_factory) = 0; + + /// + /// Get the SurfaceFactory + /// + /// @note A default BitmapSurfaceFactory is set by default, View::surface() can be safely cast + /// to BitmapSurface if you don't define your own. + /// + virtual SurfaceFactory* surface_factory() const = 0; + + /// + /// Set the ThreadFactory + /// + /// This can be used to provide a platform-specific ThreadFactory implementation for the library + /// to use when creating threads. + /// + /// @param thread_factory A user-defined ThreadFactory implementation, ownership remains with the + /// caller. + /// + virtual void set_thread_factory(ThreadFactory* thread_factory) = 0; + + /// + /// Get the ThreadFactory + /// + virtual ThreadFactory* thread_factory() const = 0; +}; + +} // namespace ultralight diff --git a/include/Ultralight/platform/Surface.h b/include/Ultralight/platform/Surface.h new file mode 100644 index 0000000..61db290 --- /dev/null +++ b/include/Ultralight/platform/Surface.h @@ -0,0 +1,204 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include +#include +#include +#include + +namespace ultralight { + +/// +/// User-defined pixel buffer surface. +/// +/// The library uses this to store pixel data when rendering Views on the CPU (see +/// ViewConfig::is_accelerated). +/// +/// You can provide the library with your own Surface implementation to reduce the latency of +/// displaying pixels in your application (Views will be drawn directly to a block of memory +/// controlled by you). +/// +/// When a View is rendered on the CPU, you can retrieve the backing Surface via View::surface(). +/// +/// @pre This is automatically managed for you when using App::Create(), if you want to override +/// Surface or SurfaceFactory, you'll need to use Renderer::Create() instead. +/// +/// ## Default Implementation +/// +/// A default Surface implementation, BitmapSurface, is automatically provided by the library when +/// you call Renderer::Create() without defining a custom SurfaceFactory. +/// +/// You should cast the Surface to a BitmapSurface to access the underlying Bitmap. +/// +/// ## Setting the Surface Implementation +/// +/// To define your own implementation, you should inherit from this class, handle the virtual +/// member functions, and then define a custom SurfaceFactory that creates/destroys an +/// instance of your class. +/// +/// After that, you should pass an instance of your custom SurfaceFactory class to +/// Platform::set_surface_factory() before calling Renderer::Create(). +/// +class UExport Surface { + public: + virtual ~Surface(); + + /// + /// Width (in pixels). + /// + virtual uint32_t width() const = 0; + + /// + /// Height (in pixels). + /// + virtual uint32_t height() const = 0; + + /// + /// Number of bytes between rows (usually width * 4) + /// + virtual uint32_t row_bytes() const = 0; + + /// + /// Size in bytes. + /// + virtual size_t size() const = 0; + + /// + /// Lock the pixel buffer and get a pointer to the beginning of the data for reading/writing. + /// + /// @note Native pixel format is premultiplied BGRA 32-bit (8 bits per channel). + /// + virtual void* LockPixels() = 0; + + /// + /// Unlock the pixel buffer. + /// + virtual void UnlockPixels() = 0; + + /// + /// Resize the pixel buffer to a certain width and height (both in pixels). + /// + /// This should never be called while pixels are locked. + /// + virtual void Resize(uint32_t width, uint32_t height) = 0; + + /// + /// Set the dirty bounds to a certain value. + /// + /// This is called after the Renderer paints to an area of the pixel buffer. (The new value will + /// be joined with the existing dirty_bounds()) + /// + virtual void set_dirty_bounds(const IntRect& bounds); + + /// + /// Get the dirty bounds. + /// + /// This value can be used to determine which portion of the pixel buffer has been updated since + /// the last call to ClearDirtyBounds(). + /// + /// The general algorithm to determine if a Surface needs display is: + ///
+  ///   if (!surface.dirty_bounds().IsEmpty()) {
+  ///       // Surface pixels are dirty and needs display.
+  ///       // Cast Surface to native Surface and use it here (pseudo code)
+  ///       DisplaySurface(surface);
+  ///
+  ///       // Once you're done, clear the dirty bounds:
+  ///       surface.ClearDirtyBounds();
+  ///  }
+  ///  
+ /// + virtual IntRect dirty_bounds() const; + + /// + /// Clear the dirty bounds. + /// + /// You should call this after you're done displaying the Surface. + /// + virtual void ClearDirtyBounds(); + + protected: + Surface(); + + IntRect dirty_bounds_; +}; + +/// +/// User-defined factory to provide your own surface implementation. +/// +/// The library uses this to create/destroy Surface instances when rendering Views on the CPU. +/// +/// @pre This is automatically managed for you when using App::Create(), if you want to override +/// Surface or SurfaceFactory, you'll need to use Renderer::Create() instead. +/// +/// ## Setting the Surface Factory +/// +/// The default factory creates/destroys a BitmapSurface but you can override this by providing your +/// own factory to Platform::set_surface_factory(). +/// +class UExport SurfaceFactory { + public: + virtual ~SurfaceFactory(); + + /// + /// Create a native Surface with a certain width and height (in pixels). + /// + virtual Surface* CreateSurface(uint32_t width, uint32_t height) = 0; + + /// + /// Destroy a native Surface previously created by CreateSurface(). + /// + virtual void DestroySurface(Surface* surface) = 0; +}; + +/// +/// The default surface implementation, backed by a bitmap. +/// +/// This is automatically provided by the library when you call Renderer::Create() without defining +/// a custom SurfaceFactory. +/// +/// This implementation uses a Bitmap to store pixel data (retrieve it via BitmapSurface::bitmap()). +/// +class UExport BitmapSurface : public Surface { + public: + virtual uint32_t width() const override; + + virtual uint32_t height() const override; + + virtual uint32_t row_bytes() const override; + + virtual size_t size() const override; + + virtual void* LockPixels() override; + + virtual void UnlockPixels() override; + + virtual void Resize(uint32_t width, uint32_t height) override; + + /// + /// Get the underlying Bitmap. + /// + RefPtr bitmap(); + + protected: + BitmapSurface(uint32_t width, uint32_t height); + virtual ~BitmapSurface(); + BitmapSurface(const BitmapSurface&) = delete; + void operator=(const BitmapSurface&) = delete; + friend class BitmapSurfaceFactory; + + void* impl_; +}; + +/// +/// Get the default Bitmap Surface Factory singleton. (Do not destroy this, this singleton is owned +/// by the library). +/// +UExport SurfaceFactory* GetBitmapSurfaceFactory(); + +} // namespace ultralight diff --git a/include/Ultralight/platform/Thread.h b/include/Ultralight/platform/Thread.h new file mode 100644 index 0000000..0616ce3 --- /dev/null +++ b/include/Ultralight/platform/Thread.h @@ -0,0 +1,93 @@ +/************************************************************************************************** + * This file is a part of Ultralight. * + * * + * See for licensing and more. * + * * + * (C) 2024 Ultralight, Inc. * + **************************************************************************************************/ +#pragma once +#include + +namespace ultralight { + +/// +/// Unique id of the thread, used for referencing the created thread later. +/// * on Windows this should match the thread identifier returned by either _beginthreadex() +/// or GetCurrentThreadId() +/// * on POSIX this can be whatever unique id you want +/// +typedef uint32_t ThreadId; + +/// +/// Platform-specific handle +/// * on Windows this is HANDLE +/// * on POSIX this is pthread_t +/// +typedef uint64_t ThreadHandle; + +/// +/// Entry point for the thread, this function should be called by the thread once it is active +/// and should be passed entry_point_data as the argument. +/// +typedef void (*ThreadEntryPoint)(void*); + +/// +/// The type of thread, you can choose to optionally handle these for better performance. +/// +enum class ThreadType : uint8_t { + Unknown = 0, + JavaScript, + Compiler, + GarbageCollection, + Network, + Graphics, + Audio, +}; + +/// +/// Result of creating a new thread. +/// +/// This struct is used to return the id and handle of the created thread. +/// +struct UExport CreateThreadResult { + ThreadId id; ///< The unique id of the thread. @see ThreadId + ThreadHandle handle; ///< The platform-specific handle of the thread. @see ThreadHandle +}; + +/// +/// User-defined factory for creating new threads. +/// +/// You can implement this interface so that the library will use your own implementation for +/// creating threads (useful for tracking thread creation, setting thread names, etc). +/// +/// ## Default Implementation +/// +/// When no factory is defined, the library will create threads using the default platform-specific +/// thread creation functions (eg, `_beginthreadex()` on Windows, `pthread_create()` on POSIX). +/// +/// ## Setting the Thread Factory +/// +/// To provide your own custom ThreadFactory implementation, you should inherit from this class, +/// handle the virtual member functions, and then pass an instance to +/// Platform::set_thread_factory(). +/// +class UExport ThreadFactory { + public: + virtual ~ThreadFactory() = default; + + /// + /// Create a new thread. + /// + /// @param name The name of the thread (can be nullptr). + /// @param type The type of thread. + /// @param entry_point The entry point for the thread. + /// @param entry_point_data The data to pass to the entry point. + /// @param result The resulting id and handle of the thread creation. + /// + /// @return Returns whether or not the thread was created successfully. + /// + virtual bool CreateThread(const char* name, ThreadType type, ThreadEntryPoint entry_point, + void* entry_point_data, CreateThreadResult& result) = 0; +}; + +} // namespace ultralight diff --git a/inspector/Base/BlobUtilities.js b/inspector/Base/BlobUtilities.js new file mode 100644 index 0000000..da6f055 --- /dev/null +++ b/inspector/Base/BlobUtilities.js @@ -0,0 +1,70 @@ +/* + * 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); + } +}; diff --git a/inspector/Base/BrowserInspectorFrontendHost.js b/inspector/Base/BrowserInspectorFrontendHost.js new file mode 100644 index 0000000..4986709 --- /dev/null +++ b/inspector/Base/BrowserInspectorFrontendHost.js @@ -0,0 +1,379 @@ +/* + * Copyright (C) 2022 Igalia S.L. + * + * 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. + */ + +if (!window.InspectorFrontendHost) { + WI.BrowserInspectorFrontendHost = class BrowserInspectorFrontendHost { + + constructor() + { + this._pendingMessages = null; + this._socket = null; + } + + // Public + + get supportsShowCertificate() + { + return false; + } + + get isRemote() + { + return true; + } + + get inspectionLevel() + { + return 1; + } + + get debuggableInfo() + { + return { + debuggableType: "web-page", + targetPlatformName: undefined, + targetBuildVersion: undefined, + targetProductVersion: undefined, + targetIsSimulator: false, + }; + } + + get platform() + { + const match = navigator.platform.match(/mac|win|linux/i); + if (match) { + const platform = match[0].toLowerCase(); + if (platform == "win") + return "windows"; + return platform; + } + return "unknown"; + } + + get platformVersionName() + { + return ""; + } + + get supportsDiagnosticLogging() + { + return false; + } + + get supportsWebExtensions() + { + return false; + } + + connect() + { + const queryParams = parseQueryString(window.location.search.substring(1)); + let url = "ws" in queryParams ? "ws://" + queryParams.ws : null; + if (!url) + return; + + const socket = new WebSocket(url); + socket.addEventListener("message", message => InspectorBackend.dispatch(message.data)); + socket.addEventListener("error", console.error); + socket.addEventListener("open", () => { this._socket = socket; }); + socket.addEventListener("close", () => { + this._socket = null; + window.close(); + }); + } + + loaded() + { + WI.updateVisibilityState(true); + } + + closeWindow() + { + this._windowVisible = false; + } + + reopen() + { + window.location.reload(); + } + + reset() + { + this.reopen(); + } + + bringToFront() + { + this._windowVisible = true; + } + + inspectedURLChanged(title) + { + document.title = title; + } + + showCertificate(certificate) + { + throw "unimplemented"; + } + + setZoomFactor(zoom) + { + } + + zoomFactor() + { + return 1; + } + + setForcedAppearance(appearance) + { + } + + userInterfaceLayoutDirection() + { + return "ltr"; + } + + supportsDockSide(side) + { + return false; + } + + requestDockSide(side) + { + throw "unimplemented"; + } + + setAttachedWindowHeight(height) + { + } + + setAttachedWindowWidth(width) + { + } + + setSheetRect(x, y, width, height) + { + } + + startWindowDrag() + { + } + + moveWindowBy(x, y) + { + } + + copyText(text) + { + this.killText(text, false, true); + } + + killText(text, shouldPrependToKillRing, shouldStartNewSequence) + { + // FIXME: restore focus to previously focused element. + let textarea = document.createElement("textarea"); + document.body.appendChild(textarea); + + if (shouldStartNewSequence) { + textarea.textContent = text; + } else { + textarea.select(); + if (!document.execCommand("paste")) + console.error("BrowserInspectorFrontendHost.killText: could not paste from clipboard"); + + if (shouldPrependToKillRing) + textarea.textContent = text + textarea.textContent; + else + textarea.textContent = textarea.textContent + text; + } + + textarea.select(); + + if (!document.execCommand("copy")) + console.error("BrowserInspectorFrontendHost.copyText: could not copy to clipboard"); + + document.body.removeChild(textarea); + } + + openURLExternally(url) + { + window.open(url, "_blank"); + } + + canSave(saveMode) + { + return false; + } + + save(saveDatas, forceSaveAs) + { + // FIXME: Create a Blob from the content, get an object URL, open it to trigger a download. + throw "unimplemented"; + } + + canLoad() + { + return false; + } + + load(path) + { + throw "unimplemented"; + } + + getPath(file) + { + return null; + } + + canPickColorFromScreen() + { + return false; + } + + pickColorFromScreen() + { + throw "unimplemented"; + } + + revealFileExternally(path) + { + } + + getCurrentX(context) + { + return 0.0; + } + + getCurrentY(context) + { + return 0.0; + } + + setPath(context, path2d) + { + console.log("setPath", context, path2d); + } + + showContextMenu(event, items) + { + this._contextMenu = WI.SoftContextMenu(items); + this._contextMenu.show(event); + } + + dispatchEventAsContextMenuEvent(event) + { + if (this._contextMenu) + this._contextMenu.show(event); + } + + sendMessageToBackend(message) + { + if (!this._socket) { + if (!this._pendingMessages) + this._pendingMessages = []; + this._pendingMessages.push(message); + } else { + this._sendPendingMessagesToBackendIfNeeded(); + this._socket.send(message); + } + } + + unbufferedLog(message) + { + console.log(message); + } + + isUnderTest() + { + return false; + } + + beep() + { + // FIXME: Implement using Audio/AudioContext. + } + + inspectInspector() + { + throw "unimplemented"; + } + + isBeingInspected() + { + return false; + } + + setAllowsInspectingInspector(allow) + { + } + + logDiagnosticEvent(eventName, content) + { + throw "unimplemented"; + } + + didShowExtensionTab(extensionID, extensionTabID, extensionFrame) + { + throw "unimplemented"; + } + + didHideExtensionTab(extensionID, extensionTabID) + { + throw "unimplemented"; + } + + didNavigateExtensionTab(extensionID, extensionTabID, newURL) + { + throw "unimplemented"; + } + + inspectedPageDidNavigate(newURL) + { + throw "unimplemented"; + } + + evaluateScriptInExtensionTab(extensionFrame, scriptSource) + { + throw "unimplemented"; + } + + // Private + + _sendPendingMessagesToBackendIfNeeded() + { + if (this._pendingMessages) { + this._pendingMessages.forEach(message => this._socket.send(message)); + this._pendingMessages = null; + } + } + }; + + InspectorFrontendHost = new WI.BrowserInspectorFrontendHost(); + + WI.dontLocalizeUserInterface = true; +} diff --git a/inspector/Base/DOMUtilities.js b/inspector/Base/DOMUtilities.js new file mode 100644 index 0000000..c983a14 --- /dev/null +++ b/inspector/Base/DOMUtilities.js @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2011 Google Inc. All rights reserved. + * Copyright (C) 2007, 2008, 2013 Apple Inc. All rights reserved. + * Copyright (C) 2008 Matt Lilek + * 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; +}; diff --git a/inspector/Base/Debouncer.js b/inspector/Base/Debouncer.js new file mode 100644 index 0000000..fe77b52 --- /dev/null +++ b/inspector/Base/Debouncer.js @@ -0,0 +1,134 @@ +/* + * 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); + } +} diff --git a/inspector/Base/DebuggableType.js b/inspector/Base/DebuggableType.js new file mode 100644 index 0000000..6f8d29a --- /dev/null +++ b/inspector/Base/DebuggableType.js @@ -0,0 +1,82 @@ +/* + * 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; +}; diff --git a/inspector/Base/EventListener.js b/inspector/Base/EventListener.js new file mode 100644 index 0000000..2519551 --- /dev/null +++ b/inspector/Base/EventListener.js @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2014, 2015 Apple Inc. All rights reserved. + * Copyright (C) 2013, 2014 University of Washington. 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.EventListener = class EventListener +{ + constructor(thisObject, fireOnce) + { + this._thisObject = thisObject; + this._emitter = null; + this._callback = null; + this._fireOnce = fireOnce; + } + + // Public + + connect(emitter, type, callback, usesCapture) + { + console.assert(!this._emitter && !this._callback, "EventListener already bound to a callback.", this); + console.assert(emitter, `Missing event emitter for event: ${type}.`); + console.assert(type, "Missing event type."); + console.assert(callback, `Missing callback for event: ${type}.`); + var emitterIsValid = emitter && (emitter instanceof WI.Object || emitter instanceof Node || (typeof emitter.addEventListener === "function")); + console.assert(emitterIsValid, "Event emitter ", emitter, ` (type: ${type}) is null or does not implement Node or WI.Object.`); + + if (!emitterIsValid || !type || !callback) + return; + + this._emitter = emitter; + this._type = type; + this._usesCapture = !!usesCapture; + + if (emitter instanceof Node) + callback = callback.bind(this._thisObject); + + if (this._fireOnce) { + var listener = this; + this._callback = function() { + listener.disconnect(); + callback.apply(this, arguments); + }; + } else + this._callback = callback; + + if (this._emitter instanceof Node) + this._emitter.addEventListener(this._type, this._callback, this._usesCapture); + else + this._emitter.addEventListener(this._type, this._callback, this._thisObject); + } + + disconnect() + { + console.assert(this._emitter && this._callback, "EventListener is not bound to a callback.", this); + + if (!this._emitter || !this._callback) + return; + + if (this._emitter instanceof Node) + this._emitter.removeEventListener(this._type, this._callback, this._usesCapture); + else + this._emitter.removeEventListener(this._type, this._callback, this._thisObject); + + if (this._fireOnce) + delete this._thisObject; + delete this._emitter; + delete this._type; + delete this._callback; + } +}; diff --git a/inspector/Base/EventListenerSet.js b/inspector/Base/EventListenerSet.js new file mode 100644 index 0000000..63c3c63 --- /dev/null +++ b/inspector/Base/EventListenerSet.js @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2014, 2015 Apple Inc. All rights reserved. + * Copyright (C) 2013, 2014 University of Washington. 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. + */ + +// This class supports adding and removing many listeners at once. +// Add DOM or Inspector event listeners to the set using `register()`. +// Use `install()` and `uninstall()` to enable or disable all listeners +// in the set at once. + +WI.EventListenerSet = class EventListenerSet +{ + constructor(defaultThisObject, name) + { + this.name = name; + this._defaultThisObject = defaultThisObject; + + this._listeners = []; + this._installed = false; + } + + // Public + + register(emitter, type, callback, thisObject, usesCapture) + { + console.assert(emitter, `Missing event emitter for event: ${type}.`); + console.assert(type, "Missing event type."); + console.assert(callback, `Missing callback for event: ${type}.`); + var emitterIsValid = emitter && (emitter instanceof WI.Object || emitter instanceof Node || (typeof emitter.addEventListener === "function")); + console.assert(emitterIsValid, "Event emitter ", emitter, ` (type: ${type}) is null or does not implement Node or WI.Object.`); + + if (!emitterIsValid || !type || !callback) + return; + + this._listeners.push({listener: new WI.EventListener(thisObject || this._defaultThisObject), emitter, type, callback, usesCapture}); + } + + unregister() + { + if (this._installed) + this.uninstall(); + this._listeners = []; + } + + install() + { + console.assert(!this._installed, "Already installed listener group: " + this.name); + if (this._installed) + return; + + this._installed = true; + + for (var data of this._listeners) + data.listener.connect(data.emitter, data.type, data.callback, data.usesCapture); + } + + uninstall(unregisterListeners) + { + console.assert(this._installed, "Trying to uninstall listener group " + this.name + ", but it isn't installed."); + if (!this._installed) + return; + + this._installed = false; + + for (var data of this._listeners) + data.listener.disconnect(); + + if (unregisterListeners) + this._listeners = []; + } +}; diff --git a/inspector/Base/FileUtilities.js b/inspector/Base/FileUtilities.js new file mode 100644 index 0000000..9e06e13 --- /dev/null +++ b/inspector/Base/FileUtilities.js @@ -0,0 +1,275 @@ +/* + * 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 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} = {}) + { + let inputElement = document.createElement("input"); + inputElement.type = "file"; + inputElement.value = null; + inputElement.multiple = !!multiple; + 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", +}; diff --git a/inspector/Base/HTTPUtilities.js b/inspector/Base/HTTPUtilities.js new file mode 100644 index 0000000..4e6d66c --- /dev/null +++ b/inspector/Base/HTTPUtilities.js @@ -0,0 +1,107 @@ +/* + * 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, +]); diff --git a/inspector/Base/IDLExtensions.js b/inspector/Base/IDLExtensions.js new file mode 100644 index 0000000..2387fce --- /dev/null +++ b/inspector/Base/IDLExtensions.js @@ -0,0 +1,58 @@ +/* + * 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); + }, +}); diff --git a/inspector/Base/ImageUtilities.js b/inspector/Base/ImageUtilities.js new file mode 100644 index 0000000..fb19b42 --- /dev/null +++ b/inspector/Base/ImageUtilities.js @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2013, 2015 Apple Inc. All rights reserved. + * Copyright (C) 2017 Devin Rousso . 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 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; diff --git a/inspector/Base/IterableWeakSet.js b/inspector/Base/IterableWeakSet.js new file mode 100644 index 0000000..22b4fd6 --- /dev/null +++ b/inspector/Base/IterableWeakSet.js @@ -0,0 +1,141 @@ +/* + * 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); + }); + } +} diff --git a/inspector/Base/LinkedList.js b/inspector/Base/LinkedList.js new file mode 100644 index 0000000..f9978af --- /dev/null +++ b/inspector/Base/LinkedList.js @@ -0,0 +1,112 @@ +/* + * 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. + */ + +class LinkedList +{ + constructor() + { + this.head = new LinkedListNode; + this.head.next = this.head.prev = this.head; + this.length = 0; + } + + clear() + { + this.head.next = this.head.prev = this.head; + this.length = 0; + } + + get last() + { + return this.head.prev; + } + + push(item) + { + let newNode = new LinkedListNode(item); + let last = this.last; + let head = this.head; + + last.next = newNode; + newNode.next = head; + head.prev = newNode; + newNode.prev = last; + + this.length++; + + return newNode; + } + + remove(node) + { + if (!node) + return false; + + node.prev.next = node.next; + node.next.prev = node.prev; + + this.length--; + return true; + } + + forEach(callback) + { + let node = this.head; + for (let i = 0, length = this.length; i < length; i++) { + node = node.next; + let returnValue = callback(node.value, i); + if (returnValue === false) + return; + } + } + + toArray() + { + let node = this.head; + let i = this.length; + let result = new Array(i); + while (i--) { + node = node.prev; + result[i] = node.value; + } + return result; + } + + toJSON() + { + return this.toArray(); + } +} + + +class LinkedListNode +{ + constructor(value) + { + this.value = value; + this.prev = null; + this.next = null; + } +} diff --git a/inspector/Base/ListMultimap.js b/inspector/Base/ListMultimap.js new file mode 100644 index 0000000..a415fb6 --- /dev/null +++ b/inspector/Base/ListMultimap.js @@ -0,0 +1,117 @@ +/* + * 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. + */ + +class ListMultimap +{ + constructor() + { + this._insertionOrderedEntries = new LinkedList; + this._keyMap = new Map; + } + + get size() + { + return this._insertionOrderedEntries.length; + } + + add(key, value) + { + let nodeMap = this._keyMap.get(key); + if (!nodeMap) { + nodeMap = new Map; + this._keyMap.set(key, nodeMap); + } + + let node = nodeMap.get(value); + if (!node) { + node = this._insertionOrderedEntries.push([key, value]); + nodeMap.set(value, node); + } + + return this; + } + + delete(key, value) + { + let nodeMap = this._keyMap.get(key); + if (!nodeMap) + return false; + + let node = nodeMap.get(value); + if (!node) + return false; + + nodeMap.delete(value); + this._insertionOrderedEntries.remove(node); + return true; + } + + deleteAll(key) + { + let nodeMap = this._keyMap.get(key); + if (!nodeMap) + return false; + + let list = this._insertionOrderedEntries; + let didDelete = false; + nodeMap.forEach(function(node) { + list.remove(node); + didDelete = true; + }); + + this._keyMap.delete(key); + return didDelete; + } + + has(key, value) + { + let nodeMap = this._keyMap.get(key); + if (!nodeMap) + return false; + + return nodeMap.has(value); + } + + clear() + { + this._keyMap = new Map; + this._insertionOrderedEntries = new LinkedList; + } + + forEach(callback) + { + this._insertionOrderedEntries.forEach(callback); + } + + toArray() + { + return this._insertionOrderedEntries.toArray(); + } + + toJSON() + { + return this.toArray(); + } +} diff --git a/inspector/Base/LoadLocalizedStrings.js b/inspector/Base/LoadLocalizedStrings.js new file mode 100644 index 0000000..a8fcb16 --- /dev/null +++ b/inspector/Base/LoadLocalizedStrings.js @@ -0,0 +1,140 @@ +/* + * 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() { + if (WI.dontLocalizeUserInterface) + return; + + let localizedStringsURL = InspectorFrontendHost.localizedStringsURL; + console.assert(localizedStringsURL); + if (localizedStringsURL) + document.write(""); +})(); + +WI.unlocalizedString = function(string) +{ + // Intentionally do nothing, since this is for engineering builds + // (such as in Debug UI) or in text that is standardized in English. + // For example, CSS property names and values are never localized. + return string; +}; + +WI.UIString = function(string, key, comment) +{ + "use strict"; + + if (WI.dontLocalizeUserInterface) + return string; + + // UIString(string, comment) + if (arguments.length === 2) { + comment = key; + key = undefined; + } + + key = key || string; + + if (window.localizedStrings && key in window.localizedStrings) + return window.localizedStrings[key]; + + if (!window.localizedStrings) + console.error(`Attempted to load localized string "${key}" before localizedStrings was initialized.`, comment); + + if (!this._missingLocalizedStrings) + this._missingLocalizedStrings = {}; + + if (!(key in this._missingLocalizedStrings)) { + console.error(`Localized string "${key}" was not found.`, comment); + this._missingLocalizedStrings[key] = true; + } + + return "LOCALIZED STRING NOT FOUND"; +}; + +WI.repeatedUIString = {}; + +WI.repeatedUIString.timelineRecordLayout = function() { + return WI.UIString("Layout", "Layout @ Timeline record", "Layout phase timeline records"); +}; + +WI.repeatedUIString.timelineRecordPaint = function() { + return WI.UIString("Paint", "Paint @ Timeline record", "Paint (render) phase timeline records"); +}; + +WI.repeatedUIString.timelineRecordComposite = function() { + return WI.UIString("Composite", "Composite @ Timeline record", "Composite phase timeline records, where graphic layers are combined"); +}; + +WI.repeatedUIString.debuggerStatements = function() { + return WI.UIString("Debugger Statements", "Debugger Statements @ JavaScript Breakpoint", "Break (pause) on debugger statements"); +}; + +WI.repeatedUIString.allExceptions = function() { + return WI.UIString("All Exceptions", "All Exceptions @ JavaScript Breakpoint", "Break (pause) on all exceptions"); +}; + +WI.repeatedUIString.uncaughtExceptions = function() { + return WI.UIString("Uncaught Exceptions", "Uncaught Exceptions @ JavaScript Breakpoint", "Break (pause) on uncaught (unhandled) exceptions"); +}; + +WI.repeatedUIString.assertionFailures = function() { + return WI.UIString("Assertion Failures", "Assertion Failures @ JavaScript Breakpoint", "Break (pause) when console.assert() fails"); +}; + +WI.repeatedUIString.allMicrotasks = function() { + return WI.UIString("All Microtasks", "All Microtasks @ JavaScript Breakpoint", "Break (pause) on all microtasks"); +}; + +WI.repeatedUIString.allAnimationFrames = function() { + return WI.UIString("All Animation Frames", "All Animation Frames @ Event Breakpoint", "Break (pause) on All animation frames"); +}; + +WI.repeatedUIString.allIntervals = function() { + return WI.UIString("All Intervals", "All Intervals @ Event Breakpoint", "Break (pause) on all intervals"); +}; + +WI.repeatedUIString.allEvents = function() { + return WI.UIString("All Events", "All Events @ Event Breakpoint", "Break (pause) on all events"); +}; + +WI.repeatedUIString.allTimeouts = function() { + return WI.UIString("All Timeouts", "All Timeouts @ Event Breakpoint", "Break (pause) on all timeouts"); +}; + +WI.repeatedUIString.allRequests = function() { + return WI.UIString("All Requests", "A submenu item of 'Break on' that breaks (pauses) before all network requests"); +}; + +WI.repeatedUIString.fetch = function() { + return WI.UIString("Fetch", "Resource loaded via 'fetch' method"); +}; + +WI.repeatedUIString.revealInDOMTree = function() { + return WI.UIString("Reveal in DOM Tree", "Open Elements tab and select this node in DOM tree"); +}; + +WI.repeatedUIString.showTransparencyGridTooltip = function() { + return WI.UIString("Show transparency grid", "Show transparency grid (tooltip)", "Tooltip for showing the checkered transparency grid under images and canvases") +}; diff --git a/inspector/Base/MIMETypeUtilities.js b/inspector/Base/MIMETypeUtilities.js new file mode 100644 index 0000000..dec6945 --- /dev/null +++ b/inspector/Base/MIMETypeUtilities.js @@ -0,0 +1,345 @@ +/* + * 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; +}; diff --git a/inspector/Base/Main.js b/inspector/Base/Main.js new file mode 100644 index 0000000..c03899d --- /dev/null +++ b/inspector/Base/Main.js @@ -0,0 +1,3637 @@ +/* + * Copyright (C) 2013-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.ContentViewCookieType = { + ApplicationCache: "application-cache", + CookieStorage: "cookie-storage", + Database: "database", + DatabaseTable: "database-table", + DOMStorage: "dom-storage", + Resource: "resource", // includes Frame too. + Timelines: "timelines" +}; + +WI.SelectedSidebarPanelCookieKey = "selected-sidebar-panel"; +WI.TypeIdentifierCookieKey = "represented-object-type"; + +WI.StateRestorationType = { + Load: "state-restoration-load", + Navigation: "state-restoration-navigation", + Delayed: "state-restoration-delayed", +}; + +WI.LayoutDirection = { + System: "system", + LTR: "ltr", + RTL: "rtl", +}; + +WI.loaded = function() +{ + if (InspectorFrontendHost.connect) + InspectorFrontendHost.connect(); + + // Register observers for events from the InspectorBackend. + if (InspectorBackend.registerAnimationDispatcher) + InspectorBackend.registerAnimationDispatcher(WI.AnimationObserver); + if (InspectorBackend.registerApplicationCacheDispatcher) + InspectorBackend.registerApplicationCacheDispatcher(WI.ApplicationCacheObserver); + if (InspectorBackend.registerBrowserDispatcher) + InspectorBackend.registerBrowserDispatcher(WI.BrowserObserver); + if (InspectorBackend.registerCPUProfilerDispatcher) + InspectorBackend.registerCPUProfilerDispatcher(WI.CPUProfilerObserver); + if (InspectorBackend.registerCSSDispatcher) + InspectorBackend.registerCSSDispatcher(WI.CSSObserver); + if (InspectorBackend.registerCanvasDispatcher) + InspectorBackend.registerCanvasDispatcher(WI.CanvasObserver); + if (InspectorBackend.registerConsoleDispatcher) + InspectorBackend.registerConsoleDispatcher(WI.ConsoleObserver); + if (InspectorBackend.registerDOMDispatcher) + InspectorBackend.registerDOMDispatcher(WI.DOMObserver); + if (InspectorBackend.registerDOMStorageDispatcher) + InspectorBackend.registerDOMStorageDispatcher(WI.DOMStorageObserver); + if (InspectorBackend.registerDatabaseDispatcher) + InspectorBackend.registerDatabaseDispatcher(WI.DatabaseObserver); + if (InspectorBackend.registerDebuggerDispatcher) + InspectorBackend.registerDebuggerDispatcher(WI.DebuggerObserver); + if (InspectorBackend.registerHeapDispatcher) + InspectorBackend.registerHeapDispatcher(WI.HeapObserver); + if (InspectorBackend.registerInspectorDispatcher) + InspectorBackend.registerInspectorDispatcher(WI.InspectorObserver); + if (InspectorBackend.registerLayerTreeDispatcher) + InspectorBackend.registerLayerTreeDispatcher(WI.LayerTreeObserver); + if (InspectorBackend.registerMemoryDispatcher) + InspectorBackend.registerMemoryDispatcher(WI.MemoryObserver); + if (InspectorBackend.registerNetworkDispatcher) + InspectorBackend.registerNetworkDispatcher(WI.NetworkObserver); + if (InspectorBackend.registerPageDispatcher) + InspectorBackend.registerPageDispatcher(WI.PageObserver); + if (InspectorBackend.registerRuntimeDispatcher) + InspectorBackend.registerRuntimeDispatcher(WI.RuntimeObserver); + if (InspectorBackend.registerScriptProfilerDispatcher) + InspectorBackend.registerScriptProfilerDispatcher(WI.ScriptProfilerObserver); + if (InspectorBackend.registerTargetDispatcher) + InspectorBackend.registerTargetDispatcher(WI.TargetObserver); + if (InspectorBackend.registerTimelineDispatcher) + InspectorBackend.registerTimelineDispatcher(WI.TimelineObserver); + if (InspectorBackend.registerWorkerDispatcher) + InspectorBackend.registerWorkerDispatcher(WI.WorkerObserver); + + // Listen for the ProvisionalLoadStarted event before registering for events so our code gets called before any managers or sidebars. + // This lets us save a state cookie before any managers or sidebars do any resets that would affect state (namely TimelineManager). + WI.Frame.addEventListener(WI.Frame.Event.ProvisionalLoadStarted, WI._provisionalLoadStarted, WI); + + // Populate any UIStrings that must be done early after localized strings have loaded. + WI.KeyboardShortcut.Key.Space._displayName = WI.UIString("Space"); + + // Create the singleton managers next, before the user interface elements, so the user interface can register + // as event listeners on these managers. + 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. + WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Paused, WI._debuggerDidPause, WI); + WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.Resumed, WI._debuggerDidResume, WI); + WI.domManager.addEventListener(WI.DOMManager.Event.InspectModeStateChanged, WI._inspectModeStateChanged, WI); + WI.domManager.addEventListener(WI.DOMManager.Event.DOMNodeWasInspected, WI._domNodeWasInspected, WI); + WI.domStorageManager.addEventListener(WI.DOMStorageManager.Event.DOMStorageObjectWasInspected, WI._domStorageWasInspected, WI); + WI.databaseManager.addEventListener(WI.DatabaseManager.Event.DatabaseWasInspected, WI._databaseWasInspected, WI); + WI.networkManager.addEventListener(WI.NetworkManager.Event.MainFrameDidChange, WI._mainFrameDidChange, WI); + WI.networkManager.addEventListener(WI.NetworkManager.Event.FrameWasAdded, WI._frameWasAdded, WI); + WI.browserManager.enable(); + + WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, WI._mainResourceDidChange, WI); + + document.addEventListener("DOMContentLoaded", WI.contentLoaded); + + // Create settings. + WI._showingSplitConsoleSetting = new WI.Setting("showing-split-console", false); + WI._openTabsSetting = new WI.Setting("open-tab-types", [ + WI.ElementsTabContentView.Type, + WI.ConsoleTabContentView.Type, + WI.SourcesTabContentView.Type, + WI.NetworkTabContentView.Type, + WI.TimelineTabContentView.Type, + WI.StorageTabContentView.Type, + WI.GraphicsTabContentView.Type, + WI.LayersTabContentView.Type, + WI.AuditTabContentView.Type, + ]); + WI._selectedTabIndexSetting = new WI.Setting("selected-tab-index", 0); + + // State. + WI.printStylesEnabled = false; + WI.setZoomFactor(WI.settings.zoomFactor.value); + InspectorFrontendHost.setForcedAppearance(WI.settings.frontendAppearance.value); + InspectorFrontendHost.setAllowsInspectingInspector(WI.settings.experimentalAllowInspectingInspector.value); + WI.mouseCoords = {x: 0, y: 0}; + WI.modifierKeys = {altKey: false, metaKey: false, shiftKey: false}; + WI.visible = false; + WI._windowKeydownListeners = []; + WI._overridenDeviceUserAgent = null; + WI._overridenDeviceScreenSize = null; + WI._overridenDeviceSettings = new Map; + + // Targets. + WI.backendTarget = null; + WI._backendTargetAvailablePromise = new WI.WrappedPromise; + + WI.pageTarget = null; + WI._pageTargetAvailablePromise = new WI.WrappedPromise; + + // COMPATIBILITY (iOS 13.0): Target.exists was "replaced" by differentiating "web" debuggables + // into "page" (direct) and "web-page" debuggables (multiplexing). + if (InspectorBackend.hasDomain("Target")) { + if (InspectorBackend.hasCommand("Target.exists")) { + console.assert(WI.sharedApp.debuggableType === WI.DebuggableType.WebPage); + InspectorBackend.invokeCommand("Target.exists", WI.TargetType.WebPage, InspectorBackend.backendConnection, {}, (error) => { + if (error) { + WI.sharedApp._debuggableType = WI.DebuggableType.Page; + WI.targetManager.createDirectBackendTarget(); + } + }); + } else + WI.targetManager.createMultiplexingBackendTarget(); + } else + WI.targetManager.createDirectBackendTarget(); +}; + +WI.contentLoaded = function() +{ + // If there was an uncaught exception earlier during loading, then + // abort loading more content. We could be in an inconsistent state. + if (window.__uncaughtExceptions) + return; + + // Register for global events. + document.addEventListener("beforecopy", WI._beforecopy); + document.addEventListener("copy", WI._copy); + document.addEventListener("paste", WI._paste); + + document.addEventListener("click", WI._mouseWasClicked); + document.addEventListener("dragover", WI._handleDragOver); + document.addEventListener("focus", WI._focusChanged, true); + + window.addEventListener("focus", WI._updateWindowInactiveState); + window.addEventListener("blur", WI._updateWindowInactiveState); + window.addEventListener("visibilitychange", WI._updateWindowInactiveState); + window.addEventListener("resize", WI._windowResized); + window.addEventListener("keydown", WI._windowKeyDown); + window.addEventListener("keyup", WI._windowKeyUp); + window.addEventListener("mousedown", WI._mouseDown, true); + window.addEventListener("mousemove", WI._mouseMoved, true); + window.addEventListener("pagehide", WI._pageHidden); + window.addEventListener("contextmenu", WI._contextMenuRequested); + + // Add platform style classes so the UI can be tweaked per-platform. + document.body.classList.add(WI.Platform.name + "-platform"); + if (WI.Platform.version.name) + document.body.classList.add(WI.Platform.version.name); + + document.body.classList.add(WI.sharedApp.debuggableType); + document.body.setAttribute("dir", WI.resolvedLayoutDirection()); + + WI.layoutMeasurementContainer = document.body.appendChild(document.createElement("div")); + WI.layoutMeasurementContainer.id = "layout-measurement-container"; + + WI.settings.showJavaScriptTypeInformation.addEventListener(WI.Setting.Event.Changed, WI._showJavaScriptTypeInformationSettingChanged, WI); + WI.settings.enableControlFlowProfiler.addEventListener(WI.Setting.Event.Changed, WI._enableControlFlowProfilerSettingChanged, WI); + WI.settings.resourceCachingDisabled.addEventListener(WI.Setting.Event.Changed, WI._resourceCachingDisabledSettingChanged, WI); + WI.settings.experimentalAllowInspectingInspector.addEventListener(WI.Setting.Event.Changed, WI._allowInspectingInspectorSettingChanged, WI); + + function setTabSize() { + document.body.style.tabSize = WI.settings.tabSize.value; + } + WI.settings.tabSize.addEventListener(WI.Setting.Event.Changed, setTabSize, WI); + setTabSize(); + + function setInvisibleCharacterClassName() { + document.body.classList.toggle("show-invisible-characters", WI.settings.showInvisibleCharacters.value); + } + WI.settings.showInvisibleCharacters.addEventListener(WI.Setting.Event.Changed, setInvisibleCharacterClassName, WI); + setInvisibleCharacterClassName(); + + function setWhitespaceCharacterClassName() { + document.body.classList.toggle("show-whitespace-characters", WI.settings.showWhitespaceCharacters.value); + } + WI.settings.showWhitespaceCharacters.addEventListener(WI.Setting.Event.Changed, setWhitespaceCharacterClassName, WI); + setWhitespaceCharacterClassName(); + + // Create the user interface elements. + WI.tabBar = new WI.TabBar(document.getElementById("tab-bar")); + + WI._contentElement = document.getElementById("content"); + WI._contentElement.role = "tabpanel"; + + WI.clearKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "K", WI._clear); + + WI.findString = ""; + WI.populateFindKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "E", WI._populateFind); + WI.populateFindKeyboardShortcut.implicitlyPreventsDefault = false; + WI.findNextKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "G", WI._findNext); + WI.findPreviousKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift | WI.KeyboardShortcut.Modifier.CommandOrControl, "G", WI._findPrevious); + + WI.consoleDrawer = new WI.ConsoleDrawer(document.getElementById("console-drawer")); + WI.consoleDrawer.addEventListener(WI.ConsoleDrawer.Event.CollapsedStateChanged, WI._consoleDrawerCollapsedStateDidChange, WI); + WI.consoleDrawer.addEventListener(WI.ConsoleDrawer.Event.Resized, WI._consoleDrawerDidResize, WI); + + WI.quickConsole = new WI.QuickConsole(document.getElementById("quick-console")); + + WI._consoleRepresentedObject = new WI.LogObject; + WI.consoleContentView = WI.consoleDrawer.contentViewForRepresentedObject(WI._consoleRepresentedObject); + WI.consoleLogViewController = WI.consoleContentView.logViewController; + + WI.navigationSidebar = new WI.SingleSidebar(document.getElementById("navigation-sidebar"), WI.Sidebar.Sides.Leading, WI.UIString("Navigation", "Navigation @ Sidebar", "Label for the navigation sidebar.")); + WI.navigationSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, WI._sidebarWidthDidChange, WI); + WI.navigationSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, WI._sidebarWidthDidChange, WI); + + WI.detailsSidebar = new WI.MultiSidebar(document.getElementById("details-sidebar"), WI.Sidebar.Sides.Trailing, WI.UIString("Details", "Details @ Sidebar", "Label for the details sidebar.")); + WI.detailsSidebar.addEventListener(WI.Sidebar.Event.WidthDidChange, WI._sidebarWidthDidChange, WI); + WI.detailsSidebar.addEventListener(WI.Sidebar.Event.CollapsedStateDidChange, WI._sidebarWidthDidChange, WI); + WI.detailsSidebar.addEventListener(WI.MultiSidebar.Event.MultipleSidebarsVisibleChanged, WI._sidebarWidthDidChange, WI); + + WI.searchKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "F", WI._focusSearchField); + WI._findKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "F", WI._find); + WI.saveKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "S", WI._save); + WI._saveAsKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift | WI.KeyboardShortcut.Modifier.CommandOrControl, "S", WI._saveAs); + + WI.openResourceKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "O", WI._showOpenResourceDialog); + new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "P", WI._showOpenResourceDialog); + + WI.navigationSidebarKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "0", WI.toggleNavigationSidebar); + WI.detailsSidebarKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Option, "0", WI.toggleDetailsSidebar); + + let boundIncreaseZoom = WI._increaseZoom; + let boundDecreaseZoom = WI._decreaseZoom; + WI._increaseZoomKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Plus, boundIncreaseZoom); + WI._decreaseZoomKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Minus, boundDecreaseZoom); + WI._increaseZoomKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, WI.KeyboardShortcut.Key.Plus, boundIncreaseZoom); + WI._decreaseZoomKeyboardShortcut2 = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, WI.KeyboardShortcut.Key.Minus, boundDecreaseZoom); + WI._resetZoomKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "0", WI._resetZoom); + + WI._showTabAtIndexKeyboardShortcuts = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((i) => new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Option, `${i}`, (event) => { WI._showTabAtIndexFromShortcut(i); })); + + WI.tabBrowser = new WI.TabBrowser(document.getElementById("tab-browser"), WI.tabBar, WI.navigationSidebar, WI.detailsSidebar); + WI.tabBrowser.addEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, WI._tabBrowserSelectedTabContentViewDidChange, WI); + + WI._reloadPageKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "R", WI._reloadPage); + WI._reloadPageFromOriginKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Option, "R", WI._reloadPageFromOrigin); + WI._reloadPageKeyboardShortcut.implicitlyPreventsDefault = WI._reloadPageFromOriginKeyboardShortcut.implicitlyPreventsDefault = false; + + WI._consoleTabKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Option | WI.KeyboardShortcut.Modifier.CommandOrControl, "C", WI._showConsoleTab); + WI._quickConsoleKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, WI.KeyboardShortcut.Key.Apostrophe, WI._focusConsolePrompt); + + WI._inspectModeKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "C", WI._toggleInspectMode); + + WI._undoKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "Z", WI._undoKeyboardShortcut); + WI._redoKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "Z", WI._redoKeyboardShortcut); + WI._undoKeyboardShortcut.implicitlyPreventsDefault = WI._redoKeyboardShortcut.implicitlyPreventsDefault = false; + + WI.toggleBreakpointsKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "Y", WI.debuggerToggleBreakpoints); + WI.pauseOrResumeKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control | WI.KeyboardShortcut.Modifier.CommandOrControl, "Y", WI.debuggerPauseResumeToggle); + WI.stepOverKeyboardShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.F6, WI.debuggerStepOver); + WI.stepIntoKeyboardShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.F7, WI.debuggerStepInto); + WI.stepOutKeyboardShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.F8, WI.debuggerStepOut); + + WI.pauseOrResumeAlternateKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Backslash, WI.debuggerPauseResumeToggle); + WI.stepOverAlternateKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.SingleQuote, WI.debuggerStepOver); + WI.stepIntoAlternateKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Semicolon, WI.debuggerStepInto); + WI.stepOutAlternateKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift | WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Semicolon, WI.debuggerStepOut); + + // COMPATIBILITY (iOS 13.4): Debugger.stepNext did not exist yet. + if (InspectorBackend.hasCommand("Debugger.stepNext")) { + WI.stepNextKeyboardShortcut = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.F9, WI.debuggerStepNext); + WI.stepNextAlternateKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Shift | WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.SingleQuote, WI.debuggerStepNext); + } + + WI._updateDebuggerKeyboardShortcuts(); + + WI.settingsKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, WI.KeyboardShortcut.Key.Comma, WI._handleSettingsKeyboardShortcut); + + WI._togglePreviousDockConfigurationKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "D", WI._togglePreviousDockConfiguration); + + let dockingConfigurationNavigationItems = []; + + let supportsDockRight = InspectorFrontendHost.supportsDockSide(WI.DockConfiguration.Right); + let supportsDockLeft = InspectorFrontendHost.supportsDockSide(WI.DockConfiguration.Left); + let supportsDockBottom = InspectorFrontendHost.supportsDockSide(WI.DockConfiguration.Bottom); + let supportsUndocked = InspectorFrontendHost.supportsDockSide(WI.DockConfiguration.Undocked); + + function addDockButton(identifier, tooltip, image, handler) { + let button = new WI.ButtonNavigationItem(identifier, tooltip, image); + button.element.classList.add(WI.Popover.IgnoreAutoDismissClassName); + button.addEventListener(WI.ButtonNavigationItem.Event.Clicked, handler, WI); + dockingConfigurationNavigationItems.push(button); + return button; + } + + function addDockLeftButton() { + if (!supportsDockLeft || (!supportsDockRight && !supportsDockBottom && !supportsUndocked)) + return; + + WI._dockLeftTabBarButton = addDockButton("dock-left", WI.UIString("Dock to left of window"), "Images/DockLeft.svg", WI._dockLeft); + } + + function addDockRightButton() { + if (!supportsDockRight || (!supportsDockLeft && !supportsDockBottom && !supportsUndocked)) + return; + + WI._dockRightTabBarButton = addDockButton("dock-right", WI.UIString("Dock to right of window"), "Images/DockRight.svg", WI._dockRight); + } + + if (supportsDockRight || supportsDockLeft || supportsDockBottom) + WI._closeTabBarButton = addDockButton("dock-close", WI.UIString("Close"), "Images/CloseLarge.svg", WI.close); + + if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) + addDockRightButton(); + else + addDockLeftButton(); + + if (supportsDockBottom && (supportsDockRight || supportsDockLeft || supportsUndocked)) + WI._dockBottomTabBarButton = addDockButton("dock-bottom", WI.UIString("Dock to bottom of window"), "Images/DockBottom.svg", WI._dockBottom); + + if (WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL) + addDockLeftButton(); + else + addDockRightButton(); + + if (supportsUndocked && (supportsDockRight || supportsDockLeft || supportsDockBottom)) + WI._undockTabBarButton = addDockButton("undock", WI.UIString("Detach into separate window"), "Images/Undock.svg", WI._undock); + + let inspectedPageControlNavigationItems = []; + + let elementSelectionToolTip = WI.UIString("Start element selection (%s)").format(WI._inspectModeKeyboardShortcut.displayName); + let activatedElementSelectionToolTip = WI.UIString("Stop element selection (%s)").format(WI._inspectModeKeyboardShortcut.displayName); + WI._inspectModeTabBarButton = new WI.ActivateButtonNavigationItem("inspect", elementSelectionToolTip, activatedElementSelectionToolTip, "Images/Crosshair.svg"); + WI._inspectModeTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, WI._toggleInspectMode, WI); + inspectedPageControlNavigationItems.push(WI._inspectModeTabBarButton); + + // COMPATIBILITY (iOS 12.2): Page.overrideSetting did not exist. + if (InspectorBackend.hasCommand("Page.overrideUserAgent") && InspectorBackend.hasCommand("Page.overrideSetting")) { + const deviceSettingsTooltip = WI.UIString("Device Settings"); + WI._deviceSettingsTabBarButton = new WI.ActivateButtonNavigationItem("device-settings", deviceSettingsTooltip, deviceSettingsTooltip, InspectorFrontendHost.isRemote ? "Images/Device.svg" : "Images/Computer.svg"); + WI._deviceSettingsTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, WI._handleDeviceSettingsTabBarButtonClicked, WI); + inspectedPageControlNavigationItems.push(WI._deviceSettingsTabBarButton); + + WI._deviceSettingsPopover = null; + } + + if (InspectorFrontendHost.isRemote || WI.isDebugUIEnabled()) { + let reloadToolTip; + if (WI.sharedApp.debuggableType === WI.DebuggableType.JavaScript || WI.sharedApp.debuggableType === WI.DebuggableType.ITML) + reloadToolTip = WI.UIString("Restart (%s)").format(WI._reloadPageKeyboardShortcut.displayName); + else + reloadToolTip = WI.UIString("Reload page (%s)\nReload page ignoring cache (%s)").format(WI._reloadPageKeyboardShortcut.displayName, WI._reloadPageFromOriginKeyboardShortcut.displayName); + WI._reloadTabBarButton = new WI.ButtonNavigationItem("reload", reloadToolTip, "Images/ReloadToolbar.svg"); + WI._reloadTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, WI._reloadTabBarButtonClicked, WI); + inspectedPageControlNavigationItems.push(WI._reloadTabBarButton); + + WI._downloadTabBarButton = new WI.ButtonNavigationItem("download", WI.UIString("Download Web Archive"), "Images/DownloadArrow.svg"); + WI._downloadTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, WI._downloadWebArchive, WI); + inspectedPageControlNavigationItems.push(WI._downloadTabBarButton); + } + + WI.tabBar.addNavigationItemBefore(new WI.GroupNavigationItem([ + ...dockingConfigurationNavigationItems, + ...inspectedPageControlNavigationItems, + ])); + + WI._consoleDividerNavigationItem = new WI.DividerNavigationItem; + WI.tabBar.addNavigationItemBefore(WI._consoleDividerNavigationItem); + + WI._consoleWarningsTabBarButton = new WI.ButtonNavigationItem("console-warnings", WI.UIString("0 Console warnings"), "Images/IssuesEnabled.svg"); + WI._consoleWarningsTabBarButton.imageType = WI.ButtonNavigationItem.ImageType.IMG; + WI._consoleWarningsTabBarButton.hidden = true; + WI._consoleWarningsTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, function(event) { + WI.showConsoleTab(WI.LogContentView.Scopes.Warnings, { + initiatorHint: WI.TabBrowser.TabNavigationInitiator.Dashboard, + }); + }, WI); + + WI._consoleErrorsTabBarButton = new WI.ButtonNavigationItem("console-errors", WI.UIString("0 Console errors"), "Images/ErrorsEnabled.svg"); + WI._consoleErrorsTabBarButton.imageType = WI.ButtonNavigationItem.ImageType.IMG; + WI._consoleErrorsTabBarButton.hidden = true; + WI._consoleErrorsTabBarButton.addEventListener(WI.ButtonNavigationItem.Event.Clicked, function(event) { + WI.showConsoleTab(WI.LogContentView.Scopes.Errors, { + initiatorHint: WI.TabBrowser.TabNavigationInitiator.Dashboard, + }); + }, WI); + + WI.tabBar.addNavigationItemBefore(new WI.GroupNavigationItem([ + WI._consoleWarningsTabBarButton, + WI._consoleErrorsTabBarButton, + ])); + + WI._updateInspectModeTabBarButton(); + WI._updateDownloadTabBarButton(); + WI._updateReloadTabBarButton(); + + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.MessageAdded, WI._updateConsoleTabBarButtons, WI); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.PreviousMessageRepeatCountUpdated, WI._updateConsoleTabBarButtons, WI); + WI.consoleManager.addEventListener(WI.ConsoleManager.Event.Cleared, WI._updateConsoleTabBarButtons, WI); + WI._updateConsoleTabBarButtons(); + + if (supportsDockRight || supportsDockLeft || supportsDockBottom) { + WI._dockedResizerElement = document.getElementById("docked-resizer"); + WI._dockedResizerElement.classList.add(WI.Popover.IgnoreAutoDismissClassName); + WI._dockedResizerElement.addEventListener("mousedown", WI._handleDockedResizerMouseDown); + } + + if (supportsUndocked) { + let undockedTitleAreaElement = document.getElementById("undocked-title-area"); + undockedTitleAreaElement.addEventListener("mousedown", WI._handleUndockedTitleAreaMouseDown); + } + + WI._dockingAvailable = false; + + WI._updateDockNavigationItems(); + WI._setupViewHierarchy(); + + // These tabs are always available for selecting, modulo isTabAllowed(). + // Other tabs may be engineering-only or toggled at runtime if incomplete. + let productionTabClasses = [ + WI.ElementsTabContentView, + WI.NetworkTabContentView, + WI.SourcesTabContentView, + WI.TimelineTabContentView, + WI.StorageTabContentView, + WI.GraphicsTabContentView, + WI.LayersTabContentView, + WI.AuditTabContentView, + WI.ConsoleTabContentView, + WI.SearchTabContentView, + WI.SettingsTabContentView, + ]; + + WI._knownTabClassesByType = new Map; + // Set tab classes directly. The public API triggers other updates and + // notifications that won't work or have no listeners at WI point. + for (let tabClass of productionTabClasses) + WI._knownTabClassesByType.set(tabClass.Type, tabClass); + + WI._pendingOpenTabs = []; + + WI._searchTabContentView = new WI.SearchTabContentView; + WI.tabBrowser.addTabForContentView(WI._searchTabContentView, {suppressAnimations: true}); + + WI._settingsTabContentView = new WI.SettingsTabContentView; + WI.tabBrowser.addTabForContentView(WI._settingsTabContentView, {suppressAnimations: true}); + + // Previously we may have stored duplicates in WI setting. Avoid creating duplicate tabs. + let openTabTypes = WI._openTabsSetting.value; + let seenTabTypes = new Set; + + for (let i = 0; i < openTabTypes.length; ++i) { + let tabType = openTabTypes[i]; + + if (seenTabTypes.has(tabType)) + continue; + seenTabTypes.add(tabType); + + if (!WI.isTabTypeAllowed(tabType)) { + WI._pendingOpenTabs.push({tabType, index: i}); + continue; + } + + if (!WI.isNewTabWithTypeAllowed(tabType)) + continue; + + let tabContentView = WI._createTabContentViewForType(tabType); + if (!tabContentView) + continue; + WI.tabBrowser.addTabForContentView(tabContentView, {suppressAnimations: true}); + } + + WI._restoreCookieForOpenTabs(WI.StateRestorationType.Load); + + if (WI.tabBar.normalTabCount) + WI.tabBar.selectedTabBarItem = WI._selectedTabIndexSetting.value; + else { + // If no tabs were able to be restored, show all tabs that are allowed. + for (let tabType of WI._openTabsSetting.defaultValue) { + if (WI.isNewTabWithTypeAllowed(tabType)) + WI.createNewTabWithType(tabType) + } + WI.tabBar.selectedTabBarItem = 0; + } + + // Listen to the events after restoring the saved tabs to avoid recursion. + WI.tabBar.addEventListener(WI.TabBar.Event.TabBarItemAdded, WI._rememberOpenTabs, WI); + WI.tabBar.addEventListener(WI.TabBar.Event.TabBarItemRemoved, WI._rememberOpenTabs, WI); + WI.tabBar.addEventListener(WI.TabBar.Event.TabBarItemsReordered, WI._rememberOpenTabs, WI); + + function updateConsoleSavedResultPrefixCSSVariable() { + document.body.style.setProperty("--console-saved-result-prefix", "\"" + WI.RuntimeManager.preferredSavedResultPrefix() + "\""); + } + + WI.settings.consoleSavedResultAlias.addEventListener(WI.Setting.Event.Changed, updateConsoleSavedResultPrefixCSSVariable, WI); + + updateConsoleSavedResultPrefixCSSVariable(); + + function updateZoomFactorCSSVariable() { + document.body.style.setProperty("--zoom-factor", WI.settings.zoomFactor.value); + } + + WI.settings.zoomFactor.addEventListener(WI.Setting.Event.Changed, updateZoomFactorCSSVariable, WI); + + updateZoomFactorCSSVariable(); + + WI.settings.frontendAppearance.addEventListener(WI.Setting.Event.Changed, function(event) { + InspectorFrontendHost.setForcedAppearance(WI.settings.frontendAppearance.value); + }, WI); + + // Signal that the frontend is now ready to receive messages. + WI._backendTargetAvailablePromise.promise.then(() => { + InspectorFrontendAPI.loadCompleted(); + }); + + WI._updateSheetRect(); + + // Tell the InspectorFrontendHost we loaded, which causes the window to display + // and pending InspectorFrontendAPI commands to be sent. + InspectorFrontendHost.loaded(); + + if (WI._showingSplitConsoleSetting.value) + WI.showSplitConsole(); + + // Store WI on the window in case the WebInspector global gets corrupted. + window.__frontendCompletedLoad = true; + + WI.frontendCompletedLoadTimestamp = performance.now(); + + if (WI.runBootstrapOperations) + WI.runBootstrapOperations(); + + if (InspectorFrontendHost.supportsDiagnosticLogging) { + WI.diagnosticController = new WI.DiagnosticController; + WI.diagnosticController.addRecorder(new WI.InspectedTargetTypesDiagnosticEventRecorder(WI.diagnosticController)); + WI.diagnosticController.addRecorder(new WI.TabActivityDiagnosticEventRecorder(WI.diagnosticController)); + WI.diagnosticController.addRecorder(new WI.TabNavigationDiagnosticEventRecorder(WI.diagnosticController)); + + if (InspectorFrontendHost.supportsWebExtensions) + WI.diagnosticController.addRecorder(new WI.ExtensionTabActivationDiagnosticEventRecorder(WI.diagnosticController)); + } +}; + +WI.performOneTimeFrontendInitializationsUsingTarget = function(target) +{ + if (!WI.__didPerformConsoleInitialization && target.hasDomain("Console")) { + WI.__didPerformConsoleInitialization = true; + WI.consoleManager.initializeLogChannels(target); + } + + if (!WI.__didPerformCSSInitialization && target.hasDomain("CSS")) { + WI.__didPerformCSSInitialization = true; + WI.cssManager.initializeCSSPropertyNameCompletions(target); + } +}; + +WI.initializeTarget = function(target) +{ + if (target.hasDomain("Page")) { + // COMPATIBILITY (iOS 12.2): Page.overrideUserAgent did not exist. + if (target.hasCommand("Page.overrideUserAgent") && WI._overridenDeviceUserAgent) + target.PageAgent.overrideUserAgent(WI._overridenDeviceUserAgent); + + // COMPATIBILITY (iOS 12.2): Page.overrideSetting did not exist. + if (target.hasCommand("Page.overrideSetting")) { + for (let [setting, value] of WI._overridenDeviceSettings) + target.PageAgent.overrideSetting(setting, value); + } + + // COMPATIBILITY (iOS 13.4): Page.setShowRulers was removed. + if (target.hasCommand("Page.setShowRulers") && WI.settings.showRulers.value) + target.PageAgent.setShowRulers(true); + } +}; + +WI.targetsAvailable = function() +{ + return WI._pageTargetAvailablePromise.settled; +}; + +WI.whenTargetsAvailable = function() +{ + return WI._pageTargetAvailablePromise.promise; +}; + +WI.isTabTypeAllowed = function(tabType) +{ + let tabClass = WI._knownTabClassesByType.get(tabType); + if (!tabClass) + return false; + + return tabClass.isTabAllowed(); +}; + +WI.knownTabClasses = function() +{ + return new Set(WI._knownTabClassesByType.values()); +}; + +WI._showOpenResourceDialog = function() +{ + if (!WI._openResourceDialog) + WI._openResourceDialog = new WI.OpenResourceDialog(WI); + + if (WI._openResourceDialog.visible) + return; + + WI._openResourceDialog.present(WI._contentElement); +}; + +WI._createTabContentViewForType = function(tabType) +{ + let tabClass = WI._knownTabClassesByType.get(tabType); + if (!tabClass) { + console.error("Unknown tab type", tabType); + return null; + } + + console.assert(tabClass !== WI.WebInspectorExtensionTabContentView, "Extension tabs must be created via WebInspectorExtensionController.createTabForExtension()."); + if (tabClass === WI.WebInspectorExtensionTabContentView) + return null; + + console.assert(WI.TabContentView.isPrototypeOf(tabClass)); + return new tabClass; +}; + +WI._rememberOpenTabs = function() +{ + let seenTabTypes = new Set; + let openTabs = []; + + for (let tabBarItem of WI.tabBar.tabBarItems) { + let tabContentView = tabBarItem.representedObject; + if (!(tabContentView instanceof WI.TabContentView)) + continue; + if (!tabContentView.constructor.shouldSaveTab()) + continue; + console.assert(tabContentView.type, "Tab type can't be null, undefined, or empty string", tabContentView.type, tabContentView); + openTabs.push(tabContentView.type); + seenTabTypes.add(tabContentView.type); + } + + // Keep currently unsupported tabs in the setting at their previous index. + for (let {tabType, index} of WI._pendingOpenTabs) { + if (seenTabTypes.has(tabType)) + continue; + openTabs.insertAtIndex(tabType, index); + seenTabTypes.add(tabType); + } + + WI._openTabsSetting.value = openTabs; +}; + +WI._handleSettingsKeyboardShortcut = function(event) +{ + if (event.keyIdentifier === "U+002C") { // "," + WI.tabBrowser.showTabForContentView(WI._settingsTabContentView, { + initiatorHint: WI.TabBrowser.TabNavigationInitiator.KeyboardShortcut, + }); + } +}; + +WI._tryToRestorePendingTabs = function() +{ + let stillPendingOpenTabs = []; + for (let {tabType, index} of WI._pendingOpenTabs) { + if (!WI.isTabTypeAllowed(tabType)) { + stillPendingOpenTabs.push({tabType, index}); + continue; + } + + let tabContentView = WI._createTabContentViewForType(tabType); + if (!tabContentView) + continue; + + WI.tabBrowser.addTabForContentView(tabContentView, { + suppressAnimations: true, + insertionIndex: index, + }); + + tabContentView.restoreStateFromCookie(WI.StateRestorationType.Load); + } + + WI._pendingOpenTabs = stillPendingOpenTabs; +}; + +WI.isNewTabWithTypeAllowed = function(tabType) +{ + if (tabType === WI.WebInspectorExtensionTabContentView.Type) + return false; + + let tabClass = WI._knownTabClassesByType.get(tabType); + if (!tabClass || !tabClass.isTabAllowed()) + return false; + + // Only allow one tab per class for now. + for (let tabBarItem of WI.tabBar.tabBarItems) { + let tabContentView = tabBarItem.representedObject; + if (!(tabContentView instanceof WI.TabContentView)) + continue; + if (tabContentView.constructor === tabClass) + return false; + } + + return true; +}; + +WI.createNewTabWithType = function(tabType, options = {}) +{ + console.assert(WI.isNewTabWithTypeAllowed(tabType)); + + let {referencedView, shouldReplaceTab, shouldShowNewTab} = options; + console.assert(!referencedView || referencedView instanceof WI.TabContentView, referencedView); + console.assert(!shouldReplaceTab || referencedView, "Must provide a reference view to replace a tab."); + + let tabContentView = WI._createTabContentViewForType(tabType); + const suppressAnimations = true; + WI.tabBrowser.addTabForContentView(tabContentView, { + suppressAnimations, + insertionIndex: referencedView ? WI.tabBar.tabBarItems.indexOf(referencedView.tabBarItem) : undefined, + }); + + if (shouldReplaceTab) + WI.tabBrowser.closeTabForContentView(referencedView, {...options, suppressAnimations}); + + if (shouldShowNewTab) + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +// COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type +WI.activateExtraDomains = function(domains) +{ + WI.notifications.dispatchEventToListeners(WI.Notification.ExtraDomainsActivated, {domains}); + + if (WI.mainTarget) { + if (!WI.pageTarget && WI.mainTarget.hasDomain("DOM")) + WI.pageTarget = WI.mainTarget; + + if (WI.mainTarget.hasDomain("CSS")) + WI.cssManager.initializeCSSPropertyNameCompletions(WI.assumingMainTarget()); + + if (WI.mainTarget.hasDomain("DOM")) + WI.domManager.ensureDocument(); + + if (WI.mainTarget.hasDomain("Page")) + WI.networkManager.initializeTarget(WI.mainTarget); + } + + WI._updateInspectModeTabBarButton(); + WI._updateDownloadTabBarButton(); + WI._updateReloadTabBarButton(); + + WI._tryToRestorePendingTabs(); +}; + +WI.updateWindowTitle = function() +{ + var mainFrame = WI.networkManager.mainFrame; + if (!mainFrame) + return; + + var urlComponents = mainFrame.mainResource.urlComponents; + + var lastPathComponent; + try { + lastPathComponent = decodeURIComponent(urlComponents.lastPathComponent || ""); + } catch { + lastPathComponent = urlComponents.lastPathComponent; + } + + // Build a title based on the URL components. + if (urlComponents.host && lastPathComponent) + var title = WI.displayNameForHost(urlComponents.host) + " \u2014 " + lastPathComponent; + else if (urlComponents.host) + var title = WI.displayNameForHost(urlComponents.host); + else if (lastPathComponent) + var title = lastPathComponent; + else + var title = mainFrame.url; + + // The name "inspectedURLChanged" sounds like the whole URL is required, however WI is only + // used for updating the window title and it can be any string. + InspectorFrontendHost.inspectedURLChanged(title); +}; + +WI.updateDockingAvailability = function(available) +{ + WI._dockingAvailable = available; + + WI._updateDockNavigationItems(); + + if (!WI._dockingAvailable) + WI.updateDockedState(WI.DockConfiguration.Undocked); +}; + +WI.updateDockedState = function(side) +{ + if (WI.dockConfiguration === side) + return; + + WI._previousDockConfiguration = WI.dockConfiguration; + + if (!WI._previousDockConfiguration) { + if (side === WI.DockConfiguration.Right || side === WI.DockConfiguration.Left) + WI._previousDockConfiguration = WI.DockConfiguration.Bottom; + else + WI._previousDockConfiguration = WI.resolvedLayoutDirection() === WI.LayoutDirection.RTL ? WI.DockConfiguration.Left : WI.DockConfiguration.Right; + } + + WI.dockConfiguration = side; + + switch (WI.dockConfiguration) { + case WI.DockConfiguration.Bottom: + document.body.classList.add("docked", WI.DockConfiguration.Bottom); + document.body.classList.remove("window-inactive", WI.DockConfiguration.Right, WI.DockConfiguration.Left); + break; + + case WI.DockConfiguration.Right: + document.body.classList.add("docked", WI.DockConfiguration.Right); + document.body.classList.remove("window-inactive", WI.DockConfiguration.Bottom, WI.DockConfiguration.Left); + break; + + case WI.DockConfiguration.Left: + document.body.classList.add("docked", WI.DockConfiguration.Left); + document.body.classList.remove("window-inactive", WI.DockConfiguration.Bottom, WI.DockConfiguration.Right); + break; + + default: + document.body.classList.remove("docked", WI.DockConfiguration.Right, WI.DockConfiguration.Left, WI.DockConfiguration.Bottom); + break; + } + + WI._updateDockNavigationItems(); + + if (!WI.dockedConfigurationSupportsSplitContentBrowser() && !WI.doesCurrentTabSupportSplitContentBrowser()) + WI.hideSplitConsole(); + + if (side === WI.DockConfiguration.Undocked && WI.Platform.name === "mac") { + // When undocking, the first visible focusable element steals focus. Undo this. + document.body.addEventListener("focusin", function(event) { + let firstFocusableElement = document.querySelector("[tabindex='0']:not(.hidden)"); + if (firstFocusableElement === event.target) { + if (WI.previousFocusElement) + WI.previousFocusElement.focus(); + else + event.target.blur(); + } + }, {once: true, capture: true}); + } +}; + +WI.resizeDockedFrameMouseDown = function(event) +{ + console.assert(WI.dockConfiguration && WI.dockConfiguration !== WI.DockConfiguration.Undocked); + + if (event.button !== 0 || event.ctrlKey) + return; + + event[WI.Popover.EventPreventDismissSymbol] = true; + + let isDockedBottom = WI.dockConfiguration === WI.DockConfiguration.Bottom; + + let windowProperty = isDockedBottom ? "innerHeight" : "innerWidth"; + let eventScreenProperty = isDockedBottom ? "screenY" : "screenX"; + let eventClientProperty = isDockedBottom ? "clientY" : "clientX"; + + let resizerElement = event.target; + let firstClientPosition = event[eventClientProperty]; + let lastScreenPosition = event[eventScreenProperty]; + + function dividerDrag(event) + { + if (event.button !== 0) + return; + + let position = event[eventScreenProperty]; + let delta = position - lastScreenPosition; + let clientPosition = event[eventClientProperty]; + + lastScreenPosition = position; + + if (WI.dockConfiguration === WI.DockConfiguration.Left) { + // If the mouse is travelling rightward but is positioned left of the resizer, ignore the event. + if (delta > 0 && clientPosition < firstClientPosition) + return; + + // If the mouse is travelling leftward but is positioned to the right of the resizer, ignore the event. + if (delta < 0 && clientPosition > window[windowProperty]) + return; + + // We later subtract the delta from the current position, but since the inspected view and inspector view + // are flipped when docked to left, we want dragging to have the opposite effect from docked to right. + delta *= -1; + } else { + // If the mouse is travelling downward/rightward but is positioned above/left of the resizer, ignore the event. + if (delta > 0 && clientPosition < firstClientPosition) + return; + + // If the mouse is travelling upward/leftward but is positioned below/right of the resizer, ignore the event. + if (delta < 0 && clientPosition > firstClientPosition) + return; + } + + let dimension = Math.max(0, window[windowProperty] - delta); + // If zoomed in/out, there be greater/fewer document pixels shown, but the inspector's + // width or height should be the same in device pixels regardless of the document zoom. + dimension *= WI.getZoomFactor(); + + if (isDockedBottom) + InspectorFrontendHost.setAttachedWindowHeight(dimension); + else + InspectorFrontendHost.setAttachedWindowWidth(dimension); + } + + function elementDragEnd(event) + { + if (event.button !== 0) + return; + + WI.elementDragEnd(event); + } + + let cursor = isDockedBottom ? "row-resize" : "col-resize"; + WI.elementDragStart(resizerElement, dividerDrag, elementDragEnd, event, cursor); +}; + +WI.moveUndockedWindowMouseDown = function(event) +{ + console.assert(WI.dockConfiguration === WI.DockConfiguration.Undocked); + + if (event.button !== 0 || event.ctrlKey) + return; + + event[WI.Popover.EventPreventDismissSymbol] = true; + + if (WI.Platform.name === "mac") { + InspectorFrontendHost.startWindowDrag(); + event.preventDefault(); + return; + } + + let lastScreenX = event.screenX; + let lastScreenY = event.screenY; + + function dividerDrag(event) { + if (event.button !== 0) + return; + + let x = event.screenX - lastScreenX; + let y = event.screenY - lastScreenY; + + InspectorFrontendHost.moveWindowBy(x, y); + + lastScreenX = event.screenX; + lastScreenY = event.screenY; + } + + function elementDragEnd(event) { + if (event.button !== 0) + return; + + WI.elementDragEnd(event); + } + + const cursor = "default"; + WI.elementDragStart(event.target, dividerDrag, elementDragEnd, event, cursor); +}; + +WI.updateVisibilityState = function(visible) +{ + WI.visible = visible; + WI.notifications.dispatchEventToListeners(WI.Notification.VisibilityStateDidChange); +}; + +WI.updateFindString = function(findString) +{ + if (!findString || WI.findString === findString) + return false; + + WI.findString = findString; + + return true; +}; + +WI.handlePossibleLinkClick = function(event, options = {}) +{ + let anchorElement = event.target.closest("a"); + if (!anchorElement || !anchorElement.href) + return false; + + if (WI.isBeingEdited(anchorElement)) { + // Don't follow the link when it is being edited. + return false; + } + + // Prevent the link from navigating, since we don't do any navigation by following links normally. + event.preventDefault(); + event.stopPropagation(); + + WI.openURL(anchorElement.href, { + ...options, + lineNumber: anchorElement.lineNumber, + ignoreSearchTab: !WI.isShowingSearchTab(), + }); + + return true; +}; + +WI.openURL = function(url, {alwaysOpenExternally, frame, ...options} = {}) +{ + console.assert(url); + if (!url) + return; + + // If alwaysOpenExternally is not defined, base it off the command/meta key for the current event. + if (alwaysOpenExternally === undefined || alwaysOpenExternally === null) + alwaysOpenExternally = window.event?.metaKey ?? false; + + if (alwaysOpenExternally) { + InspectorFrontendHost.openURLExternally(url); + return; + } + + let searchChildFrames = false; + if (!frame) { + frame = WI.networkManager.mainFrame; + searchChildFrames = true; + } + + let resource; + let simplifiedURL = removeURLFragment(url); + if (frame) { + // WI.Frame.resourceForURL does not check the main resource, only sub-resources. So check both. + resource = frame.url === simplifiedURL ? frame.mainResource : frame.resourcesForURL(simplifiedURL, searchChildFrames).firstValue; + } else if (WI.sharedApp.debuggableType === WI.DebuggableType.ServiceWorker) + resource = WI.mainTarget.resourceCollection.resourcesForURL(removeURLFragment(url)).firstValue; + + if (resource) { + // Context menu selections may go through this code path; don't clobber the previously-set hint. + if (!options.initiatorHint) + options.initiatorHint = WI.TabBrowser.TabNavigationInitiator.LinkClick; + + console.assert(typeof options.lineNumber === "undefined" || typeof options.lineNumber === "number"); + let positionToReveal = new WI.SourceCodePosition(options.lineNumber, 0); + WI.showSourceCode(resource, {...options, positionToReveal}); + return; + } + + InspectorFrontendHost.openURLExternally(url); +}; + +WI.close = function() +{ + if (WI._isClosing) + return; + + WI._isClosing = true; + + InspectorFrontendHost.closeWindow(); +}; + +WI.isContentAreaFocused = function() +{ + return WI._contentElement.contains(document.activeElement); +} + +WI.isConsoleFocused = function() +{ + return !WI._didAutofocusConsolePrompt && WI.quickConsole.prompt.focused; +}; + +WI.isShowingSplitConsole = function() +{ + return !WI.consoleDrawer.collapsed; +}; + +WI.dockedConfigurationSupportsSplitContentBrowser = function() +{ + return !WI.dockConfiguration || WI.dockConfiguration !== WI.DockConfiguration.Bottom; +}; + +WI.doesCurrentTabSupportSplitContentBrowser = function() +{ + var currentContentView = WI.tabBrowser.selectedTabContentView; + return !currentContentView || currentContentView.supportsSplitContentBrowser; +}; + +WI.toggleSplitConsole = function() +{ + if (!WI.doesCurrentTabSupportSplitContentBrowser()) { + WI.showConsoleTab(); + return; + } + + if (WI.isShowingSplitConsole()) + WI.hideSplitConsole(); + else + WI.showSplitConsole(); +}; + +WI.showSplitConsole = function() +{ + if (!WI.doesCurrentTabSupportSplitContentBrowser()) { + WI.showConsoleTab(); + return; + } + + WI.consoleDrawer.collapsed = false; + + if (WI.consoleDrawer.currentContentView === WI.consoleContentView) + return; + + WI.consoleDrawer.showContentView(WI.consoleContentView); +}; + +WI.hideSplitConsole = function() +{ + if (!WI.isShowingSplitConsole()) + return; + + WI.consoleDrawer.collapsed = true; +}; + +WI.showConsoleTab = function(requestedScope, options = {}) +{ + requestedScope = requestedScope || WI.LogContentView.Scopes.All; + + WI.hideSplitConsole(); + + WI.consoleContentView.scopeBar.item(requestedScope).selected = true; + + const cookie = null; + WI.showRepresentedObject(WI._consoleRepresentedObject, cookie, options); + + console.assert(WI.isShowingConsoleTab()); +}; + +WI.isShowingConsoleTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.ConsoleTabContentView; +}; + +WI.showElementsTab = function(options = {}) +{ + var tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.ElementsTabContentView); + if (!tabContentView) + tabContentView = new WI.ElementsTabContentView; + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.isShowingElementsTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.ElementsTabContentView; +}; + +WI.showSourcesTab = function(options = {}) +{ + let tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.SourcesTabContentView); + if (!tabContentView) + tabContentView = new WI.SourcesTabContentView; + + if (options.representedObjectToSelect) + tabContentView.revealAndSelectRepresentedObject(options.representedObjectToSelect); + + if (options.showScopeChainSidebar) + tabContentView.showScopeChainDetailsSidebarPanel(); + + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.isShowingSourcesTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.SourcesTabContentView; +}; + +WI.showStorageTab = function(options = {}) +{ + var tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.StorageTabContentView); + if (!tabContentView) + tabContentView = new WI.StorageTabContentView; + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.showNetworkTab = function(options = {}) +{ + let tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.NetworkTabContentView); + if (!tabContentView) + tabContentView = new WI.NetworkTabContentView; + + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.isShowingNetworkTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.NetworkTabContentView; +}; + +WI.isShowingSearchTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.SearchTabContentView; +}; + +WI.showTimelineTab = function(options = {}) +{ + var tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.TimelineTabContentView); + if (!tabContentView) + tabContentView = new WI.TimelineTabContentView; + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.isShowingTimelineTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.TimelineTabContentView; +}; + +WI.isShowingAuditTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.AuditTabContentView; +} + +WI.showLayersTab = function(options = {}) +{ + let tabContentView = WI.tabBrowser.bestTabContentViewForClass(WI.LayersTabContentView); + if (!tabContentView) + tabContentView = new WI.LayersTabContentView; + if (options.nodeToSelect) + tabContentView.selectLayerForNode(options.nodeToSelect); + WI.tabBrowser.showTabForContentView(tabContentView, options); +}; + +WI.isShowingLayersTab = function() +{ + return WI.tabBrowser.selectedTabContentView instanceof WI.LayersTabContentView; +}; + +WI.showSettingsTab = function(options = {}) +{ + WI.tabBrowser.showTabForContentView(WI._settingsTabContentView, options); + + if (options.blackboxPatternToSelect) + WI._settingsTabContentView.selectBlackboxPattern(options.blackboxPatternToSelect); +}; + +WI.indentString = function() +{ + if (WI.settings.indentWithTabs.value) + return "\t"; + return " ".repeat(WI.settings.indentUnit.value); +}; + +WI.restoreFocusFromElement = function(element) +{ + if (element && element.contains(WI.currentFocusElement)) + WI.previousFocusElement.focus(); +}; + +WI.toggleNavigationSidebar = function(event) +{ + if (!WI.navigationSidebar.collapsed || !WI.navigationSidebar.sidebarPanels.length) { + WI.navigationSidebar.collapsed = true; + return; + } + + if (!WI.navigationSidebar.selectedSidebarPanel) + WI.navigationSidebar.selectedSidebarPanel = WI.navigationSidebar.sidebarPanels[0]; + WI.navigationSidebar.collapsed = false; +}; + +WI.toggleDetailsSidebar = function(event) +{ + if (!WI.detailsSidebar.collapsed || !WI.detailsSidebar.sidebarPanels.length) { + WI.detailsSidebar.collapsed = true; + return; + } + + if (!WI.detailsSidebar.selectedSidebarPanel) + WI.detailsSidebar.selectedSidebarPanel = WI.detailsSidebar.sidebarPanels[0]; + WI.detailsSidebar.collapsed = false; +}; + +WI.getMaximumSidebarWidth = function(sidebar) +{ + console.assert(sidebar instanceof WI.Sidebar); + + const minimumContentBrowserWidth = 200; // Keep in sync with `#tab-browser` + + let minimumWidth = window.innerWidth - minimumContentBrowserWidth; + let tabContentView = WI.tabBrowser.selectedTabContentView; + console.assert(tabContentView); + if (!tabContentView) + return minimumWidth; + + if (tabContentView.navigationSidebarPanel && sidebar !== WI.navigationSidebar) + minimumWidth -= WI.navigationSidebar.width; + + if (tabContentView.detailsSidebarPanels && sidebar !== WI.detailsSidebar) { + // A sidebar within the detailsSidebar needs the minimum width of its sibilings. + for (let singleDetailsSidebar of WI.detailsSidebar.sidebars) { + if (sidebar !== singleDetailsSidebar) + minimumWidth -= singleDetailsSidebar.width; + } + } + + return minimumWidth; +}; + +WI.tabContentViewClassForRepresentedObject = function(representedObject) +{ + if (representedObject instanceof WI.DOMTree) + return WI.ElementsTabContentView; + + if (representedObject instanceof WI.TimelineRecording) + return WI.TimelineTabContentView; + + // We only support one console tab right now. So WI isn't an instanceof check. + if (representedObject === WI._consoleRepresentedObject) + return WI.ConsoleTabContentView; + + if (representedObject instanceof WI.Frame + || representedObject instanceof WI.FrameCollection + || representedObject instanceof WI.Resource + || representedObject instanceof WI.ResourceCollection + || representedObject instanceof WI.Script + || representedObject instanceof WI.ScriptCollection + || representedObject instanceof WI.CSSStyleSheet + || representedObject instanceof WI.CSSStyleSheetCollection) + return WI.SourcesTabContentView; + + if (representedObject instanceof WI.DOMStorageObject || representedObject instanceof WI.CookieStorageObject || + representedObject instanceof WI.DatabaseTableObject || representedObject instanceof WI.DatabaseObject || + representedObject instanceof WI.ApplicationCacheFrame || representedObject instanceof WI.IndexedDatabaseObjectStore || + representedObject instanceof WI.IndexedDatabase || representedObject instanceof WI.IndexedDatabaseObjectStoreIndex) + return WI.StorageTabContentView; + + if (representedObject instanceof WI.AuditTestCase || representedObject instanceof WI.AuditTestGroup + || representedObject instanceof WI.AuditTestCaseResult || representedObject instanceof WI.AuditTestGroupResult) + return WI.AuditTabContentView; + + if (representedObject instanceof WI.CanvasCollection + || representedObject instanceof WI.Canvas + || representedObject instanceof WI.Recording + || representedObject instanceof WI.ShaderProgram + || representedObject instanceof WI.AnimationCollection + || representedObject instanceof WI.Animation) + return WI.GraphicsTabContentView; + + return null; +}; + +WI.tabContentViewForRepresentedObject = function(representedObject, options = {}) +{ + let tabContentView = WI.tabBrowser.bestTabContentViewForRepresentedObject(representedObject, options); + if (tabContentView) + return tabContentView; + + var tabContentViewClass = WI.tabContentViewClassForRepresentedObject(representedObject); + if (!tabContentViewClass) { + console.error("Unknown representedObject, couldn't create TabContentView.", representedObject); + return null; + } + + tabContentView = new tabContentViewClass; + + WI.tabBrowser.addTabForContentView(tabContentView); + + return tabContentView; +}; + +WI.showRepresentedObject = function(representedObject, cookie, options = {}) +{ + let tabContentView = WI.tabContentViewForRepresentedObject(representedObject, options); + console.assert(tabContentView); + if (!tabContentView) + return; + + WI.tabBrowser.showTabForContentView(tabContentView, options); + tabContentView.showRepresentedObject(representedObject, cookie); +}; + +WI.showLocalResourceOverride = function(localResourceOverride, options = {}) +{ + console.assert(localResourceOverride instanceof WI.LocalResourceOverride); + + let cookie = {preventHighlight: true}; + + switch (localResourceOverride.type) { + case WI.LocalResourceOverride.InterceptType.Response: + case WI.LocalResourceOverride.InterceptType.ResponseSkippingNetwork: + if (options.overriddenResource) { + const onlyExisting = true; + let contentView = WI.ContentView.contentViewForRepresentedObject(options.overriddenResource, onlyExisting); + + let textEditor = null; + if (contentView instanceof WI.ResourceClusterContentView) + contentView = contentView.responseContentView; + if (contentView instanceof WI.TextResourceContentView) + textEditor = contentView.textEditor; + + if (textEditor) { + let selectedTextRange = textEditor.selectedTextRange; + cookie.startLine = selectedTextRange.startLine; + cookie.startColumn = selectedTextRange.startColumn; + cookie.endLine = selectedTextRange.endLine; + cookie.endColumn = selectedTextRange.endColumn; + + let scrollOffset = textEditor.scrollOffset; + cookie.scrollOffsetX = scrollOffset.x; + cookie.scrollOffsetY = scrollOffset.y; + } + } + break; + } + + WI.showRepresentedObject(localResourceOverride, cookie, {...options, ignoreNetworkTab: true, ignoreSearchTab: true}); +}; + +WI.showMainFrameDOMTree = function(nodeToSelect, options = {}) +{ + console.assert(WI.networkManager.mainFrame); + if (!WI.networkManager.mainFrame) + return; + WI.showRepresentedObject(WI.networkManager.mainFrame.domTree, {nodeToSelect}, options); +}; + +WI.showSourceCodeForFrame = function(frameIdentifier, options = {}) +{ + var frame = WI.networkManager.frameForIdentifier(frameIdentifier); + if (!frame) { + WI._frameIdentifierToShowSourceCodeWhenAvailable = frameIdentifier; + return; + } + + WI._frameIdentifierToShowSourceCodeWhenAvailable = undefined; + + const cookie = null; + WI.showRepresentedObject(frame, cookie, options); +}; + +WI.showSourceCode = function(sourceCode, options = {}) +{ + const positionToReveal = options.positionToReveal; + + console.assert(!positionToReveal || positionToReveal instanceof WI.SourceCodePosition, positionToReveal); + var representedObject = sourceCode; + + if (representedObject instanceof WI.Script) { + // A script represented by a resource should always show the resource. + representedObject = representedObject.resource || representedObject; + } + + var cookie = positionToReveal ? {lineNumber: positionToReveal.lineNumber, columnNumber: positionToReveal.columnNumber} : {}; + WI.showRepresentedObject(representedObject, cookie, options); +}; + +WI.showSourceCodeLocation = function(sourceCodeLocation, options = {}) +{ + WI.showSourceCode(sourceCodeLocation.displaySourceCode, { + ...options, + positionToReveal: sourceCodeLocation.displayPosition(), + }); +}; + +WI.showOriginalUnformattedSourceCodeLocation = function(sourceCodeLocation, options = {}) +{ + WI.showSourceCode(sourceCodeLocation.sourceCode, { + ...options, + positionToReveal: sourceCodeLocation.position(), + forceUnformatted: true, + }); +}; + +WI.showOriginalOrFormattedSourceCodeLocation = function(sourceCodeLocation, options = {}) +{ + WI.showSourceCode(sourceCodeLocation.sourceCode, { + ...options, + positionToReveal: sourceCodeLocation.formattedPosition(), + }); +}; + +WI.showOriginalOrFormattedSourceCodeTextRange = function(sourceCodeTextRange, options = {}) +{ + var textRangeToSelect = sourceCodeTextRange.formattedTextRange; + WI.showSourceCode(sourceCodeTextRange.sourceCode, { + ...options, + positionToReveal: textRangeToSelect.startPosition(), + textRangeToSelect, + }); +}; + +WI.showResourceRequest = function(resource, options = {}) +{ + WI.showRepresentedObject(resource, {[WI.ResourceClusterContentView.ContentViewIdentifierCookieKey]: WI.ResourceClusterContentView.Identifier.Request}, options); +}; + +WI.debuggerToggleBreakpoints = function(event) +{ + WI.debuggerManager.breakpointsEnabled = !WI.debuggerManager.breakpointsEnabled; +}; + +WI.debuggerPauseResumeToggle = function(event) +{ + if (WI.debuggerManager.paused) + WI.debuggerManager.resume(); + else + WI.debuggerManager.pause(); +}; + +WI.debuggerStepNext = function(event) +{ + WI.debuggerManager.stepNext(); +}; + +WI.debuggerStepOver = function(event) +{ + WI.debuggerManager.stepOver(); +}; + +WI.debuggerStepInto = function(event) +{ + WI.debuggerManager.stepInto(); +}; + +WI.debuggerStepOut = function(event) +{ + WI.debuggerManager.stepOut(); +}; + +WI._focusSearchField = function(event) +{ + let searchQuery = ""; + + if (WI.settings.searchFromSelection.value) { + let selection = window.getSelection(); + if (selection.type === "Range" || !selection.isCollapsed) + searchQuery = selection.toString().removeWordBreakCharacters(); + } + + WI.tabBrowser.showTabForContentView(WI._searchTabContentView, { + // Classify this as a keyboard shortcut, as the only other way to get to Search Tab is via TabBar itself. + initiatorHint: WI.TabBrowser.TabNavigationInitiator.KeyboardShortcut, + }); + + WI._searchTabContentView.focusSearchField(); + + if (searchQuery) + WI._searchTabContentView.performSearch(searchQuery); +}; + +WI._focusChanged = function(event) +{ + WI._didAutofocusConsolePrompt = false; + + // Make a caret selection inside the focused element if there isn't a range selection and there isn't already + // a caret selection inside. This is needed (at least) to remove caret from console when focus is moved. + // The selection change should not apply to text fields and text areas either. + + if (WI.isEventTargetAnEditableField(event)) { + // Still update the currentFocusElement if inside of a CodeMirror editor or an input element. + let newFocusElement = null; + if (event.target instanceof HTMLInputElement || event.target instanceof HTMLTextAreaElement) + newFocusElement = event.target; + else { + let codeMirror = WI.enclosingCodeMirror(event.target); + if (codeMirror) { + let codeMirrorElement = codeMirror.getWrapperElement(); + if (codeMirrorElement && codeMirrorElement !== WI.currentFocusElement) + newFocusElement = codeMirrorElement; + } + } + + if (newFocusElement) { + WI.previousFocusElement = WI.currentFocusElement; + WI.currentFocusElement = newFocusElement; + } + + // Due to the change in WI.isEventTargetAnEditableField (r196271), WI return + // will also get run when WI.startEditing is called on an element. We do not want + // to return early in WI case, as WI.EditingConfig handles its own editing + // completion, so only return early if the focus change target is not from WI.startEditing. + if (!WI.isBeingEdited(event.target)) + return; + } + + var selection = window.getSelection(); + if (!selection.isCollapsed) + return; + + var element = event.target; + + if (element !== WI.currentFocusElement) { + WI.previousFocusElement = WI.currentFocusElement; + WI.currentFocusElement = element; + } + + if (element.isInsertionCaretInside()) + return; + + var selectionRange = element.ownerDocument.createRange(); + selectionRange.setStart(element, 0); + selectionRange.setEnd(element, 0); + + selection.removeAllRanges(); + selection.addRange(selectionRange); +}; + +WI._mouseWasClicked = function(event) +{ + WI.handlePossibleLinkClick(event); +}; + +WI._handleDragOver = function(event) +{ + // Do nothing if another event listener handled the event already. + if (event.defaultPrevented) + return; + + // Allow dropping into editable areas. + if (WI.isEventTargetAnEditableField(event)) + return; + + // Prevent the drop from being accepted. + event.dataTransfer.dropEffect = "none"; + event.preventDefault(); +}; + +WI._debuggerDidPause = function(event) +{ + WI.showSourcesTab({showScopeChainSidebar: WI.settings.showScopeChainOnPause.value}); + WI._updateDebuggerKeyboardShortcuts(); + + InspectorFrontendHost.bringToFront(); +}; + +WI._debuggerDidResume = function(event) +{ + WI._updateDebuggerKeyboardShortcuts(); +}; + +WI._updateDebuggerKeyboardShortcuts = function() +{ + let paused = WI.debuggerManager.paused; + + WI.stepOverKeyboardShortcut.disabled = !paused; + WI.stepIntoKeyboardShortcut.disabled = !paused; + WI.stepOutKeyboardShortcut.disabled = !paused; + WI.stepOverAlternateKeyboardShortcut.disabled = !paused; + WI.stepIntoAlternateKeyboardShortcut.disabled = !paused; + WI.stepOutAlternateKeyboardShortcut.disabled = !paused; + + // COMPATIBILITY (iOS 13.4): Debugger.stepNext did not exist. + if (InspectorBackend.hasCommand("Debugger.stepNext")) { + WI.stepNextKeyboardShortcut.disabled = !paused; + WI.stepNextAlternateKeyboardShortcut.disabled = !paused; + } +}; + +WI._frameWasAdded = function(event) +{ + if (!WI._frameIdentifierToShowSourceCodeWhenAvailable) + return; + + var frame = event.data.frame; + if (frame.id !== WI._frameIdentifierToShowSourceCodeWhenAvailable) + return; + + function delayedWork() + { + const options = { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }; + WI.showSourceCodeForFrame(frame.id, options); + } + + // Delay showing the frame since FrameWasAdded is called before MainFrameChanged. + // Calling showSourceCodeForFrame before MainFrameChanged will show the frame then close it. + setTimeout(delayedWork); +}; + +WI._mainFrameDidChange = function(event) +{ + WI._updateDownloadTabBarButton(); + + WI.updateWindowTitle(); +}; + +WI._mainResourceDidChange = function(event) +{ + if (!event.target.isMainFrame()) + return; + + // Run cookie restoration after we are sure all of the Tabs and NavigationSidebarPanels + // have updated with respect to the main resource change. + setTimeout(() => { + WI._restoreCookieForOpenTabs(WI.StateRestorationType.Navigation); + }); + + WI._updateDownloadTabBarButton(); + + WI.updateWindowTitle(); +}; + +WI._provisionalLoadStarted = function(event) +{ + if (!event.target.isMainFrame()) + return; + + WI._saveCookieForOpenTabs(); +}; + +WI._restoreCookieForOpenTabs = function(restorationType) +{ + for (var tabBarItem of WI.tabBar.tabBarItems) { + var tabContentView = tabBarItem.representedObject; + if (!(tabContentView instanceof WI.TabContentView)) + continue; + if (!tabContentView.constructor.shouldSaveTab()) + continue; + tabContentView.restoreStateFromCookie(restorationType); + } + + // Only attempt to autofocus when Web Inspector is first opened. + if (WI._didAutofocusConsolePrompt === undefined) { + window.requestAnimationFrame(() => { + if (WI.isContentAreaFocused() || WI.isShowingTimelineTab() || WI.isShowingAuditTab()) + return; + + WI.quickConsole.prompt.focus(); + WI._didAutofocusConsolePrompt = true; + }); + } +}; + +WI._saveCookieForOpenTabs = function() +{ + for (var tabBarItem of WI.tabBar.tabBarItems) { + var tabContentView = tabBarItem.representedObject; + if (!(tabContentView instanceof WI.TabContentView)) + continue; + if (!tabContentView.constructor.shouldSaveTab()) + continue; + tabContentView.saveStateToCookie(); + } +}; + +WI._updateWindowInactiveState = function(event) +{ + // FIXME: We should use the :window-inactive pseudo class once https://webkit.org/b/38927 is fixed. + + if (document.activeElement?.tagName === "IFRAME") { + // An active iframe means an extension tab is active and we can't tell when the window blurs due to cross-origin restrictions. + // In this case we need to keep checking to know if the window loses focus since there is no event we can use. + setTimeout(WI._updateWindowInactiveState, 250); + } + + let inactive = !document.hasFocus(); + document.body.classList.toggle(WI.dockConfiguration === WI.DockConfiguration.Undocked ? "window-inactive" : "window-docked-inactive", inactive); +}; + +WI._windowResized = function(event) +{ + WI.tabBar.updateLayout(WI.View.LayoutReason.Resize); + WI._tabBrowserSizeDidChange(); + WI._updateSheetRect(); +}; + +WI._updateSheetRect = function() +{ + let mainElementRect = document.getElementById("main").getBoundingClientRect(); + InspectorFrontendHost.setSheetRect(mainElementRect.x, mainElementRect.y, mainElementRect.width, mainElementRect.height); +}; + +WI._updateModifierKeys = function(event) +{ + let keys = { + altKey: event.altKey, + metaKey: event.metaKey, + ctrlKey: event.ctrlKey, + shiftKey: event.shiftKey, + }; + + let changed = !Object.shallowEqual(WI.modifierKeys, keys); + + WI.modifierKeys = keys; + + document.body.classList.toggle("alt-key-pressed", WI.modifierKeys.altKey); + document.body.classList.toggle("ctrl-key-pressed", WI.modifierKeys.ctrlKey); + document.body.classList.toggle("meta-key-pressed", WI.modifierKeys.metaKey); + document.body.classList.toggle("shift-key-pressed", WI.modifierKeys.shiftKey); + + if (changed) + WI.notifications.dispatchEventToListeners(WI.Notification.GlobalModifierKeysDidChange, event); +}; + +WI._windowKeyDown = function(event) +{ + WI._updateModifierKeys(event); +}; + +WI._windowKeyUp = function(event) +{ + WI._updateModifierKeys(event); +}; + +WI._mouseDown = function(event) +{ + if (event.button === 2) { // right-click + const contextMenuEvent = new MouseEvent("contextmenu", { + view: window, + bubbles: true, + cancelable: true, + screenX: event.pageX, + screenY: event.pageY, + clientX: event.pageX, + clientY: event.pageY, + ...event + }); + event.target.dispatchEvent(contextMenuEvent); + } +}; + +WI._mouseMoved = function(event) +{ + WI._updateModifierKeys(event); + WI.mouseCoords = { + x: event.pageX, + y: event.pageY + }; +}; + +WI._pageHidden = function(event) +{ + WI._saveCookieForOpenTabs(); +}; + +WI._contextMenuRequested = function(event) +{ + let proposedContextMenu; + + // This is setting is only defined in engineering builds. + if (WI.isDebugUIEnabled()) { + proposedContextMenu = WI.ContextMenu.createFromEvent(event); + proposedContextMenu.appendSeparator(); + proposedContextMenu.appendItem(WI.unlocalizedString("Reload Web Inspector"), () => { + InspectorFrontendHost.reopen(); + }); + + let protocolSubMenu = proposedContextMenu.appendSubMenuItem(WI.unlocalizedString("Protocol Debugging"), null, false); + let isCapturingTraffic = InspectorBackend.activeTracer instanceof WI.CapturingProtocolTracer; + + protocolSubMenu.appendCheckboxItem(WI.unlocalizedString("Capture Trace"), () => { + if (isCapturingTraffic) + InspectorBackend.activeTracer = null; + else + InspectorBackend.activeTracer = new WI.CapturingProtocolTracer; + }, isCapturingTraffic); + + let trace = InspectorBackend.activeTracer?.trace; + if (trace && WI.FileUtilities.canSave(trace.saveMode)) { + protocolSubMenu.appendSeparator(); + + protocolSubMenu.appendItem(WI.unlocalizedString("Export Trace\u2026"), () => { + const forceSaveAs = true; + WI.FileUtilities.save(trace.saveMode, trace.saveData, forceSaveAs); + }, !isCapturingTraffic); + } + } else { + const onlyExisting = true; + proposedContextMenu = WI.ContextMenu.createFromEvent(event, onlyExisting); + } + + if (proposedContextMenu) + proposedContextMenu.show(); +}; + +WI.isDebugUIEnabled = function() +{ + return WI.showDebugUISetting && WI.showDebugUISetting.value; +}; + +WI._undock = function(event) +{ + InspectorFrontendHost.requestSetDockSide(WI.DockConfiguration.Undocked); +}; + +WI._dockBottom = function(event) +{ + InspectorFrontendHost.requestSetDockSide(WI.DockConfiguration.Bottom); +}; + +WI._dockRight = function(event) +{ + InspectorFrontendHost.requestSetDockSide(WI.DockConfiguration.Right); +}; + +WI._dockLeft = function(event) +{ + InspectorFrontendHost.requestSetDockSide(WI.DockConfiguration.Left); +}; + +WI._togglePreviousDockConfiguration = function(event) +{ + InspectorFrontendHost.requestSetDockSide(WI._previousDockConfiguration); +}; + +WI._updateDockNavigationItems = function() +{ + let docked = WI.dockedConfigurationSupportsSplitContentBrowser && WI.dockConfiguration !== WI.DockConfiguration.Undocked; + + if (WI._dockingAvailable || docked) { + if (WI._closeTabBarButton) + WI._closeTabBarButton.hidden = !docked; + if (WI._dockLeftTabBarButton) + WI._dockLeftTabBarButton.hidden = WI.dockConfiguration === WI.DockConfiguration.Left; + if (WI._dockBottomTabBarButton) + WI._dockBottomTabBarButton.hidden = WI.dockConfiguration === WI.DockConfiguration.Bottom; + if (WI._dockRightTabBarButton) + WI._dockRightTabBarButton.hidden = WI.dockConfiguration === WI.DockConfiguration.Right; + if (WI._undockTabBarButton) + WI._undockTabBarButton.hidden = WI.dockConfiguration === WI.DockConfiguration.Undocked; + } else { + if (WI._closeTabBarButton) + WI._closeTabBarButton.hidden = true; + if (WI._dockLeftTabBarButton) + WI._dockLeftTabBarButton.hidden = true; + if (WI._dockBottomTabBarButton) + WI._dockBottomTabBarButton.hidden = true; + if (WI._dockRightTabBarButton) + WI._dockRightTabBarButton.hidden = true; + if (WI._undockTabBarButton) + WI._undockTabBarButton.hidden = true; + } + + WI._updateTabBarDividers(); + + WI.tabBar.resetCachedWidths(); +}; + +WI._tabBrowserSizeDidChange = function() +{ + WI.tabBrowser.updateLayout(WI.View.LayoutReason.Resize); + WI.consoleDrawer.updateLayout(WI.View.LayoutReason.Resize); + WI.quickConsole.updateLayout(WI.View.LayoutReason.Resize); +}; + +WI._consoleDrawerCollapsedStateDidChange = function(event) +{ + WI._showingSplitConsoleSetting.value = WI.isShowingSplitConsole(); + + WI._consoleDrawerDidResize(); +}; + +WI._consoleDrawerDidResize = function(event) +{ + WI.tabBrowser.updateLayout(WI.View.LayoutReason.Resize); +}; + +WI._sidebarWidthDidChange = function(event) +{ + WI._tabBrowserSizeDidChange(); +}; + +WI._setupViewHierarchy = function() +{ + let rootView = WI.View.rootView(); + rootView.addSubview(WI.tabBar); + rootView.addSubview(WI.navigationSidebar); + rootView.addSubview(WI.tabBrowser); + rootView.addSubview(WI.consoleDrawer); + rootView.addSubview(WI.quickConsole); + rootView.addSubview(WI.detailsSidebar); +}; + +WI._tabBrowserSelectedTabContentViewDidChange = function(event) +{ + let selectedTabBarItem = WI.tabBar.selectedTabBarItem; + if (selectedTabBarItem) { + WI._contentElement.ariaLabel = selectedTabBarItem.displayName || ""; + + if (selectedTabBarItem.representedObject.constructor.shouldSaveTab()) + WI._selectedTabIndexSetting.value = WI.tabBar.tabBarItems.indexOf(selectedTabBarItem); + } + + if (WI.doesCurrentTabSupportSplitContentBrowser()) { + if (WI._shouldRevealSpitConsoleIfSupported) { + WI._shouldRevealSpitConsoleIfSupported = false; + WI.showSplitConsole(); + } + return; + } + + WI._shouldRevealSpitConsoleIfSupported = WI.isShowingSplitConsole(); + WI.hideSplitConsole(); +}; + +WI._handleDockedResizerMouseDown = function(event) +{ + WI.resizeDockedFrameMouseDown(event); +}; + +WI._handleUndockedTitleAreaMouseDown = function(event) +{ + WI.moveUndockedWindowMouseDown(event); +}; + +WI._domStorageWasInspected = function(event) +{ + WI.showStorageTab({initiatorHint: WI.TabBrowser.TabNavigationInitiator.Inspect}); + WI.showRepresentedObject(event.data.domStorage, null, {ignoreSearchTab: true}); +}; + +WI._databaseWasInspected = function(event) +{ + WI.showStorageTab({initiatorHint: WI.TabBrowser.TabNavigationInitiator.Inspect}); + WI.showRepresentedObject(event.data.database, null, {ignoreSearchTab: true}); +}; + +WI._domNodeWasInspected = function(event) +{ + WI.domManager.highlightDOMNodeForTwoSeconds(event.data.node.id); + + InspectorFrontendHost.bringToFront(); + + // The event can override the initiator, in cases where the Inspect code path is used internally. + let options = { + initiatorHint: event.data.initiatorHint || WI.TabBrowser.TabNavigationInitiator.Inspect, + }; + WI.showElementsTab(options); + WI.showMainFrameDOMTree(event.data.node, {...options, ignoreSearchTab: true}); +}; + +WI._inspectModeStateChanged = function(event) +{ + WI._inspectModeTabBarButton.activated = WI.domManager.inspectModeEnabled; +}; + +WI._toggleInspectMode = function(event) +{ + WI.domManager.inspectModeEnabled = !WI.domManager.inspectModeEnabled; +}; + +WI._handleDeviceSettingsTabBarButtonClicked = function(event) +{ + if (WI._deviceSettingsPopover) { + WI._deviceSettingsPopover.dismiss(); + WI._deviceSettingsPopover = null; + return; + } + + let target = WI.assumingMainTarget(); + + function updateActivatedState() { + WI._deviceSettingsTabBarButton.activated = WI._overridenDeviceUserAgent || WI._overridenDeviceSettings.size > 0; + } + + function applyOverriddenUserAgent(value, force) { + if (value === WI._overridenDeviceUserAgent) + return; + + if (!force && (!value || value === "default")) { + target.PageAgent.overrideUserAgent((error) => { + if (error) { + console.error(error); + return; + } + + WI._overridenDeviceUserAgent = null; + updateActivatedState(); + target.PageAgent.reload(); + }); + } else { + target.PageAgent.overrideUserAgent(value, (error) => { + if (error) { + console.error(error); + return; + } + + WI._overridenDeviceUserAgent = value; + updateActivatedState(); + target.PageAgent.reload(); + }); + } + } + + function applyOverriddenSetting(setting, value, callback) { + if (WI._overridenDeviceSettings.has(setting)) { + // We've just "disabled" the checkbox, so clear the override instead of applying it. + target.PageAgent.overrideSetting(setting, (error) => { + if (error) { + console.error(error); + return; + } + + WI._overridenDeviceSettings.delete(setting); + callback(false); + updateActivatedState(); + }); + } else { + target.PageAgent.overrideSetting(setting, value, (error) => { + if (error) { + console.error(error); + return; + } + + WI._overridenDeviceSettings.set(setting, value); + callback(true); + updateActivatedState(); + }); + } + } + + function createCheckbox(container, label, setting, value) { + if (!setting) + return; + + let labelElement = container.appendChild(document.createElement("label")); + + let checkboxElement = labelElement.appendChild(document.createElement("input")); + checkboxElement.type = "checkbox"; + checkboxElement.checked = WI._overridenDeviceSettings.has(setting); + checkboxElement.addEventListener("change", (event) => { + applyOverriddenSetting(setting, value, (enabled) => { + checkboxElement.checked = enabled; + }); + }); + + labelElement.append(label); + } + + function calculateTargetFrame() { + return WI.Rect.rectFromClientRect(WI._deviceSettingsTabBarButton.element.getBoundingClientRect()).pad(2); + } + + const preferredEdges = [WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X]; + + WI._deviceSettingsPopover = new WI.Popover(WI); + WI._deviceSettingsPopover.windowResizeHandler = function(event) { + WI._deviceSettingsPopover.present(calculateTargetFrame(), preferredEdges); + }; + + let contentElement = document.createElement("div"); + contentElement.classList.add("device-settings-content"); + + let table = contentElement.appendChild(document.createElement("table")); + + // FIXME: webkit.org/b/247809 Enable user agent UI once the UI tracks engine-level changes to the current UA (e.g. via API) + if (InspectorFrontendHost.isRemote) { + let userAgentRow = table.appendChild(document.createElement("tr")); + + let userAgentTitle = userAgentRow.appendChild(document.createElement("td")); + userAgentTitle.textContent = WI.UIString("User Agent:"); + + let userAgentValue = userAgentRow.appendChild(document.createElement("td")); + userAgentValue.classList.add("user-agent"); + + let userAgentValueSelect = userAgentValue.appendChild(document.createElement("select")); + + let userAgentValueInput = null; + + const userAgents = [ + [ + { name: WI.UIString("Default"), value: "default" }, + ], + [ + { name: "Safari 16.0", value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15" }, + ], + [ + { name: `Safari ${emDash} iOS 16.0 ${emDash} iPhone`, value: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1" }, + { name: `Safari ${emDash} iPadOS 16.0 ${emDash} iPad mini`, value: "Mozilla/5.0 (iPad; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Mobile/15E148 Safari/604.1" }, + { name: `Safari ${emDash} iPadOS 16.0 ${emDash} iPad`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.0 Safari/605.1.15" }, + ], + [ + { name: `Microsoft Edge ${emDash} macOS`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37" }, + { name: `Microsoft Edge ${emDash} Windows`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36 Edg/103.0.1264.37" }, + ], + [ + { name: `Google Chrome ${emDash} macOS`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.5060.53 Safari/537.36" }, + { name: `Google Chrome ${emDash} Windows`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36" }, + ], + [ + { name: `Firefox ${emDash} macOS`, value: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:101.0) Gecko/20100101 Firefox/101.0" }, + { name: `Firefox ${emDash} Windows`, value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:101.0) Gecko/20100101 Firefox/101.0" }, + ], + [ + { name: WI.UIString("Other\u2026"), value: "other" }, + ], + ]; + + let selectedOptionElement = null; + + for (let group of userAgents) { + for (let {name, value} of group) { + let optionElement = userAgentValueSelect.appendChild(document.createElement("option")); + optionElement.value = value; + optionElement.textContent = name; + + if (value === WI._overridenDeviceUserAgent) + selectedOptionElement = optionElement; + } + + if (group !== userAgents.lastValue) + userAgentValueSelect.appendChild(document.createElement("hr")); + } + + function showUserAgentInput() { + if (userAgentValueInput) + return; + + userAgentValueInput = userAgentValue.appendChild(document.createElement("input")); + userAgentValueInput.spellcheck = false; + userAgentValueInput.value = userAgentValueInput.placeholder = WI._overridenDeviceUserAgent || navigator.userAgent; + userAgentValueInput.addEventListener("click", (clickEvent) => { + clickEvent.preventDefault(); + }); + userAgentValueInput.addEventListener("change", (inputEvent) => { + applyOverriddenUserAgent(userAgentValueInput.value, true); + }); + + WI._deviceSettingsPopover.update(); + } + + if (selectedOptionElement) + userAgentValueSelect.value = selectedOptionElement.value; + else if (WI._overridenDeviceUserAgent) { + userAgentValueSelect.value = "other"; + showUserAgentInput(); + } + + userAgentValueSelect.addEventListener("change", () => { + let value = userAgentValueSelect.value; + if (value === "other") { + showUserAgentInput(); + userAgentValueInput.select(); + } else { + if (userAgentValueInput) { + userAgentValueInput.remove(); + userAgentValueInput = null; + + WI._deviceSettingsPopover.update(); + } + + applyOverriddenUserAgent(value); + } + }); + } + + if (InspectorBackend.hasCommand("Page.setScreenSizeOverride")) { + function applyOverriddenScreenSize(value, force) { + if (value === WI._overridenDeviceScreenSize) + return; + + if (!force && (!value || value === "default")) { + target.PageAgent.setScreenSizeOverride((error) => { + if (error) { + WI.reportInternalError(error); + return; + } + + WI._overridenDeviceScreenSize = null; + updateActivatedState(); + target.PageAgent.reload(); + }); + } else { + let tokens = value.split("x"); + let width = parseInt(tokens[0]); + let height = parseInt(tokens[1]); + target.PageAgent.setScreenSizeOverride(width, height, (error) => { + if (error) { + WI.reportInternalError(error); + return; + } + + WI._overridenDeviceScreenSize = value; + updateActivatedState(); + target.PageAgent.reload(); + }); + } + } + + + let screenSizeRow = table.appendChild(document.createElement("tr")); + + let screenSizeTitle = screenSizeRow.appendChild(document.createElement("td")); + screenSizeTitle.textContent = WI.UIString("Screen size:"); + + let screenSizeValue = screenSizeRow.appendChild(document.createElement("td")); + screenSizeValue.classList.add("screen-size"); + + let screenSizeValueSelect = screenSizeValue.appendChild(document.createElement("select")); + + let screenSizeValueInput = null; + + const screenSizes = [ + [ + {name: WI.UIString("Default"), value: "default"}, + ], + [ + {name: WI.UIString("1080p"), value: "1920x1080"}, + {name: WI.UIString("720p"), value: "1280x720"}, + ], + [ + {name: WI.UIString("Other\u2026"), value: "other"}, + ], + ]; + + let selectedScreenSizeOptionElement = null; + + for (let group of screenSizes) { + for (let {name, value} of group) { + let optionElement = screenSizeValueSelect.appendChild(document.createElement("option")); + optionElement.value = value; + optionElement.textContent = name; + + if (value === WI._overridenDeviceScreenSize) + selectedScreenSizeOptionElement = optionElement; + } + + if (group !== screenSizes.lastValue) + screenSizeValueSelect.appendChild(document.createElement("hr")); + } + + function showScreenSizeInput() { + if (screenSizeValueInput) + return; + + screenSizeValueInput = screenSizeValue.appendChild(document.createElement("input")); + screenSizeValueInput.spellcheck = false; + screenSizeValueInput.value = screenSizeValueInput.placeholder = WI._overridenDeviceScreenSize || (window.screen.width + "x" + window.screen.height); + screenSizeValueInput.addEventListener("click", (clickEvent) => { + clickEvent.preventDefault(); + }); + screenSizeValueInput.addEventListener("change", (inputEvent) => { + applyOverriddenScreenSize(screenSizeValueInput.value, true); + }); + + WI._deviceSettingsPopover.update(); + } + + if (selectedScreenSizeOptionElement) + screenSizeValueSelect.value = selectedScreenSizeOptionElement.value; + else if (WI._overridenDeviceScreenSize) { + screenSizeValueSelect.value = "other"; + showScreenSizeInput(); + } + + screenSizeValueSelect.addEventListener("change", () => { + let value = screenSizeValueSelect.value; + if (value === "other") { + showScreenSizeInput(); + screenSizeValueInput.select(); + } else { + if (screenSizeValueInput) { + screenSizeValueInput.remove(); + screenSizeValueInput = null; + + WI._deviceSettingsPopover.update(); + } + + applyOverriddenScreenSize(value); + } + }); + } + + const settings = [ + { + name: WI.UIString("Disable:"), + columns: [ + [ + {name: WI.UIString("Images"), setting: InspectorBackend.Enum.Page.Setting.ImagesEnabled, value: false}, + {name: WI.UIString("Styles"), setting: InspectorBackend.Enum.Page.Setting.AuthorAndUserStylesEnabled, value: false}, + {name: WI.UIString("JavaScript"), setting: InspectorBackend.Enum.Page.Setting.ScriptEnabled, value: false}, + ], + [ + {name: WI.UIString("Site-specific Hacks"), setting: InspectorBackend.Enum.Page.Setting.NeedsSiteSpecificQuirks, value: false}, + {name: WI.UIString("Cross-Origin Restrictions"), setting: InspectorBackend.Enum.Page.Setting.WebSecurityEnabled, value: false}, + ] + ], + }, + { + name: WI.UIString("Enable:"), + columns: [ + [ + {name: WI.UIString("Intelligent Tracking Prevention Debug Mode"), setting: InspectorBackend.Enum.Page.Setting.ITPDebugModeEnabled, value: true}, + // COMPATIBILITY (iOS 14.0): `Page.Setting.AdClickAttributionDebugModeEnabled` was renamed to `Page.Setting.PrivateClickMeasurementDebugModeEnabled`. + {name: WI.UIString("Private Click Measurement Debug Mode"), setting: InspectorBackend.Enum.Page.Setting.PrivateClickMeasurementDebugModeEnabled, value: true}, + {name: WI.UIString("Ad Click Attribution Debug Mode"), setting: InspectorBackend.Enum.Page.Setting.AdClickAttributionDebugModeEnabled, value: true}, + ], + ], + }, + { + name: WI.UIString("%s:").format(WI.unlocalizedString("WebRTC")), + columns: [ + [ + {name: WI.UIString("Allow Media Capture on Insecure Sites"), setting: InspectorBackend.Enum.Page.Setting.MediaCaptureRequiresSecureConnection, value: false}, + {name: WI.UIString("Disable ICE Candidate Restrictions"), setting: InspectorBackend.Enum.Page.Setting.ICECandidateFilteringEnabled, value: false}, + {name: WI.UIString("Use Mock Capture Devices"), setting: InspectorBackend.Enum.Page.Setting.MockCaptureDevicesEnabled, value: true}, + {name: WI.UIString("Disable Encryption"), setting: InspectorBackend.Enum.Page.Setting.WebRTCEncryptionEnabled, value: false}, + ], + ], + }, + ]; + + for (let group of settings) { + if (!group.columns.some((column) => column.some((item) => item.setting))) + continue; + + let settingsGroupRow = table.appendChild(document.createElement("tr")); + + let settingsGroupTitle = settingsGroupRow.appendChild(document.createElement("td")); + settingsGroupTitle.textContent = group.name; + + let settingsGroupValue = settingsGroupRow.appendChild(document.createElement("td")); + + let settingsGroupItemContainer = settingsGroupValue.appendChild(document.createElement("div")); + settingsGroupItemContainer.classList.add("container"); + + for (let column of group.columns) { + let columnElement = settingsGroupItemContainer.appendChild(document.createElement("div")); + columnElement.classList.add("column"); + + for (let item of column) + createCheckbox(columnElement, item.name, item.setting, item.value); + } + } + + contentElement.appendChild(WI.ReferencePage.DeviceSettings.createLinkElement()); + + WI._deviceSettingsPopover.presentNewContentWithFrame(contentElement, calculateTargetFrame(), preferredEdges); +}; + +WI._downloadWebArchive = function(event) +{ + WI.archiveMainFrame(); +}; + +WI._reloadInspectedInspector = function() +{ + const options = {}; + WI.runtimeManager.evaluateInInspectedWindow(`InspectorFrontendHost.reopen()`, options, function(){}); +}; + +WI._reloadPage = function(event) +{ + let target = WI.assumingMainTarget(); + if (!target.hasDomain("Page")) + return; + + event.preventDefault(); + + if (InspectorFrontendHost.inspectionLevel > 1) { + WI._reloadInspectedInspector(); + return; + } + + target.PageAgent.reload(); +}; + +WI._reloadPageFromOrigin = function(event) +{ + let target = WI.assumingMainTarget(); + if (!target.hasDomain("Page")) + return; + + event.preventDefault(); + + if (InspectorFrontendHost.inspectionLevel > 1) { + WI._reloadInspectedInspector(); + return; + } + + target.PageAgent.reload.invoke({ignoreCache: true}); +}; + +WI._reloadTabBarButtonClicked = function(event) +{ + if (InspectorFrontendHost.inspectionLevel > 1) { + WI._reloadInspectedInspector(); + return; + } + + // Reload page from origin if the button is clicked while the shift key is pressed down. + let target = WI.assumingMainTarget(); + target.PageAgent.reload.invoke({ignoreCache: WI.modifierKeys.shiftKey}); +}; + +WI._updateReloadTabBarButton = function() +{ + if (!WI._reloadTabBarButton) + return; + + WI._reloadTabBarButton.hidden = !InspectorBackend.hasDomain("Page"); + WI._updateTabBarDividers(); +}; + +WI._updateDownloadTabBarButton = function() +{ + if (!WI._reloadTabBarButton) + return; + + if (!WI.FileUtilities.canSave(WI.FileUtilities.SaveMode.SingleFile) || !InspectorBackend.hasCommand("Page.archive")) { + WI._downloadTabBarButton.hidden = true; + WI._updateTabBarDividers(); + return; + } + + if (WI._downloadingPage) { + WI._downloadTabBarButton.enabled = false; + return; + } + + WI._downloadTabBarButton.enabled = WI.canArchiveMainFrame(); +}; + +WI._updateInspectModeTabBarButton = function() +{ + WI._inspectModeTabBarButton.hidden = !InspectorBackend.hasDomain("DOM"); + WI._updateTabBarDividers(); +}; + +WI._updateTabBarDividers = function() +{ + function isHidden(navigationItem) { + return !navigationItem || navigationItem.hidden; + } + + let closeHidden = isHidden(WI._closeTabBarButton); + let dockLeftHidden = isHidden(WI._dockLeftTabBarButton); + let dockBottomHidden = isHidden(WI._dockBottomTabBarButton); + let dockRightHidden = isHidden(WI._dockRightTabBarButton); + let undockHidden = isHidden(WI._undockTabBarButton); + + let inspectModeHidden = isHidden(WI._inspectModeTabBarButton); + let deviceSettingsHidden = isHidden(WI._deviceSettingsTabBarButton); + let reloadHidden = isHidden(WI._reloadTabBarButton); + let downloadHidden = isHidden(WI._downloadTabBarButton); + + let warningsHidden = WI._consoleWarningsTabBarButton.hidden; + let errorsHidden = WI._consoleErrorsTabBarButton.hidden; + + // Hide the divider if everything to the left is hidden OR if everything to the right is hidden. + WI._consoleDividerNavigationItem.hidden = (closeHidden && dockLeftHidden && dockBottomHidden && dockRightHidden && undockHidden && inspectModeHidden && deviceSettingsHidden && reloadHidden && downloadHidden) || (warningsHidden && errorsHidden); + + WI.tabBar.needsLayout(); +}; + +WI._updateConsoleTabBarButtons = function() +{ + function pulse(element) { + // We need to force a style invalidation in the case where we already + // were animating this item after we've removed the pulsing CSS class. + if (element.classList.contains("pulsing")) { + element.classList.remove("pulsing"); + element.recalculateStyles(); + } else { + element.addEventListener("animationend", () => { + element.classList.remove("pulsing"); + }, {once: true}); + } + + element.classList.add("pulsing"); + } + let warningCount = WI.consoleManager.warningCount; + if (warningCount) { + let warningFormat = (warningCount === 1) ? WI.UIString("Click to show %d warning in the Console") : WI.UIString("Click to show %d warnings in the Console"); + WI._consoleWarningsTabBarButton.tooltip = warningFormat.format(warningCount); + WI._consoleWarningsTabBarButton.hidden = false; + + if (warningCount !== WI._consoleWarningsTabBarButton.__lastWarningCount) + pulse(WI._consoleWarningsTabBarButton.element); + } else + WI._consoleWarningsTabBarButton.hidden = true; + + let errorCount = WI.consoleManager.errorCount; + if (errorCount) { + let errorFormat = (errorCount === 1) ? WI.UIString("Click to show %d error in the Console") : WI.UIString("Click to show %d errors in the Console"); + WI._consoleErrorsTabBarButton.tooltip = errorFormat.format(errorCount); + WI._consoleErrorsTabBarButton.hidden = false; + + if (errorCount !== WI._consoleErrorsTabBarButton.__lastErrorCount) + pulse(WI._consoleErrorsTabBarButton.element); + } else + WI._consoleErrorsTabBarButton.hidden = true; + + WI._updateTabBarDividers(); + + WI._consoleWarningsTabBarButton.__lastWarningCount = warningCount; + WI._consoleErrorsTabBarButton.__lastErrorCount = errorCount; +}; + +WI._showConsoleTab = function(event) +{ + WI.showConsoleTab(); +}; + +WI._focusConsolePrompt = function(event) +{ + WI.quickConsole.prompt.focus(); +}; + +WI._focusedContentBrowser = function() +{ + if (WI.currentFocusElement) { + let contentBrowserElement = WI.currentFocusElement.closest(".content-browser"); + if (contentBrowserElement && contentBrowserElement.__view && contentBrowserElement.__view instanceof WI.ContentBrowser) + return contentBrowserElement.__view; + } + + if (WI.tabBrowser.element.contains(WI.currentFocusElement) || document.activeElement === document.body) { + let tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView.contentBrowser) + return tabContentView.contentBrowser; + return null; + } + + if (WI.consoleDrawer.element.contains(WI.currentFocusElement) + || (WI.isShowingSplitConsole() && WI.quickConsole.element.contains(WI.currentFocusElement))) + return WI.consoleDrawer; + + return null; +}; + +WI._focusedContentView = function() +{ + if (WI.tabBrowser.element.contains(WI.currentFocusElement) || document.activeElement === document.body) { + var tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView.contentBrowser) + return tabContentView.contentBrowser.currentContentView; + return tabContentView; + } + + if (WI.consoleDrawer.element.contains(WI.currentFocusElement) + || (WI.isShowingSplitConsole() && WI.quickConsole.element.contains(WI.currentFocusElement))) + return WI.consoleDrawer.currentContentView; + + return null; +}; + +WI._focusedOrVisibleContentBrowser = function() +{ + let focusedContentBrowser = WI._focusedContentBrowser(); + if (focusedContentBrowser) + return focusedContentBrowser; + + var tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView.contentBrowser) + return tabContentView.contentBrowser; + + return null; +}; + +WI.focusedOrVisibleContentView = function() +{ + let focusedContentView = WI._focusedContentView(); + if (focusedContentView) + return focusedContentView; + + var tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView.contentBrowser) + return tabContentView.contentBrowser.currentContentView; + return tabContentView; +}; + +WI._beforecopy = function(event) +{ + var selection = window.getSelection(); + + // If there is no selection, see if the focused element or focused ContentView can handle the copy event. + if (selection.isCollapsed && !WI.isEventTargetAnEditableField(event)) { + var focusedCopyHandler = WI.currentFocusElement && WI.currentFocusElement.copyHandler; + if (focusedCopyHandler && typeof focusedCopyHandler.handleBeforeCopyEvent === "function") { + focusedCopyHandler.handleBeforeCopyEvent(event); + if (event.defaultPrevented) + return; + } + + var focusedContentView = WI._focusedContentView(); + if (focusedContentView && typeof focusedContentView.handleCopyEvent === "function") { + event.preventDefault(); + return; + } + + return; + } + + if (selection.isCollapsed) + return; + + // Say we can handle it (by preventing default) to remove word break characters. + event.preventDefault(); +}; + +WI._find = function(event) +{ + let tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView && tabContentView.canHandleFindEvent) { + tabContentView.handleFindEvent(event); + return; + } + + let contentBrowser = WI._focusedOrVisibleContentBrowser(); + if (!contentBrowser) + return; + + contentBrowser.showFindBanner(); +}; + +WI._save = function(event) +{ + var contentView = WI.focusedOrVisibleContentView(); + if (!contentView || !contentView.supportsSave) + return; + + if (!WI.FileUtilities.canSave(contentView.saveMode)) + return; + + WI.FileUtilities.save(contentView.saveMode, contentView.saveData); +}; + +WI._saveAs = function(event) +{ + var contentView = WI.focusedOrVisibleContentView(); + if (!contentView || !contentView.supportsSave) + return; + + if (!WI.FileUtilities.canSave(contentView.saveMode)) + return; + + WI.FileUtilities.save(contentView.saveMode, contentView.saveData, true); +}; + +WI._clear = function(event) +{ + let contentView = WI.focusedOrVisibleContentView(); + if (!contentView || typeof contentView.handleClearShortcut !== "function") { + // If the current content view is unable to handle WI event, clear the console to reset + // the dashboard counters. + WI.consoleManager.requestClearMessages(); + return; + } + + contentView.handleClearShortcut(event); +}; + +WI._populateFind = function(event) +{ + let focusedContentView = WI._focusedContentView(); + if (focusedContentView && focusedContentView.supportsCustomFindBanner) { + focusedContentView.handlePopulateFindShortcut(); + return; + } + + let contentBrowser = WI._focusedOrVisibleContentBrowser(); + if (contentBrowser) { + contentBrowser.handlePopulateFindShortcut(); + return; + } +}; + +WI._findNext = function(event) +{ + let focusedContentView = WI._focusedContentView(); + if (focusedContentView?.supportsCustomFindBanner) { + focusedContentView.handleFindNextShortcut(); + return; + } + + let contentBrowser = WI._focusedOrVisibleContentBrowser(); + if (contentBrowser) { + contentBrowser.handleFindNextShortcut(); + return; + } +}; + +WI._findPrevious = function(event) +{ + let focusedContentView = WI._focusedContentView(); + if (focusedContentView?.supportsCustomFindBanner) { + focusedContentView.handleFindPreviousShortcut(); + return; + } + + let contentBrowser = WI._focusedOrVisibleContentBrowser(); + if (contentBrowser) { + contentBrowser.handleFindPreviousShortcut(); + return; + } +}; + +WI._copy = function(event) +{ + var selection = window.getSelection(); + + // If there is no selection, pass the copy event on to the focused element or focused ContentView. + if (selection.isCollapsed && !WI.isEventTargetAnEditableField(event)) { + var focusedCopyHandler = WI.currentFocusElement && WI.currentFocusElement.copyHandler; + if (focusedCopyHandler && typeof focusedCopyHandler.handleCopyEvent === "function") { + focusedCopyHandler.handleCopyEvent(event); + if (event.defaultPrevented) + return; + } + + var focusedContentView = WI._focusedContentView(); + if (focusedContentView && typeof focusedContentView.handleCopyEvent === "function") { + focusedContentView.handleCopyEvent(event); + return; + } + + let tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView && typeof tabContentView.handleCopyEvent === "function") { + tabContentView.handleCopyEvent(event); + return; + } + + return; + } + + if (selection.isCollapsed) + return; + + // Remove word break characters from the selection before putting it on the pasteboard. + var selectionString = selection.toString().removeWordBreakCharacters(); + event.clipboardData.setData("text/plain", selectionString); + event.preventDefault(); +}; + +WI._paste = function(event) +{ + if (event.defaultPrevented) + return; + + let selection = window.getSelection(); + + // If there is no selection, pass the paste event on to the focused element or focused ContentView. + if (!selection.isCollapsed || WI.isEventTargetAnEditableField(event)) + return; + + let focusedPasteHandler = WI.currentFocusElement && WI.currentFocusElement.pasteHandler; + if (focusedPasteHandler && focusedPasteHandler.handlePasteEvent) { + focusedPasteHandler.handlePasteEvent(event); + if (event.defaultPrevented) + return; + } + + let focusedContentView = WI._focusedContentView(); + if (focusedContentView && focusedContentView.handlePasteEvent) { + focusedContentView.handlePasteEvent(event); + return; + } + + let tabContentView = WI.tabBrowser.selectedTabContentView; + if (tabContentView && tabContentView.handlePasteEvent) { + tabContentView.handlePasteEvent(event); + return; + } +}; + +WI._increaseZoom = function(event) +{ + const epsilon = 0.0001; + const maximumZoom = 2.4; + let currentZoom = WI.getZoomFactor(); + if (currentZoom + epsilon >= maximumZoom) { + InspectorFrontendHost.beep(); + return; + } + + WI.setZoomFactor(Math.min(maximumZoom, currentZoom + 0.2)); +}; + +WI._decreaseZoom = function(event) +{ + const epsilon = 0.0001; + const minimumZoom = 0.6; + let currentZoom = WI.getZoomFactor(); + if (currentZoom - epsilon <= minimumZoom) { + InspectorFrontendHost.beep(); + return; + } + + WI.setZoomFactor(Math.max(minimumZoom, currentZoom - 0.2)); +}; + +WI._resetZoom = function(event) +{ + WI.setZoomFactor(1); +}; + +WI.getZoomFactor = function() +{ + return WI.settings.zoomFactor.value; +}; + +WI.setZoomFactor = function(factor) +{ + InspectorFrontendHost.setZoomFactor(factor); + // Round-trip through the frontend host API in case the requested factor is not used. + WI.settings.zoomFactor.value = InspectorFrontendHost.zoomFactor(); +}; + +WI.resolvedLayoutDirection = function() +{ + let layoutDirection = WI.settings.debugLayoutDirection.value; + if (layoutDirection === WI.LayoutDirection.System) + layoutDirection = InspectorFrontendHost.userInterfaceLayoutDirection(); + return layoutDirection; +}; + +WI.resolveLayoutDirectionForElement = function(element) +{ + let layoutDirection = WI.resolvedLayoutDirection(); + + // Global LTR never includes RTL containers. Return early. + if (layoutDirection === WI.LayoutDirection.LTR) + return layoutDirection; + + let style = getComputedStyle(element); + return style.direction; +}; + +WI.setLayoutDirection = function(value) +{ + console.assert(WI.isDebugUIEnabled()); + + if (!Object.values(WI.LayoutDirection).includes(value)) + WI.reportInternalError("Unknown layout direction requested: " + value); + + if (value === WI.settings.debugLayoutDirection.value) + return; + + WI.settings.debugLayoutDirection.value = value; + + InspectorFrontendHost.reopen(); +}; + +WI.undockedTitleAreaHeight = function() +{ + if (WI.dockConfiguration !== WI.DockConfiguration.Undocked) + return 0; + + if (WI.Platform.name === "mac") { + switch (WI.Platform.version.name) { + case "monterey": + case "big-sur": + /* Keep in sync with `--undocked-title-area-height` CSS variable. */ + return 27 / WI.getZoomFactor(); + + case "catalina": + case "mojave": + /* Keep in sync with `--undocked-title-area-height` CSS variable. */ + return 22 / WI.getZoomFactor(); + } + } + + return 0; +}; + +WI._showTabAtIndexFromShortcut = function(i) +{ + if (i <= WI.tabBar.tabCount) { + WI.tabBar.selectTabBarItem(i - 1, { + initiatorHint: WI.TabBrowser.TabNavigationInitiator.KeyboardShortcut, + }); + } +}; + +WI._showJavaScriptTypeInformationSettingChanged = function(event) +{ + if (WI.settings.showJavaScriptTypeInformation.value) { + for (let target of WI.targets) + target.RuntimeAgent.enableTypeProfiler(); + } else { + for (let target of WI.targets) + target.RuntimeAgent.disableTypeProfiler(); + } +}; + +WI._enableControlFlowProfilerSettingChanged = function(event) +{ + if (WI.settings.enableControlFlowProfiler.value) { + for (let target of WI.targets) + target.RuntimeAgent.enableControlFlowProfiler(); + } else { + for (let target of WI.targets) + target.RuntimeAgent.disableControlFlowProfiler(); + } +}; + +WI._resourceCachingDisabledSettingChanged = function(event) +{ + for (let target of WI.targets) + target.NetworkAgent.setResourceCachingDisabled(WI.settings.resourceCachingDisabled.value); +}; + +WI._allowInspectingInspectorSettingChanged = function(event) +{ + InspectorFrontendHost.setAllowsInspectingInspector(WI.settings.experimentalAllowInspectingInspector.value); +} + +WI.measureElement = function(element) +{ + WI.layoutMeasurementContainer.appendChild(element.cloneNode(true)); + let rect = WI.layoutMeasurementContainer.getBoundingClientRect(); + WI.layoutMeasurementContainer.removeChildren(); + return rect; +}; + +WI.elementDragStart = function(element, dividerDrag, elementDragEnd, event, cursor, eventTarget) +{ + if (WI._elementDraggingEventListener || WI._elementEndDraggingEventListener) + WI.elementDragEnd(event); + + if (element) { + // Install glass pane + if (WI._elementDraggingGlassPane) + WI._elementDraggingGlassPane.remove(); + + var glassPane = document.createElement("div"); + glassPane.style.cssText = "position:absolute;top:0;bottom:0;left:0;right:0;opacity:0;z-index:1"; + glassPane.id = "glass-pane-for-drag"; + element.ownerDocument.body.appendChild(glassPane); + WI._elementDraggingGlassPane = glassPane; + } + + WI._elementDraggingEventListener = dividerDrag; + WI._elementEndDraggingEventListener = elementDragEnd; + + var targetDocument = event.target.ownerDocument; + + WI._elementDraggingEventTarget = eventTarget || targetDocument; + WI._elementDraggingEventTarget.addEventListener("mousemove", dividerDrag, true); + WI._elementDraggingEventTarget.addEventListener("mouseup", elementDragEnd, true); + + targetDocument.body.style.cursor = cursor; + + event.preventDefault(); +}; + +WI.elementDragEnd = function(event) +{ + WI._elementDraggingEventTarget.removeEventListener("mousemove", WI._elementDraggingEventListener, true); + WI._elementDraggingEventTarget.removeEventListener("mouseup", WI._elementEndDraggingEventListener, true); + + event.target.ownerDocument.body.style.removeProperty("cursor"); + + if (WI._elementDraggingGlassPane) + WI._elementDraggingGlassPane.remove(); + + delete WI._elementDraggingGlassPane; + delete WI._elementDraggingEventTarget; + delete WI._elementDraggingEventListener; + delete WI._elementEndDraggingEventListener; + + event.preventDefault(); +}; + +WI.createMessageTextView = function(message, isError) +{ + let messageElement = document.createElement("div"); + messageElement.className = "message-text-view"; + if (isError) + messageElement.classList.add("error"); + + let textElement = messageElement.appendChild(document.createElement("div")); + textElement.className = "message"; + textElement.append(message); + + return messageElement; +}; + +WI.createNavigationItemHelp = function(formatString, navigationItem) +{ + console.assert(typeof formatString === "string"); + console.assert(navigationItem instanceof WI.ButtonNavigationItem); + + function append(a, b) { + a.append(b); + return a; + } + + let containerElement = document.createElement("div"); + containerElement.className = "navigation-item-help"; + containerElement.__navigationItem = navigationItem; + + let wrapperElement = document.createElement("div"); + wrapperElement.className = "navigation-bar"; + wrapperElement.appendChild(navigationItem.element); + + String.format(formatString, [wrapperElement], String.standardFormatters, containerElement, append); + return containerElement; +}; + +WI.createGoToArrowButton = function() +{ + var button = document.createElement("button"); + button.addEventListener("mousedown", (event) => { event.stopPropagation(); }, true); + button.className = "go-to-arrow"; + button.tabIndex = -1; + return button; +}; + +WI.createSourceCodeLocationLink = function(sourceCodeLocation, options = {}) +{ + console.assert(sourceCodeLocation); + if (!sourceCodeLocation) + return null; + + var linkElement = document.createElement("a"); + linkElement.className = "go-to-link"; + WI.linkifyElement(linkElement, sourceCodeLocation, options); + sourceCodeLocation.populateLiveDisplayLocationTooltip(linkElement); + + if (options.useGoToArrowButton) + linkElement.appendChild(WI.createGoToArrowButton()); + else + sourceCodeLocation.populateLiveDisplayLocationString(linkElement, "textContent", options.columnStyle, options.nameStyle, options.prefix); + + if (options.dontFloat) + linkElement.classList.add("dont-float"); + + return linkElement; +}; + +WI.linkifyLocation = function(url, sourceCodePosition, options = {}) +{ + var sourceCode = WI.sourceCodeForURL(url); + if (sourceCode) + return WI.linkifySourceCode(sourceCode, sourceCodePosition, options); + + var anchor = document.createElement("a"); + anchor.href = url; + anchor.lineNumber = sourceCodePosition.lineNumber; + if (options.className) + anchor.className = options.className; + anchor.append(WI.displayNameForURL(url) + ":" + sourceCodePosition.lineNumber); + return anchor; +}; + +WI.linkifySourceCode = function(sourceCode, sourceCodePosition, options = {}) +{ + let sourceCodeLocation = sourceCode.createSourceCodeLocation(sourceCodePosition.lineNumber, sourceCodePosition.columnNumber); + let linkElement = WI.createSourceCodeLocationLink(sourceCodeLocation, { + ...options, + dontFloat: true, + }); + + if (options.className) + linkElement.classList.add(options.className); + + return linkElement; +}; + +WI.linkifyElement = function(linkElement, sourceCodeLocation, options = {}) { + console.assert(sourceCodeLocation); + + if (!options.initiatorHint) + options.initiatorHint = WI.TabBrowser.TabNavigationInitiator.LinkClick; + + function showSourceCodeLocation(event) + { + event.stopPropagation(); + event.preventDefault(); + + if (event.metaKey) + WI.showOriginalUnformattedSourceCodeLocation(sourceCodeLocation, options); + else + WI.showSourceCodeLocation(sourceCodeLocation, options); + } + + linkElement.addEventListener("click", showSourceCodeLocation); + linkElement.addEventListener("contextmenu", (event) => { + let contextMenu = WI.ContextMenu.createFromEvent(event); + WI.appendContextMenuItemsForSourceCode(contextMenu, sourceCodeLocation); + }); +}; + +WI.sourceCodeForURL = function(url) +{ + let sourceCode = WI.networkManager.resourcesForURL(url).firstValue; + if (!sourceCode) { + sourceCode = WI.debuggerManager.scriptsForURL(url, WI.assumingMainTarget())[0]; + if (sourceCode) + sourceCode = sourceCode.resource || sourceCode; + } + return sourceCode || null; +}; + +WI.linkifyURLAsNode = function(url, linkText, className) +{ + let a = document.createElement("a"); + a.href = url; + a.className = className || ""; + a.textContent = linkText || url; + a.style.maxWidth = "100%"; + return a; +}; + +WI.linkifyStringAsFragmentWithCustomLinkifier = function(string, linkifier) +{ + var container = document.createDocumentFragment(); + var linkStringRegEx = /(?:[a-zA-Z][a-zA-Z0-9+.-]{2,}:\/\/|www\.)[\w$\-_+*'=\|\/\\(){}[\]%@&#~,:;.!?]{2,}[\w$\-_+*=\|\/\\({%@&#~]/; + var lineColumnRegEx = /:(\d+)(:(\d+))?$/; + + while (string) { + var linkString = linkStringRegEx.exec(string); + if (!linkString) + break; + + linkString = linkString[0]; + var linkIndex = string.indexOf(linkString); + var nonLink = string.substring(0, linkIndex); + container.append(nonLink); + + if (linkString.startsWith("data:") || linkString.startsWith("javascript:") || linkString.startsWith("mailto:")) { + container.append(linkString); + string = string.substring(linkIndex + linkString.length, string.length); + continue; + } + + var title = linkString; + var realURL = linkString.startsWith("www.") ? "http://" + linkString : linkString; + var lineColumnMatch = lineColumnRegEx.exec(realURL); + if (lineColumnMatch) + realURL = realURL.substring(0, realURL.length - lineColumnMatch[0].length); + + var lineNumber; + if (lineColumnMatch) + lineNumber = parseInt(lineColumnMatch[1]) - 1; + + var linkNode = linkifier(title, realURL, lineNumber); + container.appendChild(linkNode); + string = string.substring(linkIndex + linkString.length, string.length); + } + + if (string) + container.append(string); + + return container; +}; + +WI.linkifyStringAsFragment = function(string) +{ + function linkifier(title, url, lineNumber) + { + var urlNode = WI.linkifyURLAsNode(url, title, undefined); + if (lineNumber !== undefined) + urlNode.lineNumber = lineNumber; + + return urlNode; + } + + return WI.linkifyStringAsFragmentWithCustomLinkifier(string, linkifier); +}; + +WI.createResourceLink = function(resource, className) +{ + function handleClick(event) + { + event.stopPropagation(); + event.preventDefault(); + + WI.showRepresentedObject(resource); + } + + let linkNode = document.createElement("a"); + linkNode.classList.add("resource-link", className); + linkNode.title = resource.url; + linkNode.textContent = (resource.urlComponents.lastPathComponent || resource.displayURL).insertWordBreakCharacters(); + linkNode.addEventListener("click", handleClick); + return linkNode; +}; + +WI._undoKeyboardShortcut = function(event) +{ + if (!WI.isEditingAnyField() && !WI.isEventTargetAnEditableField(event)) { + WI.undo(); + event.preventDefault(); + } +}; + +WI._redoKeyboardShortcut = function(event) +{ + if (!WI.isEditingAnyField() && !WI.isEventTargetAnEditableField(event)) { + WI.redo(); + event.preventDefault(); + } +}; + +WI.undo = function() +{ + let target = WI.assumingMainTarget(); + if (target.hasCommand("DOM.undo")) + target.DOMAgent.undo(); +}; + +WI.redo = function() +{ + let target = WI.assumingMainTarget(); + if (target.hasCommand("DOM.redo")) + target.DOMAgent.redo(); +}; + +WI.highlightRangesWithStyleClass = function(element, resultRanges, styleClass, changes) +{ + changes = changes || []; + var highlightNodes = []; + var lineText = element.textContent; + var ownerDocument = element.ownerDocument; + var textNodeSnapshot = ownerDocument.evaluate(".//text()", element, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null); + + var snapshotLength = textNodeSnapshot.snapshotLength; + if (snapshotLength === 0) + return highlightNodes; + + var nodeRanges = []; + var rangeEndOffset = 0; + for (var i = 0; i < snapshotLength; ++i) { + var range = {}; + range.offset = rangeEndOffset; + range.length = textNodeSnapshot.snapshotItem(i).textContent.length; + rangeEndOffset = range.offset + range.length; + nodeRanges.push(range); + } + + var startIndex = 0; + for (var i = 0; i < resultRanges.length; ++i) { + var startOffset = resultRanges[i].offset; + var endOffset = startOffset + resultRanges[i].length; + + while (startIndex < snapshotLength && nodeRanges[startIndex].offset + nodeRanges[startIndex].length <= startOffset) + startIndex++; + var endIndex = startIndex; + while (endIndex < snapshotLength && nodeRanges[endIndex].offset + nodeRanges[endIndex].length < endOffset) + endIndex++; + if (endIndex === snapshotLength) + break; + + var highlightNode = ownerDocument.createElement("span"); + highlightNode.className = styleClass; + highlightNode.textContent = lineText.substring(startOffset, endOffset); + + var lastTextNode = textNodeSnapshot.snapshotItem(endIndex); + var lastText = lastTextNode.textContent; + lastTextNode.textContent = lastText.substring(endOffset - nodeRanges[endIndex].offset); + changes.push({node: lastTextNode, type: "changed", oldText: lastText, newText: lastTextNode.textContent}); + + if (startIndex === endIndex) { + lastTextNode.parentElement.insertBefore(highlightNode, lastTextNode); + changes.push({node: highlightNode, type: "added", nextSibling: lastTextNode, parent: lastTextNode.parentElement}); + highlightNodes.push(highlightNode); + + var prefixNode = ownerDocument.createTextNode(lastText.substring(0, startOffset - nodeRanges[startIndex].offset)); + lastTextNode.parentElement.insertBefore(prefixNode, highlightNode); + changes.push({node: prefixNode, type: "added", nextSibling: highlightNode, parent: lastTextNode.parentElement}); + } else { + var firstTextNode = textNodeSnapshot.snapshotItem(startIndex); + var firstText = firstTextNode.textContent; + var anchorElement = firstTextNode.nextSibling; + + firstTextNode.parentElement.insertBefore(highlightNode, anchorElement); + changes.push({node: highlightNode, type: "added", nextSibling: anchorElement, parent: firstTextNode.parentElement}); + highlightNodes.push(highlightNode); + + firstTextNode.textContent = firstText.substring(0, startOffset - nodeRanges[startIndex].offset); + changes.push({node: firstTextNode, type: "changed", oldText: firstText, newText: firstTextNode.textContent}); + + for (var j = startIndex + 1; j < endIndex; j++) { + var textNode = textNodeSnapshot.snapshotItem(j); + var text = textNode.textContent; + textNode.textContent = ""; + changes.push({node: textNode, type: "changed", oldText: text, newText: textNode.textContent}); + } + } + startIndex = endIndex; + nodeRanges[startIndex].offset = endOffset; + nodeRanges[startIndex].length = lastTextNode.textContent.length; + + } + return highlightNodes; +}; + +WI.revertDOMChanges = function(domChanges) +{ + for (var i = domChanges.length - 1; i >= 0; --i) { + var entry = domChanges[i]; + switch (entry.type) { + case "added": + entry.node.remove(); + break; + case "changed": + entry.node.textContent = entry.oldText; + break; + } + } +}; + +WI.archiveMainFrame = function() +{ + WI._downloadingPage = true; + WI._updateDownloadTabBarButton(); + + let target = WI.assumingMainTarget(); + target.PageAgent.archive((error, data) => { + WI._downloadingPage = false; + WI._updateDownloadTabBarButton(); + + if (error) { + WI.reportInternalError(error); + return; + } + + let mainFrame = WI.networkManager.mainFrame; + let archiveName = mainFrame.mainResource.urlComponents.host || mainFrame.mainResource.displayName || "Archive"; + + const forceSaveAs = true; + WI.FileUtilities.save(WI.FileUtilities.SaveMode.SingleFile, { + suggestedName: archiveName + ".webarchive", + content: data, + base64Encoded: true, + }, forceSaveAs); + }); +}; + +WI.canArchiveMainFrame = function() +{ + if (!WI.FileUtilities.canSave(WI.FileUtilities.SaveMode.SingleFile)) + return false; + + if (!InspectorBackend.hasCommand("Page.archive")) + return false; + + if (!WI.networkManager.mainFrame || !WI.networkManager.mainFrame.mainResource) + return false; + + return WI.Resource.typeFromMIMEType(WI.networkManager.mainFrame.mainResource.mimeType) === WI.Resource.Type.Document; +}; + +WI.addWindowKeydownListener = function(listener) +{ + if (typeof listener.handleKeydownEvent !== "function") + return; + + WI._windowKeydownListeners.push(listener); + + WI._updateWindowKeydownListener(); +}; + +WI.removeWindowKeydownListener = function(listener) +{ + WI._windowKeydownListeners.remove(listener); + + WI._updateWindowKeydownListener(); +}; + +WI._updateWindowKeydownListener = function() +{ + if (WI._windowKeydownListeners.length === 1) + window.addEventListener("keydown", WI._sharedWindowKeydownListener, true); + else if (!WI._windowKeydownListeners.length) + window.removeEventListener("keydown", WI._sharedWindowKeydownListener, true); +}; + +WI._sharedWindowKeydownListener = function(event) +{ + for (var i = WI._windowKeydownListeners.length - 1; i >= 0; --i) { + if (WI._windowKeydownListeners[i].handleKeydownEvent(event)) { + event.stopImmediatePropagation(); + event.preventDefault(); + break; + } + } +}; + +WI.reportInternalError = function(errorOrString, details = {}) +{ + // The 'details' object includes additional information from the caller as free-form string keys and values. + // Each key and value will be shown in the uncaught exception reporter, console error message, or in + // a pre-filled bug report generated for this internal error. + + let error = errorOrString instanceof Error ? errorOrString : new Error(errorOrString); + error.details = details; + + // The error will be displayed in the Uncaught Exception Reporter sheet if DebugUI is enabled. + if (WI.isDebugUIEnabled()) { + // This assert allows us to stop the debugger at an internal exception. It doesn't re-throw + // exceptions because the original exception would be lost through window.onerror. + // This workaround can be removed once is fixed. + console.assert(false, "An internal exception was thrown.", error); + handleInternalException(error); + } else + console.error(error); +}; + +// Many places assume the "main" target has resources. +// In the case where the main backend target is a MultiplexingBackendTarget +// that target has essentially nothing. In that case defer to the page +// target, since that is the real "main" target the frontend is assuming. +Object.defineProperty(WI, "mainTarget", +{ + get() { return WI.pageTarget || WI.backendTarget; } +}); + +// This list of targets are non-Multiplexing targets. +// So if there is a multiplexing target, and multiple sub-targets +// this is just the list of sub-targets. Almost no code expects +// to actually interact with the Multiplexing target. +Object.defineProperty(WI, "targets", +{ + get() { return WI.targetManager.targets; } +}); + +// Many places assume the main target because they cannot yet be +// used by reached by Worker debugging. Eventually, once all +// Worker domains have been implemented, all of these must be +// handled properly. +WI.assumingMainTarget = function() +{ + return WI.mainTarget; +}; + +WI.reset = async function() +{ + await WI.ObjectStore.reset(); + WI.Setting.reset(); + InspectorFrontendHost.reset(); +}; + +WI.isEngineeringBuild = false; +WI.inspectorFrontendHostAllowsEngineeringSettings = InspectorFrontendHost.engineeringSettingsAllowed(); + +WI.engineeringSettingsAllowed = function() { + return WI.isEngineeringBuild || WI.inspectorFrontendHostAllowsEngineeringSettings; +} + +// OpenResourceDialog delegate + +WI.dialogWasDismissedWithRepresentedObject = function(dialog, representedObject) +{ + if (!representedObject) + return; + + WI.showRepresentedObject(representedObject, dialog.cookie, { + ignoreSearchTab: true, + ignoreNetworkTab: true, + }); +}; + +// Popover delegate + +WI.didDismissPopover = function(popover) +{ + if (popover === WI._deviceSettingsPopover) + WI._deviceSettingsPopover = null; +}; + +WI.DockConfiguration = { + Right: "right", + Left: "left", + Bottom: "bottom", + Undocked: "undocked", +}; + +WI.DockConfigurationChangedLayoutReason = Symbol("dock-configuration-changed"); diff --git a/inspector/Base/Multimap.js b/inspector/Base/Multimap.js new file mode 100644 index 0000000..3ee721f --- /dev/null +++ b/inspector/Base/Multimap.js @@ -0,0 +1,142 @@ +/* + * 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); + } +} diff --git a/inspector/Base/Object.js b/inspector/Base/Object.js new file mode 100644 index 0000000..2941502 --- /dev/null +++ b/inspector/Base/Object.js @@ -0,0 +1,222 @@ +/* + * 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", +}; diff --git a/inspector/Base/ObjectStore.js b/inspector/Base/ObjectStore.js new file mode 100644 index 0000000..a644ca9 --- /dev/null +++ b/inspector/Base/ObjectStore.js @@ -0,0 +1,284 @@ +/* + * 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"}), +}; diff --git a/inspector/Base/Platform.js b/inspector/Base/Platform.js new file mode 100644 index 0000000..8161193 --- /dev/null +++ b/inspector/Base/Platform.js @@ -0,0 +1,31 @@ +/* + * 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, + } +}; diff --git a/inspector/Base/ReferencePage.js b/inspector/Base/ReferencePage.js new file mode 100644 index 0000000..8cca5ee --- /dev/null +++ b/inspector/Base/ReferencePage.js @@ -0,0 +1,120 @@ +/* + * 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"}); diff --git a/inspector/Base/SearchUtilities.js b/inspector/Base/SearchUtilities.js new file mode 100644 index 0000000..b7d1b13 --- /dev/null +++ b/inspector/Base/SearchUtilities.js @@ -0,0 +1,121 @@ +/* + * 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.SearchUtilities = class SearchUtilities { + static get defaultSettings() + { + return { + caseSensitive: WI.settings.searchCaseSensitive, + regularExpression: WI.settings.searchRegularExpression, + }; + } + + static createSettings(namePrefix) + { + let settings = {}; + for (let [key, defaultSetting] of Object.entries(WI.SearchUtilities.defaultSettings)) { + let setting = new WI.Setting(namePrefix + "-" + defaultSetting.name, defaultSetting.value); + defaultSetting.addEventListener(WI.Setting.Event.Changed, function(event) { + this.value = defaultSetting.value; + }, setting); + settings[key] = setting; + } + return settings; + } + + static searchRegExpForString(query, settings = {}) + { + return WI.SearchUtilities._regExpForString(query, settings, {global: true}); + } + + static filterRegExpForString(query, settings = {}) + { + return WI.SearchUtilities._regExpForString(query, settings); + } + + static createSettingsButton(settings) + { + console.assert(!isEmptyObject(settings)); + + let button = document.createElement("button"); + button.classList.add("search-settings"); + button.tabIndex = -1; + WI.addMouseDownContextMenuHandlers(button, (contextMenu) => { + if (settings.caseSensitive) { + contextMenu.appendCheckboxItem(WI.UIString("Case Sensitive", "Case Sensitive @ Context Menu", "Context menu label for whether searches should be case sensitive."), () => { + settings.caseSensitive.value = !settings.caseSensitive.value; + }, settings.caseSensitive.value); + } + + if (settings.regularExpression) { + contextMenu.appendCheckboxItem(WI.UIString("Regular Expression", "Regular Expression @ Context Menu", "Context menu label for whether searches should be treated as regular expressions."), () => { + settings.regularExpression.value = !settings.regularExpression.value; + }, settings.regularExpression.value); + } + }); + + button.appendChild(WI.ImageUtilities.useSVGSymbol("Images/Gear.svg", "glyph")); + + function toggleActive() { + button.classList.toggle("active", Object.values(settings).some((setting) => !!setting.value)); + } + settings.caseSensitive.addEventListener(WI.Setting.Event.Changed, toggleActive, button); + settings.regularExpression.addEventListener(WI.Setting.Event.Changed, toggleActive, button); + toggleActive(); + + return button; + } + + static _regExpForString(query, settings = {}, options = {}) + { + function checkSetting(setting) { + return setting instanceof WI.Setting ? setting.value : !!setting; + } + + console.assert((typeof query === "string" && query) || query instanceof RegExp); + + if (!checkSetting(settings.regularExpression)) { + try { + query = simpleGlobStringToRegExp(String(query)); + } catch { + return null; + } + } + + console.assert((typeof query === "string" && query) || query instanceof RegExp); + + let flags = ""; + if (options.global) + flags += "g" + if (!checkSetting(settings.caseSensitive)) + flags += "i"; + + try { + return new RegExp(query, flags); + } catch { + return null; + } + } +}; diff --git a/inspector/Base/Setting.js b/inspector/Base/Setting.js new file mode 100644 index 0000000..91252cb --- /dev/null +++ b/inspector/Base/Setting.js @@ -0,0 +1,263 @@ +/* + * 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), + 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), + 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), + + // 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), +}; diff --git a/inspector/Base/TargetType.js b/inspector/Base/TargetType.js new file mode 100644 index 0000000..39a1f56 --- /dev/null +++ b/inspector/Base/TargetType.js @@ -0,0 +1,35 @@ +/* + * 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); diff --git a/inspector/Base/Throttler.js b/inspector/Base/Throttler.js new file mode 100644 index 0000000..0399bf1 --- /dev/null +++ b/inspector/Base/Throttler.js @@ -0,0 +1,108 @@ +/* + * 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); + } +} diff --git a/inspector/Base/URLUtilities.js b/inspector/Base/URLUtilities.js new file mode 100644 index 0000000..b93610c --- /dev/null +++ b/inspector/Base/URLUtilities.js @@ -0,0 +1,373 @@ +/* + * 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(/^(?[^:]+):\/\/(?[^\/:]*)(?::(?[\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:[][;charset=][;base64], + let match = url.match(/^data:(?[^;,]*)?(?:;charset=(?[^;,]*?))?(?;base64)?,(?.*)$/); + 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.displayNameForHost = function(host) +{ + let extensionName = WI.browserManager.extensionNameForId(host); + if (extensionName) + return extensionName; + + // FIXME : 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; +}; diff --git a/inspector/Base/Utilities.js b/inspector/Base/Utilities.js new file mode 100644 index 0000000..8d0d0a5 --- /dev/null +++ b/inspector/Base/Utilities.js @@ -0,0 +1,1868 @@ +/* + * 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(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); +} + +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; +}; diff --git a/inspector/Base/WebInspector.js b/inspector/Base/WebInspector.js new file mode 100644 index 0000000..3e41b27 --- /dev/null +++ b/inspector/Base/WebInspector.js @@ -0,0 +1,26 @@ +/* + * 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 diff --git a/inspector/Base/YieldableTask.js b/inspector/Base/YieldableTask.js new file mode 100644 index 0000000..0ac36a5 --- /dev/null +++ b/inspector/Base/YieldableTask.js @@ -0,0 +1,154 @@ +/* + * 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); + } +}; + diff --git a/inspector/Controllers/AnimationManager.js b/inspector/Controllers/AnimationManager.js new file mode 100644 index 0000000..f97ccf9 --- /dev/null +++ b/inspector/Controllers/AnimationManager.js @@ -0,0 +1,165 @@ +/* + * 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(); + } +}; diff --git a/inspector/Controllers/Annotator.js b/inspector/Controllers/Annotator.js new file mode 100644 index 0000000..c95a87d --- /dev/null +++ b/inspector/Controllers/Annotator.js @@ -0,0 +1,110 @@ +/* + * Copyright (C) 2014 Apple Inc. All rights reserved. + * Copyright (C) 2014 Saam Barati + * + * 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.Annotator = class Annotator extends WI.Object +{ + constructor(sourceCodeTextEditor) + { + super(); + + console.assert(sourceCodeTextEditor instanceof WI.SourceCodeTextEditor, sourceCodeTextEditor); + + this._sourceCodeTextEditor = sourceCodeTextEditor; + this._timeoutIdentifier = null; + this._isActive = false; + } + + // Public + + get sourceCodeTextEditor() + { + return this._sourceCodeTextEditor; + } + + isActive() + { + return this._isActive; + } + + pause() + { + this._clearTimeoutIfNeeded(); + this._isActive = false; + } + + resume() + { + this._clearTimeoutIfNeeded(); + this._isActive = true; + this.insertAnnotations(); + } + + refresh() + { + console.assert(this._isActive); + if (!this._isActive) + return; + + this._clearTimeoutIfNeeded(); + this.insertAnnotations(); + } + + reset() + { + this._clearTimeoutIfNeeded(); + this._isActive = true; + this.clearAnnotations(); + this.insertAnnotations(); + } + + clear() + { + this.pause(); + this.clearAnnotations(); + } + + // Protected + + insertAnnotations() + { + // Implemented by subclasses. + } + + clearAnnotations() + { + // Implemented by subclasses. + } + + // Private + + _clearTimeoutIfNeeded() + { + if (this._timeoutIdentifier) { + clearTimeout(this._timeoutIdentifier); + this._timeoutIdentifier = null; + } + } +}; diff --git a/inspector/Controllers/AppController.js b/inspector/Controllers/AppController.js new file mode 100644 index 0000000..a8e9791 --- /dev/null +++ b/inspector/Controllers/AppController.js @@ -0,0 +1,76 @@ +/* + * 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.AppController = class AppController extends WI.AppControllerBase +{ + constructor() + { + super(); + + this._debuggableType = WI.DebuggableType.fromString(InspectorFrontendHost.debuggableInfo.debuggableType); + console.assert(this._debuggableType); + if (!this._debuggableType) + this._debuggableType = WI.DebuggableType.JavaScript; + } + + // Properties. + + get debuggableType() { return this._debuggableType; } + + // API. + + activateExtraDomains(domains) + { + // COMPATIBILITY (iOS 14.0): Inspector.activateExtraDomains was removed in favor of a declared debuggable type + + console.assert(this._debuggableType === WI.DebuggableType.JavaScript); + console.assert(WI.targets.length === 1); + + let target = WI.mainTarget; + console.assert(target instanceof WI.DirectBackendTarget); + console.assert(target.type === WI.TargetType.JavaScript); + + if (this._debuggableType === WI.DebuggableType.ITML || target.type === WI.TargetType.ITML) + throw new Error("Extra domains have already been activated, cannot activate again."); + + this._debuggableType = WI.DebuggableType.ITML; + target._type = WI.TargetType.ITML; + + for (let domain of domains) { + InspectorBackend.activateDomain(domain); + + target.activateExtraDomain(domain); + + let manager = WI.managers.find((manager) => manager.domains && manager.domains.includes(domain)); + if (manager) + manager.activateExtraDomain(domain); + else if (target.hasCommand(domain + ".enable")) + target._agents[domain].enable(); + } + + // FIXME: all code within WI.activateExtraDomains should be distributed elsewhere. + WI.activateExtraDomains(domains); + } +}; diff --git a/inspector/Controllers/AppControllerBase.js b/inspector/Controllers/AppControllerBase.js new file mode 100644 index 0000000..0025374 --- /dev/null +++ b/inspector/Controllers/AppControllerBase.js @@ -0,0 +1,71 @@ +/* + * 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; + } +}; diff --git a/inspector/Controllers/ApplicationCacheManager.js b/inspector/Controllers/ApplicationCacheManager.js new file mode 100644 index 0000000..4d19d8a --- /dev/null +++ b/inspector/Controllers/ApplicationCacheManager.js @@ -0,0 +1,274 @@ +/* + * 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 +}; diff --git a/inspector/Controllers/AuditManager.js b/inspector/Controllers/AuditManager.js new file mode 100644 index 0000000..c7e2f2b --- /dev/null +++ b/inspector/Controllers/AuditManager.js @@ -0,0 +1,452 @@ +/* + * 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 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 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 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 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 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 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 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 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 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 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 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 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", +}; diff --git a/inspector/Controllers/BasicBlockAnnotator.js b/inspector/Controllers/BasicBlockAnnotator.js new file mode 100644 index 0000000..2bb0e04 --- /dev/null +++ b/inspector/Controllers/BasicBlockAnnotator.js @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2015 Apple Inc. All rights reserved. + * Copyright (C) 2015 Saam Barati + * + * 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.BasicBlockAnnotator = class BasicBlockAnnotator extends WI.Annotator +{ + constructor(sourceCodeTextEditor, script) + { + super(sourceCodeTextEditor); + + this._script = script; + this._basicBlockMarkers = new Map; // Only contains unexecuted basic blocks. + } + + // Protected + + clearAnnotations() + { + for (var key of this._basicBlockMarkers.keys()) + this._clearRangeForBasicBlockMarker(key); + } + + insertAnnotations() + { + if (!this.isActive()) + return; + this._script.requestContent().then(this._annotateBasicBlockExecutionRanges.bind(this)); + } + + // Private + + _calculateBasicBlockPositions(basicBlocks, content) + { + if (!basicBlocks || !basicBlocks.length) + return; + + let lineEndings = []; + let lineEndingLengths = []; + let pattern = /\r\n?|\n/g; + let match = pattern.exec(content); + while (match) { + lineEndings.push(match.index); + lineEndingLengths.push(match[0].length); + match = pattern.exec(content); + } + + function offsetToPosition(offset) { + let lineNumber = lineEndings.upperBound(offset - 1); + if (lineNumber) { + let previousLine = lineNumber - 1; + var columnNumber = offset - lineEndings[previousLine] - lineEndingLengths[previousLine]; + } else + var columnNumber = offset; + return new WI.SourceCodePosition(lineNumber, columnNumber); + } + + for (let block of basicBlocks) { + block.startPosition = offsetToPosition(block.startOffset); + block.endPosition = offsetToPosition(block.endOffset); + } + } + + _annotateBasicBlockExecutionRanges() + { + let content = this._script.content; + console.assert(content, "Missing script content for basic block annotations."); + if (!content) + return; + + var sourceID = this._script.id; + var startTime = Date.now(); + + this._script.target.RuntimeAgent.getBasicBlocks(sourceID, function(error, basicBlocks) { + if (error) { + console.error("Error in getting basic block locations: " + error); + return; + } + + if (!this.isActive()) + return; + + this._calculateBasicBlockPositions(basicBlocks, content); + + let {startPosition, endPosition} = this.sourceCodeTextEditor.visibleRangePositions(); + basicBlocks = basicBlocks.filter(function(block) { + // Viewport: [--] + // Block: [--] + if (block.startPosition.isAfter(endPosition)) + return false; + + // Viewport: [--] + // Block: [--] + if (block.endPosition.isBefore(startPosition)) + return false; + + return true; + }); + + for (var block of basicBlocks) { + var key = block.startOffset + ":" + block.endOffset; + var hasKey = this._basicBlockMarkers.has(key); + var hasExecuted = block.hasExecuted; + if (hasKey && hasExecuted) + this._clearRangeForBasicBlockMarker(key); + else if (!hasKey && !hasExecuted) { + var marker = this._highlightTextForBasicBlock(block); + this._basicBlockMarkers.set(key, marker); + } + } + + var totalTime = Date.now() - startTime; + var timeoutTime = Number.constrain(30 * totalTime, 500, 5000); + this._timeoutIdentifier = setTimeout(this.insertAnnotations.bind(this), timeoutTime); + }.bind(this)); + } + + _highlightTextForBasicBlock(basicBlock) + { + console.assert(basicBlock.startOffset <= basicBlock.endOffset && basicBlock.startOffset >= 0 && basicBlock.endOffset >= 0, "" + basicBlock.startOffset + ":" + basicBlock.endOffset); + console.assert(!basicBlock.hasExecuted); + + let startPosition = this.sourceCodeTextEditor.originalPositionToCurrentPosition(basicBlock.startPosition); + let endPosition = this.sourceCodeTextEditor.originalPositionToCurrentPosition(basicBlock.endPosition); + if (this._isTextRangeOnlyClosingBrace(startPosition, endPosition)) + return null; + + return this.sourceCodeTextEditor.addStyleToTextRange(startPosition, endPosition, WI.BasicBlockAnnotator.HasNotExecutedClassName); + } + + _isTextRangeOnlyClosingBrace(startPosition, endPosition) + { + var isOnlyClosingBrace = /^\s*\}$/; + return isOnlyClosingBrace.test(this.sourceCodeTextEditor.getTextInRange(startPosition, endPosition)); + } + + _clearRangeForBasicBlockMarker(key) + { + console.assert(this._basicBlockMarkers.has(key)); + var marker = this._basicBlockMarkers.get(key); + if (marker) + marker.clear(); + this._basicBlockMarkers.delete(key); + } +}; + +WI.BasicBlockAnnotator.HasNotExecutedClassName = "basic-block-has-not-executed"; diff --git a/inspector/Controllers/BranchManager.js b/inspector/Controllers/BranchManager.js new file mode 100644 index 0000000..5b6776e --- /dev/null +++ b/inspector/Controllers/BranchManager.js @@ -0,0 +1,110 @@ +/* + * 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.BranchManager = class BranchManager extends WI.Object +{ + constructor() + { + super(); + + WI.Frame.addEventListener(WI.Frame.Event.MainResourceDidChange, this._mainResourceDidChange, this); + + this.initialize(); + } + + // Public + + initialize() + { + this._originalBranch = new WI.Branch(WI.UIString("Original"), null, true); + this._currentBranch = this._originalBranch.fork(WI.UIString("Working Copy")); + this._branches = [this._originalBranch, this._currentBranch]; + } + + get branches() + { + return this._branches; + } + + get currentBranch() + { + return this._currentBranch; + } + + set currentBranch(branch) + { + console.assert(branch instanceof WI.Branch); + if (!(branch instanceof WI.Branch)) + return; + + this._currentBranch.revert(); + + this._currentBranch = branch; + + this._currentBranch.apply(); + } + + createBranch(displayName, fromBranch) + { + if (!fromBranch) + fromBranch = this._originalBranch; + + console.assert(fromBranch instanceof WI.Branch); + if (!(fromBranch instanceof WI.Branch)) + return null; + + var newBranch = fromBranch.fork(displayName); + this._branches.push(newBranch); + return newBranch; + } + + deleteBranch(branch) + { + console.assert(branch instanceof WI.Branch); + if (!(branch instanceof WI.Branch)) + return; + + console.assert(branch !== this._originalBranch); + if (branch === this._originalBranch) + return; + + this._branches.remove(branch); + + if (branch === this._currentBranch) + this._currentBranch = this._originalBranch; + } + + // Private + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WI.Frame); + + if (!event.target.isMainFrame()) + return; + + this.initialize(); + } +}; diff --git a/inspector/Controllers/BreakpointLogMessageLexer.js b/inspector/Controllers/BreakpointLogMessageLexer.js new file mode 100644 index 0000000..9b8e254 --- /dev/null +++ b/inspector/Controllers/BreakpointLogMessageLexer.js @@ -0,0 +1,197 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/BreakpointPopoverController.js b/inspector/Controllers/BreakpointPopoverController.js new file mode 100644 index 0000000..a23fef4 --- /dev/null +++ b/inspector/Controllers/BreakpointPopoverController.js @@ -0,0 +1,381 @@ +/* + * 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.BreakpointPopoverController = class BreakpointPopoverController extends WI.Object +{ + constructor() + { + super(); + + this._breakpoint = null; + this._popover = null; + this._popoverContentElement = null; + } + + // Public + + appendContextMenuItems(contextMenu, breakpoint, breakpointDisplayElement) + { + console.assert(document.body.contains(breakpointDisplayElement), "Breakpoint popover display element must be in the DOM."); + + const editBreakpoint = () => { + console.assert(!this._popover, "Breakpoint popover already exists."); + if (this._popover) + return; + + this._createPopoverContent(breakpoint); + this._popover = new WI.Popover(this); + this._popover.content = this._popoverContentElement; + + let bounds = WI.Rect.rectFromClientRect(breakpointDisplayElement.getBoundingClientRect()); + bounds.origin.x -= 1; // Move the anchor left one pixel so it looks more centered. + this._popover.present(bounds.pad(2), [WI.RectEdge.MAX_Y]); + }; + + const removeBreakpoint = () => { + WI.debuggerManager.removeBreakpoint(breakpoint); + }; + + const toggleBreakpoint = () => { + breakpoint.disabled = !breakpoint.disabled; + }; + + const toggleAutoContinue = () => { + breakpoint.autoContinue = !breakpoint.autoContinue; + }; + + const revealOriginalSourceCodeLocation = () => { + const options = { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }; + WI.showOriginalOrFormattedSourceCodeLocation(breakpoint.sourceCodeLocation, options); + }; + + if (WI.debuggerManager.isBreakpointEditable(breakpoint)) + contextMenu.appendItem(WI.UIString("Edit Breakpoint\u2026"), editBreakpoint); + + if (breakpoint.autoContinue && !breakpoint.disabled) { + contextMenu.appendItem(WI.UIString("Disable Breakpoint"), toggleBreakpoint); + contextMenu.appendItem(WI.UIString("Cancel Automatic Continue"), toggleAutoContinue); + } else if (!breakpoint.disabled) + contextMenu.appendItem(WI.UIString("Disable Breakpoint"), toggleBreakpoint); + else + contextMenu.appendItem(WI.UIString("Enable Breakpoint"), toggleBreakpoint); + + if (!breakpoint.autoContinue && !breakpoint.disabled && breakpoint.actions.length) + contextMenu.appendItem(WI.UIString("Set to Automatically Continue"), toggleAutoContinue); + + if (WI.debuggerManager.isBreakpointRemovable(breakpoint)) + contextMenu.appendItem(WI.UIString("Delete Breakpoint"), removeBreakpoint); + + if (breakpoint._sourceCodeLocation.hasMappedLocation()) { + contextMenu.appendSeparator(); + contextMenu.appendItem(WI.UIString("Reveal in Original Resource"), revealOriginalSourceCodeLocation); + } + } + + // CodeMirrorCompletionController delegate + + completionControllerShouldAllowEscapeCompletion() + { + return false; + } + + // Private + + _createPopoverContent(breakpoint) + { + console.assert(!this._popoverContentElement, "Popover content element already exists."); + if (this._popoverContentElement) + return; + + this._breakpoint = breakpoint; + this._popoverContentElement = document.createElement("div"); + this._popoverContentElement.className = "edit-breakpoint-popover-content"; + + let checkboxElement = document.createElement("input"); + checkboxElement.type = "checkbox"; + checkboxElement.checked = !this._breakpoint.disabled; + checkboxElement.addEventListener("change", this._popoverToggleEnabledCheckboxChanged.bind(this)); + + let checkboxLabel = document.createElement("label"); + checkboxLabel.className = "toggle"; + checkboxLabel.appendChild(checkboxElement); + checkboxLabel.append(this._breakpoint.sourceCodeLocation.displayLocationString()); + + let table = document.createElement("table"); + + let conditionRow = table.appendChild(document.createElement("tr")); + let conditionHeader = conditionRow.appendChild(document.createElement("th")); + let conditionData = conditionRow.appendChild(document.createElement("td")); + let conditionLabel = conditionHeader.appendChild(document.createElement("label")); + conditionLabel.textContent = WI.UIString("Condition"); + let conditionEditorElement = conditionData.appendChild(document.createElement("div")); + conditionEditorElement.classList.add("edit-breakpoint-popover-condition", WI.SyntaxHighlightedStyleClassName); + + this._conditionCodeMirror = WI.CodeMirrorEditor.create(conditionEditorElement, { + extraKeys: {Tab: false}, + lineWrapping: false, + mode: "text/javascript", + matchBrackets: true, + placeholder: WI.UIString("Conditional expression"), + scrollbarStyle: null, + value: this._breakpoint.condition || "", + }); + + let conditionCodeMirrorInputField = this._conditionCodeMirror.getInputField(); + conditionCodeMirrorInputField.id = "codemirror-condition-input-field"; + conditionLabel.setAttribute("for", conditionCodeMirrorInputField.id); + + this._conditionCodeMirrorEscapeOrEnterKeyHandler = this._conditionCodeMirrorEscapeOrEnterKey.bind(this); + this._conditionCodeMirror.addKeyMap({ + "Esc": this._conditionCodeMirrorEscapeOrEnterKeyHandler, + "Enter": this._conditionCodeMirrorEscapeOrEnterKeyHandler, + }); + + this._conditionCodeMirror.on("change", this._conditionCodeMirrorChanged.bind(this)); + this._conditionCodeMirror.on("beforeChange", this._conditionCodeMirrorBeforeChange.bind(this)); + + let completionController = new WI.CodeMirrorCompletionController(this._conditionCodeMirror, this); + completionController.addExtendedCompletionProvider("javascript", WI.javaScriptRuntimeCompletionProvider); + + // CodeMirror needs a refresh after the popover displays, to layout, otherwise it doesn't appear. + setTimeout(() => { + this._conditionCodeMirror.refresh(); + this._conditionCodeMirror.focus(); + }, 0); + + // COMPATIBILITY (iOS 9): Legacy backends don't support breakpoint ignore count. Since support + // can't be tested directly, check for CSS.getSupportedSystemFontFamilyNames. + // FIXME: Use explicit version checking once https://webkit.org/b/148680 is fixed. + if (InspectorBackend.domains.CSS.getSupportedSystemFontFamilyNames) { + let ignoreCountRow = table.appendChild(document.createElement("tr")); + let ignoreCountHeader = ignoreCountRow.appendChild(document.createElement("th")); + let ignoreCountLabel = ignoreCountHeader.appendChild(document.createElement("label")); + let ignoreCountData = ignoreCountRow.appendChild(document.createElement("td")); + this._ignoreCountInput = ignoreCountData.appendChild(document.createElement("input")); + this._ignoreCountInput.id = "edit-breakpoint-popover-ignore"; + this._ignoreCountInput.type = "number"; + this._ignoreCountInput.min = 0; + this._ignoreCountInput.value = 0; + this._ignoreCountInput.addEventListener("change", this._popoverIgnoreInputChanged.bind(this)); + + ignoreCountLabel.setAttribute("for", this._ignoreCountInput.id); + ignoreCountLabel.textContent = WI.UIString("Ignore"); + + this._ignoreCountText = ignoreCountData.appendChild(document.createElement("span")); + this._updateIgnoreCountText(); + } + + let actionRow = table.appendChild(document.createElement("tr")); + let actionHeader = actionRow.appendChild(document.createElement("th")); + let actionData = this._actionsContainer = actionRow.appendChild(document.createElement("td")); + let actionLabel = actionHeader.appendChild(document.createElement("label")); + actionLabel.textContent = WI.UIString("Action"); + + if (!this._breakpoint.actions.length) + this._popoverActionsCreateAddActionButton(); + else { + this._popoverContentElement.classList.add(WI.BreakpointPopoverController.WidePopoverClassName); + for (let i = 0; i < this._breakpoint.actions.length; ++i) { + let breakpointActionView = new WI.BreakpointActionView(this._breakpoint.actions[i], this, true); + this._popoverActionsInsertBreakpointActionView(breakpointActionView, i); + } + } + + let optionsRow = this._popoverOptionsRowElement = table.appendChild(document.createElement("tr")); + if (!this._breakpoint.actions.length) + optionsRow.classList.add(WI.BreakpointPopoverController.HiddenStyleClassName); + let optionsHeader = optionsRow.appendChild(document.createElement("th")); + let optionsData = optionsRow.appendChild(document.createElement("td")); + let optionsLabel = optionsHeader.appendChild(document.createElement("label")); + let optionsCheckbox = this._popoverOptionsCheckboxElement = optionsData.appendChild(document.createElement("input")); + let optionsCheckboxLabel = optionsData.appendChild(document.createElement("label")); + optionsCheckbox.id = "edit-breakpoint-popover-auto-continue"; + optionsCheckbox.type = "checkbox"; + optionsCheckbox.checked = this._breakpoint.autoContinue; + optionsCheckbox.addEventListener("change", this._popoverToggleAutoContinueCheckboxChanged.bind(this)); + optionsLabel.textContent = WI.UIString("Options"); + optionsCheckboxLabel.setAttribute("for", optionsCheckbox.id); + optionsCheckboxLabel.textContent = WI.UIString("Automatically continue after evaluating"); + + this._popoverContentElement.appendChild(checkboxLabel); + this._popoverContentElement.appendChild(table); + } + + _popoverToggleEnabledCheckboxChanged(event) + { + this._breakpoint.disabled = !event.target.checked; + } + + _conditionCodeMirrorChanged(codeMirror, change) + { + this._breakpoint.condition = (codeMirror.getValue() || "").trim(); + } + + _conditionCodeMirrorBeforeChange(codeMirror, change) + { + if (change.update) { + let newText = change.text.join("").replace(/\n/g, ""); + change.update(change.from, change.to, [newText]); + } + + return true; + } + + _conditionCodeMirrorEscapeOrEnterKey() + { + if (!this._popover) + return; + + this._popover.dismiss(); + } + + _popoverIgnoreInputChanged(event) + { + let ignoreCount = 0; + if (event.target.value) { + ignoreCount = parseInt(event.target.value, 10); + if (isNaN(ignoreCount) || ignoreCount < 0) + ignoreCount = 0; + } + + this._ignoreCountInput.value = ignoreCount; + this._breakpoint.ignoreCount = ignoreCount; + + this._updateIgnoreCountText(); + } + + _popoverToggleAutoContinueCheckboxChanged(event) + { + this._breakpoint.autoContinue = event.target.checked; + } + + _popoverActionsCreateAddActionButton() + { + this._popoverContentElement.classList.remove(WI.BreakpointPopoverController.WidePopoverClassName); + this._actionsContainer.removeChildren(); + + let addActionButton = this._actionsContainer.appendChild(document.createElement("button")); + addActionButton.textContent = WI.UIString("Add Action"); + addActionButton.addEventListener("click", this._popoverActionsAddActionButtonClicked.bind(this)); + } + + _popoverActionsAddActionButtonClicked(event) + { + this._popoverContentElement.classList.add(WI.BreakpointPopoverController.WidePopoverClassName); + this._actionsContainer.removeChildren(); + + let newAction = this._breakpoint.createAction(WI.BreakpointAction.Type.Log); + let newBreakpointActionView = new WI.BreakpointActionView(newAction, this); + this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, -1); + this._popoverOptionsRowElement.classList.remove(WI.BreakpointPopoverController.HiddenStyleClassName); + this._popover.update(); + } + + _popoverActionsInsertBreakpointActionView(breakpointActionView, index) + { + if (index === -1) + this._actionsContainer.appendChild(breakpointActionView.element); + else { + let nextElement = this._actionsContainer.children[index + 1] || null; + this._actionsContainer.insertBefore(breakpointActionView.element, nextElement); + } + } + + _updateIgnoreCountText() + { + if (this._breakpoint.ignoreCount === 1) + this._ignoreCountText.textContent = WI.UIString("time before stopping"); + else + this._ignoreCountText.textContent = WI.UIString("times before stopping"); + } + + breakpointActionViewAppendActionView(breakpointActionView, newAction) + { + let newBreakpointActionView = new WI.BreakpointActionView(newAction, this); + + let index = 0; + let children = this._actionsContainer.children; + for (let i = 0; children.length; ++i) { + if (children[i] === breakpointActionView.element) { + index = i; + break; + } + } + + this._popoverActionsInsertBreakpointActionView(newBreakpointActionView, index); + this._popoverOptionsRowElement.classList.remove(WI.BreakpointPopoverController.HiddenStyleClassName); + + this._popover.update(); + } + + breakpointActionViewRemoveActionView(breakpointActionView) + { + breakpointActionView.element.remove(); + + if (!this._actionsContainer.children.length) { + this._popoverActionsCreateAddActionButton(); + this._popoverOptionsRowElement.classList.add(WI.BreakpointPopoverController.HiddenStyleClassName); + this._popoverOptionsCheckboxElement.checked = false; + } + + this._popover.update(); + } + + breakpointActionViewResized(breakpointActionView) + { + this._popover.update(); + } + + willDismissPopover(popover) + { + console.assert(this._popover === popover); + this._popoverContentElement = null; + this._popoverOptionsRowElement = null; + this._popoverOptionsCheckboxElement = null; + this._actionsContainer = null; + this._popover = null; + } + + didDismissPopover(popover) + { + // Remove Evaluate and Probe actions that have no data. + let emptyActions = this._breakpoint.actions.filter(function(action) { + if (action.type !== WI.BreakpointAction.Type.Evaluate && action.type !== WI.BreakpointAction.Type.Probe) + return false; + return !(action.data && action.data.trim()); + }); + + for (let action of emptyActions) + this._breakpoint.removeAction(action); + + this._breakpoint = null; + } +}; + +WI.BreakpointPopoverController.WidePopoverClassName = "wide"; +WI.BreakpointPopoverController.HiddenStyleClassName = "hidden"; diff --git a/inspector/Controllers/BrowserManager.js b/inspector/Controllers/BrowserManager.js new file mode 100644 index 0000000..2e9704f --- /dev/null +++ b/inspector/Controllers/BrowserManager.js @@ -0,0 +1,124 @@ +/* + * 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}'.`); + } + } +}; diff --git a/inspector/Controllers/CSSManager.js b/inspector/Controllers/CSSManager.js new file mode 100644 index 0000000..60554fc --- /dev/null +++ b/inspector/Controllers/CSSManager.js @@ -0,0 +1,894 @@ +/* + * 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._overridenUserPreferences = 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("::selection"); + case 7: // PseudoId.Scrollbar + return WI.unlocalizedString("::scrollbar"); + case 8: // PseudoId.ScrollbarThumb + return WI.unlocalizedString("::scrollbar-thumb"); + case 9: // PseudoId.ScrollbarButton + return WI.unlocalizedString("::scrollbar-button"); + case 10: // PseudoId.ScrollbarTrack + return WI.unlocalizedString("::scrollbar-track"); + case 11: // PseudoId.ScrollbarTrackPiece + return WI.unlocalizedString("::scrollbar-track-piece"); + case 12: // PseudoId.ScrollbarCorner + return WI.unlocalizedString("::scrollbar-corner"); + case 13: // PseudoId.Resizer + return WI.unlocalizedString("::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.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.Scrollbar: + return WI.unlocalizedString("::scrollbar"); + case CSSManager.PseudoSelectorNames.ScrollbarThumb: + return WI.unlocalizedString("::scrollbar-thumb"); + case CSSManager.PseudoSelectorNames.ScrollbarButton: + return WI.unlocalizedString("::scrollbar-button"); + case CSSManager.PseudoSelectorNames.ScrollbarTrack: + return WI.unlocalizedString("::scrollbar-track"); + case CSSManager.PseudoSelectorNames.ScrollbarTrackPiece: + return WI.unlocalizedString("::scrollbar-track-piece"); + case CSSManager.PseudoSelectorNames.ScrollbarCorner: + return WI.unlocalizedString("::scrollbar-corner"); + case CSSManager.PseudoSelectorNames.Resizer: + return WI.unlocalizedString("::resizer"); + + 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 overridenUserPreferences() { return this._overridenUserPreferences; } + + get defaultUserPreferences() { return this._defaultUserPreferences; } + + get overridenUserPreferences() { return this._overridenUserPreferences; } + + 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._overridenUserPreferences.set(preference, value); + else + this._overridenUserPreferences.delete(preference); + + Promise.allSettled(promises).then(() => { + this.mediaQueryResultChanged(); + this.dispatchEventToListeners(WI.CSSManager.Event.OverridenUserPreferencesDidChange); + }) + } + + 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(); + + 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", + OverridenUserPreferencesDidChange: "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", + Marker: "marker", + Resizer: "resizer", + Scrollbar: "scrollbar", + ScrollbarButton: "scrollbar-button", + ScrollbarCorner: "scrollbar-corner", + ScrollbarThumb: "scrollbar-thumb", + ScrollbarTrack: "scrollbar-track", + ScrollbarTrackPiece: "scrollbar-track-piece", + Selection: "selection", +}; + +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"); diff --git a/inspector/Controllers/CSSQueryController.js b/inspector/Controllers/CSSQueryController.js new file mode 100644 index 0000000..fe00c2e --- /dev/null +++ b/inspector/Controllers/CSSQueryController.js @@ -0,0 +1,86 @@ +/* + * 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, "-_"); + } +}; diff --git a/inspector/Controllers/CallFrameTreeController.js b/inspector/Controllers/CallFrameTreeController.js new file mode 100644 index 0000000..65cb59a --- /dev/null +++ b/inspector/Controllers/CallFrameTreeController.js @@ -0,0 +1,93 @@ +/* + * 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.CallFrameTreeController = class CallFrameTreeController extends WI.Object +{ + constructor(treeOutline) + { + console.assert(treeOutline instanceof WI.TreeOutline); + + super(); + + this._treeOutline = treeOutline; + + if (this._treeOutline.selectable) + this._treeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this); + else + this._treeOutline.addEventListener(WI.TreeOutline.Event.ElementClicked, this._treeElementClicked, this); + } + + // Public + + get treeOutline() { return this._treeOutline; } + + get callFrames() + { + return this._callFrames; + } + + set callFrames(callFrames) + { + callFrames = callFrames || []; + if (this._callFrames === callFrames) + return; + + this._callFrames = callFrames; + + this._treeOutline.removeChildren(); + + for (let callFrame of this._callFrames) + this._treeOutline.appendChild(new WI.CallFrameTreeElement(callFrame)); + } + + disconnect() + { + this._treeOutline.removeEventListener(null, null, this); + } + + // Private + + _treeElementClicked(event) + { + this._showSourceCodeLocation(event.data.treeElement); + } + + _treeSelectionDidChange(event) + { + this._showSourceCodeLocation(this._treeOutline.selectedTreeElement); + } + + _showSourceCodeLocation(treeElement) + { + let callFrame = treeElement.callFrame; + if (!callFrame.sourceCodeLocation) + return; + + WI.showSourceCodeLocation(callFrame.sourceCodeLocation, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + }); + } +}; diff --git a/inspector/Controllers/CanvasManager.js b/inspector/Controllers/CanvasManager.js new file mode 100644 index 0000000..4c91979 --- /dev/null +++ b/inspector/Controllers/CanvasManager.js @@ -0,0 +1,313 @@ +/* + * 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. + */ + +// FIXME: CanvasManager lacks advanced multi-target support. (Canvases per-target) + +WI.CanvasManager = class CanvasManager extends WI.Object +{ + constructor() + { + super(); + + this._enabled = false; + this._canvasCollection = new WI.CanvasCollection; + this._canvasIdentifierMap = new Map; + this._shaderProgramIdentifierMap = new Map; + this._savedRecordings = new Set; + + 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; } + + get shaderPrograms() + { + return Array.from(this._shaderProgramIdentifierMap.values()); + } + + 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._canvasIdentifierMap.clear(); + this._shaderProgramIdentifierMap.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(canvasPayload) + { + console.assert(!this._canvasIdentifierMap.has(canvasPayload.canvasId), `Canvas already exists with id ${canvasPayload.canvasId}.`); + + let canvas = WI.Canvas.fromPayload(canvasPayload); + this._canvasCollection.add(canvas); + this._canvasIdentifierMap.set(canvas.identifier, canvas); + } + + canvasRemoved(canvasIdentifier) + { + let canvas = this._canvasIdentifierMap.take(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + this._saveRecordings(canvas); + + this._canvasCollection.remove(canvas); + + for (let program of canvas.shaderProgramCollection) + this._shaderProgramIdentifierMap.delete(program.identifier); + + canvas.shaderProgramCollection.clear(); + } + + canvasMemoryChanged(canvasIdentifier, memoryCost) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.memoryCost = memoryCost; + } + + clientNodesChanged(canvasIdentifier) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.clientNodesChanged(); + } + + recordingStarted(canvasIdentifier, initiator) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.recordingStarted(initiator); + } + + recordingProgress(canvasIdentifier, framesPayload, bufferUsed) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.recordingProgress(framesPayload, bufferUsed); + } + + recordingFinished(canvasIdentifier, recordingPayload) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.recordingFinished(recordingPayload); + } + + extensionEnabled(canvasIdentifier, extension) + { + let canvas = this._canvasIdentifierMap.get(canvasIdentifier); + console.assert(canvas); + if (!canvas) + return; + + canvas.enableExtension(extension); + } + + programCreated(shaderProgramPayload) + { + let canvas = this._canvasIdentifierMap.get(shaderProgramPayload.canvasId); + console.assert(canvas); + if (!canvas) + return; + + let programId = shaderProgramPayload.programId; + console.assert(!this._shaderProgramIdentifierMap.has(programId), `ShaderProgram already exists with id ${programId}.`); + + // 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(programId, programType, canvas, options); + this._shaderProgramIdentifierMap.set(program.identifier, program); + + canvas.shaderProgramCollection.add(program); + } + + programDeleted(programIdentifier) + { + let program = this._shaderProgramIdentifierMap.take(programIdentifier); + console.assert(program); + if (!program) + return; + + program.canvas.shaderProgramCollection.remove(program); + } + + // Private + + _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}); + } + } + + _mainResourceDidChange(event) + { + console.assert(event.target instanceof WI.Frame); + if (!event.target.isMainFrame()) + return; + + WI.Canvas.resetUniqueDisplayNameNumbers(); + + for (let canvas of this._canvasIdentifierMap.values()) + this._saveRecordings(canvas); + + this._canvasCollection.clear(); + this._canvasIdentifierMap.clear(); + this._shaderProgramIdentifierMap.clear(); + } +}; + +WI.CanvasManager.Event = { + RecordingSaved: "canvas-manager-recording-saved", +}; diff --git a/inspector/Controllers/CodeMirrorBezierEditingController.js b/inspector/Controllers/CodeMirrorBezierEditingController.js new file mode 100644 index 0000000..b0877cd --- /dev/null +++ b/inspector/Controllers/CodeMirrorBezierEditingController.js @@ -0,0 +1,63 @@ +/* + * 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.CodeMirrorBezierEditingController = class CodeMirrorBezierEditingController extends WI.CodeMirrorEditingController +{ + // Public + + get initialValue() + { + return WI.CubicBezier.fromString(this.text); + } + + get cssClassName() + { + return "cubic-bezier"; + } + + popoverWillPresent(popover) + { + this._bezierEditor = new WI.BezierEditor; + this._bezierEditor.addEventListener(WI.BezierEditor.Event.BezierChanged, this._bezierEditorBezierChanged, this); + popover.content = this._bezierEditor.element; + } + + popoverDidPresent(popover) + { + this._bezierEditor.bezier = this.value; + } + + popoverDidDismiss(popover) + { + this._bezierEditor.removeListeners(); + } + + // Private + + _bezierEditorBezierChanged(event) + { + this.value = event.data.bezier; + } +}; diff --git a/inspector/Controllers/CodeMirrorColorEditingController.js b/inspector/Controllers/CodeMirrorColorEditingController.js new file mode 100644 index 0000000..c9cb49e --- /dev/null +++ b/inspector/Controllers/CodeMirrorColorEditingController.js @@ -0,0 +1,58 @@ +/* + * 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.CodeMirrorColorEditingController = class CodeMirrorColorEditingController extends WI.CodeMirrorEditingController +{ + // Public + + get initialValue() + { + return WI.Color.fromString(this.text); + } + + get cssClassName() + { + return "color"; + } + + popoverWillPresent(popover) + { + this._colorPicker = new WI.ColorPicker; + this._colorPicker.addEventListener(WI.ColorPicker.Event.ColorChanged, this._colorPickerColorChanged, this); + popover.content = this._colorPicker.element; + } + + popoverDidPresent(popover) + { + this._colorPicker.color = this._value; + } + + // Private + + _colorPickerColorChanged(event) + { + this.value = event.target.color; + } +}; diff --git a/inspector/Controllers/CodeMirrorCompletionController.css b/inspector/Controllers/CodeMirrorCompletionController.css new file mode 100644 index 0000000..eff18cf --- /dev/null +++ b/inspector/Controllers/CodeMirrorCompletionController.css @@ -0,0 +1,29 @@ +/* + * 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. + */ + +.CodeMirror .CodeMirror-lines .completion-hint { + text-decoration: none !important; + opacity: 0.4; +} diff --git a/inspector/Controllers/CodeMirrorCompletionController.js b/inspector/Controllers/CodeMirrorCompletionController.js new file mode 100644 index 0000000..88857d8 --- /dev/null +++ b/inspector/Controllers/CodeMirrorCompletionController.js @@ -0,0 +1,926 @@ +/* + * 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.CodeMirrorCompletionController = class CodeMirrorCompletionController extends WI.Object +{ + constructor(mode, codeMirror, delegate, stopCharactersRegex) + { + console.assert(Object.values(WI.CodeMirrorCompletionController.Mode).includes(mode), mode); + console.assert(codeMirror instanceof CodeMirror, codeMirror); + + super(); + + this._mode = mode; + this._codeMirror = codeMirror; + this._stopCharactersRegex = stopCharactersRegex || null; + this._delegate = delegate || null; + + this._startOffset = NaN; + this._endOffset = NaN; + this._lineNumber = NaN; + this._prefix = ""; + this._noEndingSemicolon = false; + this._completions = []; + this._extendedCompletionProviders = {}; + + this._suggestionsView = new WI.CompletionSuggestionsView(this); + + this._keyMap = { + "Up": this._handleUpKey.bind(this), + "Down": this._handleDownKey.bind(this), + "Right": this._handleRightOrEnterKey.bind(this), + "Esc": this._handleEscapeKey.bind(this), + "Enter": this._handleRightOrEnterKey.bind(this), + "Tab": this._handleTabKey.bind(this), + "Cmd-A": this._handleHideKey.bind(this), + "Cmd-Z": this._handleHideKey.bind(this), + "Shift-Cmd-Z": this._handleHideKey.bind(this), + "Cmd-Y": this._handleHideKey.bind(this) + }; + + this._handleChangeListener = this._handleChange.bind(this); + this._handleCursorActivityListener = this._handleCursorActivity.bind(this); + this._handleHideActionListener = this._handleHideAction.bind(this); + + this._codeMirror.addKeyMap(this._keyMap); + + this._codeMirror.on("change", this._handleChangeListener); + this._codeMirror.on("cursorActivity", this._handleCursorActivityListener); + this._codeMirror.on("blur", this._handleHideActionListener); + this._codeMirror.on("scroll", this._handleHideActionListener); + + this._updatePromise = null; + } + + // Public + + get mode() { return this._mode; } + + addExtendedCompletionProvider(modeName, provider) + { + this._extendedCompletionProviders[modeName] = provider; + } + + updateCompletions(completions, implicitSuffix) + { + if (isNaN(this._startOffset) || isNaN(this._endOffset) || isNaN(this._lineNumber)) + return; + + if (!completions || !completions.length) { + this.hideCompletions(); + return; + } + + this._completions = completions; + + if (typeof implicitSuffix === "string") + this._implicitSuffix = implicitSuffix; + + var from = {line: this._lineNumber, ch: this._startOffset}; + var to = {line: this._lineNumber, ch: this._endOffset}; + + var firstCharCoords = this._codeMirror.cursorCoords(from); + var lastCharCoords = this._codeMirror.cursorCoords(to); + var bounds = new WI.Rect(firstCharCoords.left, firstCharCoords.top, lastCharCoords.right - firstCharCoords.left, firstCharCoords.bottom - firstCharCoords.top); + + // Try to restore the previous selected index, otherwise just select the first. + var index = this._currentCompletion ? completions.indexOf(this._currentCompletion) : 0; + if (index === -1) + index = 0; + + if (this._forced || completions.length > 1 || completions[index] !== this._prefix) { + // Update and show the suggestion list. + this._suggestionsView.update(completions, index); + this._suggestionsView.show(bounds); + } else if (this._implicitSuffix) { + // The prefix and the completion exactly match, but there is an implicit suffix. + // Just hide the suggestion list and keep the completion hint for the implicit suffix. + this._suggestionsView.hide(); + } else { + // The prefix and the completion exactly match, hide the completions. Return early so + // the completion hint isn't updated. + this.hideCompletions(); + return; + } + + this._applyCompletionHint(completions[index]); + + this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.CompletionsFound); + } + + isCompletionChange(change) + { + return this._ignoreChange || change.origin === WI.CodeMirrorCompletionController.CompletionOrigin || change.origin === WI.CodeMirrorCompletionController.DeleteCompletionOrigin; + } + + isShowingCompletions() + { + return this._suggestionsView.visible || (this._completionHintMarker && this._completionHintMarker.find()); + } + + isHandlingClickEvent() + { + return this._suggestionsView.isHandlingClickEvent(); + } + + commitCurrentCompletion() + { + this._removeCompletionHint(true, true); + + let replacementText = this._currentReplacementText; + if (!replacementText) + return; + + let from = {line: this._lineNumber, ch: this._startOffset}; + let cursor = {line: this._lineNumber, ch: this._endOffset}; + let to = {line: this._lineNumber, ch: this._startOffset + replacementText.length}; + + let lastChar = this._currentCompletion.charAt(this._currentCompletion.length - 1); + let isClosing = ")]}".indexOf(lastChar); + if (isClosing !== -1) + to.ch -= 1 + this._implicitSuffix.length; + + this._codeMirror.replaceRange(replacementText, from, cursor, WI.CodeMirrorCompletionController.CompletionOrigin); + + // Don't call _removeLastChangeFromHistory here to allow the committed completion to be undone. + + this._codeMirror.setCursor(to); + + this.hideCompletions(); + } + + hideCompletions() + { + this._suggestionsView.hide(); + + this._removeCompletionHint(); + + this._startOffset = NaN; + this._endOffset = NaN; + this._lineNumber = NaN; + this._prefix = ""; + this._completions = []; + this._implicitSuffix = ""; + this._forced = false; + + delete this._currentCompletion; + delete this._ignoreNextCursorActivity; + + this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.NoCompletionsFound); + } + + close() + { + this._codeMirror.removeKeyMap(this._keyMap); + + this._codeMirror.off("change", this._handleChangeListener); + this._codeMirror.off("cursorActivity", this._handleCursorActivityListener); + this._codeMirror.off("blur", this._handleHideActionListener); + this._codeMirror.off("scroll", this._handleHideActionListener); + } + + completeAtCurrentPositionIfNeeded(force) + { + this._resolveUpdatePromise(WI.CodeMirrorCompletionController.UpdatePromise.Canceled); + + var update = this._updatePromise = new WI.WrappedPromise; + + this._completeAtCurrentPosition(force); + + return update.promise; + } + + // Protected + + completionSuggestionsSelectedCompletion(suggestionsView, completionText) + { + this._applyCompletionHint(completionText); + } + + completionSuggestionsClickedCompletion(suggestionsView, completionText) + { + // The clicked suggestion causes the editor to loose focus. Restore it so the user can keep typing. + this._codeMirror.focus(); + + this._applyCompletionHint(completionText); + this._commitCompletionHint(); + } + + set noEndingSemicolon(noEndingSemicolon) + { + this._noEndingSemicolon = noEndingSemicolon; + } + + // Private + + _resolveUpdatePromise(message) + { + if (!this._updatePromise) + return; + + this._updatePromise.resolve(message); + this._updatePromise = null; + } + + get _currentReplacementText() + { + return (this._currentCompletion ?? "") + (this._implicitSuffix ?? ""); + } + + _hasPendingCompletion() + { + return !isNaN(this._startOffset) && !isNaN(this._endOffset) && !isNaN(this._lineNumber); + } + + _notifyCompletionsHiddenSoon() + { + function notify() + { + if (this._completionHintMarker) + return; + + if (this._delegate && typeof this._delegate.completionControllerCompletionsHidden === "function") + this._delegate.completionControllerCompletionsHidden(this); + } + + if (this._notifyCompletionsHiddenIfNeededTimeout) + clearTimeout(this._notifyCompletionsHiddenIfNeededTimeout); + this._notifyCompletionsHiddenIfNeededTimeout = setTimeout(notify.bind(this), WI.CodeMirrorCompletionController.CompletionsHiddenDelay); + } + + _createCompletionHintMarker(position, text) + { + var container = document.createElement("span"); + container.classList.add(WI.CodeMirrorCompletionController.CompletionHintStyleClassName); + container.textContent = text; + + container.addEventListener("mousedown", (event) => { + event.preventDefault(); + this._commitCompletionHint(); + + // The clicked hint marker causes the editor to loose focus. Restore it so the user can keep typing. + setTimeout(() => { this._codeMirror.focus(); }, 0); + }); + + this._completionHintMarker = this._codeMirror.setUniqueBookmark(position, {widget: container, insertLeft: true}); + } + + _applyCompletionHint(completionText) + { + console.assert(completionText); + if (!completionText) + return; + + function update() + { + this._currentCompletion = completionText; + + this._removeCompletionHint(true, true); + + var replacementText = this._currentReplacementText; + + var from = {line: this._lineNumber, ch: this._startOffset}; + var cursor = {line: this._lineNumber, ch: this._endOffset}; + var currentText = this._codeMirror.getRange(from, cursor); + + this._createCompletionHintMarker(cursor, replacementText.replace(currentText, "")); + } + + this._ignoreChange = true; + this._ignoreNextCursorActivity = true; + + this._codeMirror.operation(update.bind(this)); + + delete this._ignoreChange; + } + + _commitCompletionHint() + { + this._ignoreChange = true; + this._ignoreNextCursorActivity = true; + + this._codeMirror.operation(this.commitCurrentCompletion.bind(this)); + + delete this._ignoreChange; + } + + _removeLastChangeFromHistory() + { + var history = this._codeMirror.getHistory(); + + // We don't expect a undone history. But if there is one clear it. If could lead to undefined behavior. + console.assert(!history.undone.length); + history.undone = []; + + // Pop the last item from the done history. + console.assert(history.done.length); + history.done.pop(); + + this._codeMirror.setHistory(history); + } + + _removeCompletionHint(nonatomic, dontRestorePrefix) + { + if (!this._completionHintMarker) + return; + + this._notifyCompletionsHiddenSoon(); + + function clearMarker(marker) + { + if (!marker) + return; + + var range = marker.find(); + if (range) + marker.clear(); + + return null; + } + + function update() + { + this._completionHintMarker = clearMarker(this._completionHintMarker); + + if (dontRestorePrefix) + return; + + console.assert(!isNaN(this._startOffset)); + console.assert(!isNaN(this._endOffset)); + console.assert(!isNaN(this._lineNumber)); + + var from = {line: this._lineNumber, ch: this._startOffset}; + var to = {line: this._lineNumber, ch: this._endOffset}; + + this._codeMirror.replaceRange(this._prefix, from, to, WI.CodeMirrorCompletionController.DeleteCompletionOrigin); + this._removeLastChangeFromHistory(); + } + + if (nonatomic) { + update.call(this); + return; + } + + this._ignoreChange = true; + + this._codeMirror.operation(update.bind(this)); + + delete this._ignoreChange; + } + + _scanStringForExpression(modeName, string, startOffset, direction, allowMiddleAndEmpty, includeStopCharacter, ignoreInitialUnmatchedOpenBracket, stopCharactersRegex) + { + console.assert(direction === -1 || direction === 1); + + var stopCharactersRegex = stopCharactersRegex || this._stopCharactersRegex || WI.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap[modeName] || WI.CodeMirrorCompletionController.GenericStopCharactersRegex; + + function isStopCharacter(character) + { + return stopCharactersRegex.test(character); + } + + function isOpenBracketCharacter(character) + { + return WI.CodeMirrorCompletionController.OpenBracketCharactersRegex.test(character); + } + + function isCloseBracketCharacter(character) + { + return WI.CodeMirrorCompletionController.CloseBracketCharactersRegex.test(character); + } + + function matchingBracketCharacter(character) + { + return WI.CodeMirrorCompletionController.MatchingBrackets[character]; + } + + var endOffset = Math.min(startOffset, string.length); + + var endOfLineOrWord = endOffset === string.length || isStopCharacter(string.charAt(endOffset)); + + if (!endOfLineOrWord && !allowMiddleAndEmpty) + return null; + + var bracketStack = []; + var bracketOffsetStack = []; + + var startOffset = endOffset; + var firstOffset = endOffset + direction; + for (var i = firstOffset; direction > 0 ? i < string.length : i >= 0; i += direction) { + var character = string.charAt(i); + + // Ignore stop characters when we are inside brackets. + if (isStopCharacter(character) && !bracketStack.length) + break; + + if (isCloseBracketCharacter(character)) { + bracketStack.push(character); + bracketOffsetStack.push(i); + } else if (isOpenBracketCharacter(character)) { + if ((!ignoreInitialUnmatchedOpenBracket || i !== firstOffset) && (!bracketStack.length || matchingBracketCharacter(character) !== bracketStack.lastValue)) + break; + + bracketOffsetStack.pop(); + bracketStack.pop(); + } + + startOffset = i + (direction > 0 ? 1 : 0); + } + + if (bracketOffsetStack.length) + startOffset = bracketOffsetStack.pop() + 1; + + if (includeStopCharacter && startOffset > 0 && startOffset < string.length) + startOffset += direction; + + if (direction > 0) { + var tempEndOffset = endOffset; + endOffset = startOffset; + startOffset = tempEndOffset; + } + + return {string: string.substring(startOffset, endOffset), startOffset, endOffset}; + } + + _completeAtCurrentPosition(force) + { + if (this._codeMirror.somethingSelected()) { + this.hideCompletions(); + return; + } + + this._removeCompletionHint(true, true); + + var cursor = this._codeMirror.getCursor(); + var token = this._codeMirror.getTokenAt(cursor); + + // Don't try to complete inside comments or strings. + if (token.type && /\b(comment|string)\b/.test(token.type)) { + this.hideCompletions(); + return; + } + + var mode = this._codeMirror.getMode(); + var innerMode = CodeMirror.innerMode(mode, token.state).mode; + var modeName = innerMode.alternateName || innerMode.name; + + var lineNumber = cursor.line; + var lineString = this._codeMirror.getLine(lineNumber); + + var backwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, -1, force); + if (!backwardScanResult) { + this.hideCompletions(); + return; + } + + var forwardScanResult = this._scanStringForExpression(modeName, lineString, cursor.ch, 1, true, true); + var suffix = forwardScanResult.string; + + this._ignoreNextCursorActivity = true; + + this._startOffset = backwardScanResult.startOffset; + this._endOffset = backwardScanResult.endOffset; + this._lineNumber = lineNumber; + this._prefix = backwardScanResult.string; + this._completions = []; + this._implicitSuffix = ""; + this._forced = force; + + var baseExpressionStopCharactersRegex = WI.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap[modeName]; + if (baseExpressionStopCharactersRegex) + var baseScanResult = this._scanStringForExpression(modeName, lineString, this._startOffset, -1, true, false, true, baseExpressionStopCharactersRegex); + + if (!force && !backwardScanResult.string && (!baseScanResult || !baseScanResult.string)) { + this.hideCompletions(); + return; + } + + var defaultCompletions = []; + + switch (modeName) { + case "css": + defaultCompletions = this._generateCSSCompletions(token, baseScanResult ? baseScanResult.string : null, suffix); + break; + case "javascript": + defaultCompletions = this._generateJavaScriptCompletions(token, baseScanResult ? baseScanResult.string : null, suffix); + break; + } + + var extendedCompletionsProvider = this._extendedCompletionProviders[modeName]; + if (extendedCompletionsProvider) { + extendedCompletionsProvider.completionControllerCompletionsNeeded(this, defaultCompletions, baseScanResult ? baseScanResult.string : null, this._prefix, suffix, force); + return; + } + + if (this._delegate && typeof this._delegate.completionControllerCompletionsNeeded === "function") + this._delegate.completionControllerCompletionsNeeded(this, this._prefix, defaultCompletions, baseScanResult ? baseScanResult.string : null, suffix, force); + else + this.updateCompletions(defaultCompletions); + } + + _generateCSSCompletions(mainToken, base, suffix) + { + // We support completion inside CSS block context and functions. + if (mainToken.state.state === "media" || mainToken.state.state === "top") + return []; + + // Don't complete in the middle of a property name. + if (/^[a-z]/i.test(suffix)) + return []; + + var token = mainToken; + var lineNumber = this._lineNumber; + + let getPreviousToken = () => { + // Found the beginning of the line. Go to the previous line. + if (!token.start) { + --lineNumber; + + // No more lines, stop. + if (lineNumber < 0) + return null; + } + + return this._codeMirror.getTokenAt({line: lineNumber, ch: token.start ? token.start : Number.MAX_VALUE}); + }; + + // Inside a function, determine the function name. + if (token.state.state === "parens") { + // Scan backwards looking for the function paren boundary. + while (token && token.state.state === "parens" && token.string !== "(") + token = getPreviousToken(); + + // The immediately preceding token should have the function name. + if (token) + token = getPreviousToken(); + + // No completions if no function name found. + if (!token) + return []; + + let functionName = token.string; + if (!functionName) + return []; + + let functionCompletions = WI.CSSKeywordCompletions.forFunction(functionName).startsWith(this._prefix); + + if (this._delegate && this._delegate.completionControllerCSSFunctionValuesNeeded) + functionCompletions = this._delegate.completionControllerCSSFunctionValuesNeeded(this, functionName, functionCompletions); + + return functionCompletions; + } + + // Scan backwards looking for the current property. + while (token.state.state === "prop") { + let previousToken = getPreviousToken(); + if (!previousToken) + break; + token = previousToken; + } + + // If we have a property token and it's not the main token, then we are working on + // the value for that property and should complete allowed values. + if (mainToken !== token && token.type && /\bproperty\b/.test(token.type)) { + var propertyName = token.string; + + // If there is a suffix and it isn't a semicolon, then we should use a space since + // the user is editing in the middle. Likewise if the suffix starts with an open + // paren we are changing a function name so don't add a suffix. + this._implicitSuffix = " "; + if (suffix === ";") + this._implicitSuffix = this._noEndingSemicolon ? "" : ";"; + else if (suffix.startsWith("(")) + this._implicitSuffix = ""; + + // Don't use an implicit suffix if it would be the same as the existing suffix. + if (this._implicitSuffix === suffix) + this._implicitSuffix = ""; + + let completions = WI.CSSKeywordCompletions.forProperty(propertyName).startsWith(this._prefix); + + if (suffix.startsWith("(")) + completions = completions.map((x) => x.replace(/\(\)$/, "")); + + return completions; + } + + this._implicitSuffix = suffix !== ":" ? ": " : ""; + + // Complete property names. + return WI.cssManager.propertyNameCompletions.startsWith(this._prefix); + } + + _generateJavaScriptCompletions(mainToken, base, suffix) + { + // If there is a base expression then we should not attempt to match any keywords or variables. + // Allow only open bracket characters at the end of the base, otherwise leave completions with + // a base up to the delegate to figure out. + if (base && !/[({[]$/.test(base)) + return []; + + var matchingWords = []; + + var prefix = this._prefix; + + var localState = mainToken.state.localState ? mainToken.state.localState : mainToken.state; + + var declaringVariable = localState.lexical.type === "vardef"; + var insideSwitch = localState.lexical.prev ? localState.lexical.prev.info === "switch" : false; + var insideBlock = localState.lexical.prev ? localState.lexical.prev.type === "}" : false; + var insideParenthesis = localState.lexical.type === ")"; + var insideBrackets = localState.lexical.type === "]"; + + // FIXME: Include module keywords if we know this is a module environment. + // var moduleKeywords = ["default", "export", "import"]; + + const allKeywords = [ + "break", "case", "catch", "class", "const", "continue", "debugger", "default", + "delete", "do", "else", "extends", "false", "finally", "for", "function", + "if", "in", "Infinity", "instanceof", "let", "NaN", "new", "null", "of", + "return", "static", "super", "switch", "this", "throw", "true", "try", + "typeof", "undefined", "var", "void", "while", "with", "yield" + ]; + const valueKeywords = ["false", "Infinity", "NaN", "null", "this", "true", "undefined", "globalThis"]; + + const allowedKeywordsInsideBlocks = new Set(allKeywords); + const allowedKeywordsWhenDeclaringVariable = new Set(valueKeywords); + const allowedKeywordsInsideParenthesis = new Set(valueKeywords.concat(["class", "function"])); + const allowedKeywordsInsideBrackets = allowedKeywordsInsideParenthesis; + const allowedKeywordsOnlyInsideSwitch = new Set(["case", "default"]); + + function matchKeywords(keywords) + { + for (let keyword of keywords) { + if (!insideSwitch && allowedKeywordsOnlyInsideSwitch.has(keyword)) + continue; + if (insideBlock && !allowedKeywordsInsideBlocks.has(keyword)) + continue; + if (insideBrackets && !allowedKeywordsInsideBrackets.has(keyword)) + continue; + if (insideParenthesis && !allowedKeywordsInsideParenthesis.has(keyword)) + continue; + if (declaringVariable && !allowedKeywordsWhenDeclaringVariable.has(keyword)) + continue; + if (!keyword.startsWith(prefix)) + continue; + matchingWords.push(keyword); + } + } + + function matchVariables() + { + function filterVariables(variables) + { + for (var variable = variables; variable; variable = variable.next) { + // Don't match the variable if this token is in a variable declaration. + // Otherwise the currently typed text will always match and that isn't useful. + if (declaringVariable && variable.name === prefix) + continue; + + if (variable.name.startsWith(prefix) && !matchingWords.includes(variable.name)) + matchingWords.push(variable.name); + } + } + + var context = localState.context; + while (context) { + if (context.vars) + filterVariables(context.vars); + context = context.prev; + } + + if (localState.localVars) + filterVariables(localState.localVars); + if (localState.globalVars) + filterVariables(localState.globalVars); + } + + switch (suffix.substring(0, 1)) { + case "": + case " ": + matchVariables(); + matchKeywords(allKeywords); + break; + + case ".": + case "[": + matchVariables(); + matchKeywords(["false", "Infinity", "NaN", "this", "true"]); + break; + + case "(": + matchVariables(); + matchKeywords(["catch", "else", "for", "function", "if", "return", "switch", "throw", "while", "with", "yield"]); + break; + + case "{": + matchKeywords(["do", "else", "finally", "return", "try", "yield"]); + break; + + case ":": + if (insideSwitch) + matchKeywords(["case", "default"]); + break; + + case ";": + matchVariables(); + matchKeywords(valueKeywords); + matchKeywords(["break", "continue", "debugger", "return", "void"]); + break; + } + + return matchingWords; + } + + _handleUpKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._suggestionsView.selectPrevious(); + } + + _handleDownKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._suggestionsView.selectNext(); + } + + _handleRightOrEnterKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + this._commitCompletionHint(); + } + + _handleEscapeKey(codeMirror) + { + var delegateImplementsShouldAllowEscapeCompletion = this._delegate && typeof this._delegate.completionControllerShouldAllowEscapeCompletion === "function"; + if (this._hasPendingCompletion()) + this.hideCompletions(); + else if (this._codeMirror.getOption("readOnly")) + return CodeMirror.Pass; + else if (!delegateImplementsShouldAllowEscapeCompletion || this._delegate.completionControllerShouldAllowEscapeCompletion(this)) + this._completeAtCurrentPosition(true); + else + return CodeMirror.Pass; + } + + _handleTabKey(codeMirror) + { + if (!this._hasPendingCompletion()) + return CodeMirror.Pass; + + if (!this.isShowingCompletions()) + return; + + console.assert(this._completions.length); + if (!this._completions.length) + return; + + console.assert(this._currentCompletion); + if (!this._currentCompletion) + return; + + // Commit the current completion if there is only one suggestion. + if (this._completions.length === 1) { + this._commitCompletionHint(); + return; + } + + var prefixLength = this._prefix.length; + + var commonPrefix = this._completions[0]; + for (var i = 1; i < this._completions.length; ++i) { + var completion = this._completions[i]; + var lastIndex = Math.min(commonPrefix.length, completion.length); + for (var j = prefixLength; j < lastIndex; ++j) { + if (commonPrefix[j] !== completion[j]) { + commonPrefix = commonPrefix.substr(0, j); + break; + } + } + } + + // Commit the current completion if there is no common prefix that is longer. + if (commonPrefix === this._prefix) { + this._commitCompletionHint(); + return; + } + + // Set the prefix to the common prefix so _applyCompletionHint will insert the + // common prefix as commited text. Adjust _endOffset to match the new prefix. + this._prefix = commonPrefix; + this._endOffset = this._startOffset + commonPrefix.length; + + this._applyCompletionHint(this._currentCompletion); + } + + _handleChange(codeMirror, change) + { + if (this.isCompletionChange(change)) + return; + + this._ignoreNextCursorActivity = true; + + if (!change.origin || change.origin.charAt(0) !== "+") { + this.hideCompletions(); + return; + } + + // Only complete on delete if we are showing completions already. + if (change.origin === "+delete" && !this._hasPendingCompletion()) + return; + + this._completeAtCurrentPosition(false); + } + + _handleCursorActivity(codeMirror) + { + if (this._ignoreChange) + return; + + if (this._ignoreNextCursorActivity) { + delete this._ignoreNextCursorActivity; + return; + } + + this.hideCompletions(); + } + + _handleHideKey(codeMirror) + { + this.hideCompletions(); + + return CodeMirror.Pass; + } + + _handleHideAction(codeMirror) + { + // Clicking a suggestion causes the editor to blur. We don't want to hide completions in this case. + if (this.isHandlingClickEvent()) + return; + + this.hideCompletions(); + } +}; + +WI.CodeMirrorCompletionController.Mode = { + Basic: "basic", + EventBreakpoint: "event-breakpoint", + ExceptionBreakpoint: "exception-breakpoint", + FullConsoleCommandLineAPI: "full-console-command-line-api", + PausedConsoleCommandLineAPI: "paused-console-command-line-api", +}; + +WI.CodeMirrorCompletionController.UpdatePromise = { + Canceled: "code-mirror-completion-controller-canceled", + CompletionsFound: "code-mirror-completion-controller-completions-found", + NoCompletionsFound: "code-mirror-completion-controller-no-completions-found" +}; + +WI.CodeMirrorCompletionController.GenericStopCharactersRegex = /[\s=:;,]/; +WI.CodeMirrorCompletionController.DefaultStopCharactersRegexModeMap = {"css": /[\s:;,{}()]/, "javascript": /[\s=:;,!+\-*/%&|^~?<>.{}()[\]]/}; +WI.CodeMirrorCompletionController.BaseExpressionStopCharactersRegexModeMap = {"javascript": /[\s=:;,!+\-*/%&|^~?<>]/}; +WI.CodeMirrorCompletionController.OpenBracketCharactersRegex = /[({[]/; +WI.CodeMirrorCompletionController.CloseBracketCharactersRegex = /[)}\]]/; +WI.CodeMirrorCompletionController.MatchingBrackets = {"{": "}", "(": ")", "[": "]", "}": "{", ")": "(", "]": "["}; +WI.CodeMirrorCompletionController.CompletionHintStyleClassName = "completion-hint"; +WI.CodeMirrorCompletionController.CompletionsHiddenDelay = 250; +WI.CodeMirrorCompletionController.CompletionTypingDelay = 250; +WI.CodeMirrorCompletionController.CompletionOrigin = "+completion"; +WI.CodeMirrorCompletionController.DeleteCompletionOrigin = "+delete-completion"; diff --git a/inspector/Controllers/CodeMirrorDragToAdjustNumberController.css b/inspector/Controllers/CodeMirrorDragToAdjustNumberController.css new file mode 100644 index 0000000..d67d6b3 --- /dev/null +++ b/inspector/Controllers/CodeMirrorDragToAdjustNumberController.css @@ -0,0 +1,28 @@ +/* + * 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. + */ + +.CodeMirror.drag-to-adjust .CodeMirror-lines { + cursor: col-resize; +} diff --git a/inspector/Controllers/CodeMirrorDragToAdjustNumberController.js b/inspector/Controllers/CodeMirrorDragToAdjustNumberController.js new file mode 100644 index 0000000..2d55048 --- /dev/null +++ b/inspector/Controllers/CodeMirrorDragToAdjustNumberController.js @@ -0,0 +1,121 @@ +/* + * 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.CodeMirrorDragToAdjustNumberController = class CodeMirrorDragToAdjustNumberController extends WI.Object +{ + constructor(codeMirror) + { + super(); + + this._codeMirror = codeMirror; + + this._dragToAdjustController = new WI.DragToAdjustController(this); + } + + // Public + + get enabled() + { + return this._dragToAdjustController.enabled; + } + + set enabled(enabled) + { + if (this.enabled === enabled) + return; + + this._dragToAdjustController.element = this._codeMirror.getWrapperElement(); + this._dragToAdjustController.enabled = enabled; + } + + // Protected + + dragToAdjustControllerActiveStateChanged(dragToAdjustController) + { + if (!dragToAdjustController.active) + this._hoveredTokenInfo = null; + } + + dragToAdjustControllerCanBeActivated(dragToAdjustController) + { + return !this._codeMirror.getOption("readOnly"); + } + + dragToAdjustControllerCanBeAdjusted(dragToAdjustController) + { + + return this._hoveredTokenInfo && this._hoveredTokenInfo.containsNumber; + } + + dragToAdjustControllerWasAdjustedByAmount(dragToAdjustController, amount) + { + this._codeMirror.alterNumberInRange(amount, this._hoveredTokenInfo.startPosition, this._hoveredTokenInfo.endPosition, false); + } + + dragToAdjustControllerDidReset(dragToAdjustController) + { + this._hoveredTokenInfo = null; + } + + dragToAdjustControllerCanAdjustObjectAtPoint(dragToAdjustController, point) + { + var position = this._codeMirror.coordsChar({left: point.x, top: point.y}); + var token = this._codeMirror.getTokenAt(position); + + if (!token || !token.type || !token.string) { + if (this._hoveredTokenInfo) + dragToAdjustController.reset(); + return false; + } + + // Stop right here if we're hovering the same token as we were last time. + if (this._hoveredTokenInfo && this._hoveredTokenInfo.line === position.line && + this._hoveredTokenInfo.token.start === token.start && this._hoveredTokenInfo.token.end === token.end) + return this._hoveredTokenInfo.token.type.indexOf("number") !== -1; + + var containsNumber = token.type.indexOf("number") !== -1; + this._hoveredTokenInfo = { + token, + line: position.line, + containsNumber, + startPosition: { + ch: token.start, + line: position.line + }, + endPosition: { + ch: token.end, + line: position.line + } + }; + + return containsNumber; + } +}; + +CodeMirror.defineOption("dragToAdjustNumbers", true, function(codeMirror, value, oldValue) { + if (!codeMirror.dragToAdjustNumberController) + codeMirror.dragToAdjustNumberController = new WI.CodeMirrorDragToAdjustNumberController(codeMirror); + codeMirror.dragToAdjustNumberController.enabled = value; +}); diff --git a/inspector/Controllers/CodeMirrorEditingController.js b/inspector/Controllers/CodeMirrorEditingController.js new file mode 100644 index 0000000..e3ce593 --- /dev/null +++ b/inspector/Controllers/CodeMirrorEditingController.js @@ -0,0 +1,200 @@ +/* + * 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.CodeMirrorEditingController = class CodeMirrorEditingController extends WI.Object +{ + constructor(codeMirror, marker) + { + super(); + + this._codeMirror = codeMirror; + this._marker = marker; + this._delegate = null; + + this._range = marker.range; + + // The value must support .toString() and .copy() methods. + this._value = this.initialValue; + + this._keyboardShortcutEsc = new WI.KeyboardShortcut(null, WI.KeyboardShortcut.Key.Escape); + } + + // Public + + get marker() + { + return this._marker; + } + + get range() + { + return this._range; + } + + get value() + { + return this._value; + } + + set value(value) + { + this.text = value.toString(); + this._value = value; + } + + get delegate() + { + return this._delegate; + } + + set delegate(delegate) + { + this._delegate = delegate; + } + + get text() + { + var from = {line: this._range.startLine, ch: this._range.startColumn}; + var to = {line: this._range.endLine, ch: this._range.endColumn}; + return this._codeMirror.getRange(from, to); + } + + set text(text) + { + var from = {line: this._range.startLine, ch: this._range.startColumn}; + var to = {line: this._range.endLine, ch: this._range.endColumn}; + this._codeMirror.replaceRange(text, from, to); + + var lines = text.split("\n"); + var endLine = this._range.startLine + lines.length - 1; + var endColumn = lines.length > 1 ? lines.lastValue.length : this._range.startColumn + text.length; + this._range = new WI.TextRange(this._range.startLine, this._range.startColumn, endLine, endColumn); + } + + get initialValue() + { + // Implemented by subclasses. + return this.text; + } + + get cssClassName() + { + // Implemented by subclasses. + return ""; + } + + get popover() + { + return this._popover; + } + + get popoverPreferredEdges() + { + // Best to display the popover to the left or above the edited range since its end position may change, but not its start + // position. This way we minimize the chances of overlaying the edited range as it changes. + return [WI.RectEdge.MIN_X, WI.RectEdge.MIN_Y, WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X]; + } + + popoverTargetFrameWithRects(rects) + { + return WI.Rect.unionOfRects(rects); + } + + presentHoverMenu() + { + if (!this.cssClassName) + return; + + this._hoverMenu = new WI.HoverMenu(this); + this._hoverMenu.element.classList.add(this.cssClassName); + this._rects = this._marker.rects; + this._hoverMenu.present(this._rects); + } + + dismissHoverMenu(discrete) + { + if (!this._hoverMenu) + return; + + this._hoverMenu.dismiss(discrete); + } + + popoverWillPresent(popover) + { + // Implemented by subclasses. + } + + popoverDidPresent(popover) + { + // Implemented by subclasses. + } + + popoverDidDismiss(popover) + { + // Implemented by subclasses. + } + + // Protected + + handleKeydownEvent(event) + { + if (!this._keyboardShortcutEsc.matchesEvent(event) || !this._popover.visible) + return false; + + this.value = this._originalValue; + this._popover.dismiss(); + + return true; + } + + hoverMenuButtonWasPressed(hoverMenu) + { + this._popover = new WI.Popover(this); + this.popoverWillPresent(this._popover); + this._popover.present(this.popoverTargetFrameWithRects(this._rects).pad(2), this.popoverPreferredEdges); + this.popoverDidPresent(this._popover); + + WI.addWindowKeydownListener(this); + + hoverMenu.dismiss(); + + if (this._delegate && typeof this._delegate.editingControllerDidStartEditing === "function") + this._delegate.editingControllerDidStartEditing(this); + + this._originalValue = this._value.copy(); + } + + didDismissPopover(popover) + { + delete this._popover; + delete this._originalValue; + + WI.removeWindowKeydownListener(this); + this.popoverDidDismiss(); + + if (this._delegate && typeof this._delegate.editingControllerDidFinishEditing === "function") + this._delegate.editingControllerDidFinishEditing(this); + } +}; diff --git a/inspector/Controllers/CodeMirrorGradientEditingController.js b/inspector/Controllers/CodeMirrorGradientEditingController.js new file mode 100644 index 0000000..df2fcaa --- /dev/null +++ b/inspector/Controllers/CodeMirrorGradientEditingController.js @@ -0,0 +1,78 @@ +/* + * 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.CodeMirrorGradientEditingController = class CodeMirrorGradientEditingController extends WI.CodeMirrorEditingController +{ + // Public + + get initialValue() + { + return WI.Gradient.fromString(this.text); + } + + get cssClassName() + { + return "gradient"; + } + + get popoverPreferredEdges() + { + // Since the gradient editor can resize to be quite tall, let's avoid displaying the popover + // above the edited value so that it may not change which edge it attaches to upon editing a stop. + return [WI.RectEdge.MIN_X, WI.RectEdge.MAX_Y, WI.RectEdge.MAX_X]; + } + + popoverTargetFrameWithRects(rects) + { + // If a gradient is defined across several lines, we probably want to use the first line only + // as a target frame for the editor since we may reformat the gradient value to fit on a single line. + return rects[0]; + } + + popoverWillPresent(popover) + { + function handleColorPickerToggled(event) + { + popover.update(); + } + + this._gradientEditor = new WI.GradientEditor; + this._gradientEditor.addEventListener(WI.GradientEditor.Event.GradientChanged, this._gradientEditorGradientChanged, this); + this._gradientEditor.addEventListener(WI.GradientEditor.Event.ColorPickerToggled, handleColorPickerToggled, this); + popover.content = this._gradientEditor.element; + } + + popoverDidPresent(popover) + { + this._gradientEditor.gradient = this.value; + } + + // Private + + _gradientEditorGradientChanged(event) + { + this.value = event.data.gradient; + } +}; diff --git a/inspector/Controllers/CodeMirrorSpringEditingController.js b/inspector/Controllers/CodeMirrorSpringEditingController.js new file mode 100644 index 0000000..a19bd0c --- /dev/null +++ b/inspector/Controllers/CodeMirrorSpringEditingController.js @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2016 Devin Rousso . 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.CodeMirrorSpringEditingController = class CodeMirrorSpringEditingController extends WI.CodeMirrorEditingController +{ + // Public + + get initialValue() + { + return WI.Spring.fromString(this.text); + } + + get cssClassName() + { + return "spring"; + } + + popoverWillPresent(popover) + { + this._springEditor = new WI.SpringEditor; + this._springEditor.addEventListener(WI.SpringEditor.Event.SpringChanged, this._springEditorSpringChanged, this); + popover.content = this._springEditor.element; + } + + popoverDidPresent(popover) + { + this._springEditor.spring = this.value; + } + + // Private + + _springEditorSpringChanged(event) + { + this.value = event.data.spring; + } +}; diff --git a/inspector/Controllers/CodeMirrorTextKillController.js b/inspector/Controllers/CodeMirrorTextKillController.js new file mode 100644 index 0000000..efce2c8 --- /dev/null +++ b/inspector/Controllers/CodeMirrorTextKillController.js @@ -0,0 +1,138 @@ +/* + * 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.CodeMirrorTextKillController = class CodeMirrorTextKillController extends WI.Object +{ + constructor(codeMirror) + { + super(); + + console.assert(codeMirror); + + this._codeMirror = codeMirror; + this._expectingChangeEventForKill = false; + this._nextKillStartsNewSequence = true; + this._shouldPrependToKillRing = false; + + this._handleTextChangeListener = this._handleTextChange.bind(this); + this._handleEditorBlurListener = this._handleEditorBlur.bind(this); + this._handleSelectionOrCaretChangeListener = this._handleSelectionOrCaretChange.bind(this); + + // FIXME: these keybindings match CodeMirror's default keymap for OS X. + // They should probably be altered for Windows / Linux someday. + this._codeMirror.addKeyMap({ + // Overrides for the 'emacsy' keymap. + "Ctrl-K": this._handleTextKillCommand.bind(this, "killLine", false), + "Alt-D": this._handleTextKillCommand.bind(this, "delWordAfter", false), + // Overrides for the 'macDefault' keymap. + "Alt-Delete": this._handleTextKillCommand.bind(this, "delGroupAfter", false), + "Cmd-Backspace": this._handleTextKillCommand.bind(this, "delWrappedLineLeft", true), + "Cmd-Delete": this._handleTextKillCommand.bind(this, "delWrappedLineRight", false), + "Alt-Backspace": this._handleTextKillCommand.bind(this, "delGroupBefore", true), + "Ctrl-Alt-Backspace": this._handleTextKillCommand.bind(this, "delGroupAfter", false), + }); + } + + _handleTextKillCommand(command, prependsToKillRing, codeMirror) + { + // Read-only mode is dynamic in some editors, so check every time + // and ignore the shortcut if in read-only mode. + if (this._codeMirror.getOption("readOnly")) + return; + + this._shouldPrependToKillRing = prependsToKillRing; + + // Don't add the listener if it's still registered because + // a previous empty kill didn't generate change events. + if (!this._expectingChangeEventForKill) + this._codeMirror.on("changes", this._handleTextChangeListener); + + this._expectingChangeEventForKill = true; + this._codeMirror.execCommand(command); + } + + _handleTextChange(codeMirror, changes) + { + this._codeMirror.off("changes", this._handleTextChangeListener); + + // Sometimes a second change event fires after removing the listener + // if you perform an "empty kill" and type after moving the caret. + if (!this._expectingChangeEventForKill) + return; + + this._expectingChangeEventForKill = false; + + // It doesn't make sense to get more than one change per kill. + console.assert(changes.length === 1); + let change = changes[0]; + + // If an "empty kill" is followed by up/down or typing, + // the empty kill won't fire a change event, then we'll get an + // unrelated change event that shouldn't be treated as a kill. + if (change.origin !== "+delete") + return; + + // When killed text includes a newline, CodeMirror returns + // strange change objects. Special-case for when this could happen. + let killedText; + if (change.to.line === change.from.line + 1 && change.removed.length === 2) { + // An entire line was deleted, including newline (deleteLine). + if (change.removed[0].length && !change.removed[1].length) + killedText = change.removed[0] + "\n"; + // A newline was killed by itself (Ctrl-K). + else + killedText = "\n"; + } else { + console.assert(change.removed.length === 1); + killedText = change.removed[0]; + } + + InspectorFrontendHost.killText(killedText, this._shouldPrependToKillRing, this._nextKillStartsNewSequence); + + // If the editor loses focus or the caret / selection changes + // (not as a result of the kill), then the next kill should + // start a new kill ring sequence. + this._nextKillStartsNewSequence = false; + this._codeMirror.on("blur", this._handleEditorBlurListener); + this._codeMirror.on("cursorActivity", this._handleSelectionOrCaretChangeListener); + } + + _handleEditorBlur(codeMirror) + { + this._nextKillStartsNewSequence = true; + this._codeMirror.off("blur", this._handleEditorBlurListener); + this._codeMirror.off("cursorActivity", this._handleSelectionOrCaretChangeListener); + } + + _handleSelectionOrCaretChange(codeMirror) + { + if (this._expectingChangeEventForKill) + return; + + this._nextKillStartsNewSequence = true; + this._codeMirror.off("blur", this._handleEditorBlurListener); + this._codeMirror.off("cursorActivity", this._handleSelectionOrCaretChangeListener); + } +}; diff --git a/inspector/Controllers/CodeMirrorTokenTrackingController.css b/inspector/Controllers/CodeMirrorTokenTrackingController.css new file mode 100644 index 0000000..516755d --- /dev/null +++ b/inspector/Controllers/CodeMirrorTokenTrackingController.css @@ -0,0 +1,31 @@ +/* + * 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. + */ + +.CodeMirror .jump-to-symbol-highlight { + color: blue !important; + text-decoration: underline !important; + cursor: pointer !important; + -webkit-text-stroke-width: 0 !important; +} diff --git a/inspector/Controllers/CodeMirrorTokenTrackingController.js b/inspector/Controllers/CodeMirrorTokenTrackingController.js new file mode 100644 index 0000000..c3beef0 --- /dev/null +++ b/inspector/Controllers/CodeMirrorTokenTrackingController.js @@ -0,0 +1,636 @@ +/* + * 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.CodeMirrorTokenTrackingController = class CodeMirrorTokenTrackingController extends WI.Object +{ + constructor(codeMirror, delegate) + { + super(); + + console.assert(codeMirror); + + this._codeMirror = codeMirror; + this._delegate = delegate || null; + this._mode = WI.CodeMirrorTokenTrackingController.Mode.None; + + this._mouseOverDelayDuration = 0; + this._mouseOutReleaseDelayDuration = 0; + this._classNameForHighlightedRange = null; + + this._enabled = false; + this._tracking = false; + this._previousTokenInfo = null; + this._hoveredMarker = null; + this._ignoreNextMouseMove = false; + + const hidePopover = this._hidePopover.bind(this); + + this._codeMirror.addKeyMap({ + "Cmd-Enter": this._handleCommandEnterKey.bind(this), + "Esc": hidePopover + }); + + this._codeMirror.on("cursorActivity", hidePopover); + } + + // Public + + get delegate() + { + return this._delegate; + } + + set delegate(x) + { + this._delegate = x; + } + + get enabled() + { + return this._enabled; + } + + set enabled(enabled) + { + if (this._enabled === enabled) + return; + + this._enabled = enabled; + + var wrapper = this._codeMirror.getWrapperElement(); + if (enabled) { + wrapper.addEventListener("mouseenter", this); + wrapper.addEventListener("mouseleave", this); + this._updateHoveredTokenInfo({left: WI.mouseCoords.x, top: WI.mouseCoords.y}); + this._startTracking(); + } else { + wrapper.removeEventListener("mouseenter", this); + wrapper.removeEventListener("mouseleave", this); + this._stopTracking(); + } + } + + get mode() + { + return this._mode; + } + + set mode(mode) + { + var oldMode = this._mode; + + this._mode = mode || WI.CodeMirrorTokenTrackingController.Mode.None; + + if (oldMode !== this._mode && this._tracking && this._previousTokenInfo) + this._processNewHoveredToken(this._previousTokenInfo); + } + + get mouseOverDelayDuration() + { + return this._mouseOverDelayDuration; + } + + set mouseOverDelayDuration(x) + { + console.assert(x >= 0); + this._mouseOverDelayDuration = Math.max(x, 0); + } + + get mouseOutReleaseDelayDuration() + { + return this._mouseOutReleaseDelayDuration; + } + + set mouseOutReleaseDelayDuration(x) + { + console.assert(x >= 0); + this._mouseOutReleaseDelayDuration = Math.max(x, 0); + } + + get classNameForHighlightedRange() + { + return this._classNameForHighlightedRange; + } + + set classNameForHighlightedRange(x) + { + this._classNameForHighlightedRange = x || null; + } + + get candidate() + { + return this._candidate; + } + + get hoveredMarker() + { + return this._hoveredMarker; + } + + set hoveredMarker(hoveredMarker) + { + this._hoveredMarker = hoveredMarker; + } + + highlightLastHoveredRange() + { + if (this._candidate) + this.highlightRange(this._candidate.hoveredTokenRange); + } + + highlightRange(range) + { + // Nothing to do if we're trying to highlight the same range. + if (this._codeMirrorMarkedText && this._codeMirrorMarkedText.className === this._classNameForHighlightedRange) { + var highlightedRange = this._codeMirrorMarkedText.find(); + if (!highlightedRange) + return; + if (WI.compareCodeMirrorPositions(highlightedRange.from, range.start) === 0 && + WI.compareCodeMirrorPositions(highlightedRange.to, range.end) === 0) + return; + } + + this.removeHighlightedRange(); + + var className = this._classNameForHighlightedRange || ""; + this._codeMirrorMarkedText = this._codeMirror.markText(range.start, range.end, {className}); + + window.addEventListener("mousemove", this, true); + } + + removeHighlightedRange() + { + if (!this._codeMirrorMarkedText) + return; + + this._codeMirrorMarkedText.clear(); + this._codeMirrorMarkedText = null; + + window.removeEventListener("mousemove", this, true); + } + + // Private + + _startTracking() + { + if (this._tracking) + return; + + this._tracking = true; + this._ignoreNextMouseMove = false; + + var wrapper = this._codeMirror.getWrapperElement(); + wrapper.addEventListener("mousemove", this, true); + wrapper.addEventListener("mouseout", this, false); + wrapper.addEventListener("mousedown", this, false); + wrapper.addEventListener("mouseup", this, false); + wrapper.addEventListener("mousewheel", this, false); + window.addEventListener("blur", this, true); + } + + _stopTracking() + { + if (!this._tracking) + return; + + this._tracking = false; + this._candidate = null; + + var wrapper = this._codeMirror.getWrapperElement(); + wrapper.removeEventListener("mousemove", this, true); + wrapper.removeEventListener("mouseout", this, false); + wrapper.removeEventListener("mousedown", this, false); + wrapper.removeEventListener("mouseup", this, false); + wrapper.removeEventListener("mousewheel", this, false); + window.removeEventListener("blur", this, true); + window.removeEventListener("mousemove", this, true); + + this._resetTrackingStates(); + } + + handleEvent(event) + { + switch (event.type) { + case "mouseenter": + this._mouseEntered(event); + break; + case "mouseleave": + this._mouseLeft(event); + break; + case "mousemove": + if (event.currentTarget === window) + this._mouseMovedWithMarkedText(event); + else + this._mouseMovedOverEditor(event); + break; + case "mouseout": + // Only deal with a mouseout event that has the editor wrapper as the target. + if (!event.currentTarget.contains(event.relatedTarget)) + this._mouseMovedOutOfEditor(event); + break; + case "mousedown": + this._mouseButtonWasPressedOverEditor(event); + break; + case "mouseup": + this._mouseButtonWasReleasedOverEditor(event); + break; + case "mousewheel": + this._ignoreNextMouseMove = true; + break; + case "blur": + this._windowLostFocus(event); + break; + } + } + + _handleCommandEnterKey(codeMirror) + { + const tokenInfo = this._getTokenInfoForPosition(codeMirror.getCursor("head")); + tokenInfo.triggeredBy = WI.CodeMirrorTokenTrackingController.TriggeredBy.Keyboard; + this._processNewHoveredToken(tokenInfo); + } + + _hidePopover() + { + if (!this._candidate) + return CodeMirror.Pass; + + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function") { + const forceHidePopover = true; + this._delegate.tokenTrackingControllerHighlightedRangeReleased(this, forceHidePopover); + } + } + + _mouseEntered(event) + { + if (!this._tracking) + this._startTracking(); + } + + _mouseLeft(event) + { + this._stopTracking(); + } + + _mouseMovedWithMarkedText(event) + { + if (this._candidate && this._candidate.triggeredBy === WI.CodeMirrorTokenTrackingController.TriggeredBy.Keyboard) + return; + + var shouldRelease = !event.target.classList.contains(this._classNameForHighlightedRange); + if (shouldRelease && this._delegate && typeof this._delegate.tokenTrackingControllerCanReleaseHighlightedRange === "function") + shouldRelease = this._delegate.tokenTrackingControllerCanReleaseHighlightedRange(this, event.target); + + if (shouldRelease) { + if (!this._markedTextMouseoutTimer) + this._markedTextMouseoutTimer = setTimeout(this._markedTextIsNoLongerHovered.bind(this), this._mouseOutReleaseDelayDuration); + return; + } + + if (this._markedTextMouseoutTimer) + clearTimeout(this._markedTextMouseoutTimer); + + this._markedTextMouseoutTimer = 0; + } + + _markedTextIsNoLongerHovered() + { + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeReleased === "function") + this._delegate.tokenTrackingControllerHighlightedRangeReleased(this); + + this._markedTextMouseoutTimer = 0; + } + + _mouseMovedOverEditor(event) + { + if (this._ignoreNextMouseMove) { + this._ignoreNextMouseMove = false; + return; + } + + this._updateHoveredTokenInfo({left: event.pageX, top: event.pageY}); + } + + _updateHoveredTokenInfo(mouseCoords) + { + // Get the position in the text and the token at that position. + var position = this._codeMirror.coordsChar(mouseCoords); + var token = this._codeMirror.getTokenAt(position); + + if (!token || !token.type || !token.string) { + if (this._hoveredMarker && this._delegate && typeof this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker === "function") { + if (!this._codeMirror.findMarksAt(position).includes(this._hoveredMarker.codeMirrorTextMarker)) + this._delegate.tokenTrackingControllerMouseOutOfHoveredMarker(this, this._hoveredMarker); + } + + this._resetTrackingStates(); + return; + } + + // Stop right here if we're hovering the same token as we were last time. + if (this._previousTokenInfo && + this._previousTokenInfo.position.line === position.line && + this._previousTokenInfo.token.start === token.start && + this._previousTokenInfo.token.end === token.end) + return; + + // We have a new hovered token. + var tokenInfo = this._previousTokenInfo = this._getTokenInfoForPosition(position); + + if (/\bmeta\b/.test(token.type)) { + let nextTokenPosition = Object.shallowCopy(position); + nextTokenPosition.ch = tokenInfo.token.end + 1; + + let nextToken = this._codeMirror.getTokenAt(nextTokenPosition); + if (nextToken && nextToken.type && !/\bmeta\b/.test(nextToken.type)) { + console.assert(tokenInfo.token.end === nextToken.start); + + tokenInfo.token.type = nextToken.type; + tokenInfo.token.string = tokenInfo.token.string + nextToken.string; + tokenInfo.token.end = nextToken.end; + } + } else { + let previousTokenPosition = Object.shallowCopy(position); + previousTokenPosition.ch = tokenInfo.token.start - 1; + + let previousToken = this._codeMirror.getTokenAt(previousTokenPosition); + if (previousToken && previousToken.type && /\bmeta\b/.test(previousToken.type)) { + console.assert(tokenInfo.token.start === previousToken.end); + + tokenInfo.token.string = previousToken.string + tokenInfo.token.string; + tokenInfo.token.start = previousToken.start; + } + } + + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + + if (this._codeMirrorMarkedText || !this._mouseOverDelayDuration) + this._processNewHoveredToken(tokenInfo); + else + this._tokenHoverTimer = setTimeout(this._processNewHoveredToken.bind(this, tokenInfo), this._mouseOverDelayDuration); + } + + _getTokenInfoForPosition(position) + { + var token = this._codeMirror.getTokenAt(position); + var innerMode = CodeMirror.innerMode(this._codeMirror.getMode(), token.state); + var codeMirrorModeName = innerMode.mode.alternateName || innerMode.mode.name; + return { + token, + position, + innerMode, + modeName: codeMirrorModeName + }; + } + + _mouseMovedOutOfEditor(event) + { + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + this._previousTokenInfo = null; + this._selectionMayBeInProgress = false; + } + + _mouseButtonWasPressedOverEditor(event) + { + this._selectionMayBeInProgress = true; + } + + _mouseButtonWasReleasedOverEditor(event) + { + this._selectionMayBeInProgress = false; + this._mouseMovedOverEditor(event); + + if (this._codeMirrorMarkedText && this._previousTokenInfo) { + var position = this._codeMirror.coordsChar({left: event.pageX, top: event.pageY}); + var marks = this._codeMirror.findMarksAt(position); + for (var i = 0; i < marks.length; ++i) { + if (marks[i] === this._codeMirrorMarkedText) { + if (this._delegate && typeof this._delegate.tokenTrackingControllerHighlightedRangeWasClicked === "function") + this._delegate.tokenTrackingControllerHighlightedRangeWasClicked(this); + + break; + } + } + } + } + + _windowLostFocus(event) + { + this._resetTrackingStates(); + } + + _processNewHoveredToken(tokenInfo) + { + console.assert(tokenInfo); + + if (this._selectionMayBeInProgress) + return; + + this._candidate = null; + + switch (this._mode) { + case WI.CodeMirrorTokenTrackingController.Mode.NonSymbolTokens: + this._candidate = this._processNonSymbolToken(tokenInfo); + break; + case WI.CodeMirrorTokenTrackingController.Mode.JavaScriptExpression: + case WI.CodeMirrorTokenTrackingController.Mode.JavaScriptTypeInformation: + this._candidate = this._processJavaScriptExpression(tokenInfo); + break; + case WI.CodeMirrorTokenTrackingController.Mode.MarkedTokens: + this._candidate = this._processMarkedToken(tokenInfo); + break; + } + + if (!this._candidate) + return; + + this._candidate.triggeredBy = tokenInfo.triggeredBy; + + if (this._markedTextMouseoutTimer) + clearTimeout(this._markedTextMouseoutTimer); + + this._markedTextMouseoutTimer = 0; + + if (this._delegate && typeof this._delegate.tokenTrackingControllerNewHighlightCandidate === "function") + this._delegate.tokenTrackingControllerNewHighlightCandidate(this, this._candidate); + } + + _processNonSymbolToken(tokenInfo) + { + // Ignore any symbol tokens. + var type = tokenInfo.token.type; + if (!type) + return null; + + var startPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + var endPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: {start: startPosition, end: endPosition}, + }; + } + + _processJavaScriptExpression(tokenInfo) + { + // Only valid within JavaScript. + if (tokenInfo.modeName !== "javascript") + return null; + + var startPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + var endPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + + function tokenIsInRange(token, range) + { + return token.line >= range.start.line && token.ch >= range.start.ch && + token.line <= range.end.line && token.ch <= range.end.ch; + } + + // If the hovered token is within a selection, use the selection as our expression. + if (this._codeMirror.somethingSelected()) { + var selectionRange = { + start: this._codeMirror.getCursor("start"), + end: this._codeMirror.getCursor("end") + }; + + if (tokenIsInRange(startPosition, selectionRange) || tokenIsInRange(endPosition, selectionRange)) { + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: selectionRange, + expression: this._codeMirror.getSelection(), + expressionRange: selectionRange, + }; + } + } + + // We only handle vars, definitions, properties, and the keyword 'this'. + var type = tokenInfo.token.type; + var isProperty = type.indexOf("property") !== -1; + var isKeyword = type.indexOf("keyword") !== -1; + if (!isProperty && !isKeyword && type.indexOf("variable") === -1 && type.indexOf("def") === -1) + return null; + + // Not object literal property names, but yes if an object literal shorthand property, which is a variable. + let state = tokenInfo.innerMode.state; + if (isProperty && state.lexical && state.lexical.type === "}") { + // Peek ahead to see if the next token is "}" or ",". If it is, we are a shorthand and therefore a variable. + let shorthand = false; + let mode = tokenInfo.innerMode.mode; + let position = {line: tokenInfo.position.line, ch: tokenInfo.token.end}; + WI.walkTokens(this._codeMirror, mode, position, function(tokenType, string) { + if (tokenType) + return false; + if (string === "(") + return false; + if (string === "," || string === "}") { + shorthand = true; + return false; + } + return true; + }); + + if (!shorthand) + return null; + } + + // Only the "this" keyword. + if (isKeyword && tokenInfo.token.string !== "this") + return null; + + // Work out the full hovered expression. + var expression = tokenInfo.token.string; + var expressionStartPosition = {line: tokenInfo.position.line, ch: tokenInfo.token.start}; + while (true) { + var token = this._codeMirror.getTokenAt(expressionStartPosition); + if (!token) + break; + + var isDot = !token.type && token.string === "."; + var isExpression = token.type && token.type.includes("m-javascript"); + if (!isDot && !isExpression) + break; + + if (isExpression) { + // Disallow operators. We want the hovered expression to be just a single operand. + // Also, some operators can modify values, such as pre-increment and assignment operators. + if (token.type.includes("operator")) + break; + + // Don't break out of a template string quasi group. + if (token.type.includes("string-2")) + break; + } + + expression = token.string + expression; + expressionStartPosition.ch = token.start; + } + + // Return the candidate for this token and expression. + return { + hoveredToken: tokenInfo.token, + hoveredTokenRange: {start: startPosition, end: endPosition}, + expression, + expressionRange: {start: expressionStartPosition, end: endPosition}, + }; + } + + _processMarkedToken(tokenInfo) + { + return this._processNonSymbolToken(tokenInfo); + } + + _resetTrackingStates() + { + if (this._tokenHoverTimer) + clearTimeout(this._tokenHoverTimer); + + this._tokenHoverTimer = 0; + + this._selectionMayBeInProgress = false; + this._previousTokenInfo = null; + this.removeHighlightedRange(); + } +}; + +WI.CodeMirrorTokenTrackingController.JumpToSymbolHighlightStyleClassName = "jump-to-symbol-highlight"; + +WI.CodeMirrorTokenTrackingController.Mode = { + None: "none", + NonSymbolTokens: "non-symbol-tokens", + JavaScriptExpression: "javascript-expression", + JavaScriptTypeInformation: "javascript-type-information", + MarkedTokens: "marked-tokens" +}; + +WI.CodeMirrorTokenTrackingController.TriggeredBy = { + Keyboard: "keyboard", + Hover: "hover" +}; diff --git a/inspector/Controllers/ConsoleManager.js b/inspector/Controllers/ConsoleManager.js new file mode 100644 index 0000000..d44045b --- /dev/null +++ b/inspector/Controllers/ConsoleManager.js @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2013 Apple Inc. All rights reserved. + * Copyright (C) 2015 Tobias Reiss + * + * 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; + + 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; } + + 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}); + } + } + + messagesCleared() + { + if (this._remoteObjectsToRelease) { + for (let remoteObject of this._remoteObjectsToRelease) + remoteObject.release(); + this._remoteObjectsToRelease = null; + } + + WI.ConsoleCommandResultMessage.clearMaximumSavedResultIndex(); + + if (this._clearMessagesRequested) { + // Frontend requested "clear console" and Backend successfully completed the request. + this._clearMessagesRequested = false; + + this._warningCount = 0; + this._errorCount = 0; + this._issues = []; + + this._lastMessageLevel = null; + + this.dispatchEventToListeners(WI.ConsoleManager.Event.Cleared); + } else { + // 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); + } + } + + 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; + } + + _delayedMessagesCleared() + { + if (this._isNewPageOrReload) { + this._isNewPageOrReload = false; + + if (!WI.settings.clearLogOnNavigate.value) + return; + } + + this._warningCount = 0; + this._errorCount = 0; + this._issues = []; + + this._lastMessageLevel = null; + + // A console.clear() or command line clear() happened. + this.dispatchEventToListeners(WI.ConsoleManager.Event.Cleared); + } + + _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", +}; diff --git a/inspector/Controllers/DOMDebuggerManager.js b/inspector/Controllers/DOMDebuggerManager.js new file mode 100644 index 0000000..eda75bb --- /dev/null +++ b/inspector/Controllers/DOMDebuggerManager.js @@ -0,0 +1,1079 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/DOMManager.js b/inspector/Controllers/DOMManager.js new file mode 100644 index 0000000..5a9fbb0 --- /dev/null +++ b/inspector/Controllers/DOMManager.js @@ -0,0 +1,899 @@ +/* + * 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 buildHighlightConfig(mode) + { + mode = mode || "all"; + + let highlightConfig = {showInfo: mode === "all"}; + + if (mode === "all" || mode === "content") + highlightConfig.contentColor = {r: 111, g: 168, b: 220, a: 0.66}; + + if (mode === "all" || mode === "padding") + highlightConfig.paddingColor = {r: 147, g: 196, b: 125, a: 0.66}; + + if (mode === "all" || mode === "border") + highlightConfig.borderColor = {r: 255, g: 229, b: 153, a: 0.66}; + + if (mode === "all" || mode === "margin") + highlightConfig.marginColor = {r: 246, g: 178, b: 107, a: 0.66}; + + return highlightConfig; + } + + 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(nodeIds, DOMManager.buildHighlightConfig(mode)); + } + + highlightSelector(selectorText, frameId, mode) + { + if (this._hideDOMNodeHighlightTimeout) { + clearTimeout(this._hideDOMNodeHighlightTimeout); + this._hideDOMNodeHighlightTimeout = undefined; + } + + let target = WI.assumingMainTarget(); + target.DOMAgent.highlightSelector(DOMManager.buildHighlightConfig(mode), selectorText, frameId); + } + + 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(); + let commandArguments = { + enabled, + highlightConfig: DOMManager.buildHighlightConfig(), + showRulers: WI.settings.showRulersDuringElementSelection.value, + }; + target.DOMAgent.setInspectModeEnabled.invoke(commandArguments, (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", +}; diff --git a/inspector/Controllers/DOMStorageManager.js b/inspector/Controllers/DOMStorageManager.js new file mode 100644 index 0000000..b740923 --- /dev/null +++ b/inspector/Controllers/DOMStorageManager.js @@ -0,0 +1,258 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/DatabaseManager.js b/inspector/Controllers/DatabaseManager.js new file mode 100644 index 0000000..bc27c68 --- /dev/null +++ b/inspector/Controllers/DatabaseManager.js @@ -0,0 +1,155 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/DebuggerManager.js b/inspector/Controllers/DebuggerManager.js new file mode 100644 index 0000000..97c22c4 --- /dev/null +++ b/inspector/Controllers/DebuggerManager.js @@ -0,0 +1,1737 @@ +/* + * 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: 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", +}; diff --git a/inspector/Controllers/DiagnosticController.js b/inspector/Controllers/DiagnosticController.js new file mode 100644 index 0000000..1b2240e --- /dev/null +++ b/inspector/Controllers/DiagnosticController.js @@ -0,0 +1,95 @@ +/* + * 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; + } +}; diff --git a/inspector/Controllers/DiagnosticEventRecorder.js b/inspector/Controllers/DiagnosticEventRecorder.js new file mode 100644 index 0000000..4932746 --- /dev/null +++ b/inspector/Controllers/DiagnosticEventRecorder.js @@ -0,0 +1,76 @@ +/* + * 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(); + } +}; diff --git a/inspector/Controllers/DragToAdjustController.js b/inspector/Controllers/DragToAdjustController.js new file mode 100644 index 0000000..825e67d --- /dev/null +++ b/inspector/Controllers/DragToAdjustController.js @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2014 Antoine Quint + * + * 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.DragToAdjustController = class DragToAdjustController +{ + constructor(delegate) + { + this._delegate = delegate; + + this._element = null; + this._active = false; + this._enabled = false; + this._dragging = false; + this._tracksMouseClickAndDrag = false; + } + + // Public + + get element() + { + return this._element; + } + + set element(element) + { + this._element = element; + } + + get enabled() + { + return this._enabled; + } + + set enabled(enabled) + { + if (this._enabled === enabled) + return; + + if (enabled) { + this._element.addEventListener("mouseenter", this); + this._element.addEventListener("mouseleave", this); + } else { + this._element.removeEventListener("mouseenter", this); + this._element.removeEventListener("mouseleave", this); + } + } + + get active() + { + return this._active; + } + + set active(active) + { + if (!this._element) + return; + + if (this._active === active) + return; + + if (active) { + WI.notifications.addEventListener(WI.Notification.GlobalModifierKeysDidChange, this._modifiersDidChange, this); + this._element.addEventListener("mousemove", this); + } else { + WI.notifications.removeEventListener(WI.Notification.GlobalModifierKeysDidChange, this._modifiersDidChange, this); + this._element.removeEventListener("mousemove", this); + this._setTracksMouseClickAndDrag(false); + } + + this._active = active; + + if (this._delegate && typeof this._delegate.dragToAdjustControllerActiveStateChanged === "function") + this._delegate.dragToAdjustControllerActiveStateChanged(this); + } + + reset() + { + this._setTracksMouseClickAndDrag(false); + this._element.classList.remove(WI.DragToAdjustController.StyleClassName); + + if (this._delegate && typeof this._delegate.dragToAdjustControllerDidReset === "function") + this._delegate.dragToAdjustControllerDidReset(this); + } + + // Protected + + handleEvent(event) + { + switch (event.type) { + case "mouseenter": + if (!this._dragging) { + if (this._delegate && typeof this._delegate.dragToAdjustControllerCanBeActivated === "function") + this.active = this._delegate.dragToAdjustControllerCanBeActivated(this); + else + this.active = true; + } + break; + case "mouseleave": + if (!this._dragging) + this.active = false; + break; + case "mousemove": + if (this._dragging) + this._mouseWasDragged(event); + else + this._mouseMoved(event); + break; + case "mousedown": + this._mouseWasPressed(event); + break; + case "mouseup": + this._mouseWasReleased(event); + break; + case "contextmenu": + event.preventDefault(); + break; + } + } + + // Private + + _setDragging(dragging) + { + if (this._dragging === dragging) + return; + + console.assert(window.event); + if (dragging) + WI.elementDragStart(this._element, this, this, window.event, "col-resize", window); + else + WI.elementDragEnd(window.event); + + this._dragging = dragging; + } + + _setTracksMouseClickAndDrag(tracksMouseClickAndDrag) + { + if (this._tracksMouseClickAndDrag === tracksMouseClickAndDrag) + return; + + if (tracksMouseClickAndDrag) { + this._element.classList.add(WI.DragToAdjustController.StyleClassName); + window.addEventListener("mousedown", this, true); + window.addEventListener("contextmenu", this, true); + } else { + this._element.classList.remove(WI.DragToAdjustController.StyleClassName); + window.removeEventListener("mousedown", this, true); + window.removeEventListener("contextmenu", this, true); + this._setDragging(false); + } + + this._tracksMouseClickAndDrag = tracksMouseClickAndDrag; + } + + _modifiersDidChange(event) + { + var canBeAdjusted = WI.modifierKeys.altKey; + if (canBeAdjusted && this._delegate && typeof this._delegate.dragToAdjustControllerCanBeAdjusted === "function") + canBeAdjusted = this._delegate.dragToAdjustControllerCanBeAdjusted(this); + + this._setTracksMouseClickAndDrag(canBeAdjusted); + } + + _mouseMoved(event) + { + var canBeAdjusted = event.altKey; + if (canBeAdjusted && this._delegate && typeof this._delegate.dragToAdjustControllerCanAdjustObjectAtPoint === "function") + canBeAdjusted = this._delegate.dragToAdjustControllerCanAdjustObjectAtPoint(this, WI.Point.fromEvent(event)); + + this._setTracksMouseClickAndDrag(canBeAdjusted); + } + + _mouseWasPressed(event) + { + this._lastX = event.screenX; + + this._setDragging(true); + + event.preventDefault(); + event.stopPropagation(); + } + + _mouseWasDragged(event) + { + var x = event.screenX; + var amount = x - this._lastX; + + if (Math.abs(amount) < 1) + return; + + this._lastX = x; + + if (event.ctrlKey) + amount /= 10; + else if (event.shiftKey) + amount *= 10; + + if (this._delegate && typeof this._delegate.dragToAdjustControllerWasAdjustedByAmount === "function") + this._delegate.dragToAdjustControllerWasAdjustedByAmount(this, amount); + + event.preventDefault(); + event.stopPropagation(); + } + + _mouseWasReleased(event) + { + this._setDragging(false); + + event.preventDefault(); + event.stopPropagation(); + + this.reset(); + } +}; + +WI.DragToAdjustController.StyleClassName = "drag-to-adjust"; diff --git a/inspector/Controllers/ExtensionTabActivationDiagnosticEventRecorder.js b/inspector/Controllers/ExtensionTabActivationDiagnosticEventRecorder.js new file mode 100644 index 0000000..b3f5b8a --- /dev/null +++ b/inspector/Controllers/ExtensionTabActivationDiagnosticEventRecorder.js @@ -0,0 +1,71 @@ +/* + * 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.ExtensionTabActivationDiagnosticEventRecorder = class ExtensionTabActivationDiagnosticEventRecorder extends WI.DiagnosticEventRecorder +{ + constructor(controller) + { + super("ExtensionTabActivation", controller); + + this._reportedExtensionTabIDs = new Set; + } + + // Protected + + setup() + { + WI.tabBrowser.addEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._selectedTabContentViewDidChange, this); + } + + teardown() + { + WI.tabBrowser.removeEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._selectedTabContentViewDidChange, this); + } + + // Private + + _selectedTabContentViewDidChange(event) + { + let selectedTab = event.data.incomingTab; + if (!(selectedTab instanceof WI.WebInspectorExtensionTabContentView)) + return; + + let extension = selectedTab.extension; + console.assert(extension instanceof WI.WebInspectorExtension, "Extension tab should have an associated extension."); + + // Only report the first selection of an extension tab. + if (this._reportedExtensionTabIDs.has(selectedTab.extensionTabID)) + return; + + this._reportedExtensionTabIDs.add(selectedTab.extensionTabID); + + this.logDiagnosticEvent(this.name, { + extensionBundleIdentifier: extension.extensionBundleIdentifier, + extensionTabName: selectedTab.tabInfo().displayName, + activeExtensionTabCount: WI.sharedApp.extensionController.activeExtensionTabContentViews().length, + }); + } +}; + diff --git a/inspector/Controllers/Formatter.js b/inspector/Controllers/Formatter.js new file mode 100644 index 0000000..54a6580 --- /dev/null +++ b/inspector/Controllers/Formatter.js @@ -0,0 +1,167 @@ +/* + * 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.Formatter = class Formatter +{ + constructor(codeMirror, builder) + { + console.assert(codeMirror); + console.assert(builder); + + this._codeMirror = codeMirror; + this._builder = builder; + + this._lastToken = null; + this._lastContent = ""; + } + + // Public + + format(from, to) + { + console.assert(this._builder.originalContent === null); + if (this._builder.originalContent !== null) + return; + + var outerMode = this._codeMirror.getMode(); + var content = this._codeMirror.getRange(from, to); + var state = CodeMirror.copyState(outerMode, this._codeMirror.getTokenAt(from).state); + this._builder.setOriginalContent(content); + + var lineOffset = 0; + var lines = content.split("\n"); + for (var i = 0; i < lines.length; ++i) { + var line = lines[i]; + var startOfNewLine = true; + var firstTokenOnLine = true; + var stream = new CodeMirror.StringStream(line); + while (!stream.eol()) { + var innerMode = CodeMirror.innerMode(outerMode, state); + var token = outerMode.token(stream, state); + var isWhiteSpace = token === null && /^\s*$/.test(stream.current()); + this._handleToken(innerMode.mode, token, state, stream, lineOffset + stream.start, isWhiteSpace, startOfNewLine, firstTokenOnLine); + stream.start = stream.pos; + startOfNewLine = false; + if (firstTokenOnLine && !isWhiteSpace) + firstTokenOnLine = false; + } + + if (firstTokenOnLine) + this._handleEmptyLine(); + + lineOffset += line.length + 1; // +1 for the "\n" removed in split. + this._handleLineEnding(lineOffset - 1); // -1 for the index of the "\n". + } + + this._builder.finish(); + } + + // Private + + _handleToken(mode, token, state, stream, originalPosition, isWhiteSpace, startOfNewLine, firstTokenOnLine) + { + // String content of the token. + var content = stream.current(); + + // Start of a new line. Insert a newline to be safe if code was not-ASI safe. These are collapsed. + if (startOfNewLine) + this._builder.appendNewline(); + + // Whitespace. Remove all spaces or collapse to a single space. + if (isWhiteSpace) { + this._builder.appendSpace(); + return; + } + + // Avoid some hooks for content in comments. + var isComment = token && /\bcomment\b/.test(token); + + if (mode.modifyStateForTokenPre) + mode.modifyStateForTokenPre(this._lastToken, this._lastContent, token, state, content, isComment); + + // Should we remove the last whitespace? + if (this._builder.lastTokenWasWhitespace && mode.removeLastWhitespace(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.removeLastWhitespace(); + + // Should we remove the last newline? + if (this._builder.lastTokenWasNewline && mode.removeLastNewline(this._lastToken, this._lastContent, token, state, content, isComment, firstTokenOnLine)) + this._builder.removeLastNewline(); + + // Add whitespace after the last token? + if (!this._builder.lastTokenWasWhitespace && mode.shouldHaveSpaceAfterLastToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendSpace(); + + // Add whitespace before this token? + if (!this._builder.lastTokenWasWhitespace && mode.shouldHaveSpaceBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendSpace(); + + // Should we dedent before this token? + var dedents = mode.dedentsBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment); + while (dedents-- > 0) + this._builder.dedent(); + + // Should we add a newline before this token? + if (mode.newlineBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.appendNewline(); + + // Should we indent before this token? + if (mode.indentBeforeToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.indent(); + + // Append token. + this._builder.appendToken(content, originalPosition); + + // Let the pretty printer update any state it keeps track of. + if (mode.modifyStateForTokenPost) + mode.modifyStateForTokenPost(this._lastToken, this._lastContent, token, state, content, isComment); + + // Should we indent or dedent after this token? + if (!isComment && mode.indentAfterToken(this._lastToken, this._lastContent, token, state, content, isComment)) + this._builder.indent(); + + // Should we add newlines after this token? + var newlines = mode.newlinesAfterToken(this._lastToken, this._lastContent, token, state, content, isComment); + if (newlines) + this._builder.appendMultipleNewlines(newlines); + + // Record this token as the last token. + this._lastToken = token; + this._lastContent = content; + } + + _handleEmptyLine() + { + // Preserve original whitespace only lines by adding a newline. + // However, don't do this if the builder just added multiple newlines. + if (!(this._builder.lastTokenWasNewline && this._builder.lastNewlineAppendWasMultiple)) + this._builder.appendNewline(true); + } + + _handleLineEnding(originalNewLinePosition) + { + // Record the original line ending. + this._builder.addOriginalLineEnding(originalNewLinePosition); + } +}; diff --git a/inspector/Controllers/FormatterSourceMap.js b/inspector/Controllers/FormatterSourceMap.js new file mode 100644 index 0000000..d52bc8a --- /dev/null +++ b/inspector/Controllers/FormatterSourceMap.js @@ -0,0 +1,107 @@ +/* + * 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; + } +}; diff --git a/inspector/Controllers/GestureController.js b/inspector/Controllers/GestureController.js new file mode 100644 index 0000000..d55b210 --- /dev/null +++ b/inspector/Controllers/GestureController.js @@ -0,0 +1,245 @@ +/* + * 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.GestureController = class GestureController +{ + constructor(target, delegate, {container, supportsScale, supportsTranslate}) + { + console.assert(target instanceof Node, target); + console.assert(!container || container instanceof Node, container); + console.assert(!supportsScale || typeof delegate.gestureControllerDidScale === "function", delegate.gestureControllerDidScale); + console.assert(!supportsTranslate || typeof delegate.gestureControllerDidTranslate === "function", delegate.gestureControllerDidTranslate); + console.assert(supportsScale || supportsTranslate, "expects at least one gesture"); + + this._target = target; + this._delegate = delegate; + + this._scale = 1; + this._translate = {x: 0, y: 0}; + + this._mouseWheelDelta = 0; + + container ||= target; + + this._supportsScale = supportsScale || false; + if (this._supportsScale) { + container.addEventListener("wheel", this._handleWheel.bind(this)); + container.addEventListener("gesturestart", this._handleGestureStart.bind(this)); + container.addEventListener("gesturechange", this._handleGestureChange.bind(this)); + container.addEventListener("gestureend", this._handleGestureEnd.bind(this)); + } + + this._supportsTranslate = supportsTranslate || false; + if (this._supportsTranslate) { + console.assert(!container.draggable, "cannot have both a translate gesture and dragging"); + container.addEventListener("mousedown", this._handleMouseDown.bind(this)); + } + } + + // Public + + get scale() + { + return this._scale; + } + + set scale(scale) + { + console.assert(this._supportsScale); + + scale = Number.constrain(scale, 0.01, 100); + if (scale === this._scale) + return; + + this._scale = scale; + + this._delegate.gestureControllerDidScale(this); + } + + get translate() + { + return this._translate; + } + + set translate(translate) + { + console.assert(this._supportsTranslate); + + if (translate.x === this._translate.x && translate.y === this._translate.y) + return; + + this._translate = translate; + + this._delegate.gestureControllerDidTranslate(this); + } + + reset() + { + this.scale = 1; + this.translate = {x: 0, y: 0}; + + this._mouseWheelDelta = 0; + } + + // Private + + _startScaleInteraction(event) + { + this._scaleInteractionStartScale = this._scale; + if (this._supportsTranslate) + this._scaleInteractionStartTranslate = this._translate; + + if (event.target === this._target) { + let elementBounds = this._target.getBoundingClientRect(); + this._scaleInteractionStartPosition = { + x: (event.pageX - elementBounds.left - (elementBounds.width / 2)) / this._scaleInteractionStartScale, + y: (event.pageY - elementBounds.top - (elementBounds.height / 2)) / this._scaleInteractionStartScale, + }; + } else + this._scaleInteractionStartPosition = {x: 0, y: 0}; + } + + _updateScaleInteraction(scale) + { + this.scale = this._scaleInteractionStartScale * scale; + + if (this._supportsTranslate) { + this.translate = { + x: this._scaleInteractionStartTranslate.x - (this._scaleInteractionStartPosition.x * (this._scale - this._scaleInteractionStartScale)), + y: this._scaleInteractionStartTranslate.y - (this._scaleInteractionStartPosition.y * (this._scale - this._scaleInteractionStartScale)), + }; + } + } + + _endScaleInteraction() { + this._scaleInteractionStartScale = NaN; + if (this._supportsTranslate) + this._scaleInteractionStartTranslate = null; + + this._scaleInteractionStartPosition = null; + } + + _handleWheel(event) + { + // Ignore wheel events while handing gestures. + if (this._handlingGesture) + return; + + // Require twice the vertical delta to overcome horizontal scrolling. + // This prevents most cases of inadvertent zooming for slightly diagonal scrolls. + if (Math.abs(event.deltaX) >= Math.abs(event.deltaY) * 0.5) + return; + + let deviceDirection = event.webkitDirectionInvertedFromDevice ? -1 : 1; + let delta = (event.deltaZ || event.deltaY || event.deltaX) * deviceDirection / 1000; + + // Reset accumulated wheel delta when direction changes. + if (delta < 0 && this._mouseWheelDelta >= 0 || delta >= 0 && this._mouseWheelDelta < 0) + this._mouseWheelDelta = 0; + + this._mouseWheelDelta += delta; + + this._startScaleInteraction(event); + this._updateScaleInteraction(1 - this._mouseWheelDelta); + this._endScaleInteraction(); + + event.preventDefault(); + event.stopPropagation(); + } + + _handleGestureStart(event) + { + console.assert(!this._handlingGesture); + this._handlingGesture = true; + + this._startScaleInteraction(event); + + event.preventDefault(); + event.stopPropagation(); + } + + _handleGestureChange(event) + { + console.assert(this._handlingGesture); + + this._updateScaleInteraction(event.scale); + + event.preventDefault(); + event.stopPropagation(); + } + + _handleGestureEnd(event) + { + console.assert(this._handlingGesture); + + this._handlingGesture = false; + + this._endScaleInteraction(); + } + + _handleMouseDown(event) + { + if (event.target.draggable) + return; + + if (event.button !== 0) + return; + + this._translateInteractionStartTranslate = this._translate; + + this._translateInteractionStartPosition = { + x: event.pageX, + y: event.pageY, + }; + + console.assert(!this._boundHandleMouseMove); + this._boundHandleMouseMove = this._handleMouseMove.bind(this); + window.addEventListener("mousemove", this._boundHandleMouseMove, {capture: true}); + + console.assert(!this._boundHandleMouseUp); + this._boundHandleMouseUp = this._handleMouseUp.bind(this); + window.addEventListener("mouseup", this._boundHandleMouseUp, {capture: true}); + } + + _handleMouseMove(event) + { + this.translate = { + x: this._translateInteractionStartTranslate.x + (event.pageX - this._translateInteractionStartPosition.x), + y: this._translateInteractionStartTranslate.y + (event.pageY - this._translateInteractionStartPosition.y), + }; + } + + _handleMouseUp(event) + { + window.removeEventListener("mousemove", this._boundHandleMouseMove, {capture: true}); + this._boundHandleMouseMove = null; + + window.removeEventListener("mouseup", this._boundHandleMouseUp, {capture: true}); + this._boundHandleMouseUp = null; + + this._translateInteractionStartTranslate = null; + this._translateInteractionStartPosition = null; + } +}; diff --git a/inspector/Controllers/HARBuilder.js b/inspector/Controllers/HARBuilder.js new file mode 100644 index 0000000..50fc5eb --- /dev/null +++ b/inspector/Controllers/HARBuilder.js @@ -0,0 +1,428 @@ +/* + * 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: Web Inspector: Include details in HAR Export + // http://www.softwareishard.com/blog/har-12-spec/#cache + return {}; + } + + static timings(resource) + { + // FIXME: 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; + } +}; diff --git a/inspector/Controllers/HeapManager.js b/inspector/Controllers/HeapManager.js new file mode 100644 index 0000000..b058ab4 --- /dev/null +++ b/inspector/Controllers/HeapManager.js @@ -0,0 +1,144 @@ +/* + * 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: 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" +}; diff --git a/inspector/Controllers/IndexedDBManager.js b/inspector/Controllers/IndexedDBManager.js new file mode 100644 index 0000000..b6a4bbc --- /dev/null +++ b/inspector/Controllers/IndexedDBManager.js @@ -0,0 +1,258 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/InspectedTargetTypesDiagnosticEventRecorder.js b/inspector/Controllers/InspectedTargetTypesDiagnosticEventRecorder.js new file mode 100644 index 0000000..d012d08 --- /dev/null +++ b/inspector/Controllers/InspectedTargetTypesDiagnosticEventRecorder.js @@ -0,0 +1,146 @@ +/* + * 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.InspectedTargetTypesDiagnosticEventRecorder = class InspectedTargetTypesDiagnosticEventRecorder extends WI.DiagnosticEventRecorder +{ + constructor(controller) + { + super("InspectedTargetTypes", controller); + + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + + // Static + + static get initialDelayBeforeSamplingInterval() + { + return 5 * 1000; // In milliseconds. + } + + // Protected + + setup() + { + // If it's been less than 5 seconds since the frontend loaded, wait a bit. + if (performance.now() - WI.frontendCompletedLoadTimestamp < InspectedTargetTypesDiagnosticEventRecorder.initialDelayBeforeSamplingInterval) + this._startInitialDelayBeforeSamplingTimer(); + else + this._sampleInspectedTarget(); + + } + + teardown() + { + this._stopInitialDelayBeforeSamplingTimer(); + } + + // Private + + _startInitialDelayBeforeSamplingTimer() + { + if (this._initialDelayBeforeSamplingTimerIdentifier) { + clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier); + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + + // All intervals are in milliseconds. + let maximumInitialDelay = InspectedTargetTypesDiagnosticEventRecorder.initialDelayBeforeSamplingInterval; + let elapsedTime = performance.now() - WI.frontendCompletedLoadTimestamp; + let remainingTime = maximumInitialDelay - elapsedTime; + let initialDelay = Number.constrain(remainingTime, 0, maximumInitialDelay); + this._initialDelayBeforeSamplingTimerIdentifier = setTimeout(this._sampleInspectedTarget.bind(this), initialDelay); + } + + _stopInitialDelayBeforeSamplingTimer() + { + if (this._initialDelayBeforeSamplingTimerIdentifier) { + clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier); + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + } + + _sampleInspectedTarget() + { + this._stopInitialDelayBeforeSamplingTimer(); + + this.logDiagnosticEvent(this.name, { + debuggableType: this._determineDebuggableType(), + targetPlatformName: this._determineTargetPlatformName(), + targetBuildVersion: this._determineTargetBuildVersion(), + targetProductVersion: this._determineTargetProductVersion(), + targetIsSimulator: this._determineTargetIsSimulator(), + }); + } + + _determineDebuggableType() + { + this._ensureCachedDebuggableInfo(); + + return this._cachedDebuggableInfo.debuggableType; + } + + _determineTargetPlatformName() + { + this._ensureCachedDebuggableInfo(); + + return this._cachedDebuggableInfo.targetPlatformName; + } + + _determineTargetBuildVersion() + { + this._ensureCachedDebuggableInfo(); + + return this._cachedDebuggableInfo.targetBuildVersion; + } + + _determineTargetProductVersion() + { + this._ensureCachedDebuggableInfo(); + + return this._cachedDebuggableInfo.targetProductVersion; + } + + _determineTargetIsSimulator() + { + this._ensureCachedDebuggableInfo(); + + return !!this._cachedDebuggableInfo.targetIsSimulator; + } + + _ensureCachedDebuggableInfo() + { + if (this._cachedDebuggableInfo) + return; + + let debuggableInfo = InspectorFrontendHost.debuggableInfo; + this._cachedDebuggableInfo = { + debuggableType: WI.DebuggableType.fromString(debuggableInfo.debuggableType) || WI.DebuggableType.JavaScript, + targetPlatformName: debuggableInfo.targetPlatformName || "Unknown", + targetBuildVersion: debuggableInfo.targetBuildVersion || "Unknown", + targetProductVersion: debuggableInfo.targetProductVersion || "Unknown", + targetIsSimulator: debuggableInfo.targetIsSimulator, + }; + } +}; diff --git a/inspector/Controllers/JavaScriptLogViewController.js b/inspector/Controllers/JavaScriptLogViewController.js new file mode 100644 index 0000000..14546ac --- /dev/null +++ b/inspector/Controllers/JavaScriptLogViewController.js @@ -0,0 +1,394 @@ +/* + * 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.JavaScriptLogViewController = class JavaScriptLogViewController extends WI.Object +{ + constructor(element, scrollElement, textPrompt, delegate, historySettingIdentifier) + { + super(); + + console.assert(textPrompt instanceof WI.ConsolePrompt); + console.assert(historySettingIdentifier); + + this._element = element; + this._scrollElement = scrollElement; + + this._promptHistorySetting = new WI.Setting(historySettingIdentifier, null); + + this._prompt = textPrompt; + this._prompt.delegate = this; + this._prompt.history = this._promptHistorySetting.value; + + this.delegate = delegate; + + this._cleared = true; + this._previousMessageView = null; + this._lastCommitted = {text: "", special: false}; + this._repeatCountWasInterrupted = false; + + this._sessions = []; + this._currentSessionOrGroup = null; + + this.messagesAlternateClearKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._element); + + this._messagesFindNextKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._element); + this._messagesFindPreviousKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._element); + + this._promptAlternateClearKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.Control, "L", this.requestClearMessages.bind(this), this._prompt.element); + this._promptFindNextKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl, "G", this._handleFindNextShortcut.bind(this), this._prompt.element); + this._promptFindPreviousKeyboardShortcut = new WI.KeyboardShortcut(WI.KeyboardShortcut.Modifier.CommandOrControl | WI.KeyboardShortcut.Modifier.Shift, "G", this._handleFindPreviousShortcut.bind(this), this._prompt.element); + + WI.settings.showConsoleMessageTimestamps.addEventListener(WI.Setting.Event.Changed, this._handleShowConsoleMessageTimestampsSettingChanged, this); + + this._pendingMessagesForSessionOrGroup = new Map; + this._scheduledRenderIdentifier = 0; + + this._consoleMessageViews = []; + this._showTimestamps = WI.settings.showConsoleMessageTimestamps.value; + + this.startNewSession(); + } + + // Public + + clear() + { + this._cleared = true; + + const clearPreviousSessions = true; + this.startNewSession(clearPreviousSessions, {newSessionReason: WI.ConsoleSession.NewSessionReason.ConsoleCleared}); + } + + startNewSession(clearPreviousSessions = false, data = {}) + { + if (clearPreviousSessions) { + this._pendingMessagesForSessionOrGroup.clear(); + + if (this._sessions.length) { + for (let session of this._sessions) + session.element.remove(); + + this._sessions = []; + this._currentSessionOrGroup = null; + } + } + + // First session shows the time when the console was opened. + if (!this._sessions.length) + data.timestamp = Date.now(); + + let lastSession = this._sessions.lastValue; + + // Remove empty session. + if (lastSession && !lastSession.hasMessages() && !this._pendingMessagesForSessionOrGroup.has(lastSession)) { + this._sessions.pop(); + lastSession.element.remove(); + } + + let consoleSession = new WI.ConsoleSession(data); + + this._previousMessageView = null; + this._lastCommitted = {text: "", special: false}; + this._repeatCountWasInterrupted = false; + + this._sessions.push(consoleSession); + this._currentSessionOrGroup = consoleSession; + + this._element.appendChild(consoleSession.element); + + // Make sure the new session is visible. + consoleSession.element.scrollIntoView(); + } + + appendImmediateExecutionWithResult(text, result, {addSpecialUserLogClass, shouldRevealConsole, handleClick} = {}) + { + console.assert(result instanceof WI.RemoteObject); + + if (this._lastCommitted.text !== text || this._lastCommitted.special !== addSpecialUserLogClass) { + let classNames = []; + if (addSpecialUserLogClass) + classNames.push("special-user-log"); + + let commandMessageView = new WI.ConsoleCommandView(text, {classNames, handleClick}); + this._appendConsoleMessageView(commandMessageView, true); + this._lastCommitted = {text, special: addSpecialUserLogClass}; + } + + function saveResultCallback(savedResultIndex) + { + let commandResultMessage = new WI.ConsoleCommandResultMessage(result.target, result, false, savedResultIndex, shouldRevealConsole); + let commandResultMessageView = new WI.ConsoleMessageView(commandResultMessage); + this._appendConsoleMessageView(commandResultMessageView, true); + } + + WI.runtimeManager.saveResult(result, saveResultCallback.bind(this)); + } + + appendConsoleMessage(consoleMessage) + { + var consoleMessageView = new WI.ConsoleMessageView(consoleMessage); + this._appendConsoleMessageView(consoleMessageView); + return consoleMessageView; + } + + updatePreviousMessageRepeatCount(count, timestamp) + { + console.assert(this._previousMessageView); + if (!this._previousMessageView) + return false; + + var previousIgnoredCount = this._previousMessageView[WI.JavaScriptLogViewController.IgnoredRepeatCount] || 0; + var previousVisibleCount = this._previousMessageView.repeatCount; + this._previousMessageView.timestamp = timestamp; + + if (!this._repeatCountWasInterrupted) { + this._previousMessageView.repeatCount = count - previousIgnoredCount; + return true; + } + + var consoleMessage = this._previousMessageView.message; + var duplicatedConsoleMessageView = new WI.ConsoleMessageView(consoleMessage); + duplicatedConsoleMessageView[WI.JavaScriptLogViewController.IgnoredRepeatCount] = previousIgnoredCount + previousVisibleCount; + duplicatedConsoleMessageView.repeatCount = 1; + this._appendConsoleMessageView(duplicatedConsoleMessageView); + + return true; + } + + isScrolledToBottom() + { + // Lie about being scrolled to the bottom if we have a pending request to scroll to the bottom soon. + return this._scrollToBottomTimeout || this._scrollElement.isScrolledToBottom(); + } + + scrollToBottom() + { + if (this._scrollToBottomTimeout) + return; + + function delayedWork() + { + this._scrollToBottomTimeout = null; + this._scrollElement.scrollTop = this._scrollElement.scrollHeight; + } + + // Don't scroll immediately so we are not causing excessive layouts when there + // are many messages being added at once. + this._scrollToBottomTimeout = setTimeout(delayedWork.bind(this), 0); + } + + requestClearMessages() + { + WI.consoleManager.requestClearMessages(); + } + + // Protected + + consolePromptHistoryDidChange(prompt) + { + this._promptHistorySetting.value = this._prompt.history; + } + + consolePromptShouldCommitText(prompt, text, cursorIsAtLastPosition, handler) + { + // Always commit the text if we are not at the last position. + if (!cursorIsAtLastPosition) { + handler(true); + return; + } + + function parseFinished(error, result, message, range) + { + handler(result !== InspectorBackend.Enum.Runtime.SyntaxErrorType.Recoverable); + } + + WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.parse(text, parseFinished.bind(this)); + } + + consolePromptTextCommitted(prompt, text) + { + console.assert(text); + + if (this._lastCommitted.text !== text || this._lastCommitted.special) { + let commandMessageView = new WI.ConsoleCommandView(text); + this._appendConsoleMessageView(commandMessageView, true); + this._lastCommitted = {text, special: false}; + } + + function printResult(result, wasThrown, savedResultIndex) + { + if (!result || this._cleared) + return; + + let shouldRevealConsole = true; + let commandResultMessage = new WI.ConsoleCommandResultMessage(result.target, result, wasThrown, savedResultIndex, shouldRevealConsole); + let commandResultMessageView = new WI.ConsoleMessageView(commandResultMessage); + this._appendConsoleMessageView(commandResultMessageView, true); + } + + let options = { + objectGroup: WI.RuntimeManager.ConsoleObjectGroup, + includeCommandLineAPI: true, + doNotPauseOnExceptionsAndMuteConsole: false, + returnByValue: false, + generatePreview: true, + saveResult: true, + emulateUserGesture: WI.settings.emulateInUserGesture.value, + sourceURLAppender: appendWebInspectorConsoleEvaluationSourceURL, + }; + + WI.runtimeManager.evaluateInInspectedWindow(text, options, printResult.bind(this)); + } + + // Private + + _handleFindNextShortcut() + { + this.delegate.highlightNextSearchMatch(); + } + + _handleFindPreviousShortcut() + { + this.delegate.highlightPreviousSearchMatch(); + } + + _appendConsoleMessageView(messageView, repeatCountWasInterrupted) + { + let pendingMessagesForSession = this._pendingMessagesForSessionOrGroup.get(this._currentSessionOrGroup); + if (!pendingMessagesForSession) { + pendingMessagesForSession = []; + this._pendingMessagesForSessionOrGroup.set(this._currentSessionOrGroup, pendingMessagesForSession); + } + pendingMessagesForSession.push(messageView); + this._consoleMessageViews.push(messageView); + + this._cleared = false; + this._repeatCountWasInterrupted = repeatCountWasInterrupted || false; + + if (!repeatCountWasInterrupted) + this._previousMessageView = messageView; + + if (messageView.message && messageView.message.source !== WI.ConsoleMessage.MessageSource.JS) + this._lastCommitted = {test: "", special: false}; + + if (WI.consoleContentView.isAttached) + this.renderPendingMessagesSoon(); + + if (!WI.isShowingConsoleTab() && messageView.message && messageView.message.shouldRevealConsole) + WI.showSplitConsole(); + } + + renderPendingMessages() + { + if (this._scheduledRenderIdentifier) { + cancelAnimationFrame(this._scheduledRenderIdentifier); + this._scheduledRenderIdentifier = 0; + } + + if (!this._pendingMessagesForSessionOrGroup.size) + return; + + let wasScrolledToBottom = this.isScrolledToBottom(); + let savedCurrentConsoleGroup = this._currentSessionOrGroup; + let lastMessageView = null; + + const maxMessagesPerFrame = 100; + let renderedMessages = 0; + for (let [session, messages] of this._pendingMessagesForSessionOrGroup) { + this._currentSessionOrGroup = session; + + let messagesToRender = messages.splice(0, maxMessagesPerFrame - renderedMessages); + for (let message of messagesToRender) { + message.render(); + this._didRenderConsoleMessageView(message); + } + + lastMessageView = messagesToRender.lastValue; + + if (!messages.length) + this._pendingMessagesForSessionOrGroup.delete(session); + + renderedMessages += messagesToRender.length; + if (renderedMessages >= maxMessagesPerFrame) + break; + } + + this._currentSessionOrGroup = savedCurrentConsoleGroup; + + this._currentSessionOrGroup.element.classList.toggle("timestamps-visible", this._showTimestamps); + + if (wasScrolledToBottom || lastMessageView instanceof WI.ConsoleCommandView || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Result || lastMessageView.message.type === WI.ConsoleMessage.MessageType.Image) + this.scrollToBottom(); + + WI.quickConsole.needsLayout(); + + if (this._pendingMessagesForSessionOrGroup.size) + this.renderPendingMessagesSoon(); + } + + renderPendingMessagesSoon() + { + if (this._scheduledRenderIdentifier) + return; + + this._scheduledRenderIdentifier = requestAnimationFrame(() => this.renderPendingMessages()); + } + + _didRenderConsoleMessageView(messageView) + { + var type = messageView instanceof WI.ConsoleCommandView ? null : messageView.message.type; + if (type === WI.ConsoleMessage.MessageType.EndGroup) { + var parentGroup = this._currentSessionOrGroup.parentGroup; + if (parentGroup) + this._currentSessionOrGroup = parentGroup; + } else { + if (type === WI.ConsoleMessage.MessageType.StartGroup || type === WI.ConsoleMessage.MessageType.StartGroupCollapsed) { + var group = new WI.ConsoleGroup(this._currentSessionOrGroup); + var groupElement = group.render(messageView); + this._currentSessionOrGroup.append(groupElement); + this._currentSessionOrGroup = group; + } else + this._currentSessionOrGroup.addMessageView(messageView); + } + + if (this.delegate && typeof this.delegate.didAppendConsoleMessageView === "function") + this.delegate.didAppendConsoleMessageView(messageView); + } + + _handleShowConsoleMessageTimestampsSettingChanged() + { + this._showTimestamps = WI.settings.showConsoleMessageTimestamps.value; + this._currentSessionOrGroup.element.classList.toggle("timestamps-visible", this._showTimestamps); + if (this._showTimestamps) { + for (let consoleMessageView of this._consoleMessageViews) { + if (consoleMessageView instanceof WI.ConsoleMessageView) + consoleMessageView.renderTimestamp(); + } + } + } +}; + +WI.JavaScriptLogViewController.CachedPropertiesDuration = 30_000; +WI.JavaScriptLogViewController.IgnoredRepeatCount = Symbol("ignored-repeat-count"); diff --git a/inspector/Controllers/JavaScriptRuntimeCompletionProvider.js b/inspector/Controllers/JavaScriptRuntimeCompletionProvider.js new file mode 100644 index 0000000..c5f2f0f --- /dev/null +++ b/inspector/Controllers/JavaScriptRuntimeCompletionProvider.js @@ -0,0 +1,425 @@ +/* + * 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. + */ + +Object.defineProperty(WI, "javaScriptRuntimeCompletionProvider", +{ + get: function() + { + if (!WI.JavaScriptRuntimeCompletionProvider._instance) + WI.JavaScriptRuntimeCompletionProvider._instance = new WI.JavaScriptRuntimeCompletionProvider; + return WI.JavaScriptRuntimeCompletionProvider._instance; + } +}); + +WI.JavaScriptRuntimeCompletionProvider = class JavaScriptRuntimeCompletionProvider extends WI.Object +{ + constructor() + { + super(); + + console.assert(!WI.JavaScriptRuntimeCompletionProvider._instance); + + this._ongoingCompletionRequests = 0; + + WI.debuggerManager.addEventListener(WI.DebuggerManager.Event.ActiveCallFrameDidChange, this._clearLastProperties, this); + } + + // Static + + static get _commandLineAPIKeys() + { + if (!JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys) { + JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys = [ + "$_", + "assert", + "clear", + "count", + "countReset", + "debug", + "dir", + "dirxml", + "error", + "group", + "groupCollapsed", + "groupEnd", + "info", + "inspect", + "keys", + "log", + "profile", + "profileEnd", + "queryHolders", + "queryInstances", + "queryObjects", + "record", + "recordEnd", + "screenshot", + "table", + "takeHeapSnapshot", + "time", + "timeEnd", + "timeLog", + "timeStamp", + "trace", + "values", + "warn", + ]; + } + return JavaScriptRuntimeCompletionProvider.__cachedCommandLineAPIKeys; + } + + // Protected + + completionControllerCompletionsNeeded(completionController, defaultCompletions, base, prefix, suffix, forced) + { + // Don't allow non-forced empty prefix completions unless the base is that start of property access. + if (!forced && !prefix && !/[.[]$/.test(base)) { + completionController.updateCompletions(null); + return; + } + + // If the base ends with an open parentheses or open curly bracket then treat it like there is + // no base so we get global object completions. + if (/[({]$/.test(base)) + base = ""; + + var lastBaseIndex = base.length - 1; + var dotNotation = base[lastBaseIndex] === "."; + var bracketNotation = base[lastBaseIndex] === "["; + + if (dotNotation || bracketNotation) { + base = base.substring(0, lastBaseIndex); + + // Don't suggest anything for an empty base that is using dot notation. + // Bracket notation with an empty base will be treated as an array. + if (!base && dotNotation) { + completionController.updateCompletions(defaultCompletions); + return; + } + + // Don't allow non-forced empty prefix completions if the user is entering a number, since it might be a float. + // But allow number completions if the base already has a decimal, so "10.0." will suggest Number properties. + if (!forced && !prefix && dotNotation && base.indexOf(".") === -1 && parseInt(base, 10) == base) { + completionController.updateCompletions(null); + return; + } + + // An empty base with bracket notation is not property access, it is an array. + // Clear the bracketNotation flag so completions are not quoted. + if (!base && bracketNotation) + bracketNotation = false; + } + + // Start an completion request. We must now decrement before calling completionController.updateCompletions. + this._incrementOngoingCompletionRequests(); + + // If the base is the same as the last time, we can reuse the property names we have already gathered. + // Doing this eliminates delay caused by the async nature of the code below and it only calls getters + // and functions once instead of repetitively. Sure, there can be difference each time the base is evaluated, + // but this optimization gives us more of a win. We clear the cache after 30 seconds or when stepping in the + // debugger to make sure we don't use stale properties in most cases. + if (this._lastMode === completionController.mode && this._lastBase === base && this._lastPropertyNames) { + receivedPropertyNames.call(this, this._lastPropertyNames); + return; + } + + this._lastMode = completionController.mode; + this._lastBase = base; + this._lastPropertyNames = null; + + var activeCallFrame = WI.debuggerManager.activeCallFrame; + if (!base && activeCallFrame && !this._alwaysEvaluateInWindowContext) + activeCallFrame.collectScopeChainVariableNames(receivedPropertyNames.bind(this)); + else { + let options = {objectGroup: "completion", includeCommandLineAPI: true, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: false, generatePreview: false, saveResult: false}; + WI.runtimeManager.evaluateInInspectedWindow(base, options, evaluated.bind(this)); + } + + function updateLastPropertyNames(propertyNames) + { + if (this._clearLastPropertiesTimeout) + clearTimeout(this._clearLastPropertiesTimeout); + this._clearLastPropertiesTimeout = setTimeout(this._clearLastProperties.bind(this), WI.JavaScriptLogViewController.CachedPropertiesDuration); + + this._lastPropertyNames = propertyNames || []; + } + + function evaluated(result, wasThrown) + { + if (wasThrown || !result || result.type === "undefined" || (result.type === "object" && result.subtype === "null")) { + this._decrementOngoingCompletionRequests(); + + updateLastPropertyNames.call(this, []); + completionController.updateCompletions(defaultCompletions); + + return; + } + + function inspectedPage_evalResult_getArrayCompletions(primitiveType) + { + var array = this; + var arrayLength; + + var resultSet = {}; + for (var o = array; o; o = o.__proto__) { + try { + if (o === array && o.length) { + // If the array type has a length, don't include a list of all the indexes. + // Include it at the end and the frontend can build the list. + arrayLength = o.length; + } else { + var names = Object.getOwnPropertyNames(o); + for (var i = 0; i < names.length; ++i) + resultSet[names[i]] = true; + } + } catch { } + } + + if (arrayLength) + resultSet["length"] = arrayLength; + + return resultSet; + } + + function inspectedPage_evalResult_getCompletions(primitiveType) + { + var object; + if (primitiveType === "string") + object = new String(""); + else if (primitiveType === "number") + object = new Number(0); + else if (primitiveType === "boolean") + object = new Boolean(false); + else if (primitiveType === "symbol") + object = Symbol(); + else + object = this; + + var resultSet = {}; + for (var o = object; o; o = o.__proto__) { + try { + var names = Object.getOwnPropertyNames(o); + for (var i = 0; i < names.length; ++i) + resultSet[names[i]] = true; + } catch (e) { } + } + + return resultSet; + } + + if (result.subtype === "array") + result.callFunctionJSON(inspectedPage_evalResult_getArrayCompletions, undefined, receivedArrayPropertyNames.bind(this)); + else if (result.type === "object" || result.type === "function") + result.callFunctionJSON(inspectedPage_evalResult_getCompletions, undefined, receivedObjectPropertyNames.bind(this)); + else if (result.type === "string" || result.type === "number" || result.type === "boolean" || result.type === "symbol") { + let options = {objectGroup: "completion", includeCommandLineAPI: false, doNotPauseOnExceptionsAndMuteConsole: true, returnByValue: true, generatePreview: false, saveResult: false}; + WI.runtimeManager.evaluateInInspectedWindow("(" + inspectedPage_evalResult_getCompletions + ")(\"" + result.type + "\")", options, receivedPropertyNamesFromEvaluate.bind(this)); + } else + console.error("Unknown result type: " + result.type); + } + + function receivedPropertyNamesFromEvaluate(object, wasThrown, result) + { + receivedPropertyNames.call(this, result && !wasThrown ? Object.keys(result.value) : null); + } + + function receivedObjectPropertyNames(propertyNames) + { + receivedPropertyNames.call(this, Object.keys(propertyNames)); + } + + function receivedArrayPropertyNames(propertyNames) + { + if (propertyNames && typeof propertyNames.length === "number") { + // FIXME Web Inspector: autocompletion of array indexes can't handle large arrays in a performant way + var max = Math.min(propertyNames.length, 1000); + for (var i = 0; i < max; ++i) + propertyNames[i] = true; + } + + receivedObjectPropertyNames.call(this, propertyNames); + } + + function receivedPropertyNames(propertyNames) + { + console.assert(!propertyNames || Array.isArray(propertyNames)); + propertyNames = propertyNames || []; + + updateLastPropertyNames.call(this, propertyNames); + + this._decrementOngoingCompletionRequests(); + + if (!base) { + propertyNames.pushAll(JavaScriptRuntimeCompletionProvider._commandLineAPIKeys); + + let savedResultAlias = WI.settings.consoleSavedResultAlias.value; + if (savedResultAlias) + propertyNames.push(savedResultAlias + "_"); + + let target = WI.runtimeManager.activeExecutionContext.target; + let targetData = WI.debuggerManager.paused ? WI.debuggerManager.dataForTarget(target) : {}; + + function shouldExposeEvent() { + switch (completionController.mode) { + case WI.CodeMirrorCompletionController.Mode.FullConsoleCommandLineAPI: + case WI.CodeMirrorCompletionController.Mode.EventBreakpoint: + return true; + case WI.CodeMirrorCompletionController.Mode.PausedConsoleCommandLineAPI: + return targetData.pauseReason === WI.DebuggerManager.PauseReason.Listener || targetData.pauseReason === WI.DebuggerManager.PauseReason.EventListener; + } + return false; + } + if (shouldExposeEvent()) { + propertyNames.push("$event"); + if (savedResultAlias) + propertyNames.push(savedResultAlias + "event"); + } + + function shouldExposeException() { + switch (completionController.mode) { + case WI.CodeMirrorCompletionController.Mode.FullConsoleCommandLineAPI: + case WI.CodeMirrorCompletionController.Mode.ExceptionBreakpoint: + return true; + case WI.CodeMirrorCompletionController.Mode.PausedConsoleCommandLineAPI: + return targetData.pauseReason === WI.DebuggerManager.PauseReason.Exception; + } + return false; + } + if (shouldExposeException()) { + propertyNames.push("$exception"); + if (savedResultAlias) + propertyNames.push(savedResultAlias + "exception"); + } + + switch (target.type) { + case WI.TargetType.Page: + propertyNames.push("$"); + propertyNames.push("$$"); + propertyNames.push("$0"); + if (savedResultAlias) + propertyNames.push(savedResultAlias + "0"); + propertyNames.push("$x"); + // fallthrough + case WI.TargetType.ServiceWorker: + case WI.TargetType.Worker: + propertyNames.push("copy"); + propertyNames.push("getEventListeners"); + propertyNames.push("monitorEvents"); + propertyNames.push("unmonitorEvents"); + break; + } + + // FIXME: Due to caching, sometimes old $n values show up as completion results even though they are not available. We should clear that proactively. + for (var i = 1; i <= WI.ConsoleCommandResultMessage.maximumSavedResultIndex; ++i) { + propertyNames.push("$" + i); + if (savedResultAlias) + propertyNames.push(savedResultAlias + i); + } + } + + var implicitSuffix = ""; + if (bracketNotation) { + var quoteUsed = prefix[0] === "'" ? "'" : "\""; + if (suffix !== "]" && suffix !== quoteUsed) + implicitSuffix = "]"; + } + + var completions = defaultCompletions; + let knownCompletions = new Set(completions); + + for (var i = 0; i < propertyNames.length; ++i) { + var property = propertyNames[i]; + + if (dotNotation && !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(property)) + continue; + + if (bracketNotation) { + if (parseInt(property) != property) + property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + (suffix !== quoteUsed ? quoteUsed : ""); + } + + if (!property.startsWith(prefix) || knownCompletions.has(property)) + continue; + + completions.push(property); + knownCompletions.add(property); + } + + function compare(a, b) + { + // Try to sort in numerical order first. + let numericCompareResult = a - b; + if (!isNaN(numericCompareResult)) + return numericCompareResult; + + // Sort __defineGetter__, __lookupGetter__, and friends last. + let aRareProperty = a.startsWith("__") && a.endsWith("__"); + let bRareProperty = b.startsWith("__") && b.endsWith("__"); + if (aRareProperty && !bRareProperty) + return 1; + if (!aRareProperty && bRareProperty) + return -1; + + // Not numbers, sort as strings. + return a.extendedLocaleCompare(b); + } + + completions.sort(compare); + + completionController.updateCompletions(completions, implicitSuffix); + } + } + + // Private + + _incrementOngoingCompletionRequests() + { + this._ongoingCompletionRequests++; + + console.assert(this._ongoingCompletionRequests <= 50, "Ongoing requests probably should not get this high. We may be missing a balancing decrement."); + } + + _decrementOngoingCompletionRequests() + { + this._ongoingCompletionRequests--; + + console.assert(this._ongoingCompletionRequests >= 0, "Unbalanced increments / decrements."); + + if (this._ongoingCompletionRequests <= 0) + WI.runtimeManager.activeExecutionContext.target.RuntimeAgent.releaseObjectGroup("completion"); + } + + _clearLastProperties() + { + if (this._clearLastPropertiesTimeout) { + clearTimeout(this._clearLastPropertiesTimeout); + delete this._clearLastPropertiesTimeout; + } + + // Clear the cache of property names so any changes while stepping or sitting idle get picked up if the same + // expression is evaluated again. + this._lastPropertyNames = null; + } +}; diff --git a/inspector/Controllers/LayerTreeManager.js b/inspector/Controllers/LayerTreeManager.js new file mode 100644 index 0000000..bf23127 --- /dev/null +++ b/inspector/Controllers/LayerTreeManager.js @@ -0,0 +1,211 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/MemoryManager.js b/inspector/Controllers/MemoryManager.js new file mode 100644 index 0000000..d4c82f6 --- /dev/null +++ b/inspector/Controllers/MemoryManager.js @@ -0,0 +1,100 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/NetworkManager.js b/inspector/Controllers/NetworkManager.js new file mode 100644 index 0000000..6a46bb4 --- /dev/null +++ b/inspector/Controllers/NetworkManager.js @@ -0,0 +1,1677 @@ +/* + * 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._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.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: 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(); + } + + 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 .")} + */ +`.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: 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: 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; + + 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: InspectorBackend.Enum.Network.NetworkStage.Request, + }); + } + + async responseIntercepted(target, requestId, response) + { + for (let localResourceOverride of this.localResourceOverridesForURL(response.url)) { + if (localResourceOverride.disabled) + 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.statusCode; + + 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; + } + } + + // 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: InspectorBackend.Enum.Network.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.type === WI.LocalResourceOverride.InterceptType.Response ? InspectorBackend.Enum.Network.NetworkStage.Response : InspectorBackend.Enum.Network.NetworkStage.Request, + 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); + } + + _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; + + this._sourceMapURLMap.clear(); + this._downloadingSourceMaps.clear(); + } +}; + +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", +}; diff --git a/inspector/Controllers/QueryController.js b/inspector/Controllers/QueryController.js new file mode 100644 index 0000000..bb5580c --- /dev/null +++ b/inspector/Controllers/QueryController.js @@ -0,0 +1,146 @@ +/* + * 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; + } +}; diff --git a/inspector/Controllers/ResourceQueryController.js b/inspector/Controllers/ResourceQueryController.js new file mode 100644 index 0000000..33da64d --- /dev/null +++ b/inspector/Controllers/ResourceQueryController.js @@ -0,0 +1,122 @@ +/* + * 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, "_.-/"); + } +}; diff --git a/inspector/Controllers/RuntimeManager.js b/inspector/Controllers/RuntimeManager.js new file mode 100644 index 0000000..6ebfcd4 --- /dev/null +++ b/inspector/Controllers/RuntimeManager.js @@ -0,0 +1,291 @@ +/* + * 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 ` 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 + anonymous = true; + } else if (statement.type === "ExpressionStatement" + && statement.expression.type === "AssignmentExpression" + && statement.expression.right.type === "AwaitExpression" + && statement.expression.left.type === "Identifier") { + // x = await + 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 + 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", +}; diff --git a/inspector/Controllers/SelectionController.js b/inspector/Controllers/SelectionController.js new file mode 100644 index 0000000..b0b2ea1 --- /dev/null +++ b/inspector/Controllers/SelectionController.js @@ -0,0 +1,499 @@ +/* + * 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"), +}; diff --git a/inspector/Controllers/StackTraceTreeController.js b/inspector/Controllers/StackTraceTreeController.js new file mode 100644 index 0000000..7a758dd --- /dev/null +++ b/inspector/Controllers/StackTraceTreeController.js @@ -0,0 +1,217 @@ +/* + * 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.StackTraceTreeController = class StackTraceTreeController extends WI.Object +{ + constructor(treeOutline) + { + console.assert(treeOutline instanceof WI.TreeOutline); + + super(); + + this._stackTrace = null; + + this._treeOutline = treeOutline; + + if (this._treeOutline.selectable) + this._treeOutline.addEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this); + else + this._treeOutline.addEventListener(WI.TreeOutline.Event.ElementClicked, this._treeElementClicked, this); + } + + // Static + + static groupBlackboxedStackTrace(parent, stackTrace, {rememberBlackboxedCallFrameGroupToAutoExpand} = {}) + { + let parentIsNode = parent instanceof Node; + console.assert(parentIsNode || parent instanceof WI.TreeOutline || parent instanceof WI.TreeElement, parent); + console.assert(stackTrace instanceof WI.StackTrace, stackTrace); + + let CallFrameUIClass = parentIsNode ? WI.CallFrameView : WI.CallFrameTreeElement; + + let activeCallFrameTreeElement = WI.StackTraceTreeController._groupBlackboxedCallFrames(parent, stackTrace.callFrames, {rememberBlackboxedCallFrameGroupToAutoExpand}); + + let parentStackTrace = stackTrace.parentStackTrace; + while (parentStackTrace) { + console.assert(parentStackTrace.callFrames.length, "StackTrace should have non-empty call frames array."); + if (!parentStackTrace.callFrames.length) + break; + + let boundaryCallFrame; + if (parentStackTrace.topCallFrameIsBoundary) { + boundaryCallFrame = parentStackTrace.callFrames[0]; + console.assert(boundaryCallFrame.nativeCode && !boundaryCallFrame.sourceCodeLocation); + } else { + // Create a generic native CallFrame for the asynchronous boundary. + boundaryCallFrame = new WI.CallFrame(parentStackTrace.callFrames[0].target, { + functionName: WI.UIString("(async)"), + nativeCode: true, + }); + } + + parent.appendChild(new CallFrameUIClass(boundaryCallFrame, {showFunctionName: true, isAsyncBoundaryCallFrame: true})); + + let startIndex = parentStackTrace.topCallFrameIsBoundary ? 1 : 0; + let parentCallFrames = startIndex ? parentStackTrace.callFrames.slice(startIndex) : parentStackTrace.callFrames; + WI.StackTraceTreeController._groupBlackboxedCallFrames(parent, parentCallFrames, {rememberBlackboxedCallFrameGroupToAutoExpand}); + + if (parentStackTrace.truncated) { + let truncatedCallFrame = new WI.CallFrame(parentStackTrace.callFrames[0].target, { + functionName: WI.UIString("(call frames truncated)"), + nativeCode: true, + }); + parent.appendChild(new CallFrameUIClass(truncatedCallFrame, {showFunctionName: true, isTruncatedBoundaryCallFrame: true})); + } + + parentStackTrace = parentStackTrace.parentStackTrace; + } + + return activeCallFrameTreeElement; + } + + static _groupBlackboxedCallFrames(parent, callFrames, {rememberBlackboxedCallFrameGroupToAutoExpand} = {}) + { + let parentIsNode = parent instanceof Node; + console.assert(parentIsNode || parent instanceof WI.TreeOutline || parent instanceof WI.TreeElement, parent); + console.assert(Array.isArray(callFrames) && callFrames.length && callFrames.every((callFrame) => callFrame instanceof WI.CallFrame), callFrames); + + let BlackboxedGroupUIClass = parentIsNode ? WI.BlackboxedGroupView : WI.BlackboxedGroupTreeElement; + let blackboxedGroupUIOptions = {rememberBlackboxedCallFrameGroupToAutoExpand}; + + let CallFrameUIClass = parentIsNode ? WI.CallFrameView : WI.CallFrameTreeElement; + let callFrameUIOptions = {showFunctionName: true, indicateIfBlackboxed: true}; + + let activeCallFrame = WI.debuggerManager.activeCallFrame; + let activeCallFrameTreeElement = null; + + let blackboxedCallFrameGroupStartIndex = undefined; + + function displayable(callFrame) { + if (parentIsNode) { + if (!callFrame.sourceCodeLocation && callFrame.functionName === null) + return false; + + if (callFrame.isConsoleEvaluation && !WI.settings.debugShowConsoleEvaluations.value) + return false; + } + + return true; + } + + // Add one extra iteration to handle call stacks that start blackboxed. + for (let i = 0; i < callFrames.length + 1; ++i) { + let callFrame = callFrames[i]; + + if (callFrame) { + if (callFrames.length > 1 && !displayable(callFrame)) + continue; + + if (callFrame.blackboxed) { + blackboxedCallFrameGroupStartIndex ??= i; + continue; + } + } + + if (blackboxedCallFrameGroupStartIndex !== undefined) { + let blackboxedCallFrameGroup = callFrames.slice(blackboxedCallFrameGroupStartIndex, i).filter(displayable); + blackboxedCallFrameGroupStartIndex = undefined; + + if (!rememberBlackboxedCallFrameGroupToAutoExpand || !WI.debuggerManager.shouldAutoExpandBlackboxedCallFrameGroup(blackboxedCallFrameGroup)) + parent.appendChild(new BlackboxedGroupUIClass(blackboxedCallFrameGroup, blackboxedGroupUIOptions)); + else { + for (let blackboxedCallFrame of blackboxedCallFrameGroup) + parent.appendChild(new CallFrameUIClass(blackboxedCallFrame, callFrameUIOptions)); + } + } + + if (!callFrame) + continue; + + let callFrameTreeElement = new CallFrameUIClass(callFrame, callFrameUIOptions); + if (callFrame === activeCallFrame && !parentIsNode) + activeCallFrameTreeElement = callFrameTreeElement; + parent.appendChild(callFrameTreeElement); + } + + return activeCallFrameTreeElement; + } + + // Public + + get treeOutline() { return this._treeOutline; } + + get stackTrace() + { + return this._stackTrace; + } + + set stackTrace(stackTrace) + { + stackTrace = stackTrace || null; + if (this._stackTrace === stackTrace) + return; + + this._stackTrace = stackTrace; + + this._treeOutline.removeChildren(); + + if (this._stackTrace) + WI.StackTraceTreeController.groupBlackboxedStackTrace(this._treeOutline, this._stackTrace); + } + + disconnect() + { + if (this._treeOutline.selectable) + this._treeOutline.removeEventListener(WI.TreeOutline.Event.SelectionDidChange, this._treeSelectionDidChange, this); + else + this._treeOutline.removeEventListener(WI.TreeOutline.Event.ElementClicked, this._treeElementClicked, this); + } + + // Private + + _treeElementClicked(event) + { + this._showSourceCodeLocation(event.data.treeElement); + } + + _treeSelectionDidChange(event) + { + this._showSourceCodeLocation(this._treeOutline.selectedTreeElement); + } + + _showSourceCodeLocation(treeElement) + { + let callFrame = treeElement.callFrame; + if (!callFrame.sourceCodeLocation) + return; + + WI.showSourceCodeLocation(callFrame.sourceCodeLocation, { + ignoreNetworkTab: true, + ignoreSearchTab: true, + // Treat call frame clicks as link clicks since it jumps to a source location. + initiatorHint: WI.TabBrowser.TabNavigationInitiator.LinkClick, + }); + } +}; diff --git a/inspector/Controllers/TabActivityDiagnosticEventRecorder.js b/inspector/Controllers/TabActivityDiagnosticEventRecorder.js new file mode 100644 index 0000000..3d7e987 --- /dev/null +++ b/inspector/Controllers/TabActivityDiagnosticEventRecorder.js @@ -0,0 +1,201 @@ +/* + * 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.TabActivityDiagnosticEventRecorder = class TabActivityDiagnosticEventRecorder extends WI.DiagnosticEventRecorder +{ + constructor(controller) + { + super("TabActivity", controller); + + this._inspectorHasFocus = true; + this._lastUserInteractionTimestamp = undefined; + + this._eventSamplingTimerIdentifier = undefined; + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + + // Static + + // In milliseconds. + static get eventSamplingInterval() { return 60 * 1000; } + static get initialDelayBeforeSamplingInterval() { return 10 * 1000; } + + // Protected + + setup() + { + const options = { + capture: true, + }; + window.addEventListener("focus", this, options); + window.addEventListener("blur", this, options); + window.addEventListener("keydown", this, options); + window.addEventListener("mousedown", this, options); + + // If it's been less than 10 seconds since the frontend loaded, wait a bit. + if (performance.now() - WI.frontendCompletedLoadTimestamp < TabActivityDiagnosticEventRecorder.initialDelayBeforeSamplingInterval) + this._startInitialDelayBeforeSamplingTimer(); + else + this._startEventSamplingTimer(); + + } + + teardown() + { + const options = { + capture: true, + }; + window.removeEventListener("focus", this, options); + window.removeEventListener("blur", this, options); + window.removeEventListener("keydown", this, options); + window.removeEventListener("mousedown", this, options); + + this._stopInitialDelayBeforeSamplingTimer(); + this._stopEventSamplingTimer(); + + } + + // Public + + handleEvent(event) + { + switch (event.type) { + case "focus": + this._handleWindowFocus(event); + break; + case "blur": + this._handleWindowBlur(event); + break; + case "keydown": + this._handleWindowKeyDown(event); + break; + case "mousedown": + this._handleWindowMouseDown(event); + break; + } + } + + // Private + + _startInitialDelayBeforeSamplingTimer() + { + if (this._initialDelayBeforeSamplingTimerIdentifier) { + clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier); + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + + // All intervals are in milliseconds. + let maximumInitialDelay = TabActivityDiagnosticEventRecorder.initialDelayBeforeSamplingInterval; + let elapsedTime = performance.now() - WI.frontendCompletedLoadTimestamp; + let remainingTime = maximumInitialDelay - elapsedTime; + let initialDelay = Number.constrain(remainingTime, 0, maximumInitialDelay); + this._initialDelayBeforeSamplingTimerIdentifier = setTimeout(this._sampleCurrentTabActivity.bind(this), initialDelay); + } + + _stopInitialDelayBeforeSamplingTimer() + { + if (this._initialDelayBeforeSamplingTimerIdentifier) { + clearTimeout(this._initialDelayBeforeSamplingTimerIdentifier); + this._initialDelayBeforeSamplingTimerIdentifier = undefined; + } + } + + _startEventSamplingTimer() + { + if (this._eventSamplingTimerIdentifier) { + clearTimeout(this._eventSamplingTimerIdentifier); + this._eventSamplingTimerIdentifier = undefined; + } + + this._eventSamplingTimerIdentifier = setTimeout(this._sampleCurrentTabActivity.bind(this), TabActivityDiagnosticEventRecorder.eventSamplingInterval); + } + + _stopEventSamplingTimer() + { + if (this._eventSamplingTimerIdentifier) { + clearTimeout(this._eventSamplingTimerIdentifier); + this._eventSamplingTimerIdentifier = undefined; + } + } + + _sampleCurrentTabActivity() + { + // Set up the next timer first so later code can bail out if there's nothing to do. + this._stopEventSamplingTimer(); + this._stopInitialDelayBeforeSamplingTimer(); + this._startEventSamplingTimer(); + + let intervalSinceLastUserInteraction = performance.now() - this._lastUserInteractionTimestamp; + if (intervalSinceLastUserInteraction > TabActivityDiagnosticEventRecorder.eventSamplingInterval) { + if (WI.settings.debugAutoLogDiagnosticEvents.valueRespectingDebugUIAvailability) + console.log("TabActivity: sample not reported, last user interaction was %.1f seconds ago.".format(intervalSinceLastUserInteraction / 1000)); + return; + } + + let selectedTabContentView = WI.tabBrowser.selectedTabContentView; + console.assert(selectedTabContentView); + if (!selectedTabContentView) + return; + + let tabType = selectedTabContentView.type; + let interval = TabActivityDiagnosticEventRecorder.eventSamplingInterval / 1000; + this.logDiagnosticEvent(this.name, {tabType, interval}); + } + + _didObserveUserInteraction() + { + if (!this._inspectorHasFocus) + return; + + this._lastUserInteractionTimestamp = performance.now(); + } + + _handleWindowFocus(event) + { + if (event.target !== window) + return; + + this._inspectorHasFocus = true; + } + + _handleWindowBlur(event) + { + if (event.target !== window) + return; + + this._inspectorHasFocus = false; + } + + _handleWindowKeyDown(event) + { + this._didObserveUserInteraction(); + } + + _handleWindowMouseDown(event) + { + this._didObserveUserInteraction(); + } +}; + diff --git a/inspector/Controllers/TabNavigationDiagnosticEventRecorder.js b/inspector/Controllers/TabNavigationDiagnosticEventRecorder.js new file mode 100644 index 0000000..137394a --- /dev/null +++ b/inspector/Controllers/TabNavigationDiagnosticEventRecorder.js @@ -0,0 +1,90 @@ +/* + * 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.TabNavigationDiagnosticEventRecorder = class TabNavigationDiagnosticEventRecorder extends WI.DiagnosticEventRecorder +{ + constructor(controller) + { + super("TabNavigation", controller); + } + + // Protected + + setup() + { + WI.tabBrowser.addEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._selectedTabContentViewDidChange, this); + } + + teardown() + { + WI.tabBrowser.removeEventListener(WI.TabBrowser.Event.SelectedTabContentViewDidChange, this._selectedTabContentViewDidChange, this); + } + + // Private + + _selectedTabContentViewDidChange(event) + { + let outgoingTabType = event.data.outgoingTab.identifier; + let incomingTabType = event.data.incomingTab.identifier; + let initiator = WI.TabNavigationDiagnosticEventRecorder.tabBrowserInitiatorToEventInitiator(event.data.initiator || WI.TabBrowser.TabNavigationInitiator.Unknown); + if (!initiator) { + // If initiator is null, then there is a missing value in the switch. This is a programming error. + WI.reportInternalError("Value of 'initiator' could not be parsed: " + event.data.initiator); + return; + } + + this.logDiagnosticEvent(this.name, {outgoingTabType, incomingTabType, initiator}); + } + + static tabBrowserInitiatorToEventInitiator(tabBrowserInitiator) + { + switch (tabBrowserInitiator) { + case WI.TabBrowser.TabNavigationInitiator.TabClick: + return "tab-click"; + case WI.TabBrowser.TabNavigationInitiator.LinkClick: + return "link-click"; + case WI.TabBrowser.TabNavigationInitiator.ButtonClick: + return "button-click"; + case WI.TabBrowser.TabNavigationInitiator.ContextMenu: + return "context-menu"; + case WI.TabBrowser.TabNavigationInitiator.Dashboard: + return "dashboard"; + case WI.TabBrowser.TabNavigationInitiator.Breakpoint: + return "breakpoint"; + case WI.TabBrowser.TabNavigationInitiator.Inspect: + return "inspect"; + case WI.TabBrowser.TabNavigationInitiator.KeyboardShortcut: + return "keyboard-shortcut"; + case WI.TabBrowser.TabNavigationInitiator.FrontendAPI: + return "frontend-api"; + case WI.TabBrowser.TabNavigationInitiator.Unknown: + return "unknown"; + } + + console.error("Unhandled initiator type: " + tabBrowserInitiator); + return null; + } +}; + diff --git a/inspector/Controllers/TargetManager.js b/inspector/Controllers/TargetManager.js new file mode 100644 index 0000000..bebd4d5 --- /dev/null +++ b/inspector/Controllers/TargetManager.js @@ -0,0 +1,312 @@ +/* + * 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", +}; diff --git a/inspector/Controllers/TimelineManager.js b/inspector/Controllers/TimelineManager.js new file mode 100644 index 0000000..2a369a7 --- /dev/null +++ b/inspector/Controllers/TimelineManager.js @@ -0,0 +1,1451 @@ +/* + * 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. + // 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: 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: 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: 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 diff --git a/inspector/Controllers/TypeTokenAnnotator.js b/inspector/Controllers/TypeTokenAnnotator.js new file mode 100644 index 0000000..0e5f675 --- /dev/null +++ b/inspector/Controllers/TypeTokenAnnotator.js @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2014, 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.TypeTokenAnnotator = class TypeTokenAnnotator extends WI.Annotator +{ + constructor(sourceCodeTextEditor, script) + { + super(sourceCodeTextEditor); + + this._script = script; + this._typeTokenNodes = []; + this._typeTokenBookmarks = []; + } + + // Protected + + insertAnnotations() + { + if (!this.isActive()) + return; + + var scriptSyntaxTree = this._script.scriptSyntaxTree; + + if (!scriptSyntaxTree) { + this._script.requestScriptSyntaxTree((syntaxTree) => { + // After requesting the tree, we still might get a null tree from a parse error. + if (syntaxTree) + this.insertAnnotations(); + }); + return; + } + + if (!scriptSyntaxTree.parsedSuccessfully) + return; + + let {startPosition, endPosition} = this.sourceCodeTextEditor.visibleRangePositions(); + + let startTime = Date.now(); + let allNodesInRange = scriptSyntaxTree.filterByRange(startPosition, endPosition); + scriptSyntaxTree.updateTypes(allNodesInRange, (nodesWithUpdatedTypes) => { + // Because this is an asynchronous call, we could have been deactivated before the callback function is called. + if (!this.isActive()) + return; + + nodesWithUpdatedTypes.forEach(this._insertTypeToken, this); + + let totalTime = Date.now() - startTime; + let timeoutTime = Number.constrain(8 * totalTime, 500, 2000); + this._timeoutIdentifier = setTimeout(() => { + this._timeoutIdentifier = null; + this.insertAnnotations(); + }, timeoutTime); + }); + } + + clearAnnotations() + { + this._clearTypeTokens(); + } + + // Private + + _insertTypeToken(node) + { + if (node.type === WI.ScriptSyntaxTree.NodeType.Identifier) { + if (!node.attachments.__typeToken && node.attachments.types && node.attachments.types.valid) + this._insertToken(node, false, WI.TypeTokenView.TitleType.Variable, node.name); + + if (node.attachments.__typeToken) + node.attachments.__typeToken.update(node.attachments.types); + + return; + } + + console.assert(node.type === WI.ScriptSyntaxTree.NodeType.FunctionDeclaration || node.type === WI.ScriptSyntaxTree.NodeType.FunctionExpression || node.type === WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression); + + var functionReturnType = node.attachments.returnTypes; + if (!functionReturnType || !functionReturnType.valid) + return; + + // If a function does not have an explicit return statement with an argument (i.e, "return x;" instead of "return;") + // then don't show a return type unless we think it's a constructor. + var scriptSyntaxTree = this._script._scriptSyntaxTree; + if (!node.attachments.__typeToken && (scriptSyntaxTree.containsNonEmptyReturnStatement(node.body) || !functionReturnType.typeSet.isContainedIn(WI.TypeSet.TypeBit.Undefined))) { + var functionName = node.id ? node.id.name : null; + this._insertToken(node, true, WI.TypeTokenView.TitleType.ReturnStatement, functionName); + } + + if (node.attachments.__typeToken) + node.attachments.__typeToken.update(node.attachments.returnTypes); + } + + _insertToken(node, shouldTranslateOffsetToAfterParameterList, typeTokenTitleType, functionOrVariableName) + { + let tokenPosition = this.sourceCodeTextEditor.originalPositionToCurrentPosition(node.startPosition); + let currentOffset = this.sourceCodeTextEditor.currentPositionToCurrentOffset(tokenPosition); + let sourceString = this.sourceCodeTextEditor.string; + + if (shouldTranslateOffsetToAfterParameterList) { + // Translate the position to the closing parenthesis of the function arguments: + // translate from: [type-token] function foo() {} => to: function foo() [type-token] {} + currentOffset = this._translateToOffsetAfterFunctionParameterList(node, currentOffset, sourceString); + tokenPosition = this.sourceCodeTextEditor.currentOffsetToCurrentPosition(currentOffset); + } + + // Note: bookmarks render to the left of the character they're being displayed next to. + // This is why right margin checks the current offset. And this is okay to do because JavaScript can't be written right-to-left. + var isSpaceRegexp = /\s/; + var shouldHaveLeftMargin = currentOffset !== 0 && !isSpaceRegexp.test(sourceString[currentOffset - 1]); + var shouldHaveRightMargin = !isSpaceRegexp.test(sourceString[currentOffset]); + var typeToken = new WI.TypeTokenView(this, shouldHaveRightMargin, shouldHaveLeftMargin, typeTokenTitleType, functionOrVariableName); + var bookmark = this.sourceCodeTextEditor.setInlineWidget(tokenPosition, typeToken.element); + node.attachments.__typeToken = typeToken; + this._typeTokenNodes.push(node); + this._typeTokenBookmarks.push(bookmark); + } + + _translateToOffsetAfterFunctionParameterList(node, offset, sourceString) + { + // The assumption here is that we get the offset starting at the function keyword (or after the get/set keywords). + // We will return the offset for the closing parenthesis in the function declaration. + // All this code is just a way to find this parenthesis while ignoring comments. + + var isMultiLineComment = false; + var isSingleLineComment = false; + var shouldIgnore = false; + const isArrowFunction = node.type === WI.ScriptSyntaxTree.NodeType.ArrowFunctionExpression; + + function isLineTerminator(char) + { + // Reference EcmaScript 5 grammar for single line comments and line terminators: + // http://www.ecma-international.org/ecma-262/5.1/#sec-7.3 + // http://www.ecma-international.org/ecma-262/5.1/#sec-7.4 + return char === "\n" || char === "\r" || char === "\u2028" || char === "\u2029"; + } + + while (((!isArrowFunction && sourceString[offset] !== ")") + || (isArrowFunction && sourceString[offset] !== ">") + || shouldIgnore) + && offset < sourceString.length) { + if (isSingleLineComment && isLineTerminator(sourceString[offset])) { + isSingleLineComment = false; + shouldIgnore = false; + } else if (isMultiLineComment && sourceString[offset] === "*" && sourceString[offset + 1] === "/") { + isMultiLineComment = false; + shouldIgnore = false; + offset++; + } else if (!shouldIgnore && sourceString[offset] === "/") { + offset++; + if (sourceString[offset] === "*") + isMultiLineComment = true; + else if (sourceString[offset] === "/") + isSingleLineComment = true; + else + throw new Error("Bad parsing. Couldn't parse comment preamble."); + shouldIgnore = true; + } + + offset++; + } + + return offset + 1; + } + + _clearTypeTokens() + { + this._typeTokenNodes.forEach(function(node) { + node.attachments.__typeToken = null; + }); + this._typeTokenBookmarks.forEach(function(bookmark) { + bookmark.clear(); + }); + + this._typeTokenNodes = []; + this._typeTokenBookmarks = []; + } +}; diff --git a/inspector/Controllers/WebInspectorExtensionController.js b/inspector/Controllers/WebInspectorExtensionController.js new file mode 100644 index 0000000..5d78eec --- /dev/null +++ b/inspector/Controllers/WebInspectorExtensionController.js @@ -0,0 +1,464 @@ +/* + * 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}; + } + + 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 = this._frameForFrameURL(frameURL); + if (!frame) { + WI.reportInternalError("evaluateScriptForExtension: No frame matched provided frameURL: " + frameURL); + return WI.WebInspectorExtension.ErrorCode.InvalidRequest; + } + + 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 = 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: 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 ${2}\nsnippet iframe.\n ${3}\nsnippet iframe#\n ${3}\nsnippet img\n ${2}${3}\nsnippet img.\n ${3}${4}\nsnippet img#\n ${3}${4}\nsnippet input\n ${5}\nsnippet input.\n ${6}\nsnippet input:text\n ${4}\nsnippet input:submit\n ${4}\nsnippet input:hidden\n ${4}\nsnippet input:button\n ${4}\nsnippet input:image\n ${5}\nsnippet input:checkbox\n ${3}\nsnippet input:radio\n ${3}\nsnippet input:color\n ${4}\nsnippet input:date\n ${4}\nsnippet input:datetime\n ${4}\nsnippet input:datetime-local\n ${4}\nsnippet input:email\n ${4}\nsnippet input:file\n ${4}\nsnippet input:month\n ${4}\nsnippet input:number\n ${4}\nsnippet input:password\n ${4}\nsnippet input:range\n ${4}\nsnippet input:reset\n ${4}\nsnippet input:search\n ${4}\nsnippet input:time\n ${4}\nsnippet input:url\n ${4}\nsnippet input:week\n ${4}\nsnippet ins\n ${1}\nsnippet kbd\n ${1}\nsnippet keygen\n ${1}\nsnippet label\n \nsnippet label:i\n \n ${7}\nsnippet label:s\n \n \nsnippet legend\n ${1}\nsnippet legend+\n ${1}\nsnippet li\n
  • ${1}
  • \nsnippet li.\n
  • ${2}
  • \nsnippet li+\n
  • ${1}
  • \n li+${2}\nsnippet lia\n
  • ${1}
  • \nsnippet lia+\n
  • ${1}
  • \n lia+${3}\nsnippet link\n ${5}\nsnippet link:atom\n ${2}\nsnippet link:css\n ${4}\nsnippet link:favicon\n ${2}\nsnippet link:rss\n ${2}\nsnippet link:touch\n ${2}\nsnippet map\n \n ${2}\n \nsnippet map.\n \n ${3}\n \nsnippet map#\n \n ${5}${6}\n ${7}\nsnippet mark\n ${1}\nsnippet menu\n \n ${1}\n \nsnippet menu:c\n \n ${1}\n \nsnippet menu:t\n \n ${1}\n \nsnippet meta\n ${3}\nsnippet meta:compat\n ${3}\nsnippet meta:refresh\n ${3}\nsnippet meta:utf\n ${3}\nsnippet meter\n ${1}\nsnippet nav\n \nsnippet nav.\n \nsnippet nav#\n \nsnippet noscript\n \nsnippet object\n \n ${3}\n ${4}\n# Embed QT Movie\nsnippet movie\n \n \n \n \n \n ${6}\nsnippet ol\n
      \n ${1}\n
    \nsnippet ol.\n
      \n ${2}\n
    \nsnippet ol+\n
      \n
    1. ${1}
    2. \n li+${2}\n
    \nsnippet opt\n \nsnippet opt+\n \n opt+${3}\nsnippet optt\n \nsnippet optgroup\n \n \n opt+${3}\n \nsnippet output\n ${1}\nsnippet p\n

    ${1}

    \nsnippet param\n ${3}\nsnippet pre\n
    \n		${1}\n	
    \nsnippet progress\n ${1}\nsnippet q\n ${1}\nsnippet rp\n ${1}\nsnippet rt\n ${1}\nsnippet ruby\n \n ${1}\n \nsnippet s\n ${1}\nsnippet samp\n \n ${1}\n \nsnippet script\n