diff options
Diffstat (limited to 'tools/shared/src/main/scala/scala/scalajs/tools/sourcemap')
-rw-r--r-- | tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala | 144 | ||||
-rw-r--r-- | tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala | 213 |
2 files changed, 357 insertions, 0 deletions
diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala new file mode 100644 index 0000000..1bf2254 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/JSFileBuilder.scala @@ -0,0 +1,144 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2013-2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sourcemap + +import scala.annotation.tailrec + +import scala.collection.mutable + +import java.io._ +import java.util.regex.Pattern +import java.net.{ URI, URISyntaxException } + +import scala.scalajs.ir.Position +import scala.scalajs.tools.{javascript => js} +import scala.scalajs.tools.io._ +import scala.scalajs.tools.optimizer.JSTreeBuilder + +class JSFileBuilder(val name: String, + protected val outputWriter: Writer) extends JSTreeBuilder { + def addLine(line: String): Unit = { + outputWriter.write(line) + outputWriter.write('\n') + } + + def addLines(lines: Seq[String]): Unit = + lines.foreach(addLine) + + def addFile(file: VirtualJSFile): Unit = + addPartsOfFile(file)(!_.startsWith("//# sourceMappingURL=")) + + def addPartsOfFile(file: VirtualJSFile)(selector: String => Boolean): Unit = { + for (line <- file.readLines() if selector(line)) + addLine(line) + } + + /** Add a JavaScript tree representing a statement. + * The tree must be a valid JavaScript tree (typically obtained by + * desugaring a full-fledged IR tree). + */ + def addJSTree(tree: js.Trees.Tree): Unit = { + val printer = new js.Printers.JSTreePrinter(outputWriter) + printer.printTopLevelTree(tree) + // Do not close the printer: we do not have ownership of the writers + } + + /** Closes the underlying writer(s). + */ + def closeWriters(): Unit = { + outputWriter.close() + } +} + +class JSFileBuilderWithSourceMapWriter(n: String, ow: Writer, + protected val sourceMapWriter: SourceMapWriter) + extends JSFileBuilder(n, ow) { + + override def addLine(line: String): Unit = { + super.addLine(line) + sourceMapWriter.nextLine() + } + + private final val NotSelected = -1 + + override def addPartsOfFile(file: VirtualJSFile)( + selector: String => Boolean): Unit = { + val br = new BufferedReader(file.reader) + try { + // Select lines, and remember offsets + val offsets = new mutable.ArrayBuffer[Int] // (maybe NotSelected) + val selectedLineLengths = new mutable.ArrayBuffer[Int] + var line: String = br.readLine() + var selectedCount = 0 + while (line != null) { + if (selector(line)) { + super.addLine(line) // super call not to advance line in source map + offsets += selectedCount + selectedLineLengths += line.length + selectedCount += 1 + } else { + offsets += NotSelected + } + line = br.readLine() + } + + /* We ignore a potential source map. + * This happens typically for corejslib.js and other helper files + * written directly in JS. + * We generate a fake line-by-line source map for these on the fly + */ + val sourceFile = file.toURI + + for (lineNumber <- 0 until offsets.size) { + val offset = offsets(lineNumber) + if (offset != NotSelected) { + val originalPos = Position(sourceFile, lineNumber, 0) + sourceMapWriter.startNode(0, originalPos, None) + sourceMapWriter.endNode(selectedLineLengths(offset)) + sourceMapWriter.nextLine() + } + } + } finally { + br.close() + } + } + + override def addJSTree(tree: js.Trees.Tree): Unit = { + val printer = new js.Printers.JSTreePrinterWithSourceMap( + outputWriter, sourceMapWriter) + printer.printTopLevelTree(tree) + // Do not close the printer: we do not have ownership of the writers + } + + override def complete(): Unit = { + super.complete() + sourceMapWriter.complete() + } + +} + +class JSFileBuilderWithSourceMap(n: String, ow: Writer, + sourceMapOutputWriter: Writer, + relativizeSourceMapBasePath: Option[URI] = None) + extends JSFileBuilderWithSourceMapWriter( + n, ow, + new SourceMapWriter(sourceMapOutputWriter, n, + relativizeSourceMapBasePath)) { + + override def complete(): Unit = { + addLine("//# sourceMappingURL=" + name + ".map") + super.complete() + } + + override def closeWriters(): Unit = { + super.closeWriters() + sourceMapOutputWriter.close() + } +} diff --git a/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala new file mode 100644 index 0000000..5d8bdb1 --- /dev/null +++ b/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala @@ -0,0 +1,213 @@ +/* __ *\ +** ________ ___ / / ___ __ ____ Scala.js tools ** +** / __/ __// _ | / / / _ | __ / // __/ (c) 2014, LAMP/EPFL ** +** __\ \/ /__/ __ |/ /__/ __ |/_// /_\ \ http://scala-js.org/ ** +** /____/\___/_/ |_/____/_/ | |__/ /____/ ** +** |/____/ ** +\* */ + + +package scala.scalajs.tools.sourcemap + +import java.io.Writer +import java.net.URI + +import scala.collection.mutable.{ ListBuffer, HashMap, Stack, StringBuilder } + +import scala.scalajs.ir +import ir.Position +import ir.Position._ +import ir.Utils + +object SourceMapWriter { + private val Base64Map = + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + + "abcdefghijklmnopqrstuvwxyz" + + "0123456789+/" + + // Some constants for writeBase64VLQ + // Each base-64 digit covers 6 bits, but 1 is used for the continuation + private final val VLQBaseShift = 5 + private final val VLQBase = 1 << VLQBaseShift + private final val VLQBaseMask = VLQBase - 1 + private final val VLQContinuationBit = VLQBase + + private def jsonString(s: String) = + "\"" + Utils.escapeJS(s) + "\"" +} + +class SourceMapWriter( + val out: Writer, + val generatedFile: String, + val relativizeBaseURI: Option[URI] = None) { + + import SourceMapWriter._ + + private val sources = new ListBuffer[String] + private val _srcToIndex = new HashMap[SourceFile, Int] + + private val names = new ListBuffer[String] + private val _nameToIndex = new HashMap[String, Int] + + private val nodePosStack = new Stack[(Position, Option[String])] + nodePosStack.push((NoPosition, None)) + + private var lineCountInGenerated = 0 + private var lastColumnInGenerated = 0 + private var firstSegmentOfLine = true + private var lastSource: SourceFile = null + private var lastSourceIndex = 0 + private var lastLine: Int = 0 + private var lastColumn: Int = 0 + private var lastNameIndex: Int = 0 + + private var pendingColumnInGenerated: Int = -1 + private var pendingPos: Position = NoPosition + private var pendingName: Option[String] = None + + writeHeader() + + private def sourceToIndex(source: SourceFile) = + _srcToIndex.getOrElseUpdate(source, + (sources += sourceToURI(source)).size-1) + + /** Relatively hacky way to get a Web-friendly URI to the source file */ + private def sourceToURI(source: SourceFile): String = { + val uri = source + val relURI = relativizeBaseURI.fold(uri)(Utils.relativize(_, uri)) + + Utils.fixFileURI(relURI).toASCIIString + } + + private def nameToIndex(name: String) = + _nameToIndex.getOrElseUpdate(name, (names += name).size-1) + + private def writeHeader(): Unit = { + out.write("{\n") + out.write("\"version\": 3,\n") + out.write("\"file\": " + jsonString(generatedFile) + ",\n") + out.write("\"mappings\": \"") + } + + def nextLine(): Unit = { + writePendingSegment() + out.write(';') + lineCountInGenerated += 1 + lastColumnInGenerated = 0 + firstSegmentOfLine = true + pendingColumnInGenerated = -1 + pendingPos = nodePosStack.top._1 + pendingName = nodePosStack.top._2 + } + + def startNode(column: Int, originalPos: Position, + originalName: Option[String] = None): Unit = { + nodePosStack.push((originalPos, originalName)) + startSegment(column, originalPos, originalName) + } + + def endNode(column: Int): Unit = { + nodePosStack.pop() + startSegment(column, nodePosStack.top._1, nodePosStack.top._2) + } + + private def startSegment(startColumn: Int, originalPos: Position, + originalName: Option[String]): Unit = { + // There is no point in outputting a segment with the same information + if ((originalPos == pendingPos) && (originalName == pendingName)) + return + + // Write pending segment if it covers a non-empty range + if (startColumn != pendingColumnInGenerated) + writePendingSegment() + + // New pending + pendingColumnInGenerated = startColumn + pendingPos = originalPos + pendingName = + if (startColumn != pendingColumnInGenerated) originalName + else pendingName orElse originalName + } + + private def writePendingSegment() { + if (pendingColumnInGenerated < 0) + return + + // Segments of a line are separated by ',' + if (firstSegmentOfLine) firstSegmentOfLine = false + else out.write(',') + + // Generated column field + writeBase64VLQ(pendingColumnInGenerated-lastColumnInGenerated) + lastColumnInGenerated = pendingColumnInGenerated + + // If the position is NoPosition, stop here + if (!pendingPos.isDefined) + return + + // Extract relevant properties of pendingPos + val source = pendingPos.source + val line = pendingPos.line + val column = pendingPos.column + + // Source index field + if (source == lastSource) { // highly likely + writeBase64VLQ0() + } else { + val sourceIndex = sourceToIndex(source) + writeBase64VLQ(sourceIndex-lastSourceIndex) + lastSource = source + lastSourceIndex = sourceIndex + } + + // Line field + writeBase64VLQ(line - lastLine) + lastLine = line + + // Column field + writeBase64VLQ(column - lastColumn) + lastColumn = column + + // Name field + if (pendingName.isDefined) { + val nameIndex = nameToIndex(pendingName.get) + writeBase64VLQ(nameIndex-lastNameIndex) + lastNameIndex = nameIndex + } + } + + def complete(): Unit = { + writePendingSegment() + + out.write("\",\n") + out.write( + sources.map(jsonString(_)).mkString("\"sources\": [", ", ", "],\n")) + out.write( + names.map(jsonString(_)).mkString("\"names\": [", ", ", "],\n")) + out.write("\"lineCount\": "+lineCountInGenerated+"\n") + out.write("}\n") + } + + /** Write the Base 64 VLQ of an integer to the mappings + * Inspired by the implementation in Closure Compiler: + * http://code.google.com/p/closure-compiler/source/browse/src/com/google/debugging/sourcemap/Base64VLQ.java + */ + private def writeBase64VLQ(value0: Int): Unit = { + // Sign is encoded in the least significant bit + var value = + if (value0 < 0) ((-value0) << 1) + 1 + else value0 << 1 + + // Write as many base-64 digits as necessary to encode value + do { + var digit = value & VLQBaseMask + value = value >>> VLQBaseShift + if (value != 0) + digit |= VLQContinuationBit + out.write(Base64Map.charAt(digit)) + } while (value != 0) + } + + private def writeBase64VLQ0(): Unit = + out.write('A') +} |