All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.sommeri.sourcemap.SourceMapConsumerV3 Maven / Gradle / Ivy

Go to download

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

There is a newer version: 1.17.2
Show newest version
/*
 * 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.github.sommeri.sourcemap.Base64VLQ.CharIterator;
import com.github.sommeri.sourcemap.Mapping.OriginalMapping;
import com.github.sommeri.sourcemap.Mapping.OriginalMapping.Builder;

/**
 * Class for parsing version 3 of the SourceMap format, as produced by the
 * Closure Compiler, etc.
 * http://code.google.com/p/closure-compiler/wiki/SourceMaps
 * 
 * @author [email protected] (John Lenz)
 */
public class SourceMapConsumerV3 implements SourceMapConsumer, SourceMappingReversable {
  static final int UNMAPPED = -1;

  //TODO: (source map separation):  added this
  private String file;
  private String sourceRoot;
  
  private String[] sources;
  private String[] names;
  private int lineCount;
  // Slots in the lines list will be null if the line does not have any entries.
  private ArrayList> lines = null;
  /** originalFile path ==> original line ==> target mappings */
  private Map>> reverseSourceMapping;

  public SourceMapConsumerV3() {

  }

  static class DefaultSourceMapSupplier implements SourceMapSupplier {
    @Override
    public String getSourceMap(String url) {
      return null;
    }
  }

  /**
   * Parses the given contents containing a source map.
   */
  @Override
  public void parse(String contents) throws SourceMapParseException {
    parse(contents, null);
  }

  /**
   * Parses the given contents containing a source map.
   */
  public void parse(String contents, SourceMapSupplier sectionSupplier) throws SourceMapParseException {
    try {
      JSONObject sourceMapRoot = new JSONObject(contents);
      parse(sourceMapRoot, sectionSupplier);
    } catch (JSONException ex) {
      throw new SourceMapParseException("JSON parse exception: " + ex);
    }
  }

  /**
   * Parses the given contents containing a source map.
   */
  public void parse(JSONObject sourceMapRoot) throws SourceMapParseException {
    parse(sourceMapRoot, null);
  }

  /**
   * Parses the given contents containing a source map.
   */
  public void parse(JSONObject sourceMapRoot, SourceMapSupplier sectionSupplier) throws SourceMapParseException {
    try {
      // Check basic assertions about the format.
      int version = sourceMapRoot.getInt("version");
      if (version != 3) {
        throw new SourceMapParseException("Unknown version: " + version);
      }

      this.file = sourceMapRoot.getString("file");
      if (file.isEmpty()) {
        //TODO: (source map separation):  commented this - I need more tolerant parser
        //throw new SourceMapParseException("File entry is missing or empty ");
      }
      if (sourceMapRoot.has("sourceRoot"))
        this.sourceRoot = sourceMapRoot.getString("sourceRoot");
      
      if (sourceMapRoot.has("sections")) {
        // Looks like a index map, try to parse it that way.
        parseMetaMap(sourceMapRoot, sectionSupplier);
        return;
      }

      lineCount = sourceMapRoot.getInt("lineCount");
      String lineMap = sourceMapRoot.getString("mappings");

      sources = getJavaStringArray(sourceMapRoot.getJSONArray("sources"));
      names = getJavaStringArray(sourceMapRoot.getJSONArray("names"));

      lines = new ArrayList>(lineCount);

      new MappingBuilder(lineMap).build();
    } catch (JSONException ex) {
      throw new SourceMapParseException("JSON parse exception: " + ex);
    }
  }

  /**
   * @param sourceMapRoot
   * @throws SourceMapParseException
   */
  private void parseMetaMap(JSONObject sourceMapRoot, SourceMapSupplier sectionSupplier) throws SourceMapParseException {
    if (sectionSupplier == null) {
      sectionSupplier = new DefaultSourceMapSupplier();
    }

    try {
      // Check basic assertions about the format.
      int version = sourceMapRoot.getInt("version");
      if (version != 3) {
        throw new SourceMapParseException("Unknown version: " + version);
      }

      String file = sourceMapRoot.getString("file");
      if (file.isEmpty()) {
        throw new SourceMapParseException("File entry is missing or empty");
      }

      if (sourceMapRoot.has("lineCount") || sourceMapRoot.has("mappings") || sourceMapRoot.has("sources") || sourceMapRoot.has("names")) {
        throw new SourceMapParseException("Invalid map format");
      }

      SourceMapGeneratorV3 generator = new SourceMapGeneratorV3();
      JSONArray sections = sourceMapRoot.getJSONArray("sections");
      for (int i = 0, count = sections.length(); i < count; i++) {
        JSONObject section = sections.getJSONObject(i);
        if (section.has("map") && section.has("url")) {
          throw new SourceMapParseException("Invalid map format: section may not have both 'map' and 'url'");
        }
        JSONObject offset = section.getJSONObject("offset");
        int line = offset.getInt("line");
        int column = offset.getInt("column");
        String mapSectionContents;
        if (section.has("url")) {
          String url = section.getString("url");
          mapSectionContents = sectionSupplier.getSourceMap(url);
          if (mapSectionContents == null) {
            throw new SourceMapParseException("Unable to retrieve: " + url);
          }
        } else if (section.has("map")) {
          mapSectionContents = section.getString("map");
        } else {
          throw new SourceMapParseException("Invalid map format: section must have either 'map' or 'url'");
        }
        generator.mergeMapSection(line, column, mapSectionContents);
      }

      StringBuilder sb = new StringBuilder();
      try {
        generator.appendTo(sb, file);
      } catch (IOException e) {
        // Can't happen.
        throw new RuntimeException(e);
      }

      parse(sb.toString());
    } catch (IOException ex) {
      throw new SourceMapParseException("IO exception: " + ex);
    } catch (JSONException ex) {
      throw new SourceMapParseException("JSON parse exception: " + ex);
    }
  }

  @Override
  public OriginalMapping getMappingForLine(int lineNumber, int column) {
    // Normalize the line and column numbers to 0.
    lineNumber--;
    column--;

    if (lineNumber < 0 || lineNumber >= lines.size()) {
      return null;
    }

    Preconditions.checkState(lineNumber >= 0);
    Preconditions.checkState(column >= 0);

    // If the line is empty return the previous mapping.
    if (lines.get(lineNumber) == null) {
      return getPreviousMapping(lineNumber);
    }

    ArrayList entries = lines.get(lineNumber);
    // No empty lists.
    Preconditions.checkState(entries.size() > 0);
    if (entries.get(0).getGeneratedColumn() > column) {
      return getPreviousMapping(lineNumber);
    }

    int index = search(entries, column, 0, entries.size() - 1);
    Preconditions.checkState(index >= 0, "unexpected: " + index);
    return getOriginalMappingForEntry(entries.get(index));
  }

  @Override
  public Collection getOriginalSources() {
    return Arrays.asList(sources);
  }

  public String getFile() {
    return file;
  }

  public String getSourceRoot() {
    return sourceRoot;
  }

  
  @Override
  public Collection getReverseMapping(String originalFile, int line, int column) {
    // TODO(user): This implementation currently does not make use of the column
    // parameter.

    // Synchronization needs to be handled by callers.
    if (reverseSourceMapping == null) {
      createReverseMapping();
    }

    Map> sourceLineToCollectionMap = reverseSourceMapping.get(originalFile);

    if (sourceLineToCollectionMap == null) {
      return Collections.emptyList();
    } else {
      Collection mappings = sourceLineToCollectionMap.get(line);

      if (mappings == null) {
        return Collections.emptyList();
      } else {
        return mappings;
      }
    }
  }

  private String[] getJavaStringArray(JSONArray array) throws JSONException {
    int len = array.length();
    String[] result = new String[len];
    for (int i = 0; i < len; i++) {
      result[i] = array.getString(i);
    }
    return result;
  }

