blob: 61f6cbf431d123275e767c8954663ae598c5b308 [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * 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.
*/
/**
* @implements {SDK.TargetManager.Observer}
* @unrestricted
*/
Components.Linkifier = class {
/**
* @param {number=} maxLengthForDisplayedURLs
* @param {boolean=} useLinkDecorator
*/
constructor(maxLengthForDisplayedURLs, useLinkDecorator) {
this._maxLength = maxLengthForDisplayedURLs || UI.MaxLengthForDisplayedURLs;
/** @type {!Map<!SDK.Target, !Array<!Element>>} */
this._anchorsByTarget = new Map();
/** @type {!Map<!SDK.Target, !Bindings.LiveLocationPool>} */
this._locationPoolByTarget = new Map();
this._useLinkDecorator = !!useLinkDecorator;
Components.Linkifier._instances.add(this);
SDK.targetManager.observeTargets(this);
}
/**
* @param {!Components.LinkDecorator} decorator
*/
static setLinkDecorator(decorator) {
console.assert(!Components.Linkifier._decorator, 'Cannot re-register link decorator.');
Components.Linkifier._decorator = decorator;
decorator.addEventListener(Components.LinkDecorator.Events.LinkIconChanged, onLinkIconChanged);
for (const linkifier of Components.Linkifier._instances)
linkifier._updateAllAnchorDecorations();
/**
* @param {!Common.Event} event
*/
function onLinkIconChanged(event) {
const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data);
const links = uiSourceCode[Components.Linkifier._sourceCodeAnchors] || [];
for (const link of links)
Components.Linkifier._updateLinkDecorations(link);
}
}
_updateAllAnchorDecorations() {
for (const anchors of this._anchorsByTarget.values()) {
for (const anchor of anchors)
Components.Linkifier._updateLinkDecorations(anchor);
}
}
/**
* @param {!Element} anchor
* @param {!Workspace.UILocation} uiLocation
*/
static _bindUILocation(anchor, uiLocation) {
Components.Linkifier._linkInfo(anchor).uiLocation = uiLocation;
if (!uiLocation)
return;
const uiSourceCode = uiLocation.uiSourceCode;
let sourceCodeAnchors = uiSourceCode[Components.Linkifier._sourceCodeAnchors];
if (!sourceCodeAnchors) {
sourceCodeAnchors = new Set();
uiSourceCode[Components.Linkifier._sourceCodeAnchors] = sourceCodeAnchors;
}
sourceCodeAnchors.add(anchor);
}
/**
* @param {!Element} anchor
*/
static _unbindUILocation(anchor) {
const info = Components.Linkifier._linkInfo(anchor);
if (!info.uiLocation)
return;
const uiSourceCode = info.uiLocation.uiSourceCode;
info.uiLocation = null;
const sourceCodeAnchors = uiSourceCode[Components.Linkifier._sourceCodeAnchors];
if (sourceCodeAnchors)
sourceCodeAnchors.delete(anchor);
}
/**
* @override
* @param {!SDK.Target} target
*/
targetAdded(target) {
this._anchorsByTarget.set(target, []);
this._locationPoolByTarget.set(target, new Bindings.LiveLocationPool());
}
/**
* @override
* @param {!SDK.Target} target
*/
targetRemoved(target) {
const locationPool = /** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.remove(target));
locationPool.disposeAll();
const anchors = this._anchorsByTarget.remove(target);
for (const anchor of anchors) {
const info = Components.Linkifier._linkInfo(anchor);
info.liveLocation = null;
Components.Linkifier._unbindUILocation(anchor);
if (info.fallback) {
anchor.href = info.fallback.href;
anchor.title = info.fallback.title;
anchor.className = info.fallback.className;
anchor.textContent = info.fallback.textContent;
anchor[Components.Linkifier._infoSymbol] = info.fallback[Components.Linkifier._infoSymbol];
}
}
}
/**
* @param {?SDK.Target} target
* @param {?string} scriptId
* @param {string} sourceURL
* @param {number} lineNumber
* @param {number=} columnNumber
* @param {string=} classes
* @return {?Element}
*/
maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes) {
let fallbackAnchor = null;
if (sourceURL) {
fallbackAnchor = Components.Linkifier.linkifyURL(
sourceURL,
{className: classes, lineNumber: lineNumber, columnNumber: columnNumber, maxLength: this._maxLength});
}
if (!target || target.isDisposed())
return fallbackAnchor;
const debuggerModel = target.model(SDK.DebuggerModel);
if (!debuggerModel)
return fallbackAnchor;
const rawLocation =
(scriptId ? debuggerModel.createRawLocationByScriptId(scriptId, lineNumber, columnNumber || 0) : null) ||
debuggerModel.createRawLocationByURL(sourceURL, lineNumber, columnNumber || 0);
if (!rawLocation)
return fallbackAnchor;
const anchor = Components.Linkifier._createLink('', classes || '');
const info = Components.Linkifier._linkInfo(anchor);
info.enableDecorator = this._useLinkDecorator;
info.fallback = fallbackAnchor;
info.liveLocation = Bindings.debuggerWorkspaceBinding.createLiveLocation(
rawLocation, this._updateAnchor.bind(this, anchor),
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(rawLocation.debuggerModel.target())));
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.debuggerModel.target()));
anchors.push(anchor);
return anchor;
}
/**
* @param {?SDK.Target} target
* @param {?string} scriptId
* @param {string} sourceURL
* @param {number} lineNumber
* @param {number=} columnNumber
* @param {string=} classes
* @return {!Element}
*/
linkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes) {
const scriptLink = this.maybeLinkifyScriptLocation(target, scriptId, sourceURL, lineNumber, columnNumber, classes);
return scriptLink ||
Components.Linkifier.linkifyURL(
sourceURL,
{className: classes, lineNumber: lineNumber, columnNumber: columnNumber, maxLength: this._maxLength});
}
/**
* @param {!SDK.DebuggerModel.Location} rawLocation
* @param {string} fallbackUrl
* @param {string=} classes
* @return {!Element}
*/
linkifyRawLocation(rawLocation, fallbackUrl, classes) {
return this.linkifyScriptLocation(
rawLocation.debuggerModel.target(), rawLocation.scriptId, fallbackUrl, rawLocation.lineNumber,
rawLocation.columnNumber, classes);
}
/**
* @param {?SDK.Target} target
* @param {!Protocol.Runtime.CallFrame} callFrame
* @param {string=} classes
* @return {?Element}
*/
maybeLinkifyConsoleCallFrame(target, callFrame, classes) {
return this.maybeLinkifyScriptLocation(
target, callFrame.scriptId, callFrame.url, callFrame.lineNumber, callFrame.columnNumber, classes);
}
/**
* @param {!SDK.Target} target
* @param {!Protocol.Runtime.StackTrace} stackTrace
* @param {string=} classes
* @return {!Element}
*/
linkifyStackTraceTopFrame(target, stackTrace, classes) {
console.assert(stackTrace.callFrames && stackTrace.callFrames.length);
const topFrame = stackTrace.callFrames[0];
const fallbackAnchor = Components.Linkifier.linkifyURL(topFrame.url, {
className: classes,
lineNumber: topFrame.lineNumber,
columnNumber: topFrame.columnNumber,
maxLength: this._maxLength
});
if (target.isDisposed())
return fallbackAnchor;
const debuggerModel = target.model(SDK.DebuggerModel);
const rawLocations = debuggerModel.createRawLocationsByStackTrace(stackTrace);
if (rawLocations.length === 0)
return fallbackAnchor;
const anchor = Components.Linkifier._createLink('', classes || '');
const info = Components.Linkifier._linkInfo(anchor);
info.enableDecorator = this._useLinkDecorator;
info.fallback = fallbackAnchor;
info.liveLocation = Bindings.debuggerWorkspaceBinding.createStackTraceTopFrameLiveLocation(
rawLocations, this._updateAnchor.bind(this, anchor),
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(target)));
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(target));
anchors.push(anchor);
return anchor;
}
/**
* @param {!SDK.CSSLocation} rawLocation
* @param {string=} classes
* @return {!Element}
*/
linkifyCSSLocation(rawLocation, classes) {
const anchor = Components.Linkifier._createLink('', classes || '');
const info = Components.Linkifier._linkInfo(anchor);
info.enableDecorator = this._useLinkDecorator;
info.liveLocation = Bindings.cssWorkspaceBinding.createLiveLocation(
rawLocation, this._updateAnchor.bind(this, anchor),
/** @type {!Bindings.LiveLocationPool} */ (this._locationPoolByTarget.get(rawLocation.cssModel().target())));
const anchors = /** @type {!Array<!Element>} */ (this._anchorsByTarget.get(rawLocation.cssModel().target()));
anchors.push(anchor);
return anchor;
}
reset() {
for (const target of this._anchorsByTarget.keysArray()) {
this.targetRemoved(target);
this.targetAdded(target);
}
}
dispose() {
for (const target of this._anchorsByTarget.keysArray())
this.targetRemoved(target);
SDK.targetManager.unobserveTargets(this);
Components.Linkifier._instances.delete(this);
}
/**
* @param {!Element} anchor
* @param {!Bindings.LiveLocation} liveLocation
*/
_updateAnchor(anchor, liveLocation) {
Components.Linkifier._unbindUILocation(anchor);
const uiLocation = liveLocation.uiLocation();
if (!uiLocation)
return;
Components.Linkifier._bindUILocation(anchor, uiLocation);
const text = uiLocation.linkText(true /* skipTrim */);
Components.Linkifier._setTrimmedText(anchor, text, this._maxLength);
let titleText = uiLocation.uiSourceCode.url();
if (typeof uiLocation.lineNumber === 'number')
titleText += ':' + (uiLocation.lineNumber + 1);
anchor.title = titleText;
anchor.classList.toggle('webkit-html-blackbox-link', liveLocation.isBlackboxed());
Components.Linkifier._updateLinkDecorations(anchor);
}
/**
* @param {!Element} anchor
*/
static _updateLinkDecorations(anchor) {
const info = Components.Linkifier._linkInfo(anchor);
if (!info || !info.enableDecorator)
return;
if (!Components.Linkifier._decorator || !info.uiLocation)
return;
if (info.icon && info.icon.parentElement)
anchor.removeChild(info.icon);
const icon = Components.Linkifier._decorator.linkIcon(info.uiLocation.uiSourceCode);
if (icon) {
icon.style.setProperty('margin-right', '2px');
anchor.insertBefore(icon, anchor.firstChild);
}
info.icon = icon;
}
/**
* @param {string} url
* @param {!Components.LinkifyURLOptions=} options
* @return {!Element}
*/
static linkifyURL(url, options) {
options = options || {};
const text = options.text;
const className = options.className || '';
const lineNumber = options.lineNumber;
const columnNumber = options.columnNumber;
const preventClick = options.preventClick;
const maxLength = options.maxLength || UI.MaxLengthForDisplayedURLs;
if (!url || url.trim().toLowerCase().startsWith('javascript:')) {
const element = createElementWithClass('span', className);
element.textContent = text || url || Common.UIString('(unknown)');
return element;
}
let linkText = text || Bindings.displayNameForURL(url);
if (typeof lineNumber === 'number' && !text)
linkText += ':' + (lineNumber + 1);
const title = linkText !== url ? url : '';
const link = Components.Linkifier._createLink(linkText, className, maxLength, title, url, preventClick);
const info = Components.Linkifier._linkInfo(link);
if (typeof lineNumber === 'number')
info.lineNumber = lineNumber;
if (typeof columnNumber === 'number')
info.columnNumber = columnNumber;
return link;
}
/**
* @param {!Object} revealable
* @param {string} text
* @param {string=} fallbackHref
* @return {!Element}
*/
static linkifyRevealable(revealable, text, fallbackHref) {
const link = Components.Linkifier._createLink(text, '', UI.MaxLengthForDisplayedURLs, undefined, fallbackHref);
Components.Linkifier._linkInfo(link).revealable = revealable;
return link;
}
/**
* @param {string} text
* @param {string} className
* @param {number=} maxLength
* @param {string=} title
* @param {string=} href
* @param {boolean=} preventClick
* @returns{!Element}
*/
static _createLink(text, className, maxLength, title, href, preventClick) {
const link = createElementWithClass('span', className);
link.classList.add('devtools-link');
if (title)
link.title = title;
if (href)
link.href = href;
Components.Linkifier._setTrimmedText(link, text, maxLength);
link[Components.Linkifier._infoSymbol] = {
icon: null,
enableDecorator: false,
uiLocation: null,
liveLocation: null,
url: href || null,
lineNumber: null,
columnNumber: null,
revealable: null,
fallback: null
};
if (!preventClick)
link.addEventListener('click', Components.Linkifier._handleClick, false);
else
link.classList.add('devtools-link-prevent-click');
return link;
}
/**
* @param {!Element} link
* @param {string} text
* @param {number=} maxLength
*/
static _setTrimmedText(link, text, maxLength) {
link.removeChildren();
if (maxLength && text.length > maxLength) {
const middleSplit = splitMiddle(text, maxLength);
appendTextWithoutHashes(middleSplit[0]);
appendHiddenText(middleSplit[1]);
appendTextWithoutHashes(middleSplit[2]);
} else {
appendTextWithoutHashes(text);
}
/**
* @param {string} string
*/
function appendHiddenText(string) {
const ellipsisNode = link.createChild('span', 'devtools-link-ellipsis').createTextChild('\u2026');
ellipsisNode[Components.Linkifier._untruncatedNodeTextSymbol] = string;
}
/**
* @param {string} string
*/
function appendTextWithoutHashes(string) {
const hashSplit = TextUtils.TextUtils.splitStringByRegexes(string, [/[a-f0-9]{20,}/g]);
for (const match of hashSplit) {
if (match.regexIndex === -1) {
link.createTextChild(match.value);
} else {
link.createTextChild(match.value.substring(0, 7));
appendHiddenText(match.value.substring(7));
}
}
}
/**
* @param {string} string
* @param {number} maxLength
* @return {!Array<string>}
*/
function splitMiddle(string, maxLength) {
let leftIndex = Math.floor(maxLength / 2);
let rightIndex = string.length - Math.ceil(maxLength / 2) + 1;
// Do not truncate between characters that use multiple code points (emojis).
if (string.codePointAt(rightIndex - 1) >= 0x10000) {
rightIndex++;
leftIndex++;
}
if (leftIndex > 0 && string.codePointAt(leftIndex - 1) >= 0x10000)
leftIndex--;
return [string.substring(0, leftIndex), string.substring(leftIndex, rightIndex), string.substring(rightIndex)];
}
}
/**
* @param {!Node} node
* @return {string}
*/
static untruncatedNodeText(node) {
return node[Components.Linkifier._untruncatedNodeTextSymbol] || node.textContent;
}
/**
* @param {?Element} link
* @return {?Components._LinkInfo}
*/
static _linkInfo(link) {
return /** @type {?Components._LinkInfo} */ (link ? link[Components.Linkifier._infoSymbol] || null : null);
}
/**
* @param {!Event} event
*/
static _handleClick(event) {
const link = /** @type {!Element} */ (event.currentTarget);
event.consume(true);
if (UI.isBeingEdited(/** @type {!Node} */ (event.target)) || link.hasSelection())
return;
const actions = Components.Linkifier._linkActions(link);
if (actions.length)
actions[0].handler.call(null);
}
/**
* @return {!Common.Setting}
*/
static _linkHandlerSetting() {
if (!Components.Linkifier._linkHandlerSettingInstance) {
Components.Linkifier._linkHandlerSettingInstance =
Common.settings.createSetting('openLinkHandler', Common.UIString('auto'));
}
return Components.Linkifier._linkHandlerSettingInstance;
}
/**
* @param {string} title
* @param {!Components.Linkifier.LinkHandler} handler
*/
static registerLinkHandler(title, handler) {
Components.Linkifier._linkHandlers.set(title, handler);
self.runtime.sharedInstance(Components.Linkifier.LinkHandlerSettingUI)._update();
}
/**
* @param {string} title
*/
static unregisterLinkHandler(title) {
Components.Linkifier._linkHandlers.delete(title);
self.runtime.sharedInstance(Components.Linkifier.LinkHandlerSettingUI)._update();
}
/**
* @param {!Element} link
* @return {?Workspace.UILocation}
*/
static uiLocation(link) {
const info = Components.Linkifier._linkInfo(link);
return info ? info.uiLocation : null;
}
/**
* @param {?Element} link
* @return {!Array<{title: string, handler: function()}>}
*/
static _linkActions(link) {
const info = Components.Linkifier._linkInfo(link);
const result = [];
if (!info)
return result;
let url = '';
let uiLocation = null;
if (info.uiLocation) {
uiLocation = info.uiLocation;
url = uiLocation.uiSourceCode.contentURL();
} else if (info.url) {
url = info.url;
const uiSourceCode = Workspace.workspace.uiSourceCodeForURL(url) ||
Workspace.workspace.uiSourceCodeForURL(Common.ParsedURL.urlWithoutHash(url));
uiLocation = uiSourceCode ? uiSourceCode.uiLocation(info.lineNumber || 0, info.columnNumber || 0) : null;
}
const resource = url ? Bindings.resourceForURL(url) : null;
const contentProvider = uiLocation ? uiLocation.uiSourceCode : resource;
const revealable = info.revealable || uiLocation || resource;
if (revealable) {
const destination = Common.Revealer.revealDestination(revealable);
result.push({
section: 'reveal',
title: destination ? ls`Reveal in ${destination}` : ls`Reveal`,
handler: () => Common.Revealer.reveal(revealable)
});
}
if (contentProvider) {
const lineNumber = uiLocation ? uiLocation.lineNumber : info.lineNumber || 0;
for (const title of Components.Linkifier._linkHandlers.keys()) {
const handler = Components.Linkifier._linkHandlers.get(title);
const action = {
section: 'reveal',
title: Common.UIString('Open using %s', title),
handler: handler.bind(null, contentProvider, lineNumber)
};
if (title === Components.Linkifier._linkHandlerSetting().get())
result.unshift(action);
else
result.push(action);
}
}
if (resource || info.url) {
result.push({
section: 'reveal',
title: UI.openLinkExternallyLabel(),
handler: () => InspectorFrontendHost.openInNewTab(url)
});
result.push(
{section: 'clipboard', title: UI.copyLinkAddressLabel(), handler: () => InspectorFrontendHost.copyText(url)});
}
return result;
}
};
/** @type {!Set<!Components.Linkifier>} */
Components.Linkifier._instances = new Set();
/** @type {?Components.LinkDecorator} */
Components.Linkifier._decorator = null;
Components.Linkifier._sourceCodeAnchors = Symbol('Linkifier.anchors');
Components.Linkifier._infoSymbol = Symbol('Linkifier.info');
Components.Linkifier._untruncatedNodeTextSymbol = Symbol('Linkifier.untruncatedNodeText');
/**
* @typedef {{
* icon: ?UI.Icon,
* enableDecorator: boolean,
* uiLocation: ?Workspace.UILocation,
* liveLocation: ?Bindings.LiveLocation,
* url: ?string,
* lineNumber: ?number,
* columnNumber: ?number,
* revealable: ?Object,
* fallback: ?Element
* }}
*/
Components._LinkInfo;
/**
* @typedef {{
* text: (string|undefined),
* className: (string|undefined),
* lineNumber: (number|undefined),
* columnNumber: (number|undefined),
* preventClick: (boolean|undefined),
* maxLength: (number|undefined)
* }}
*/
Components.LinkifyURLOptions;
/**
* The maximum length before strings are considered too long for finding URLs.
* @const
* @type {number}
*/
Components.Linkifier.MaxLengthToIgnoreLinkifier = 10000;
/**
* @typedef {function(!Common.ContentProvider, number)}
*/
Components.Linkifier.LinkHandler;
/** @type {!Map<string, !Components.Linkifier.LinkHandler>} */
Components.Linkifier._linkHandlers = new Map();
/**
* @extends {Common.EventTarget}
* @interface
*/
Components.LinkDecorator = function() {};
Components.LinkDecorator.prototype = {
/**
* @param {!Workspace.UISourceCode} uiSourceCode
* @return {?UI.Icon}
*/
linkIcon(uiSourceCode) {}
};
Components.LinkDecorator.Events = {
LinkIconChanged: Symbol('LinkIconChanged')
};
/**
* @implements {UI.ContextMenu.Provider}
* @unrestricted
*/
Components.Linkifier.LinkContextMenuProvider = class {
/**
* @override
* @param {!Event} event
* @param {!UI.ContextMenu} contextMenu
* @param {!Object} target
*/
appendApplicableItems(event, contextMenu, target) {
let targetNode = /** @type {!Node} */ (target);
while (targetNode && !targetNode[Components.Linkifier._infoSymbol])
targetNode = targetNode.parentNodeOrShadowHost();
const link = /** @type {?Element} */ (targetNode);
const actions = Components.Linkifier._linkActions(link);
for (const action of actions)
contextMenu.section(action.section).appendItem(action.title, action.handler);
}
};
/**
* @implements {UI.SettingUI}
* @unrestricted
*/
Components.Linkifier.LinkHandlerSettingUI = class {
constructor() {
this._element = createElementWithClass('select', 'chrome-select');
this._element.addEventListener('change', this._onChange.bind(this), false);
this._update();
}
_update() {
this._element.removeChildren();
const names = Components.Linkifier._linkHandlers.keysArray();
names.unshift(Common.UIString('auto'));
for (const name of names) {
const option = createElement('option');
option.textContent = name;
option.selected = name === Components.Linkifier._linkHandlerSetting().get();
this._element.appendChild(option);
}
this._element.disabled = names.length <= 1;
}
/**
* @param {!Event} event
*/
_onChange(event) {
const value = event.target.value;
Components.Linkifier._linkHandlerSetting().set(value);
}
/**
* @override
* @return {?Element}
*/
settingElement() {
return UI.SettingsUI.createCustomSetting(Common.UIString('Link handling:'), this._element);
}
};
/**
* @implements {UI.ContextMenu.Provider}
* @unrestricted
*/
Components.Linkifier.ContentProviderContextMenuProvider = class {
/**
* @override
* @param {!Event} event
* @param {!UI.ContextMenu} contextMenu
* @param {!Object} target
*/
appendApplicableItems(event, contextMenu, target) {
const contentProvider = /** @type {!Common.ContentProvider} */ (target);
if (!contentProvider.contentURL())
return;
contextMenu.revealSection().appendItem(
UI.openLinkExternallyLabel(), () => InspectorFrontendHost.openInNewTab(contentProvider.contentURL()));
for (const title of Components.Linkifier._linkHandlers.keys()) {
const handler = Components.Linkifier._linkHandlers.get(title);
contextMenu.revealSection().appendItem(
Common.UIString('Open using %s', title), handler.bind(null, contentProvider, 0));
}
if (contentProvider instanceof SDK.NetworkRequest)
return;
contextMenu.clipboardSection().appendItem(
UI.copyLinkAddressLabel(), () => InspectorFrontendHost.copyText(contentProvider.contentURL()));
}
};