001 /* 002 * Copyright 2010-2015 JetBrains s.r.o. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017 package org.jetbrains.kotlin.js.sourceMap; 018 019 import com.google.dart.compiler.common.SourceInfo; 020 import com.google.dart.compiler.util.TextOutput; 021 import com.intellij.openapi.util.text.StringUtil; 022 import com.intellij.util.PairConsumer; 023 import gnu.trove.TObjectIntHashMap; 024 025 import java.io.File; 026 import java.util.ArrayList; 027 import java.util.List; 028 029 public class SourceMap3Builder implements SourceMapBuilder { 030 private final StringBuilder out = new StringBuilder(8192); 031 private final File generatedFile; 032 private final TextOutput textOutput; 033 private final PairConsumer<SourceMapBuilder, Object> sourceInfoConsumer; 034 035 private String lastSource; 036 private int lastSourceIndex; 037 038 private final TObjectIntHashMap<String> sources = new TObjectIntHashMap<String>() { 039 @Override 040 public int get(String key) { 041 int index = index(key); 042 return index < 0 ? -1 : _values[index]; 043 } 044 }; 045 046 private final List<String> orderedSources = new ArrayList<String>(); 047 048 private int previousGeneratedColumn = -1; 049 private int previousSourceIndex; 050 private int previousSourceLine; 051 private int previousSourceColumn; 052 053 public SourceMap3Builder(File generatedFile, TextOutput textOutput, PairConsumer<SourceMapBuilder, Object> sourceInfoConsumer) { 054 this.generatedFile = generatedFile; 055 this.textOutput = textOutput; 056 this.sourceInfoConsumer = sourceInfoConsumer; 057 } 058 059 @Override 060 public File getOutFile() { 061 return new File(generatedFile.getParentFile(), generatedFile.getName() + ".map"); 062 } 063 064 @Override 065 public String build() { 066 StringBuilder sb = new StringBuilder(out.length() + (128 * orderedSources.size())); 067 sb.append("{\"version\":3,\"file\":\"").append(generatedFile.getName()).append('"').append(','); 068 appendSources(sb); 069 sb.append(",\"names\":["); 070 sb.append("],\"mappings\":\""); 071 sb.append(out); 072 sb.append("\"}"); 073 return sb.toString(); 074 } 075 076 private void appendSources(StringBuilder sb) { 077 boolean isNotFirst = false; 078 sb.append('"').append("sources").append("\":["); 079 for (String source : orderedSources) { 080 if (isNotFirst) { 081 sb.append(','); 082 } 083 else { 084 isNotFirst = true; 085 } 086 sb.append('"').append("file://").append(source).append('"'); 087 } 088 sb.append(']'); 089 } 090 091 @Override 092 public void newLine() { 093 out.append(';'); 094 previousGeneratedColumn = -1; 095 } 096 097 @Override 098 public void skipLinesAtBeginning(int count) { 099 out.insert(0, StringUtil.repeatSymbol(';', count)); 100 } 101 102 @Override 103 public void processSourceInfo(Object sourceInfo) { 104 if (sourceInfo instanceof SourceInfo) { 105 throw new UnsupportedOperationException("SourceInfo is not yet supported"); 106 } 107 sourceInfoConsumer.consume(this, sourceInfo); 108 } 109 110 private int getSourceIndex(String source) { 111 if (source.equals(lastSource)) { 112 return lastSourceIndex; 113 } 114 115 int sourceIndex = sources.get(source); 116 if (sourceIndex == -1) { 117 sourceIndex = orderedSources.size(); 118 sources.put(source, sourceIndex); 119 orderedSources.add(source); 120 } 121 122 lastSource = source; 123 lastSourceIndex = sourceIndex; 124 125 return sourceIndex; 126 } 127 128 @Override 129 public void addMapping(String source, int sourceLine, int sourceColumn) { 130 if (previousGeneratedColumn == -1) { 131 previousGeneratedColumn = 0; 132 } 133 else { 134 out.append(','); 135 } 136 137 int columnDiff = textOutput.getColumn() - previousGeneratedColumn; 138 // TODO fix sections overlapping 139 // assert columnDiff != 0; 140 Base64VLQ.encode(out, columnDiff); 141 previousGeneratedColumn = textOutput.getColumn(); 142 int sourceIndex = getSourceIndex(source); 143 Base64VLQ.encode(out, sourceIndex - previousSourceIndex); 144 previousSourceIndex = sourceIndex; 145 146 Base64VLQ.encode(out, sourceLine - previousSourceLine); 147 previousSourceLine = sourceLine; 148 149 Base64VLQ.encode(out, sourceColumn - previousSourceColumn); 150 previousSourceColumn = sourceColumn; 151 } 152 153 @Override 154 public void addLink() { 155 textOutput.print("\n//@ sourceMappingURL="); 156 textOutput.print(generatedFile.getName()); 157 textOutput.print(".map\n"); 158 } 159 160 private static final class Base64VLQ { 161 // A Base64 VLQ digit can represent 5 bits, so it is base-32. 162 private static final int VLQ_BASE_SHIFT = 5; 163 private static final int VLQ_BASE = 1 << VLQ_BASE_SHIFT; 164 165 // A mask of bits for a VLQ digit (11111), 31 decimal. 166 private static final int VLQ_BASE_MASK = VLQ_BASE - 1; 167 168 // The continuation bit is the 6th bit. 169 private static final int VLQ_CONTINUATION_BIT = VLQ_BASE; 170 171 @SuppressWarnings("SpellCheckingInspection") 172 private static final char[] BASE64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); 173 174 private Base64VLQ() { 175 } 176 177 private static int toVLQSigned(int value) { 178 return value < 0 ? ((-value) << 1) + 1 : value << 1; 179 } 180 181 public static void encode(StringBuilder out, int value) { 182 value = toVLQSigned(value); 183 do { 184 int digit = value & VLQ_BASE_MASK; 185 value >>>= VLQ_BASE_SHIFT; 186 if (value > 0) { 187 digit |= VLQ_CONTINUATION_BIT; 188 } 189 out.append(BASE64_MAP[digit]); 190 } 191 while (value > 0); 192 } 193 } 194 }