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

org.daisy.pipeline.css.SourceMapReader Maven / Gradle / Ivy

There is a newer version: 5.3.1
Show newest version
package org.daisy.pipeline.css;

import java.net.URI;
import java.net.URL;
import java.text.CharacterIterator;
import java.text.StringCharacterIterator;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;
import java.util.TreeMap;

import cz.vutbr.web.css.SourceLocator;
import cz.vutbr.web.csskit.antlr.SourceMap;

import mjson.Json;
import mjson.Json.MalformedJsonException;

import org.daisy.common.file.URLs;

/**
 * Reads a source map from a JSON object and returns it as a {@link SourceMap} object.
 */
public final class SourceMapReader {

	private SourceMapReader() {}

	public static SourceMap read(String json, URI base) {
		NavigableMap sourceMap; {
			try {
				Json jsonObject = Json.read(json);
				List sources; {
					Json jsonArray = jsonObject.at("sources");
					if (jsonArray == null)
						throw new RuntimeException("Missing 'sources'");
					if (!jsonArray.isArray())
						throw new RuntimeException("Expected array");
					sources = new ArrayList<>();
					for (Json s : jsonArray.asJsonList()) {
						if (!s.isString())
							throw new RuntimeException("Expected string");
						// the source can be either
						if (s.asString().matches("^\\w+:.*"))
							// an absolute URL (with encoded path)
							sources.add(URLs.asURL(URLs.resolve(base, URLs.asURI(s.asString()))));
						else
							// or a (unencoded) file path (relative to the current working directory)
							try {
								sources.add(URLs.asURL(URLs.resolve(base, new URI(null, null, s.asString(), null)))); }
							catch (java.net.URISyntaxException e) {
								throw new RuntimeException(e); } // should not happen
					}
				}
				CharacterIterator mappings; {
					Json jsonString = jsonObject.at("mappings");
					if (jsonString == null)
						throw new RuntimeException("Missing 'mappings'");
					if (!jsonString.isString())
						throw new RuntimeException("Expected string");
					mappings = new StringCharacterIterator(jsonString.asString());
				}
				sourceMap = new TreeMap<>();
				int line = 0;
				int column = 0;
				int source = 0;
				int sourceLine = 0;
				int sourceColumn = 0;
				char c = mappings.first();
				while (c != CharacterIterator.DONE) {
					if (c != ',' && c != ';') {
						// first value is the column within the generated file
						column += Base64VlqDecoder.readInteger(mappings);
						c = mappings.next();
						if (c != CharacterIterator.DONE && c != ',' && c != ';') {
							// second value is the original file
							source += Base64VlqDecoder.readInteger(mappings);
							if (source < 0)
								throw new RuntimeException("Negative index");
							if (source >= sources.size())
								throw new RuntimeException("Index exceeds size of sources array");
							c = mappings.next();
							if (c == CharacterIterator.DONE || c == ',' || c == ';')
								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
							// third value is the line withing the original file
							sourceLine += Base64VlqDecoder.readInteger(mappings);
							c = mappings.next();
							if (c == CharacterIterator.DONE || c == ',' || c == ';')
								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
							// fourth value is the column withing the original file
							sourceColumn += Base64VlqDecoder.readInteger(mappings);
							c = mappings.next();
							// ignore fifth value if present
							if (c != CharacterIterator.DONE && c != ',' && c != ';')
								c = mappings.next();
							if (c != CharacterIterator.DONE && c != ',' && c != ';')
								throw new RuntimeException("Segment must have of 1, 4 or 5 values");
							LineColumn pos = new LineColumn(line, column);
							if (!sourceMap.containsKey(pos))
								sourceMap.put(pos, new SourceLineColumn(sources.get(source), sourceLine, sourceColumn));
						}
					}
					if (c == ',') {
						c = mappings.next();
						continue;
					}
					if (c == ';') {
						line++;
						column = 0;
						c = mappings.next();
						continue;
					}
					break;
				}
			} catch (MalformedJsonException e) {
				throw new IllegalArgumentException("Source map could not be parsed: " + json, e);
			} catch (RuntimeException e) {
				throw new IllegalArgumentException("Source map could not be parsed: " + json, e);
			}
		}
		return new SourceMap() {
			public SourceLocator get(int line, int column) {
				return sourceMap.get(new LineColumn(line, column));
			}
			public SourceLocator floor(int line, int column) {
				Map.Entry entry = sourceMap.floorEntry(new LineColumn(line, column));
				return entry == null ? null : entry.getValue();
			}
			public SourceLocator ceiling(int line, int column) {
				Map.Entry entry = sourceMap.ceilingEntry(new LineColumn(line, column));
				return entry == null ? null : entry.getValue();
			}
			@Override
			public String toString() {
				return sourceMap.toString();
			}
		};
	}

	private static class LineColumn implements Comparable {
		private final int line;
		private final int column;
		public LineColumn(int line, int column) {
			this.line = line;
			this.column = column;
		}
		public int compareTo(LineColumn that) {
			if (this.line < that.line)
				return -1;
			else if (this.line > that.line)
				return 1;
			else if (this.column < that.column)
				return -1;
			else if (this.column > that.column)
				return 1;
			else
				return 0;
		}
		public String toString() {
			return line + ":" + column;
		}
	}

	private static class SourceLineColumn implements SourceLocator {
		private final URL url;
		private final int line;
		private final int column;
		public SourceLineColumn(URL url, int line, int column) {
			this.url = url;
			this.line = line;
			this.column = column;
		}
		public URL getURL() {
			return url;
		}
		public int getLineNumber() {
			return line;
		}
		public int getColumnNumber() {
			return column;
		}
		public String toString() {
			return url + ":" + line + ":" + column;
		}
	}

	private static class Base64VlqDecoder {

		private static final String base64Decoder = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

		private static int readInteger(CharacterIterator base64String) throws IllegalArgumentException {
			char c = base64String.current();
			if (c == CharacterIterator.DONE)
				throw new RuntimeException("End of string");
			int result = 0;
			int shift = 0;
			while (true) {
				int i = base64Decoder.indexOf(c);
				if (i < 0)
					throw new RuntimeException("Unexpected base64 character: " + c);
				if (i < 32) {
					result += (i << shift);
					break;
				} else {
					i &= 31;
					result += (i << shift);
					shift += 5;
					c = base64String.next();
					if (c == CharacterIterator.DONE)
						throw new RuntimeException("Expected character after continuation character");
				}
			}
			if (result % 2 == 0)
				return result >> 1;
			else
				return - (result >> 1);
		}
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy