1/*
2 * Copyright (C) 2012 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31/**
32 * @interface
33 */
34WebInspector.LinkifierFormatter = function()
35{
36}
37
38WebInspector.LinkifierFormatter.prototype = {
39    /**
40     * @param {!Element} anchor
41     * @param {!WebInspector.UILocation} uiLocation
42     */
43    formatLiveAnchor: function(anchor, uiLocation) { }
44}
45
46/**
47 * @constructor
48 * @implements {WebInspector.TargetManager.Observer}
49 * @param {!WebInspector.LinkifierFormatter=} formatter
50 */
51WebInspector.Linkifier = function(formatter)
52{
53    this._formatter = formatter || new WebInspector.Linkifier.DefaultFormatter(WebInspector.Linkifier.MaxLengthForDisplayedURLs);
54    /** @type {!Map.<!WebInspector.Target, !Array.<{anchor: !Element, location: !WebInspector.LiveLocation}>>}*/
55    this._liveLocationsByTarget = new Map();
56    WebInspector.targetManager.observeTargets(this);
57}
58
59/**
60 * @param {!WebInspector.Linkifier.LinkHandler} handler
61 */
62WebInspector.Linkifier.setLinkHandler = function(handler)
63{
64    WebInspector.Linkifier._linkHandler = handler;
65}
66
67/**
68 * @param {string} url
69 * @param {number=} lineNumber
70 * @return {boolean}
71 */
72WebInspector.Linkifier.handleLink = function(url, lineNumber)
73{
74    if (!WebInspector.Linkifier._linkHandler)
75        return false;
76    return WebInspector.Linkifier._linkHandler.handleLink(url, lineNumber)
77}
78
79/**
80 * @param {!Object} revealable
81 * @param {string} text
82 * @param {string=} fallbackHref
83 * @param {number=} fallbackLineNumber
84 * @param {string=} title
85 * @param {string=} classes
86 * @return {!Element}
87 */
88WebInspector.Linkifier.linkifyUsingRevealer = function(revealable, text, fallbackHref, fallbackLineNumber, title, classes)
89{
90    var a = document.createElement("a");
91    a.className = (classes || "") + " webkit-html-resource-link";
92    a.textContent = text.trimMiddle(WebInspector.Linkifier.MaxLengthForDisplayedURLs);
93    a.title = title || text;
94    if (fallbackHref) {
95        a.href = fallbackHref;
96        a.lineNumber = fallbackLineNumber;
97    }
98    /**
99     * @param {!Event} event
100     * @this {Object}
101     */
102    function clickHandler(event)
103    {
104        event.stopImmediatePropagation();
105        event.preventDefault();
106        if (fallbackHref && WebInspector.Linkifier.handleLink(fallbackHref, fallbackLineNumber))
107            return;
108
109        WebInspector.Revealer.reveal(this);
110    }
111    a.addEventListener("click", clickHandler.bind(revealable), false);
112    return a;
113}
114
115WebInspector.Linkifier.prototype = {
116    /**
117     * @param {!WebInspector.Target} target
118     */
119    targetAdded: function(target)
120    {
121        this._liveLocationsByTarget.set(target, []);
122    },
123
124    /**
125     * @param {!WebInspector.Target} target
126     */
127    targetRemoved: function(target)
128    {
129        var liveLocations = this._liveLocationsByTarget.remove(target);
130        for (var i = 0; i < liveLocations.length; ++i) {
131            delete liveLocations[i].anchor.__uiLocation;
132            var anchor = liveLocations[i].anchor;
133            if (anchor.__fallbackAnchor) {
134                anchor.href = anchor.__fallbackAnchor.href;
135                anchor.lineNumber = anchor.__fallbackAnchor.lineNumber;
136                anchor.title = anchor.__fallbackAnchor.title;
137                anchor.className = anchor.__fallbackAnchor.className;
138                anchor.textContent = anchor.__fallbackAnchor.textContent;
139            }
140            liveLocations[i].location.dispose();
141        }
142    },
143
144    /**
145     * @param {?WebInspector.Target} target
146     * @param {?string} scriptId
147     * @param {string} sourceURL
148     * @param {number} lineNumber
149     * @param {number=} columnNumber
150     * @param {string=} classes
151     * @return {!Element}
152     */
153    linkifyScriptLocation: function(target, scriptId, sourceURL, lineNumber, columnNumber, classes)
154    {
155        var rawLocation = target && !target.isDetached() ? target.debuggerModel.createRawLocationByScriptId(scriptId, sourceURL, lineNumber, columnNumber || 0) : null;
156        var fallbackAnchor = WebInspector.linkifyResourceAsNode(sourceURL, lineNumber, classes);
157        if (!rawLocation)
158            return fallbackAnchor;
159
160        var anchor = this._createAnchor(classes);
161        var liveLocation = WebInspector.debuggerWorkspaceBinding.createLiveLocation(rawLocation, this._updateAnchor.bind(this, anchor));
162        this._liveLocationsByTarget.get(rawLocation.target()).push({ anchor: anchor, location: liveLocation });
163        anchor.__fallbackAnchor = fallbackAnchor;
164        return anchor;
165    },
166
167    /**
168     * @param {!WebInspector.DebuggerModel.Location} rawLocation
169     * @param {string} fallbackUrl
170     * @param {string=} classes
171     * @return {!Element}
172     */
173    linkifyRawLocation: function(rawLocation, fallbackUrl, classes)
174    {
175        return this.linkifyScriptLocation(rawLocation.target(), rawLocation.scriptId, fallbackUrl, rawLocation.lineNumber, rawLocation.columnNumber, classes);
176    },
177
178    /**
179     * @param {?WebInspector.Target} target
180     * @param {!ConsoleAgent.CallFrame} callFrame
181     * @param {string=} classes
182     * @return {!Element}
183     */
184    linkifyConsoleCallFrame: function(target, callFrame, classes)
185    {
186        // FIXME(62725): console stack trace line/column numbers are one-based.
187        var lineNumber = callFrame.lineNumber ? callFrame.lineNumber - 1 : 0;
188        var columnNumber = callFrame.columnNumber ? callFrame.columnNumber - 1 : 0;
189        var anchor = this.linkifyScriptLocation(target, callFrame.scriptId, callFrame.url, lineNumber, columnNumber, classes);
190
191        var script = target && target.debuggerModel.scriptForId(callFrame.scriptId);
192        var blackboxed = script ?
193            WebInspector.BlackboxSupport.isBlackboxed(script.sourceURL, script.isContentScript()) :
194            WebInspector.BlackboxSupport.isBlackboxedURL(callFrame.url);
195        if (blackboxed)
196            anchor.classList.add("webkit-html-blackbox-link");
197
198        return anchor;
199    },
200
201    /**
202     * @param {!WebInspector.CSSLocation} rawLocation
203     * @param {string=} classes
204     * @return {?Element}
205     */
206    linkifyCSSLocation: function(rawLocation, classes)
207    {
208        var anchor = this._createAnchor(classes);
209        var liveLocation = WebInspector.cssWorkspaceBinding.createLiveLocation(rawLocation, this._updateAnchor.bind(this, anchor));
210        if (!liveLocation)
211            return null;
212        this._liveLocationsByTarget.get(rawLocation.target()).push({ anchor: anchor, location: liveLocation });
213        return anchor;
214    },
215
216    /**
217     * @param {!WebInspector.CSSMedia} media
218     * @return {?Element}
219     */
220    linkifyMedia: function(media)
221    {
222        var location = media.rawLocation();
223        if (location)
224            return this.linkifyCSSLocation(location);
225
226        // The "linkedStylesheet" case.
227        return WebInspector.linkifyResourceAsNode(media.sourceURL, undefined, "subtitle", media.sourceURL);
228    },
229
230    /**
231     * @param {string=} classes
232     * @return {!Element}
233     */
234    _createAnchor: function(classes)
235    {
236        var anchor = document.createElement("a");
237        anchor.className = (classes || "") + " webkit-html-resource-link";
238
239        /**
240         * @param {!Event} event
241         */
242        function clickHandler(event)
243        {
244            if (!anchor.__uiLocation)
245                return;
246            event.stopImmediatePropagation();
247            event.preventDefault();
248            if (WebInspector.Linkifier.handleLink(anchor.__uiLocation.uiSourceCode.url, anchor.__uiLocation.lineNumber))
249                return;
250            WebInspector.Revealer.reveal(anchor.__uiLocation);
251        }
252        anchor.addEventListener("click", clickHandler, false);
253        return anchor;
254    },
255
256    reset: function()
257    {
258        var keys = this._liveLocationsByTarget.keys();
259        for (var i = 0; i < keys.length; ++i) {
260            var target = keys[i];
261            this.targetRemoved(target);
262            this.targetAdded(target);
263        }
264    },
265
266    /**
267     * @param {!Element} anchor
268     * @param {!WebInspector.UILocation} uiLocation
269     */
270    _updateAnchor: function(anchor, uiLocation)
271    {
272        anchor.__uiLocation = uiLocation;
273        this._formatter.formatLiveAnchor(anchor, uiLocation);
274    }
275}
276
277/**
278 * @constructor
279 * @implements {WebInspector.LinkifierFormatter}
280 * @param {number=} maxLength
281 */
282WebInspector.Linkifier.DefaultFormatter = function(maxLength)
283{
284    this._maxLength = maxLength;
285}
286
287WebInspector.Linkifier.DefaultFormatter.prototype = {
288    /**
289     * @param {!Element} anchor
290     * @param {!WebInspector.UILocation} uiLocation
291     */
292    formatLiveAnchor: function(anchor, uiLocation)
293    {
294        var text = uiLocation.linkText();
295        if (this._maxLength)
296            text = text.trimMiddle(this._maxLength);
297        anchor.textContent = text;
298
299        var titleText = uiLocation.uiSourceCode.originURL();
300        if (typeof uiLocation.lineNumber === "number")
301            titleText += ":" + (uiLocation.lineNumber + 1);
302        anchor.title = titleText;
303    }
304}
305
306/**
307 * @constructor
308 * @extends {WebInspector.Linkifier.DefaultFormatter}
309 */
310WebInspector.Linkifier.DefaultCSSFormatter = function()
311{
312    WebInspector.Linkifier.DefaultFormatter.call(this, WebInspector.Linkifier.DefaultCSSFormatter.MaxLengthForDisplayedURLs);
313}
314
315WebInspector.Linkifier.DefaultCSSFormatter.MaxLengthForDisplayedURLs = 30;
316
317WebInspector.Linkifier.DefaultCSSFormatter.prototype = {
318    /**
319     * @param {!Element} anchor
320     * @param {!WebInspector.UILocation} uiLocation
321     */
322    formatLiveAnchor: function(anchor, uiLocation)
323    {
324        WebInspector.Linkifier.DefaultFormatter.prototype.formatLiveAnchor.call(this, anchor, uiLocation);
325        anchor.classList.add("webkit-html-resource-link");
326        anchor.setAttribute("data-uncopyable", anchor.textContent);
327        anchor.textContent = "";
328    },
329    __proto__: WebInspector.Linkifier.DefaultFormatter.prototype
330}
331
332/**
333 * The maximum number of characters to display in a URL.
334 * @const
335 * @type {number}
336 */
337WebInspector.Linkifier.MaxLengthForDisplayedURLs = 150;
338
339/**
340 * @interface
341 */
342WebInspector.Linkifier.LinkHandler = function()
343{
344}
345
346WebInspector.Linkifier.LinkHandler.prototype = {
347    /**
348     * @param {string} url
349     * @param {number=} lineNumber
350     * @return {boolean}
351     */
352    handleLink: function(url, lineNumber) {}
353}
354
355/**
356 * @param {!WebInspector.Target} target
357 * @param {string} scriptId
358 * @param {number} lineNumber
359 * @param {number=} columnNumber
360 * @return {string}
361 */
362WebInspector.Linkifier.liveLocationText = function(target, scriptId, lineNumber, columnNumber)
363{
364    var script = target.debuggerModel.scriptForId(scriptId);
365    if (!script)
366        return "";
367    var location = /** @type {!WebInspector.DebuggerModel.Location} */ (target.debuggerModel.createRawLocation(script, lineNumber, columnNumber || 0));
368    var uiLocation = /** @type {!WebInspector.UILocation} */ (WebInspector.debuggerWorkspaceBinding.rawLocationToUILocation(location));
369    return uiLocation.linkText();
370}
371