  private class MappingBuilder {
    private static final int MAX_ENTRY_VALUES = 5;
    private final StringCharIterator content;
    private int line = 0;
    private int previousCol = 0;
    private int previousSrcId = 0;
    private int previousSrcLine = 0;
    private int previousSrcColumn = 0;
    private int previousNameId = 0;

    MappingBuilder(String lineMap) {
      this.content = new StringCharIterator(lineMap);
    }

    void build() {
      int[] temp = new int[MAX_ENTRY_VALUES];
      ArrayList entries = new ArrayList();
      while (content.hasNext()) {
        // ';' denotes a new line.
        if (tryConsumeToken(';')) {
          // The line is complete, store the result for the line,
          // null if the line is empty.
          ArrayList result;
          if (entries.size() > 0) {
            result = entries;
            // A new array list for the next line.
            entries = new ArrayList();
          } else {
            result = null;
          }
          lines.add(result);
          entries.clear();
          line++;
          previousCol = 0;
        } else {
          // grab the next entry for the current line.
          int entryValues = 0;
          while (!entryComplete()) {
            temp[entryValues] = nextValue();
            entryValues++;
          }
          Entry entry = decodeEntry(temp, entryValues);

          validateEntry(entry);
          entries.add(entry);

          // Consume the separating token, if there is one.
          tryConsumeToken(',');
        }
      }
    }

    /**
     * Sanity check the entry.
     */
    private void validateEntry(Entry entry) {
      Preconditions.checkState(line < lineCount);
      Preconditions.checkState(entry.getSourceFileId() == UNMAPPED || entry.getSourceFileId() < sources.length);
      Preconditions.checkState(entry.getNameId() == UNMAPPED || entry.getNameId() < names.length);
    }

    /**
     * Decodes the next entry, using the previous encountered values to decode
     * the relative values.
     * 
     * @param vals
     *          An array of integers that represent values in the entry.
     * @param entryValues
     *          The number of entries in the array.
     * @return The entry object.
     */
    private Entry decodeEntry(int[] vals, int entryValues) {
      Entry entry;
      switch (entryValues) {
      // The first values, if present are in the following order:
      //   0: the starting column in the current line of the generated file
      //   1: the id of the original source file
      //   2: the starting line in the original source
      //   3: the starting column in the original source
      //   4: the id of the original symbol name
      // The values are relative to the last encountered value for that field.
      // Note: the previously column value for the generated file is reset
      // to '0' when a new line is encountered.  This is done in the 'build'
      // method.

      case 1:
        // An unmapped section of the generated file.
        entry = new UnmappedEntry(vals[0] + previousCol);
        // Set the values see for the next entry.
        previousCol = entry.getGeneratedColumn();
        return entry;

      case 4:
        // A mapped section of the generated file.
        entry = new UnnamedEntry(vals[0] + previousCol, vals[1] + previousSrcId, vals[2] + previousSrcLine, vals[3] + previousSrcColumn);
        // Set the values see for the next entry.
        previousCol = entry.getGeneratedColumn();
        previousSrcId = entry.getSourceFileId();
        previousSrcLine = entry.getSourceLine();
        previousSrcColumn = entry.getSourceColumn();
        return entry;

      case 5:
        // A mapped section of the generated file, that has an associated
        // name.
        entry = new NamedEntry(vals[0] + previousCol, vals[1] + previousSrcId, vals[2] + previousSrcLine, vals[3] + previousSrcColumn, vals[4] + previousNameId);
        // Set the values see for the next entry.
        previousCol = entry.getGeneratedColumn();
        previousSrcId = entry.getSourceFileId();
        previousSrcLine = entry.getSourceLine();
        previousSrcColumn = entry.getSourceColumn();
        previousNameId = entry.getNameId();
        return entry;

      default:
        throw new IllegalStateException("Unexpected number of values for entry:" + entryValues);
      }
    }

