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
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
43 if (s.asString().matches("^\\w+:.*"))
44
45 sources.add(URLs.asURL(URLs.resolve(base, URLs.asURI(s.asString()))));
46 else
47
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); }
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
72 column += Base64VlqDecoder.readInteger(mappings);
73 c = mappings.next();
74 if (c != CharacterIterator.DONE && c != ',' && c != ';') {
75
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
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
90 sourceColumn += Base64VlqDecoder.readInteger(mappings);
91 c = mappings.next();
92
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 }