summaryrefslogtreecommitdiff
path: root/tools/shared/src/main/scala/scala/scalajs/tools/sourcemap
diff options
context:
space:
mode:
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.scala144
-rw-r--r--tools/shared/src/main/scala/scala/scalajs/tools/sourcemap/SourceMapWriter.scala213
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')
+}