com.github.sommeri.sourcemap.SourceMapGeneratorV3 Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of less4j Show documentation
Show all versions of less4j Show documentation
Less language is an extension of css and less4j compiles it into regular css. It adds several dynamic features into css: variables, expressions, nested rules.
Less4j is a port. The original compiler was written in JavaScript and is called less.js. The less language is mostly defined in less.js documentation/issues and by what less.js actually do. Links to less.js:
* home page: http://lesscss.org/
* source code & issues: https://github.com/cloudhead/less.js
/*
* Copyright 2011 The Closure Compiler Authors.
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.github.sommeri.sourcemap;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.github.sommeri.sourcemap.SourceMapConsumerV3.EntryVisitor;
/**
* Collects information mapping the generated (compiled) source back to
* its original source for debugging purposes.
*
* @author [email protected] (John Lenz)
*/
public class SourceMapGeneratorV3 implements SourceMapGenerator {
private static final int UNMAPPED = -1;
/**
* A pre-order traversal ordered list of mappings stored in this map.
*/
private List mappings = new ArrayList();
/**
* A map of source names to source name index
*/
private LinkedHashMap sourceFileKeyMap = new LinkedHashMap();
private LinkedList sourceFileNameMap = new LinkedList();
private LinkedList sourceFileContentMap = new LinkedList();
/**
* A map of source names to source name index
*/
private LinkedHashMap originalNameMap = new LinkedHashMap();
/**
* Cache of the last mappings source name.
*/
private String lastSourceFileName = null;
/**
* Cache of the last mappings source content.
*/
private String lastSourceFileContent = null;
/**
* Cache of the last mappings source name index.
*/
private int lastSourceFileIndex = -1;
/**
* For validation store the last mapping added.
*/
private Mapping lastMapping;
/**
* The position that the current source map is offset in the
* buffer being used to generated the compiled source file.
*/
private FilePosition offsetPosition = new FilePosition(0, 0);
/**
* The position that the current source map is offset in the
* generated the compiled source file by the addition of a
* an output wrapper prefix.
*/
private FilePosition prefixPosition = new FilePosition(0, 0);
/**
* {@inheritDoc}
*/
@Override
public void reset() {
mappings.clear();
lastMapping = null;
sourceFileKeyMap.clear();
sourceFileNameMap.clear();
sourceFileContentMap.clear();
originalNameMap.clear();
lastSourceFileName = null;
lastSourceFileContent = null;
lastSourceFileIndex = -1;
offsetPosition = new FilePosition(0, 0);
prefixPosition = new FilePosition(0, 0);
}
/**
* @param validate Whether to perform (potentially costly) validation on the
* generated source map.
*/
@Override
public void validate(boolean validate) {
// Nothing currently.
}
/**
* Sets the prefix used for wrapping the generated source file before
* it is written. This ensures that the source map is adjusted for the
* change in character offsets.
*
* @param prefix The prefix that is added before the generated source code.
*/
@Override
public void setWrapperPrefix(String prefix) {
// Determine the current line and character position.
int prefixLine = 0;
int prefixIndex = 0;
for (int i = 0; i < prefix.length(); ++i) {
if (prefix.charAt(i) == '\n') {
prefixLine++;
prefixIndex = 0;
} else {
prefixIndex++;
}
}
prefixPosition = new FilePosition(prefixLine, prefixIndex);
}
/**
* Sets the source code that exists in the buffer for which the
* generated code is being generated. This ensures that the source map
* accurately reflects the fact that the source is being appended to
* an existing buffer and as such, does not start at line 0, position 0
* but rather some other line and position.
*
* @param offsetLine The index of the current line being printed.
* @param offsetIndex The column index of the current character being printed.
*/
@Override
public void setStartingPosition(int offsetLine, int offsetIndex) {
Preconditions.checkState(offsetLine >= 0);
Preconditions.checkState(offsetIndex >= 0);
offsetPosition = new FilePosition(offsetLine, offsetIndex);
}
/**
* Adds a mapping for the given node. Mappings must be added in order.
* @param startPosition The position on the starting line
* @param endPosition The position on the ending line.
*/
@Override
public void addMapping(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) {
// Don't bother if there is not sufficient information to be useful.
if ((sourceName == null && sourceContent == null) || sourceStartPosition.getLine() < 0) {
return;
}
FilePosition adjustedStart = startPosition;
FilePosition adjustedEnd = endPosition;
if (offsetPosition.getLine() != 0 || offsetPosition.getColumn() != 0) {
// If the mapping is found on the first line, we need to offset
// its character position by the number of characters found on
// the *last* line of the source file to which the code is
// being generated.
adjustedStart = adjustPosition(startPosition, offsetPosition);
adjustedEnd = adjustPosition(endPosition, offsetPosition);
}
// Create the new mapping.
Mapping mapping = new Mapping();
mapping.sourceFile = sourceName;
mapping.sourceContent = sourceContent;
mapping.originalPosition = sourceStartPosition;
mapping.originalName = symbolName;
mapping.startPosition = adjustedStart;
mapping.endPosition = adjustedEnd;
// Validate the mappings are in a proper order.
if (lastMapping != null) {
int lastLine = lastMapping.startPosition.getLine();
int lastColumn = lastMapping.startPosition.getColumn();
int nextLine = mapping.startPosition.getLine();
int nextColumn = mapping.startPosition.getColumn();
Preconditions.checkState(nextLine > lastLine || (nextLine == lastLine && nextColumn >= lastColumn), "Incorrect source mappings order, previous : (" + lastLine + "," + lastColumn + ")\n" + "new : (" + nextLine + "," + nextColumn + ")\nnode : %s");
}
lastMapping = mapping;
mappings.add(mapping);
}
public void addSourceFile(String sourceName, String sourceContent) {
getSourceId(sourceName, sourceContent);
}
private FilePosition adjustPosition(FilePosition mapping, FilePosition offset) {
int offsetLine = offset.getLine();
int offsetColumn = offset.getColumn();
if (mapping.getLine() > 0) {
offsetColumn = 0;
}
FilePosition adjustedStart = new FilePosition(mapping.getLine() + offsetLine, mapping.getColumn() + offsetColumn);
return adjustedStart;
}
private List getMappings() {
return mappings;
}
@Override
public void offsetAndAppend(SourceMapGenerator otherGenerator, FilePosition offset) {
if (!(otherGenerator instanceof SourceMapGeneratorV3)) {
throw new IllegalStateException("Incompatible generator supplied.");
}
SourceMapGeneratorV3 other = (SourceMapGeneratorV3) otherGenerator;
List otherMappings = other.getMappings();
for (Mapping mapping : otherMappings) {
Mapping adjustedMapping = new Mapping();
adjustedMapping.sourceFile = mapping.sourceFile;
adjustedMapping.originalPosition = mapping.originalPosition;
adjustedMapping.originalName = mapping.originalName;
adjustedMapping.startPosition = adjustPosition(mapping.startPosition, offset);
adjustedMapping.endPosition = adjustPosition(mapping.endPosition, offset);
mappings.add(adjustedMapping);
}
}
class ConsumerEntryVisitor implements EntryVisitor {
@Override
public void visit(String sourceName, String sourceContent, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition) {
addMapping(sourceName, sourceContent, symbolName, sourceStartPosition, startPosition, endPosition);
}
}
public void mergeMapSection(int line, int column, String mapSectionContents) throws SourceMapParseException {
setStartingPosition(line, column);
SourceMapConsumerV3 section = new SourceMapConsumerV3();
section.parse(mapSectionContents);
section.visitMappings(new ConsumerEntryVisitor());
}
/**
* Writes out the source map in the following format (line numbers are for
* reference only and are not part of the format):
*
* 1. {
* 2. version: 3,
* 3. file: "out.js",
* 4. lineCount: 2,
* 5. sourceRoot: "",
* 6. sources: ["foo.js", "bar.js"],
* 7. names: ["src", "maps", "are", "fun"],
* 8. mappings: "a;;abcde,abcd,a;"
* 9. }
*
* Line 1: The entire file is a single JSON object
* Line 2: File revision (always the first entry in the object)
* Line 3: The name of the file that this source map is associated with.
* Line 4: The number of lines represented in the source map.
* Line 5: An optional source root, useful for relocating source files on a
* server or removing repeated prefix values in the "sources" entry.
* Line 6: A list of sources used by the "mappings" entry relative to the
* sourceRoot.
* Line 7: A list of symbol names used by the "mapping" entry. This list
* may be incomplete.
* Line 8: The mappings field.
*/
@Override
public void appendTo(Appendable out, String name) throws IOException {
int maxLine = prepMappings();
// Add the header fields.
out.append("{\n");
appendFirstField(out, "version", "3");
appendField(out, "file", escapeString(name));
appendField(out, "lineCount", String.valueOf(maxLine + 1));
// Add the mappings themselves.
appendFieldStart(out, "mappings");
// out.append("[");
(new LineMapper(out)).appendLineMappings();
// out.append("]");
appendFieldEnd(out);
// Files names
appendFieldStart(out, "sources");
out.append("[");
addSourceNameMap(out);
out.append("]");
appendFieldEnd(out);
// Files content
appendFieldStart(out, "sourcesContent");
out.append("[");
addSourceContentMap(out);
out.append("]");
appendFieldEnd(out);
// Synbols names
appendFieldStart(out, "names");
out.append("[");
addSymbolNameMap(out);
out.append("]");
appendFieldEnd(out);
out.append("\n}\n");
}
/**
* Writes the source name map to 'out'.
*/
private void addSourceNameMap(Appendable out) throws IOException {
addValuesList(out, sourceFileNameMap);
}
/**
* Writes the source content map to 'out'.
*/
private void addSourceContentMap(Appendable out) throws IOException {
addValuesList(out, sourceFileContentMap);
}
/**
* Writes the source name map to 'out'.
*/
private void addSymbolNameMap(Appendable out) throws IOException {
addNameMap(out, originalNameMap);
}
private void addNameMap(Appendable out, Map map) throws IOException {
int i = 0;
for (Entry entry : map.entrySet()) {
String key = entry.getKey();
if (i != 0) {
out.append(",");
}
out.append(key == null ? null : escapeString(key));
i++;
}
}
private void addValuesList(Appendable out, List list) throws IOException {
int i = 0;
for (String string : list) {
if (i != 0) {
out.append(",");
}
out.append(string == null ? null : escapeString(string));
i++;
}
}
/**
* Escapes the given string for JSON.
*/
private static String escapeString(String value) {
return SourceMapUtil.escapeString(value);
}
// Source map field helpers.
private static void appendFirstField(Appendable out, String name, CharSequence value) throws IOException {
out.append("\"");
out.append(name);
out.append("\"");
out.append(":");
out.append(value);
}
private static void appendField(Appendable out, String name, CharSequence value) throws IOException {
out.append(",\n");
out.append("\"");
out.append(name);
out.append("\"");
out.append(":");
out.append(value);
}
private static void appendFieldStart(Appendable out, String name) throws IOException {
appendField(out, name, "");
}
private static void appendFieldEnd(Appendable out) throws IOException {
}
/**
* Assigns sequential ids to used mappings, and returns the last line mapped.
*/
private int prepMappings() throws IOException {
// Mark any unused mappings.
(new MappingTraversal()).traverse(new UsedMappingCheck());
// Renumber used mappings and keep track of the last line.
int id = 0;
int maxLine = 0;
for (Mapping m : mappings) {
if (m.used) {
m.id = id++;
int endPositionLine = m.endPosition.getLine();
maxLine = Math.max(maxLine, endPositionLine);
}
}
// Adjust for the prefix.
return maxLine + prefixPosition.getLine();
}
/**
* A mapping from a given position in an input source file to a given position
* in the generated code.
*/
static class Mapping {
/**
* A unique ID for this mapping for record keeping purposes.
*/
int id = UNMAPPED;
/**
* The source file index.
*/
String sourceFile;
/**
* The source file content.
*/
String sourceContent;
/**
* The position of the code in the input source file. Both
* the line number and the character index are indexed by
* 1 for legacy reasons via the Rhino Node class.
*/
FilePosition originalPosition;
/**
* The starting position of the code in the generated source
* file which this mapping represents. Indexed by 0.
*/
FilePosition startPosition;
/**
* The ending position of the code in the generated source
* file which this mapping represents. Indexed by 0.
*/
FilePosition endPosition;
/**
* The original name of the token found at the position
* represented by this mapping (if any).
*/
String originalName;
/**
* Whether the mapping is actually used by the source map.
*/
boolean used = false;
}
/**
* Mark any visited mapping as "used".
*/
private class UsedMappingCheck implements MappingVisitor {
/**
* @throws IOException
*/
@Override
public void visit(Mapping m, int line, int col, int nextLine, int nextCol) throws IOException {
if (m != null) {
m.used = true;
}
}
}
private interface MappingVisitor {
/**
* @param m The mapping for the current code segment. null if the segment
* is unmapped.
* @param line The starting line for this code segment.
* @param col The starting column for this code segment.
* @param endLine The ending line
* @param endCol The ending column
* @throws IOException
*/
void visit(Mapping m, int line, int col, int endLine, int endCol) throws IOException;
}
/**
* Walk the mappings and visit each segment of the mappings, unmapped
* segments are visited with a null mapping, unused mapping are not visited.
*/
private class MappingTraversal {
// The last line and column written
private int line;
private int col;
MappingTraversal() {
}
// Append the line mapping entries.
void traverse(MappingVisitor v) throws IOException {
// The mapping list is ordered as a pre-order traversal. The mapping
// positions give us enough information to rebuild the stack and this
// allows the building of the source map in O(n) time.
Deque stack = new ArrayDeque();
for (Mapping m : mappings) {
// Find the closest ancestor of the current mapping:
// An overlapping mapping is an ancestor of the current mapping, any
// non-overlapping mappings are siblings (or cousins) and must be
// closed in the reverse order of when they encountered.
while (!stack.isEmpty() && !isOverlapped(stack.peek(), m)) {
Mapping previous = stack.pop();
maybeVisit(v, previous);
}
// Any gaps between the current line position and the start of the
// current mapping belong to the parent.
Mapping parent = stack.peek();
maybeVisitParent(v, parent, m);
stack.push(m);
}
// There are no more children to be had, simply close the remaining
// mappings in the reverse order of when they encountered.
while (!stack.isEmpty()) {
Mapping m = stack.pop();
maybeVisit(v, m);
}
}
/**
* @return The line adjusted for the prefix position.
*/
private int getAdjustedLine(FilePosition p) {
return p.getLine() + prefixPosition.getLine();
}
/**
* @return The column adjusted for the prefix position.
*/
private int getAdjustedCol(FilePosition p) {
int rawLine = p.getLine();
int rawCol = p.getColumn();
// Only the first line needs the character position adjusted.
return (rawLine != 0) ? rawCol : rawCol + prefixPosition.getColumn();
}
/**
* @return Whether m1 ends before m2 starts.
*/
private boolean isOverlapped(Mapping m1, Mapping m2) {
// No need to use adjusted values here, relative positions are sufficient.
int l1 = m1.endPosition.getLine();
int l2 = m2.startPosition.getLine();
int c1 = m1.endPosition.getColumn();
int c2 = m2.startPosition.getColumn();
return (l1 == l2 && c1 >= c2) || l1 > l2;
}
/**
* Write any needed entries from the current position to the end of the
* provided mapping.
*/
private void maybeVisit(MappingVisitor v, Mapping m) throws IOException {
int nextLine = getAdjustedLine(m.endPosition);
int nextCol = getAdjustedCol(m.endPosition);
// If this anything remaining in this mapping beyond the
// current line and column position, write it out now.
if (line < nextLine || (line == nextLine && col < nextCol)) {
visit(v, m, nextLine, nextCol);
}
}
/**
* Write any needed entries to complete the provided mapping.
*/
private void maybeVisitParent(MappingVisitor v, Mapping parent, Mapping m) throws IOException {
int nextLine = getAdjustedLine(m.startPosition);
int nextCol = getAdjustedCol(m.startPosition);
// If the previous value is null, no mapping exists.
Preconditions.checkState(line < nextLine || col <= nextCol);
if (line < nextLine || (line == nextLine && col < nextCol)) {
visit(v, parent, nextLine, nextCol);
}
}
/**
* Write any entries needed between the current position the next position
* and update the current position.
*/
private void visit(MappingVisitor v, Mapping m, int nextLine, int nextCol) throws IOException {
Preconditions.checkState(line <= nextLine);
Preconditions.checkState(line < nextLine || col < nextCol);
if (line == nextLine && col == nextCol) {
// Nothing to do.
Preconditions.checkState(false);
return;
}
v.visit(m, line, col, nextLine, nextCol);
line = nextLine;
col = nextCol;
}
}
/**
* Appends the index source map to the given buffer.
*
* @param out The stream to which the map will be appended.
* @param name The name of the generated source file that this source map
* represents.
* @param sections An ordered list of map sections to include in the index.
* @throws IOException
*/
@Override
public void appendIndexMapTo(Appendable out, String name, List sections) throws IOException {
// Add the header fields.
out.append("{\n");
appendFirstField(out, "version", "3");
appendField(out, "file", escapeString(name));
// Add the line character maps.
appendFieldStart(out, "sections");
out.append("[\n");
boolean first = true;
for (SourceMapSection section : sections) {
if (first) {
first = false;
} else {
out.append(",\n");
}
out.append("{\n");
appendFirstField(out, "offset", offsetValue(section.getLine(), section.getColumn()));
if (section.getSectionType() == SourceMapSection.SectionType.URL) {
appendField(out, "url", escapeString(section.getSectionValue()));
} else if (section.getSectionType() == SourceMapSection.SectionType.MAP) {
appendField(out, "map", section.getSectionValue());
} else {
throw new IOException("Unexpected section type");
}
out.append("\n}");
}
out.append("\n]");
appendFieldEnd(out);
out.append("\n}\n");
}
private CharSequence offsetValue(int line, int column) throws IOException {
StringBuilder out = new StringBuilder();
out.append("{\n");
appendFirstField(out, "line", String.valueOf(line));
appendField(out, "column", String.valueOf(column));
out.append("\n}");
return out;
}
private int getSourceId(String sourceName, String sourceContent) {
if (sourceName != lastSourceFileName || sourceContent != lastSourceFileContent) {
//if it is different file then the one before
lastSourceFileName = sourceName;
lastSourceFileContent = sourceContent;
String key = sourceName != null ? sourceName : sourceContent;
Integer index = sourceFileKeyMap.get(key);
if (index != null) {
lastSourceFileIndex = index;
} else {
lastSourceFileIndex = sourceFileKeyMap.size();
sourceFileKeyMap.put(key, lastSourceFileIndex);
sourceFileNameMap.add(sourceName);
sourceFileContentMap.add(sourceContent);
}
}
return lastSourceFileIndex;
}
private int getNameId(String symbolName) {
int originalNameIndex;
Integer index = originalNameMap.get(symbolName);
if (index != null) {
originalNameIndex = index;
} else {
originalNameIndex = originalNameMap.size();
originalNameMap.put(symbolName, originalNameIndex);
}
return originalNameIndex;
}
private class LineMapper implements MappingVisitor {
// The destination.
private final Appendable out;
private int previousLine = -1;
private int previousColumn = 0;
// Previous values used for storing relative ids.
private int previousSourceFileId;
private int previousSourceLine;
private int previousSourceColumn;
private int previousNameId;
LineMapper(Appendable out) {
this.out = out;
}
/**
* As each segment is visited write out the appropriate line mapping.
*/
@Override
public void visit(Mapping m, int line, int col, int nextLine, int nextCol) throws IOException {
if (previousLine != line) {
previousColumn = 0;
}
if (line != nextLine || col != nextCol) {
if (previousLine == line) { // not the first entry for the line
out.append(',');
}
writeEntry(m, col);
previousLine = line;
previousColumn = col;
}
for (int i = line; i <= nextLine; i++) {
if (i == nextLine) {
break;
}
closeLine(false);
openLine(false);
}
}
/**
* Writes an entry for the given column (of the generated text) and
* associated mapping.
* The values are stored as relative to the last seen values for each
* field and encoded as Base64VLQs.
*/
void writeEntry(Mapping m, int column) throws IOException {
// The relative generated column number
Base64VLQ.encode(out, column - previousColumn);
previousColumn = column;
if (m != null) {
// The relative source file id
int sourceId = getSourceId(m.sourceFile, m.sourceContent);
Base64VLQ.encode(out, sourceId - previousSourceFileId);
previousSourceFileId = sourceId;
// The relative source file line and column
int srcline = m.originalPosition.getLine();
int srcColumn = m.originalPosition.getColumn();
Base64VLQ.encode(out, srcline - previousSourceLine);
previousSourceLine = srcline;
Base64VLQ.encode(out, srcColumn - previousSourceColumn);
previousSourceColumn = srcColumn;
if (m.originalName != null) {
// The relative id for the associated symbol name
int nameId = getNameId(m.originalName);
Base64VLQ.encode(out, (nameId - previousNameId));
previousNameId = nameId;
}
}
}
// Append the line mapping entries.
void appendLineMappings() throws IOException {
// Start the first line.
openLine(true);
(new MappingTraversal()).traverse(this);
// And close the final line.
closeLine(true);
}
/**
* Begin the entry for a new line.
*/
private void openLine(boolean firstEntry) throws IOException {
if (firstEntry) {
out.append('\"');
}
}
/**
* End the entry for a line.
*/
private void closeLine(boolean finalEntry) throws IOException {
out.append(';');
if (finalEntry) {
out.append('\"');
}
}
}
}