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    }