forked from mirrors/gecko-dev
		
	This also updates layoutdebug.js, which in some ways pretends to be like a browser window but is its own special snowflake. I kept the method naming conventions similar to the main browser window. Depends on D168394 Differential Revision: https://phabricator.services.mozilla.com/D168395
		
			
				
	
	
		
			510 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			510 lines
		
	
	
	
		
			16 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 | 
						|
/* vim: set sts=2 sw=2 et tw=80: */
 | 
						|
/* This Source Code Form is subject to the terms of the Mozilla Public
 | 
						|
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 | 
						|
 * You can obtain one at http://mozilla.org/MPL/2.0/. */
 | 
						|
 | 
						|
"use strict";
 | 
						|
 | 
						|
/**
 | 
						|
 * This module provides helpers used by the other specialized `ext-devtools-*.js` modules
 | 
						|
 * and the implementation of the `devtools_page`.
 | 
						|
 */
 | 
						|
 | 
						|
ChromeUtils.defineESModuleGetters(this, {
 | 
						|
  DevToolsShim: "chrome://devtools-startup/content/DevToolsShim.sys.mjs",
 | 
						|
});
 | 
						|
 | 
						|
var { ExtensionParent } = ChromeUtils.import(
 | 
						|
  "resource://gre/modules/ExtensionParent.jsm"
 | 
						|
);
 | 
						|
 | 
						|
var { HiddenExtensionPage, watchExtensionProxyContextLoad } = ExtensionParent;
 | 
						|
 | 
						|
// Get the devtools preference given the extension id.
 | 
						|