    private boolean tryConsumeToken(char token) {
      if (content.hasNext() && content.peek() == token) {
        // consume the comma
        content.next();
        return true;
      }
      return false;
    }

    private boolean entryComplete() {
      if (!content.hasNext()) {
        return true;
      }

      char c = content.peek();
      return (c == ';' || c == ',');
    }

    private int nextValue() {
      return Base64VLQ.decode(content);
    }
  }

  /**
   * Perform a binary search on the array to find a section that covers the
   * target column.
   */
  private int search(ArrayList entries, int target, int start, int end) {
    while (true) {
      int mid = ((end - start) / 2) + start;
      int compare = compareEntry(entries, mid, target);
      if (compare == 0) {
        return mid;
      } else if (compare < 0) {
        // it is in the upper half
        start = mid + 1;
        if (start > end) {
          return end;
        }
      } else {
        // it is in the lower half
        end = mid - 1;
        if (end < start) {
          return end;
        }
      }
    }
  }

  /**
   * Compare an array entry's column value to the target column value.
   */
  private int compareEntry(ArrayList entries, int entry, int target) {
    return entries.get(entry).getGeneratedColumn() - target;
  }

  /**
   * Returns the mapping entry that proceeds the supplied line or null if no
   * such entry exists.
   */
  private OriginalMapping getPreviousMapping(int lineNumber) {
    do {
      if (lineNumber == 0) {
        return null;
      }
      lineNumber--;
    } while (lines.get(lineNumber) == null);
    ArrayList entries = lines.get(lineNumber);
    return getOriginalMappingForEntry(entries.get(entries.size() - 1));
  }

  /**
   * Creates an "OriginalMapping" object for the given entry object.
   */
  private OriginalMapping getOriginalMappingForEntry(Entry entry) {
    if (entry.getSourceFileId() == UNMAPPED) {
      return null;
    } else {
      // Adjust the line/column here to be start at 1.
      Builder x = OriginalMapping.newBuilder().setOriginalFile(sources[entry.getSourceFileId()]).setLineNumber(entry.getSourceLine() + 1).setColumnPosition(entry.getSourceColumn() + 1);
      if (entry.getNameId() != UNMAPPED) {
        x.setIdentifier(names[entry.getNameId()]);
      }
      return x.build();
    }
  }

  /**
   * Reverse the source map; the created mapping will allow us to quickly go
   * from a source file and line number to a collection of target
   * OriginalMappings.
   */
  private void createReverseMapping() {
    reverseSourceMapping = new HashMap>>();

    for (int targetLine = 0; targetLine < lines.size(); targetLine++) {
      ArrayList entries = lines.get(targetLine);

      if (entries != null) {
        for (Entry entry : entries) {
          if (entry.getSourceFileId() != UNMAPPED && entry.getSourceLine() != UNMAPPED) {
            String originalFile = sources[entry.getSourceFileId()];

            if (!reverseSourceMapping.containsKey(originalFile)) {
              reverseSourceMapping.put(originalFile, new HashMap>());
            }

            Map> lineToCollectionMap = reverseSourceMapping.get(originalFile);

            int sourceLine = entry.getSourceLine();

            if (!lineToCollectionMap.containsKey(sourceLine)) {
              lineToCollectionMap.put(sourceLine, new ArrayList(1));
            }

            Collection mappings = lineToCollectionMap.get(sourceLine);

            Builder builder = OriginalMapping.newBuilder().setLineNumber(targetLine).setColumnPosition(entry.getGeneratedColumn());

            mappings.add(builder.build());
          }
        }
      }
    }
  }

  /**
   * A implementation of the Base64VLQ CharIterator used for decoding the
   * mappings encoded in the JSON string.
   */
  private static class StringCharIterator implements CharIterator {
    final String content;
    final int length;
    int current = 0;

    StringCharIterator(String content) {
      this.content = content;
      this.length = content.length();
    }

