/*
* Copyright (c) 2013, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.debug.core.sourcemaps;
import com.google.common.base.Charsets;
import com.google.common.io.CharStreams;
import com.google.common.io.Files;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Path;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
// //@ sourceMappingURL=/path/to/file.js.map
/**
* This maps from a generated file back to the original source files. It also supports the reverse
* mapping; from locations in the source files to locations in the generated file.
*
* @see http://www.html5rocks.com/en/tutorials/developertools/sourcemaps/
*/
public class SourceMap {
public static final String SOURCE_MAP_EXT = ".map";
public static SourceMap createFrom(File file) throws IOException {
String contents = Files.toString(file, Charsets.UTF_8);
try {
return createFrom(Path.fromOSString(file.getAbsolutePath()), contents);
} catch (JSONException e) {
throw new IOException(e);
}
}
public static SourceMap createFrom(IFile file) throws IOException, CoreException {
Reader reader = new InputStreamReader(file.getContents(), file.getCharset());
try {
String contents = CharStreams.toString(reader);
return createFrom(file.getFullPath(), contents);
} catch (JSONException e) {
throw new IOException(e);
} finally {
reader.close();
}
}
private static SourceMap createFrom(IPath path, JSONObject jsonObject) throws JSONException {
return new SourceMap(path, jsonObject);
}
private static SourceMap createFrom(IPath path, String contents) throws JSONException {
if (contents.startsWith(")]}")) {
contents = contents.substring(3);
}
return createFrom(path, new JSONObject(contents));
}
private IPath path;
/**
* The format version; must be a positive integer. The current version of the spec is 3.
*/
private int version;
/**
* The name of the generated code that this source map is associated with.
*/
private String file;
/**
* An optional source root, useful for relocating source files on a server or removing repeated
* values in the "sources" entry. This value is prepended to the individual entries in the
* "source" field.
*/
private String sourceRoot;
/**
* A list of original sources used by the "mappings" entry.
*/
private String[] sources;
/**
* A list of symbol names used by the "mappings" entry.
*/
private String[] names;
/**
* An optional list of source content, useful when the "source" can’t be hosted.
*/
@SuppressWarnings("unused")
private String sourcesContent[];
/**
* The full list of source map entries.
*/
private SourceMapInfoEntry[] entries;
public SourceMap() {
}
public SourceMap(IPath path, JSONObject obj) throws JSONException {
// {
// version : 3,
// file: "out.js",
// sourceRoot : "",
// sources: ["foo.js", "bar.js"],
// names: ["src", "maps", "are", "fun"],
// mappings: "AAgBC,SAAQ,CAAEA"
// }
this.path = path;
version = obj.optInt("version");
file = obj.optString("file");
sourceRoot = obj.optString("sourceRoot");
sources = parseStringArray(obj.getJSONArray("sources"));
sourcesContent = parseStringArray(obj.optJSONArray("sourcesContent"));
names = parseStringArray(obj.getJSONArray("names"));
// Prepend sourceRoot to the sources entries.
if (sourceRoot != null && sourceRoot.length() > 0) {
for (int i = 0; i < sources.length; i++) {
sources[i] = sourceRoot + sources[i];
}
}
String mapStr = obj.getString("mappings");
List<SourceMapInfoEntry> result = SourceMapDecoder.decode(sources, names, mapStr);
entries = result.toArray(new SourceMapInfoEntry[result.size()]);
}
public String getFile() {
return file;
}
/**
* Map from a location in the generated file back to the original source.
*
* @param line the line in the generated source
* @param column the column in the generated source; -1 means the column is not interesting
* @return the corresponding location in the original source
*/
public SourceMapInfo getMappingFor(int line, int column) {
int index = findIndexForLine(line);
if (index == -1) {
return null;
}
SourceMapInfoEntry entry = entries[index];
// If column == -1, return the first mapping for that line.
if (column == -1) {
return entry.getInfo();
}
// Search for a matching mapping.
while (index < entries.length) {
entry = entries[index];
if (entry.column <= column) {
if (entry.endColumn == -1) {
return entry.getInfo();
}
if (column < entry.endColumn) {
return entry.getInfo();
}
}
index++;
}
// no mapping found
return null;
}
public IFile getMapSource() {
String name = path.lastSegment();
if (name.endsWith(SOURCE_MAP_EXT)) {
name = name.substring(0, name.length() - SOURCE_MAP_EXT.length());
IPath newPath = path.removeLastSegments(1).append(name);
IResource resource = ResourcesPlugin.getWorkspace().getRoot().findMember(newPath);
if (resource instanceof IFile) {
return (IFile) resource;
}
}
return null;
}
public IPath getPath() {
return path;
}
/**
* Map from a location in a source file to a location in the generated source file.
*
* @param file
* @param line
* @param column
* @return
*/
public List<SourceMapInfo> getReverseMappingsFor(String file, int line) {
// TODO(devoncarew): calculate this information once for O(1) lookup
for (SourceMapInfoEntry entry : entries) {
SourceMapInfo info = entry.getInfo();
if (info == null) {
continue;
}
if (line == info.getLine()) {
if (file.equals(info.getFile())) {
// TODO(devoncarew): there will be several entries on this line
// We need to choose one that has a non-zero range, or is a catch-all entry
return Collections.singletonList(new SourceMapInfo(
path.toString(),
entry.line,
entry.column));
}
}
}
return Collections.emptyList();
}
public String[] getSourceNames() {
return sources;
}
/**
* The format version; must be a positive integer. The current version of the specification is 3.
*/
public int getVersion() {
return version;
}
@Override
public String toString() {
return "[" + getPath().lastSegment() + ", "
+ NumberFormat.getNumberInstance().format(entries.length) + " lines]";
}
private int findIndexForLine(int line) {
// TODO(devoncarew): test this binary search
int location = Arrays.binarySearch(
entries,
SourceMapInfoEntry.forLine(line),
SourceMapInfoEntry.lineComparator());
if (location < 0) {
return -1;
}
while (location > 0 && entries[location - 1].line == line) {
location--;
}
return location;
// for (int i = 0; i < entries.length; i++) {
// SourceMapInfoEntry entry = entries[i];
//
// if (entry.line == line) {
// return i;
// } else if (entry.line > line) {
// return -1;
// }
// }
//
// return -1;
}
private String[] parseStringArray(JSONArray arr) throws JSONException {
if (arr == null) {
return null;
} else {
String[] strs = new String[arr.length()];
for (int i = 0; i < arr.length(); i++) {
strs[i] = arr.getString(i);
}
return strs;
}
}
}