| /* | 
 |  * 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())); | 
 |   } | 
 | }; |