Source code

Revision control

Copy as Markdown

Other Tools

/* 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";
const {
Component,
const {
getUnicodeUrl,
getUnicodeUrlPath,
getUnicodeHostname,
const {
getSourceNames,
parseURL,
getSourceMappedFile,
const { LocalizationHelper } = require("resource://devtools/shared/l10n.js");
const {
MESSAGE_SOURCE,
const l10n = new LocalizationHelper(
"devtools/client/locales/components.properties"
);
const webl10n = new LocalizationHelper(
"devtools/client/locales/webconsole.properties"
);
function savedFrameToLocation(frame) {
const { source: url, line, column, sourceId } = frame;
return {
url,
line,
column,
// The sourceId will be a string if it's a source actor ID, otherwise
// it is either a Spidermonkey-internal ID from a SavedFrame or missing,
// and in either case we can't use the ID for anything useful.
id: typeof sourceId === "string" ? sourceId : null,
};
}
/**
* Get the tooltip message.
* @param {string|undefined} messageSource
* @param {string} url
* @returns {string}
*/
function getTooltipMessage(messageSource, url) {
if (messageSource && messageSource === MESSAGE_SOURCE.CSS) {
return l10n.getFormatStr("frame.viewsourceinstyleeditor", url);
}
return l10n.getFormatStr("frame.viewsourceindebugger", url);
}
class Frame extends Component {
static get propTypes() {
return {
// Optional className that will be put into the element.
className: PropTypes.string,
// SavedFrame, or an object containing all the required properties.
frame: PropTypes.shape({
functionDisplayName: PropTypes.string,
// This could be a SavedFrame with a numeric sourceId, or it could
// be a SavedFrame-like client-side object, in which case the
// "sourceId" will be a source actor ID.
sourceId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
source: PropTypes.string.isRequired,
line: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
column: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}).isRequired,
// Clicking on the frame link -- probably should link to the debugger.
onClick: PropTypes.func,
// Option to display a function name before the source link.
showFunctionName: PropTypes.bool,
// Option to display a function name even if it's anonymous.
showAnonymousFunctionName: PropTypes.bool,
// Option to display a host name after the source link.
showHost: PropTypes.bool,
// Option to display a host name if the filename is empty or just '/'
showEmptyPathAsHost: PropTypes.bool,
// Option to display a full source instead of just the filename.
showFullSourceUrl: PropTypes.bool,
// Service to enable the source map feature for console.
sourceMapURLService: PropTypes.object,
// The source of the message
messageSource: PropTypes.string,
};
}
static get defaultProps() {
return {
showFunctionName: false,
showAnonymousFunctionName: false,
showHost: false,
showEmptyPathAsHost: false,
showFullSourceUrl: false,
};
}
constructor(props) {
super(props);
this.state = {
originalLocation: null,
};
this._locationChanged = this._locationChanged.bind(this);
}
UNSAFE_componentWillMount() {
if (this.props.sourceMapURLService) {
const location = savedFrameToLocation(this.props.frame);
// Many things that make use of this component either:
// a) Pass in no sourceId because they have no way to know.
// b) Pass in no sourceId because the actor wasn't created when the
// server sent its response.
//
// and due to that, we need to use subscribeByLocation in order to
// handle both cases with an without an ID.
this.unsubscribeSourceMapURLService =
this.props.sourceMapURLService.subscribeByLocation(
location,
this._locationChanged
);
}
}
componentWillUnmount() {
if (this.unsubscribeSourceMapURLService) {
this.unsubscribeSourceMapURLService();
}
}
_locationChanged(originalLocation) {
this.setState({ originalLocation });
}
/**
* Get current location's source, line, and column.
* @returns {{source: string, line: number|null, column: number|null}}
*/
#getCurrentLocationInfo = () => {
const { frame } = this.props;
const { originalLocation } = this.state;
const generatedLocation = savedFrameToLocation(frame);
const currentLocation = originalLocation || generatedLocation;
const source = currentLocation.url || "";
const line =
currentLocation.line != void 0 ? Number(currentLocation.line) : null;
const column =
currentLocation.column != void 0 ? Number(currentLocation.column) : null;
return {
source,
line,
column,
};
};
/**
* Get unicode hostname of the source link.
* @returns {string}
*/
#getCurrentLocationUnicodeHostName = () => {
const { source } = this.#getCurrentLocationInfo();
const { host } = getSourceNames(source);
return host ? getUnicodeHostname(host) : "";
};
/**
* Check if the current location is linkable.
* @returns {boolean}
*/
#isCurrentLocationLinkable = () => {
const { frame } = this.props;
const { originalLocation } = this.state;
const generatedLocation = savedFrameToLocation(frame);
// Reparse the URL to determine if we should link this; `getSourceNames`
// has already cached this indirectly. We don't want to attempt to
// link to "self-hosted" and "(unknown)".
// Source mapped sources might not necessary linkable, but they
// are still valid in the debugger.
// If we have a source ID then we can show the source in the debugger.
return !!(
originalLocation ||
generatedLocation.id ||
!!parseURL(generatedLocation.url)
);
};
/**
* Get the props of the top element.
*/
#getTopElementProps = () => {
const { className } = this.props;
const { source, line, column } = this.#getCurrentLocationInfo();
const { long } = getSourceNames(source);
const props = {
"data-url": long,
className: "frame-link" + (className ? ` ${className}` : ""),
};
// If we have a line number > 0.
if (line) {
// Add `data-line` attribute for testing
props["data-line"] = line;
// Intentionally exclude 0
if (column) {
// Add `data-column` attribute for testing
props["data-column"] = column;
}
}
return props;
};
/**
* Get the props of the source element.
*/
#getSourceElementsProps = () => {
const { frame, onClick, messageSource } = this.props;
const generatedLocation = savedFrameToLocation(frame);
const { source, line, column } = this.#getCurrentLocationInfo();
const { long } = getSourceNames(source);
let url = getUnicodeUrl(long);
// Exclude all falsy values, including `0`, as line numbers start with 1.
if (line) {
url += `:${line}`;
// Intentionally exclude 0
if (column) {
url += `:${column}`;
}
}
const isLinkable = this.#isCurrentLocationLinkable();
// Inner el is useful for achieving ellipsis on the left and correct LTR/RTL
// ordering. See CSS styles for frame-link-source-[inner] and bug 1290056.
const tooltipMessage = getTooltipMessage(messageSource, url);
const sourceElConfig = {
key: "source",
className: "frame-link-source",
title: isLinkable ? tooltipMessage : url,
};
if (isLinkable) {
return {
...sourceElConfig,
onClick: e => {
e.preventDefault();
e.stopPropagation();
onClick(generatedLocation);
},
href: source,
draggable: false,
};
}
return sourceElConfig;
};
/**
* Render the source elements.
* @returns {React.ReactNode}
*/
#renderSourceElements = () => {
const { line, column } = this.#getCurrentLocationInfo();
const sourceElements = [this.#renderDisplaySource()];
if (line) {
let lineInfo = `:${line}`;
// Intentionally exclude 0
if (column) {
lineInfo += `:${column}`;
}
sourceElements.push(
dom.span(
{
key: "line",
className: "frame-link-line",
},
lineInfo
)
);
}
if (this.#isCurrentLocationLinkable()) {
return dom.a(this.#getSourceElementsProps(), sourceElements);
}
// If source is not a URL (self-hosted, eval, etc.), don't make
// it an anchor link, as we can't link to it.
return dom.span(this.#getSourceElementsProps(), sourceElements);
};
/**
* Render the display source.
* @returns {React.ReactNode}
*/
#renderDisplaySource = () => {
const { showEmptyPathAsHost, showFullSourceUrl } = this.props;
const { originalLocation } = this.state;
const { source } = this.#getCurrentLocationInfo();
const { short, long, host } = getSourceNames(source);
const unicodeShort = getUnicodeUrlPath(short);
const unicodeLong = getUnicodeUrl(long);
let displaySource = showFullSourceUrl ? unicodeLong : unicodeShort;
if (originalLocation) {
displaySource = getSourceMappedFile(displaySource);
// In case of pretty-printed HTML file, we would only get the formatted suffix; replace
// it with the full URL instead
if (showEmptyPathAsHost && displaySource == ":formatted") {
displaySource = host + displaySource;
}
} else if (
showEmptyPathAsHost &&
(displaySource === "" || displaySource === "/")
) {
displaySource = host;
}
return dom.span(
{
key: "filename",
className: "frame-link-filename",
},
displaySource
);
};
/**
* Render the function display name.
* @returns {React.ReactNode}
*/
#renderFunctionDisplayName = () => {
const { frame, showFunctionName, showAnonymousFunctionName } = this.props;
if (!showFunctionName) {
return null;
}
const functionDisplayName = frame.functionDisplayName;
if (functionDisplayName || showAnonymousFunctionName) {
return [
dom.span(
{
key: "function-display-name",
className: "frame-link-function-display-name",
},
functionDisplayName || webl10n.getStr("stacktrace.anonymousFunction")
),
" ",
];
}
return null;
};
render() {
const { showHost } = this.props;
const elements = [
this.#renderFunctionDisplayName(),
this.#renderSourceElements(),
];
const unicodeHost = showHost
? this.#getCurrentLocationUnicodeHostName()
: null;
if (unicodeHost) {
elements.push(" ");
elements.push(
dom.span(
{
key: "host",
className: "frame-link-host",
},
unicodeHost
)
);
}
return dom.span(this.#getTopElementProps(), ...elements);
}
}
module.exports = Frame;