/* 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 . */ // @flow /** * Utils for working with Source URLs * @module utils/source */ import { isOriginalId, isGeneratedId } from "devtools-source-map"; import { getUnicodeUrl } from "devtools-modules"; import { endTruncateStr } from "./utils"; import { truncateMiddleText } from "../utils/text"; import { parse as parseURL } from "../utils/url"; import { renderWasmText } from "./wasm"; import { toEditorPosition } from "./editor"; export { isMinified } from "./isMinified"; import { getURL, getFileExtension } from "./sources-tree"; import { prefs, features } from "./prefs"; import type { SourceId, Source, SourceActor, SourceContent, SourceLocation } from "../types"; import { isFulfilled, type AsyncValue } from "./async-value"; import type { Symbols } from "../reducers/types"; type transformUrlCallback = string => string; export const sourceTypes = { coffee: "coffeescript", js: "javascript", jsx: "react", ts: "typescript", vue: "vue" }; /** * Trims the query part or reference identifier of a url string, if necessary. * * @memberof utils/source * @static */ function trimUrlQuery(url: string): string { const length = url.length; const q1 = url.indexOf("?"); const q2 = url.indexOf("&"); const q3 = url.indexOf("#"); const q = Math.min( q1 != -1 ? q1 : length, q2 != -1 ? q2 : length, q3 != -1 ? q3 : length ); return url.slice(0, q); } export function shouldBlackbox(source: ?Source) { if (!source) { return false; } if (!source.url) { return false; } if (isOriginalId(source.id) && !features.originalBlackbox) { return false; } return true; } export function shouldPrettyPrint( source: Source, content: SourceContent ): boolean { if ( !source || isPretty(source) || !isJavaScript(source, content) || isOriginal(source) || (prefs.clientSourceMapsEnabled && source.sourceMapURL) ) { return false; } return true; } /** * Returns true if the specified url and/or content type are specific to * javascript files. * * @return boolean * True if the source is likely javascript. * * @memberof utils/source * @static */ export function isJavaScript(source: Source, content: SourceContent): boolean { const url = source.url; const contentType = content.type === "wasm" ? null : content.contentType; return ( (url && /\.(jsm|js)?$/.test(trimUrlQuery(url))) || !!(contentType && contentType.includes("javascript")) ); } /** * @memberof utils/source * @static */ export function isPretty(source: Source): boolean { const url = source.url; return isPrettyURL(url); } export function isPrettyURL(url: string): boolean { return url ? /formatted$/.test(url) : false; } export function isThirdParty(source: Source) { const url = source.url; if (!source || !url) { return false; } return !!url.match(/(node_modules|bower_components)/); } /** * @memberof utils/source * @static */ export function getPrettySourceURL(url: ?string): string { if (!url) { url = ""; } return `${url}:formatted`; } /** * @memberof utils/source * @static */ export function getRawSourceURL(url: string): string { return url ? url.replace(/:formatted$/, "") : url; } function resolveFileURL( url: string, transformUrl: transformUrlCallback = initialUrl => initialUrl, truncate: boolean = true ) { url = getRawSourceURL(url || ""); const name = transformUrl(url); if (!truncate) { return name; } return endTruncateStr(name, 50); } export function getFormattedSourceId(id: string) { const sourceId = id.split("/")[1]; return `SOURCE${sourceId}`; } /** * Gets a readable filename from a source URL for display purposes. * If the source does not have a URL, the source ID will be returned instead. * * @memberof utils/source * @static */ export function getFilename(source: Source) { const { url, id } = source; if (!getRawSourceURL(url)) { return getFormattedSourceId(id); } const { filename } = getURL(source); return getRawSourceURL(filename); } /** * Provides a middle-trunated filename * * @memberof utils/source * @static */ export function getTruncatedFileName( source: Source, querystring: string = "", length: number = 30 ) { return truncateMiddleText(`${getFilename(source)}${querystring}`, length); } /* Gets path for files with same filename for editor tabs, breakpoints, etc. * Pass the source, and list of other sources * * @memberof utils/source * @static */ export function getDisplayPath(mySource: Source, sources: Source[]) { const filename = getFilename(mySource); // Find sources that have the same filename, but different paths // as the original source const similarSources = sources.filter( source => getRawSourceURL(mySource.url) != getRawSourceURL(source.url) && filename == getFilename(source) ); if (similarSources.length == 0) { return undefined; } // get an array of source path directories e.g. ['a/b/c.html'] => [['b', 'a']] const paths = [mySource, ...similarSources].map(source => getURL(source) .path.split("/") .reverse() .slice(1) ); // create an array of similar path directories and one dis-similar directory // for example [`a/b/c.html`, `a1/b/c.html`] => ['b', 'a'] // where 'b' is the similar directory and 'a' is the dis-similar directory. let similar = true; const displayPath = []; for (let i = 0; similar && i < paths[0].length; i++) { const [dir, ...dirs] = paths.map(path => path[i]); displayPath.push(dir); similar = dirs.includes(dir); } return displayPath.reverse().join("/"); } /** * Gets a readable source URL for display purposes. * If the source does not have a URL, the source ID will be returned instead. * * @memberof utils/source * @static */ export function getFileURL(source: Source, truncate: boolean = true) { const { url, id } = source; if (!url) { return getFormattedSourceId(id); } return resolveFileURL(url, getUnicodeUrl, truncate); } const contentTypeModeMap = { "text/javascript": { name: "javascript" }, "text/typescript": { name: "javascript", typescript: true }, "text/coffeescript": { name: "coffeescript" }, "text/typescript-jsx": { name: "jsx", base: { name: "javascript", typescript: true } }, "text/jsx": { name: "jsx" }, "text/x-elm": { name: "elm" }, "text/x-clojure": { name: "clojure" }, "text/x-clojurescript": { name: "clojure" }, "text/wasm": { name: "text" }, "text/html": { name: "htmlmixed" } }; export function getSourcePath(url: string) { if (!url) { return ""; } const { path, href } = parseURL(url); // for URLs like "about:home" the path is null so we pass the full href return path || href; } /** * Returns amount of lines in the source. If source is a WebAssembly binary, * the function returns amount of bytes. */ export function getSourceLineCount(content: SourceContent): number { if (content.type === "wasm") { const { binary } = content.value; return binary.length; } return content.value.split("\n").length; } /** * * Checks if a source is minified based on some heuristics * @param key * @param text * @return boolean * @memberof utils/source * @static */ /** * * Returns Code Mirror mode for source content type * @param contentType * @return String * @memberof utils/source * @static */ // eslint-disable-next-line complexity export function getMode( source: Source, content: SourceContent, symbols?: Symbols ): { name: string, base?: Object } { const { url } = source; if (content.type !== "text") { return { name: "text" }; } const { contentType, value: text } = content; if ((url && url.match(/\.jsx$/i)) || (symbols && symbols.hasJsx)) { if (symbols && symbols.hasTypes) { return { name: "text/typescript-jsx" }; } return { name: "jsx" }; } if (symbols && symbols.hasTypes) { if (symbols.hasJsx) { return { name: "text/typescript-jsx" }; } return { name: "text/typescript" }; } const languageMimeMap = [ { ext: ".c", mode: "text/x-csrc" }, { ext: ".kt", mode: "text/x-kotlin" }, { ext: ".cpp", mode: "text/x-c++src" }, { ext: ".m", mode: "text/x-objectivec" }, { ext: ".rs", mode: "text/x-rustsrc" }, { ext: ".hx", mode: "text/x-haxe" } ]; // check for C and other non JS languages if (url) { const result = languageMimeMap.find(({ ext }) => url.endsWith(ext)); if (result !== undefined) { return { name: result.mode }; } } // if the url ends with .marko we set the name to Javascript so // syntax highlighting works for marko too if (url && url.match(/\.marko$/i)) { return { name: "javascript" }; } // Use HTML mode for files in which the first non whitespace // character is `<` regardless of extension. const isHTMLLike = text.match(/^\s*); if (!contentType) { if (isHTMLLike) { return { name: "htmlmixed" }; } return { name: "text" }; } // // @flow or /* @flow */ if (text.match(/^\s*(\/\/ @flow|\/\* @flow \*\/)/)) { return contentTypeModeMap["text/typescript"]; } if (/script|elm|jsx|clojure|wasm|html/.test(contentType)) { if (contentType in contentTypeModeMap) { return contentTypeModeMap[contentType]; } return contentTypeModeMap["text/javascript"]; } if (isHTMLLike) { return { name: "htmlmixed" }; } return { name: "text" }; } export function isInlineScript(source: SourceActor): boolean { return source.introductionType === "scriptElement"; } export function getTextAtPosition( sourceId: SourceId, asyncContent: AsyncValue | null, location: SourceLocation ) { if (!asyncContent || !isFulfilled(asyncContent)) { return ""; } const content = asyncContent.value; const line = location.line; const column = location.column || 0; if (content.type === "wasm") { const { line: editorLine } = toEditorPosition(location); const lines = renderWasmText(sourceId, content); return lines[editorLine]; } const lineText = content.value.split("\n")[line - 1]; if (!lineText) { return ""; } return lineText.slice(column, column + 100).trim(); } export function getSourceClassnames(source: Object, symbols?: Symbols) { // Conditionals should be ordered by priority of icon! const defaultClassName = "file"; if (!source || !source.url) { return defaultClassName; } if (isPretty(source)) { return "prettyPrint"; } if (source.isBlackBoxed) { return "blackBox"; } if (symbols && !symbols.loading && symbols.framework) { return symbols.framework.toLowerCase(); } return sourceTypes[getFileExtension(source)] || defaultClassName; } export function getRelativeUrl(source: Source, root: string) { const { group, path } = getURL(source); if (!root) { return path; } // + 1 removes the leading "/" const url = group + path; return url.slice(url.indexOf(root) + root.length + 1); } export function underRoot(source: Source, root: string) { return source.url && source.url.includes(root); } export function isOriginal(source: Source) { // Pretty-printed sources are given original IDs, so no need // for any additional check return isOriginalId(source.id); } export function isGenerated(source: Source) { return isGeneratedId(source.id); } export function getSourceQueryString(source: ?Source) { if (!source) { return; } return parseURL(getRawSourceURL(source.url)).search; } export function isUrlExtension(url: string) { return /\/?(chrome|moz)-extension:\//.test(url); } export function getPlainUrl(url: string): string { const queryStart = url.indexOf("?"); return queryStart !== -1 ? url.slice(0, queryStart) : url; }