1   package org.daisy.pipeline.css;
2   
3   import java.net.URI;
4   import java.net.URL;
5   import java.text.CharacterIterator;
6   import java.text.StringCharacterIterator;
7   import java.util.ArrayList;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.NavigableMap;
11  import java.util.TreeMap;
12  
13  import cz.vutbr.web.css.SourceLocator;
14  import cz.vutbr.web.csskit.antlr.SourceMap;
15  
16  import mjson.Json;
17  import mjson.Json.MalformedJsonException;
18  
19  import org.daisy.common.file.URLs;
20  
21  /**
22   * Reads a source map from a JSON object and returns it as a {@link SourceMap} object.
23   */
24  public final class SourceMapReader {
25  
26  	private SourceMapReader() {}
27  
28  	public static SourceMap read(String json, URI base) {
29  		NavigableMap<LineColumn,SourceLocator> sourceMap; {
30  			try {
31  				Json jsonObject = Json.read(json);
32  				List<URL> sources; {
33  					Json jsonArray = jsonObject.at("sources");
34  					if (jsonArray == null)
35  						throw new RuntimeException("Missing 'sources'");
36  					if (!jsonArray.isArray())
37  						throw new RuntimeException("Expected array");
38  					sources = new ArrayList<>();
39  					for (Json s : jsonArray.asJsonList()) {
40  						if (!s.isString())
41  							throw new RuntimeException("Expected string");
42  						// the source can be either
43  						if (s.asString().matches("^\\w+:.*"))
44  							// an absolute URL (with encoded path)
45  							sources.add(URLs.asURL(URLs.resolve(base, URLs.asURI(s.asString()))));
46  						else
47  							// or a (unencoded) file path (relative to the current working directory)
48  							try {
49  								sources.add(URLs.asURL(URLs.resolve(base, new URI(null, null, s.asString(), null)))); }
50  							catch (java.net.URISyntaxException e) {
51  								throw new RuntimeException(e); } // should not happen
52  					}
53  				}
54  				CharacterIterator mappings; {
55  					Json jsonString = jsonObject.at("mappings");
56  					if (jsonString == null)
57  						throw new RuntimeException("Missing 'mappings'");
58  					if (!jsonString.isString())
59  						throw new RuntimeException("Expected string");
60  					mappings = new StringCharacterIterator(jsonString.asString());
61  				}
62  				sourceMap = new TreeMap<>();
63  				int line = 0;
64  				int column = 0;
65  				int source = 0;
66  				int sourceLine = 0;
67  				int sourceColumn = 0;
68  				char c = mappings.first();
69  				while (c != CharacterIterator.DONE) {
70  					if (c != ',' && c != ';') {
71  						// first value is the column within the generated file
72  						column += Base64VlqDecoder.readInteger(mappings);
73  						c = mappings.next();
74  						if (c != CharacterIterator.DONE && c != ',' && c != ';') {
75  							// second value is the original file
76  							source += Base64VlqDecoder.readInteger(mappings);
77  							if (source < 0)
78  								throw new RuntimeException("Negative index");
79  							if (source >= sources.size())
80  								throw new RuntimeException("Index exceeds size of sources array");
81  							c = mappings.next();
82  							if (c == CharacterIterator.DONE || c == ',' || c == ';')
83  								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
84  							// third value is the line withing the original file
85  							sourceLine += Base64VlqDecoder.readInteger(mappings);
86  							c = mappings.next();
87  							if (c == CharacterIterator.DONE || c == ',' || c == ';')
88  								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
89  							// fourth value is the column withing the original file
90  							sourceColumn += Base64VlqDecoder.readInteger(mappings);
91  							c = mappings.next();
92  							// ignore fifth value if present
93  							if (c != CharacterIterator.DONE && c != ',' && c != ';')
94  								c = mappings.next();
95  							if (c != CharacterIterator.DONE && c != ',' && c != ';')
96  								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
97  							LineColumn pos = new LineColumn(line, column);
98  							if (!sourceMap.containsKey(pos))
99  								sourceMap.put(pos, new SourceLineColumn(sources.get(source), sourceLine, sourceColumn));
100 						}
101 					}
102 					if (c == ',') {
103 						c = mappings.next();
104 						continue;
105 					}
106 					if (c == ';') {
107 						line++;
108 						column = 0;
109 						c = mappings.next();
110 						continue;
111 					}
112 					break;
113 				}
114 			} catch (MalformedJsonException e) {
115 				throw new IllegalArgumentException("Source map could not be parsed: " + json, e);
116 			} catch (RuntimeException e) {
117 				throw new IllegalArgumentException("Source map could not be parsed: " + json, e);
118 			}
119 		}
120 		return new SourceMap() {
121 			public SourceLocator get(int line, int column) {
122 				return sourceMap.get(new LineColumn(line, column));
123 			}
124 			public SourceLocator floor(int line, int column) {
125 				Map.Entry<LineColumn,SourceLocator> entry = sourceMap.floorEntry(new LineColumn(line, column));
126 				return entry == null ? null : entry.getValue();
127 			}
128 			public SourceLocator ceiling(int line, int column) {
129 				Map.Entry<LineColumn,SourceLocator> entry = sourceMap.ceilingEntry(new LineColumn(line, column));
130 				return entry == null ? null : entry.getValue();
131 			}
132 			@Override
133 			public String toString() {
134 				return sourceMap.toString();
135 			}
136 		};
137 	}
138 
139 	private static class LineColumn implements Comparable<LineColumn> {
140 		private final int line;
141 		private final int column;
142 		public LineColumn(int line, int column) {
143 			this.line = line;
144 			this.column = column;
145 		}
146 		public int compareTo(LineColumn that) {
147 			if (this.line < that.line)
148 				return -1;
149 			else if (this.line > that.line)
150 				return 1;
151 			else if (this.column < that.column)
152 				return -1;
153 			else if (this.column > that.column)
154 				return 1;
155 			else
156 				return 0;
157 		}
158 		public String toString() {
159 			return line + ":" + column;
160 		}
161 	}
162 
163 	private static class SourceLineColumn implements SourceLocator {
164 		private final URL url;
165 		private final int line;
166 		private final int column;
167 		public SourceLineColumn(URL url, int line, int column) {
168 			this.url = url;
169 			this.line = line;
170 			this.column = column;
171 		}
172 		public URL getURL() {
173 			return url;
174 		}
175 		public int getLineNumber() {
176 			return line;
177 		}
178 		public int getColumnNumber() {
179 			return column;
180 		}
181 		public String toString() {
182 			return url + ":" + line + ":" + column;
183 		}
184 	}
185 
186 	private static class Base64VlqDecoder {
187 
188 		private static final String base64Decoder = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
189 
190 		private static int readInteger(CharacterIterator base64String) throws IllegalArgumentException {
191 			char c = base64String.current();
192 			if (c == CharacterIterator.DONE)
193 				throw new RuntimeException("End of string");
194 			int result = 0;
195 			int shift = 0;
196 			while (true) {
197 				int i = base64Decoder.indexOf(c);
198 				if (i < 0)
199 					throw new RuntimeException("Unexpected base64 character: " + c);
200 				if (i < 32) {
201 					result += (i << shift);
202 					break;
203 				} else {
204 					i &= 31;
205 					result += (i << shift);
206 					shift += 5;
207 					c = base64String.next();
208 					if (c == CharacterIterator.DONE)
209 						throw new RuntimeException("Expected character after continuation character");
210 				}
211 			}
212 			if (result % 2 == 0)
213 				return result >> 1;
214 			else
215 				return - (result >> 1);
216 		}
217 	}
218 }