function getDevToolsPrefBranchName(extensionId) {
 | 
						|
  return `devtools.webextensions.${extensionId}`;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Retrieve the tabId for the given devtools toolbox.
 | 
						|
 *
 | 
						|
 * @param {Toolbox} toolbox
 | 
						|
 *   A devtools toolbox instance.
 | 
						|
 *
 | 
						|
 * @returns {number}
 | 
						|
 *   The corresponding WebExtensions tabId.
 | 
						|
 */
 | 
						|
global.getTargetTabIdForToolbox = toolbox => {
 | 
						|
  let { descriptorFront } = toolbox.commands;
 | 
						|
 | 
						|
  if (!descriptorFront.isLocalTab) {
 | 
						|
    throw new Error(
 | 
						|
      "Unexpected target type: only local tabs are currently supported."
 | 
						|
    );
 | 
						|
  }
 | 
						|
 | 
						|
  let parentWindow = descriptorFront.localTab.linkedBrowser.ownerGlobal;
 | 
						|
  let tab = parentWindow.gBrowser.getTabForBrowser(
 | 
						|
    descriptorFront.localTab.linkedBrowser
 | 
						|
  );
 | 
						|
 | 
						|
  return tabTracker.getId(tab);
 | 
						|
};
 | 
						|
 | 
						|
// Get the WebExtensionInspectedWindowActor eval options (needed to provide the $0 and inspect
 | 
						|
// binding provided to the evaluated js code).
 | 
						|
global.getToolboxEvalOptions = async function(context) {
 | 
						|
  const options = {};
 | 
						|
  const toolbox = context.devToolsToolbox;
 | 
						|
  const selectedNode = toolbox.selection;
 | 
						|
 | 
						|
  if (selectedNode && selectedNode.nodeFront) {
 | 
						|
    // If there is a selected node in the inspector, we hand over
 | 
						|
    // its actor id to the eval request in order to provide the "$0" binding.
 | 
						|
    options.toolboxSelectedNodeActorID = selectedNode.nodeFront.actorID;
 | 
						|
  }
 | 
						|
 | 
						|
  // Provide the console actor ID to implement the "inspect" binding.
 | 
						|
  const consoleFront = await toolbox.target.getFront("console");
 | 
						|
  options.toolboxConsoleActorID = consoleFront.actor;
 | 
						|
 | 
						|
  return options;
 | 
						|
};
 | 
						|
 | 
						|
/**
 | 
						|
 * The DevToolsPage represents the "devtools_page" related to a particular
 | 
						|
 * Toolbox and WebExtension.
 | 
						|
 *
 | 
						|
 * The devtools_page contexts are invisible WebExtensions contexts, similar to the
 | 
						|
 * background page, associated to a single developer toolbox (e.g. If an add-on
 | 
						|
 * registers a devtools_page and the user opens 3 developer toolbox in 3 webpages,
 | 
						|
 * 3 devtools_page contexts will be created for that add-on).
 | 
						|
 *
 | 
						|
 * @param {Extension}              extension
 | 
						|
 *   The extension that owns the devtools_page.
 | 
						|
 * @param {object}                 options
 | 
						|
 * @param {Toolbox}                options.toolbox
 | 
						|
 *   The developer toolbox instance related to this devtools_page.
 | 
						|
 * @param {string}                 options.url
 | 
						|
 *   The path to the devtools page html page relative to the extension base URL.
 | 
						|
 * @param {DevToolsPageDefinition} options.devToolsPageDefinition
 | 
						|
 *   The instance of the devToolsPageDefinition class related to this DevToolsPage.
 | 
						|
 */
 | 
						|
class DevToolsPage extends HiddenExtensionPage {
 | 
						|
  constructor(extension, options) {
 | 
						|
    super(extension, "devtools_page");
 | 
						|
 | 
						|
    this.url = extension.baseURI.resolve(options.url);
 | 
						|
    this.toolbox = options.toolbox;
 | 
						|
    this.devToolsPageDefinition = options.devToolsPageDefinition;
 | 
						|
 | 
						|
    this.unwatchExtensionProxyContextLoad = null;
 | 
						|
 | 
						|
    this.waitForTopLevelContext = new Promise(resolve => {
 | 
						|
      this.resolveTopLevelContext = resolve;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  async build() {
 | 
						|
    await this.createBrowserElement();
 | 
						|
 | 
						|
    // Listening to new proxy contexts.
 | 
						|
    this.unwatchExtensionProxyContextLoad = watchExtensionProxyContextLoad(
 | 
						|
      this,
 | 
						|
      context => {
 | 
						|
        // Keep track of the toolbox and target associated to the context, which is
 | 
						|
        // needed by the API methods implementation.
 | 
						|
        context.devToolsToolbox = this.toolbox;
 | 
						|
 | 
						|
        if (!this.topLevelContext) {
 | 
						|
          this.topLevelContext = context;
 | 
						|
 | 
						|
          // Ensure this devtools page is destroyed, when the top level context proxy is
 | 
						|
          // closed.
 | 
						|
          this.topLevelContext.callOnClose(this);
 | 
						|
 | 
						|
          this.resolveTopLevelContext(context);
 | 
						|
        }
 | 
						|
      }
 | 
						|
    );
 | 
						|
 | 
						|
    extensions.emit("extension-browser-inserted", this.browser, {
 | 
						|
      devtoolsToolboxInfo: {
 | 
						|
        inspectedWindowTabId: getTargetTabIdForToolbox(this.toolbox),
 | 
						|
        themeName: DevToolsShim.getTheme(),
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    this.browser.fixupAndLoadURIString(this.url, {
 | 
						|
      triggeringPrincipal: this.extension.principal,
 | 
						|
    });
 | 
						|
 | 
						|
    await this.waitForTopLevelContext;
 | 
						|
  }
 | 
						|
 | 
						|
  close() {
 | 
						|
    if (this.closed) {
 | 
						|
      throw new Error("Unable to shutdown a closed DevToolsPage instance");
 | 
						|
    }
 | 
						|
 | 
						|
    this.closed = true;
 | 
						|
 | 
						|
    // Unregister the devtools page instance from the devtools page definition.
 | 
						|
    this.devToolsPageDefinition.forgetForToolbox(this.toolbox);
 | 
						|
 | 
						|
    // Unregister it from the resources to cleanup when the context has been closed.
 | 
						|
    if (this.topLevelContext) {
 | 
						|
      this.topLevelContext.forgetOnClose(this);
 | 
						|
    }
 | 
						|
 | 
						|
    // Stop watching for any new proxy contexts from the devtools page.
 | 
						|
    if (this.unwatchExtensionProxyContextLoad) {
 | 
						|
      this.unwatchExtensionProxyContextLoad();
 | 
						|
      this.unwatchExtensionProxyContextLoad = null;
 | 
						|
    }
 | 
						|
 | 
						|
    super.shutdown();
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * The DevToolsPageDefinitions class represents the "devtools_page" manifest property
 | 
						|
 * of a WebExtension.
 | 
						|
 *
 | 
						|
 * A DevToolsPageDefinition instance is created automatically when a WebExtension
 | 
						|
 * which contains the "devtools_page" manifest property has been loaded, and it is
 | 
						|
 * automatically destroyed when the related WebExtension has been unloaded,
 | 
						|
 * and so there will be at most one DevtoolsPageDefinition per add-on.
 | 
						|
 *
 | 
						|
 * Every time a developer tools toolbox is opened, the DevToolsPageDefinition creates
 | 
						|
 * and keep track of a DevToolsPage instance (which represents the actual devtools_page
 | 
						|
 * instance related to that particular toolbox).
 | 
						|
 *
 | 
						|
 * @param {Extension} extension
 | 
						|
 *   The extension that owns the devtools_page.
 | 
						|
 * @param {string}    url
 | 
						|
 *   The path to the devtools page html page relative to the extension base URL.
 | 
						|
 */
 | 
						|
class DevToolsPageDefinition {
 | 
						|
  constructor(extension, url) {
 | 
						|
    this.url = url;
 | 
						|
    this.extension = extension;
 | 
						|
 | 
						|
    // Map[Toolbox -> DevToolsPage]
 | 
						|
    this.devtoolsPageForToolbox = new Map();
 | 
						|
  }
 | 
						|
 | 
						|
  onThemeChanged(themeName) {
 | 
						|
    Services.ppmm.broadcastAsyncMessage("Extension:DevToolsThemeChanged", {
 | 
						|
      themeName,
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  buildForToolbox(toolbox) {
 | 
						|
    if (
 | 
						|
      !this.extension.canAccessWindow(
 | 
						|
        toolbox.commands.descriptorFront.localTab.ownerGlobal
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      // We should never create a devtools page for a toolbox related to a private browsing window
 | 
						|
      // if the extension is not allowed to access it.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.devtoolsPageForToolbox.has(toolbox)) {
 | 
						|
      return Promise.reject(
 | 
						|
        new Error("DevtoolsPage has been already created for this toolbox")
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    const devtoolsPage = new DevToolsPage(this.extension, {
 | 
						|
      toolbox,
 | 
						|
      url: this.url,
 | 
						|
      devToolsPageDefinition: this,
 | 
						|
    });
 | 
						|
 | 
						|
    // If this is the first DevToolsPage, subscribe to the theme-changed event
 | 
						|
    if (this.devtoolsPageForToolbox.size === 0) {
 | 
						|
      DevToolsShim.on("theme-changed", this.onThemeChanged);
 | 
						|
    }
 | 
						|
    this.devtoolsPageForToolbox.set(toolbox, devtoolsPage);
 | 
						|
 | 
						|
    return devtoolsPage.build();
 | 
						|
  }
 | 
						|
 | 
						|
  shutdownForToolbox(toolbox) {
 | 
						|
    if (this.devtoolsPageForToolbox.has(toolbox)) {
 | 
						|
      const devtoolsPage = this.devtoolsPageForToolbox.get(toolbox);
 | 
						|
      devtoolsPage.close();
 | 
						|
 | 
						|
      // `devtoolsPage.close()` should remove the instance from the map,
 | 
						|
      // raise an exception if it is still there.
 | 
						|
      if (this.devtoolsPageForToolbox.has(toolbox)) {
 | 
						|
        throw new Error(
 | 
						|
          `Leaked DevToolsPage instance for target "${toolbox.commands.descriptorFront.url}", extension "${this.extension.policy.debugName}"`
 | 
						|
        );
 | 
						|
      }
 | 
						|
 | 
						|
      // If this was the last DevToolsPage, unsubscribe from the theme-changed event
 | 
						|
      if (this.devtoolsPageForToolbox.size === 0) {
 | 
						|
        DevToolsShim.off("theme-changed", this.onThemeChanged);
 | 
						|
      }
 | 
						|
      this.extension.emit("devtools-page-shutdown", toolbox);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  forgetForToolbox(toolbox) {
 | 
						|
    this.devtoolsPageForToolbox.delete(toolbox);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Build the devtools_page instances for all the existing toolboxes
 | 
						|
   * (if the toolbox target is supported).
 | 
						|
   */
 | 
						|
  build() {
 | 
						|
    // Iterate over the existing toolboxes and create the devtools page for them
 | 
						|
    // (if the toolbox target is supported).
 | 
						|
    for (let toolbox of DevToolsShim.getToolboxes()) {
 | 
						|
      if (
 | 
						|
        !toolbox.commands.descriptorFront.isLocalTab ||
 | 
						|
        !this.extension.canAccessWindow(
 | 
						|
          toolbox.commands.descriptorFront.localTab.ownerGlobal
 | 
						|
        )
 | 
						|
      ) {
 | 
						|
        // Skip any non-local tab and private browsing windows if the extension
 | 
						|
        // is not allowed to access them.
 | 
						|
        continue;
 | 
						|
      }
 | 
						|
 | 
						|
      // Ensure that the WebExtension is listed in the toolbox options.
 | 
						|
      toolbox.registerWebExtension(this.extension.uuid, {
 | 
						|
        name: this.extension.name,
 | 
						|
        pref: `${getDevToolsPrefBranchName(this.extension.id)}.enabled`,
 | 
						|
      });
 | 
						|
 | 
						|
      this.buildForToolbox(toolbox);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Shutdown all the devtools_page instances.
 | 
						|
   */
 | 
						|
  shutdown() {
 | 
						|
    for (let toolbox of this.devtoolsPageForToolbox.keys()) {
 | 
						|
      this.shutdownForToolbox(toolbox);
 | 
						|
    }
 | 
						|
 | 
						|
    if (this.devtoolsPageForToolbox.size > 0) {
 | 
						|
      throw new Error(
 | 
						|
        `Leaked ${this.devtoolsPageForToolbox.size} DevToolsPage instances in devtoolsPageForToolbox Map`
 | 
						|
      );
 | 
						|
    }
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
this.devtools = class extends ExtensionAPI {
 | 
						|
  constructor(extension) {
 | 
						|
    super(extension);
 | 
						|
 | 
						|
    this._initialized = false;
 | 
						|
 | 
						|
    // DevToolsPageDefinition instance (created in onManifestEntry).
 | 
						|
    this.pageDefinition = null;
 | 
						|
 | 
						|
    this.onToolboxReady = this.onToolboxReady.bind(this);
 | 
						|
    this.onToolboxDestroy = this.onToolboxDestroy.bind(this);
 | 
						|
 | 
						|
    /* eslint-disable mozilla/balanced-listeners */
 | 
						|
    extension.on("add-permissions", (ignoreEvent, permissions) => {
 | 
						|
      if (permissions.permissions.includes("devtools")) {
 | 
						|
        Services.prefs.setBoolPref(
 | 
						|
          `${getDevToolsPrefBranchName(extension.id)}.enabled`,
 | 
						|
          true
 | 
						|
        );
 | 
						|
 | 
						|
        this._initialize();
 | 
						|
      }
 | 
						|
    });
 | 
						|
 | 
						|
    extension.on("remove-permissions", (ignoreEvent, permissions) => {
 | 
						|
      if (permissions.permissions.includes("devtools")) {
 | 
						|
        Services.prefs.setBoolPref(
 | 
						|
          `${getDevToolsPrefBranchName(extension.id)}.enabled`,
 | 
						|
          false
 | 
						|
        );
 | 
						|
 | 
						|
        this._uninitialize();
 | 
						|
      }
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  onManifestEntry() {
 | 
						|
    this._initialize();
 | 
						|
  }
 | 
						|
 | 
						|
  static onUninstall(extensionId) {
 | 
						|
    // Remove the preference branch on uninstall.
 | 
						|
    const prefBranch = Services.prefs.getBranch(
 | 
						|
      `${getDevToolsPrefBranchName(extensionId)}.`
 | 
						|
    );
 | 
						|
 | 
						|
    prefBranch.deleteBranch("");
 | 
						|
  }
 | 
						|
 | 
						|
  _initialize() {
 | 
						|
    const { extension } = this;
 | 
						|
 | 
						|
    if (!extension.hasPermission("devtools") || this._initialized) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.initDevToolsPref();
 | 
						|
 | 
						|
    // Create the devtools_page definition.
 | 
						|
    this.pageDefinition = new DevToolsPageDefinition(
 | 
						|
      extension,
 | 
						|
      extension.manifest.devtools_page
 | 
						|
    );
 | 
						|
 | 
						|
    // Build the extension devtools_page on all existing toolboxes (if the extension
 | 
						|
    // devtools_page is not disabled by the related preference).
 | 
						|
    if (!this.isDevToolsPageDisabled()) {
 | 
						|
      this.pageDefinition.build();
 | 
						|
    }
 | 
						|
 | 
						|
    DevToolsShim.on("toolbox-ready", this.onToolboxReady);
 | 
						|
    DevToolsShim.on("toolbox-destroy", this.onToolboxDestroy);
 | 
						|
    this._initialized = true;
 | 
						|
  }
 | 
						|
 | 
						|
  _uninitialize() {
 | 
						|
    // devtoolsPrefBranch is set in onManifestEntry, and nullified
 | 
						|
    // later in onShutdown.  If it isn't set, then onManifestEntry
 | 
						|
    // did not initialize devtools for the extension.
 | 
						|
    if (!this._initialized) {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    DevToolsShim.off("toolbox-ready", this.onToolboxReady);
 | 
						|
    DevToolsShim.off("toolbox-destroy", this.onToolboxDestroy);
 | 
						|
 | 
						|
    // Shutdown the extension devtools_page from all existing toolboxes.
 | 
						|
    this.pageDefinition.shutdown();
 | 
						|
    this.pageDefinition = null;
 | 
						|
 | 
						|
    // Iterate over the existing toolboxes and unlist the devtools webextension from them.
 | 
						|
    for (let toolbox of DevToolsShim.getToolboxes()) {
 | 
						|
      toolbox.unregisterWebExtension(this.extension.uuid);
 | 
						|
    }
 | 
						|
 | 
						|
    this.uninitDevToolsPref();
 | 
						|
    this._initialized = false;
 | 
						|
  }
 | 
						|
 | 
						|
  onShutdown() {
 | 
						|
    this._uninitialize();
 | 
						|
  }
 | 
						|
 | 
						|
  getAPI(context) {
 | 
						|
    return {
 | 
						|
      devtools: {},
 | 
						|
    };
 | 
						|
  }
 | 
						|
 | 
						|
  onToolboxReady(toolbox) {
 | 
						|
    if (
 | 
						|
      !toolbox.commands.descriptorFront.isLocalTab ||
 | 
						|
      !this.extension.canAccessWindow(
 | 
						|
        toolbox.commands.descriptorFront.localTab.ownerGlobal
 | 
						|
      )
 | 
						|
    ) {
 | 
						|
      // Skip any non-local (as remote tabs are not yet supported, see Bug 1304378 for additional details
 | 
						|
      // related to remote targets support), and private browsing windows if the extension
 | 
						|
      // is not allowed to access them.
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Ensure that the WebExtension is listed in the toolbox options.
 | 
						|
    toolbox.registerWebExtension(this.extension.uuid, {
 | 
						|
      name: this.extension.name,
 | 
						|
      pref: `${getDevToolsPrefBranchName(this.extension.id)}.enabled`,
 | 
						|
    });
 | 
						|
 | 
						|
    // Do not build the devtools page if the extension has been disabled
 | 
						|
    // (e.g. based on the devtools preference).
 | 
						|
    if (toolbox.isWebExtensionEnabled(this.extension.uuid)) {
 | 
						|
      this.pageDefinition.buildForToolbox(toolbox);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  onToolboxDestroy(toolbox) {
 | 
						|
    if (!toolbox.commands.descriptorFront.isLocalTab) {
 | 
						|
      // Only local tabs are currently supported (See Bug 1304378 for additional details
 | 
						|
      // related to remote targets support).
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    this.pageDefinition.shutdownForToolbox(toolbox);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Initialize the DevTools preferences branch for the extension and
 | 
						|
   * start to observe it for changes on the "enabled" preference.
 | 
						|
   */
 | 
						|
  initDevToolsPref() {
 | 
						|
    const prefBranch = Services.prefs.getBranch(
 | 
						|
      `${getDevToolsPrefBranchName(this.extension.id)}.`
 | 
						|
    );
 | 
						|
 | 
						|
    // Initialize the devtools extension preference if it doesn't exist yet.
 | 
						|
    if (prefBranch.getPrefType("enabled") === prefBranch.PREF_INVALID) {
 | 
						|
      prefBranch.setBoolPref("enabled", true);
 | 
						|
    }
 | 
						|
 | 
						|
    this.devtoolsPrefBranch = prefBranch;
 | 
						|
    this.devtoolsPrefBranch.addObserver("enabled", this);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Stop from observing the DevTools preferences branch for the extension.
 | 
						|
   */
 | 
						|
  uninitDevToolsPref() {
 | 
						|
    this.devtoolsPrefBranch.removeObserver("enabled", this);
 | 
						|
    this.devtoolsPrefBranch = null;
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Test if the extension's devtools_page has been disabled using the
 | 
						|
   * DevTools preference.
 | 
						|
   *
 | 
						|
   * @returns {boolean}
 | 
						|
   *          true if the devtools_page for this extension is disabled.
 | 
						|
   */
 | 
						|
  isDevToolsPageDisabled() {
 | 
						|
    return !this.devtoolsPrefBranch.getBoolPref("enabled", false);
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Observes the changed preferences on the DevTools preferences branch
 | 
						|
   * related to the extension.
 | 
						|
   *
 | 
						|
   * @param {nsIPrefBranch} subject  The observed preferences branch.
 | 
						|
   * @param {string}        topic    The notified topic.
 | 
						|
   * @param {string}        prefName The changed preference name.
 | 
						|
   */
 | 
						|
  observe(subject, topic, prefName) {
 | 
						|
    // We are currently interested only in the "enabled" preference from the
 | 
						|
    // WebExtension devtools preferences branch.
 | 
						|
    if (subject !== this.devtoolsPrefBranch || prefName !== "enabled") {
 | 
						|
      return;
 | 
						|
    }
 | 
						|
 | 
						|
    // Shutdown or build the devtools_page on any existing toolbox.
 | 
						|
    if (this.isDevToolsPageDisabled()) {
 | 
						|
      this.pageDefinition.shutdown();
 | 
						|
    } else {
 | 
						|
      this.pageDefinition.build();
 | 
						|
    }
 | 
						|
  }
 | 
						|
};
 |