    @Override
    public char next() {
      return content.charAt(current++);
    }

    char peek() {
      return content.charAt(current);
    }

    @Override
    public boolean hasNext() {
      return current < length;
    }
  }

  /**
   * Represents a mapping entry in the source map.
   */
  private interface Entry {
    int getGeneratedColumn();

    int getSourceFileId();

    int getSourceLine();

    int getSourceColumn();

    int getNameId();
  }

  /**
   * This class represents a portion of the generated file, that is not mapped
   * to a section in the original source.
   */
  private static class UnmappedEntry implements Entry {
    private final int column;

    UnmappedEntry(int column) {
      this.column = column;
    }

    @Override
    public int getGeneratedColumn() {
      return column;
    }

    @Override
    public int getSourceFileId() {
      return UNMAPPED;
    }

    @Override
    public int getSourceLine() {
      return UNMAPPED;
    }

    @Override
    public int getSourceColumn() {
      return UNMAPPED;
    }

    @Override
    public int getNameId() {
      return UNMAPPED;
    }
  }

  /**
   * This class represents a portion of the generated file, that is mapped to a
   * section in the original source.
   */
  private static class UnnamedEntry extends UnmappedEntry {
    private final int srcFile;
    private final int srcLine;
    private final int srcColumn;

    UnnamedEntry(int column, int srcFile, int srcLine, int srcColumn) {
      super(column);
      this.srcFile = srcFile;
      this.srcLine = srcLine;
      this.srcColumn = srcColumn;
    }

    @Override
    public int getSourceFileId() {
      return srcFile;
    }

    @Override
    public int getSourceLine() {
      return srcLine;
    }

    @Override
    public int getSourceColumn() {
      return srcColumn;
    }

    @Override
    public int getNameId() {
      return UNMAPPED;
    }
  }

  /**
   * This class represents a portion of the generated file, that is mapped to a
   * section in the original source, and is associated with a name.
   */
  private static class NamedEntry extends UnnamedEntry {
    private final int name;

    NamedEntry(int column, int srcFile, int srcLine, int srcColumn, int name) {
      super(column, srcFile, srcLine, srcColumn);
      this.name = name;
    }

    @Override
    public int getNameId() {
      return name;
    }
  }

  public static interface EntryVisitor {
    void visit(String sourceName, String symbolName, FilePosition sourceStartPosition, FilePosition startPosition, FilePosition endPosition);
  }

  public void visitMappings(EntryVisitor visitor) {
    boolean pending = false;
    String sourceName = null;
    String symbolName = null;
    FilePosition sourceStartPosition = null;
    FilePosition startPosition = null;

    final int lineCount = lines.size();
    for (int i = 0; i < lineCount; i++) {
      ArrayList line = lines.get(i);
      if (line != null) {
        final int entryCount = line.size();
        for (int j = 0; j < entryCount; j++) {
          Entry entry = line.get(j);
          if (pending) {
            FilePosition endPosition = new FilePosition(i, entry.getGeneratedColumn());
            visitor.visit(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
            pending = false;
          }

          if (entry.getSourceFileId() != UNMAPPED) {
            pending = true;
            sourceName = sources[entry.getSourceFileId()];
            symbolName = (entry.getNameId() != UNMAPPED) ? names[entry.getNameId()] : null;
            sourceStartPosition = new FilePosition(entry.getSourceLine(), entry.getSourceColumn());
            startPosition = new FilePosition(i, entry.getGeneratedColumn());
          }
        }
      }
    }
    //TODO: (closure report) (source map separation) I added this to because last mapping was never visited 
    if (pending) {
      FilePosition endPosition = new FilePosition(startPosition.getLine(), startPosition.getColumn());
      visitor.visit(sourceName, symbolName, sourceStartPosition, startPosition, endPosition);
    }

    //TODO: source map (closure report) - investigate and maybe fill bug to closure - they generate additional mappings to mark ends which is weird.

  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy