Skip to content

Commit

Permalink
Add sourcemap reader (#108)
Browse files Browse the repository at this point in the history
  • Loading branch information
Speedphoenix authored May 2, 2024
1 parent a3920c0 commit 7a15992
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 5 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Currently supported formats are :
| ICO | Windows ICO/CUR File format |||
| JPG | Image file format |||
| LZ4 | Compressed file |||
| MAP | Source Maps |||
| MP3 | Compressed audio |||
| NEKO | NekoVM bytecode |||
| PBJ | PixelBender Binary file |||
Expand Down Expand Up @@ -51,15 +52,15 @@ Package Structure

Each format lies in its own package, for example `format.pdf` contains classes for PDF.

The `format.tools` package contain some tools that might be shared by several formats but don't belong to a specific one.
The `format.tools` package contains some tools that might be shared by several formats but don't belong to a specific one.

Each format must provide the following files :
* one `Data.hx` file that contain only data structures / enums used by the format. If there is really a lot, they can be separated into several files, but it's often my easy for the end user to only have to do one single `import format.xxx.Data` to access to all the defined types.
* one `Reader.hx` class which enable to read the file format from an `haxe.io.Input`
* one `Writer.hx` class which enable to write the file format to an `haxe.io.Output`
* one `Data.hx` file that contains only data structures / enums used by the format. If there are really a lot, they can be separated into several files, but it's often easier for the end user to only have to do one single `import format.xxx.Data` to access to all the defined types.
* one `Reader.hx` class which enables reading the file format from an `haxe.io.Input`
* one `Writer.hx` class which enables writing the file format to an `haxe.io.Output`
* some other classes that might be necessary for manipulating the data structures

It's important in particular that the data structures storing the decoded information are separated from the actual classes manipulating it. This enable full access to all the file format infos and the ability to easily write libraries that manipulate the format, even if later the Reader implementation is changed for example.
It's important in particular that the data structures storing the decoded information are separated from the actual classes manipulating it. This enables full access to all the file format infos and the ability to easily write libraries that manipulate the format, even if later the Reader implementation is changed for example.

Contributing
============
Expand Down
229 changes: 229 additions & 0 deletions format/map/Data.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package format.map;

import haxe.DynamicAccess;

@:allow(format.map.Reader)
class Data {

/** Specification version. The only supported version is 3. */
public var version (default,null) : Int = 3;

/** File with the generated code that this source map is associated with. */
public var file (default,null) : Null<String>;

/** This value is prepended to the individual entries in the `sources` field. */
public var sourceRoot (default,null) : String = '';

/** A list of original source files. */
public var sources (default,null) : Array<String>;

/** A list of contents of files mentioned in `sources` if those files cannot be hosted. */
public var sourcesContent (default,null) : Array<String>;

/** A list of symbol names used in `mappings` */
public var names (default,null) : Array<String>;

/** Decoded mappings data */
var mappings : Array<Array<Mapping>> = [];

function new() {}

/**
* Get position in original source file.
* Returns `null` if provided `line` and/or `column` don't exist in compiled file.
* @param line - `1`-based line number in generated file.
* @param column - zero-based column number in generated file.
*/
public function originalPositionFor (line:Int, column:Int = 0) : Null<SourcePos> {
if (line < 1 || line > mappings.length) return null;

var pos : SourcePos = null;
for (mapping in mappings[line - 1]) {
if (mapping.generatedColumn <= column) {
pos = mapping.getSourcePos(this, line);
break;
}
}

return pos;
}

/**
* Invoke `callback` for each mapped position.
*/
public function eachMapping (callback:SourcePos->Void) {
for (line in 0...mappings.length) {
for (mapping in mappings[line]) {
callback(mapping.getSourcePos(this, line + 1));
}
}
}
}

/**
* Structure of a raw source map data.
*/
abstract DataRaw(DynamicAccess<Any>) to DynamicAccess<Any> {

/** Specification version. The only supported version is 3. */
public var version(get,never) : Int;

/** File with the generated code that this source map is associated with. */
public var file(get,never) : Null<String>;

/** This value is prepended to the individual entries in the `sources` field. */
public var sourceRoot(get,never) : Null<String>;

/** A list of original source files. */
public var sources(get,never) : Array<String>;

/** A list of contents of files mentioned in `sources` if those files cannot be hosted. */
public var sourcesContent(get,never) : Null<Array<String>>;

/** A list of symbol names used in `mappings` */
public var names(get,never) : Array<String>;

/** Encoded mappings data. */
public var mappings(get,never) : String;


inline function get_version() : Int
return this.get('version');

inline function get_file() : Null<String>
return this.get('file');

inline function get_sourceRoot() : Null<String>
return this.get('sourceRoot');

inline function get_sources() : Array<String>
return this.get('sources');

inline function get_sourcesContent() : Null<Array<String>>
return this.get('sourcesContent');

inline function get_names() : Array<String>
return this.get('names');

inline function get_mappings() : String
return this.get('mappings');
}

abstract SourcePos(DynamicAccess<Any>) to DynamicAccess<Any> {

/** Original source file. */
public var source(get,set) : Null<String>;

/** "1"-based line in the original file. */
public var originalLine(get,set) : Null<Int>;

/** Zero-based starting column of the line in the original file. */
public var originalColumn(get,set) : Null<Int>;

/** Zero-based starting column of the line in the generated file. */
public var generatedColumn(get,set) : Int;

/** "1"-based line in the generated file. */
public var generatedLine(get,set) : Int;

/** Original symbol name. */
public var name(get,set) : Null<String>;

public function new() {
this = {};
}

inline function get_source() : Null<String>
return this.get('source');

inline function set_source(v:Null<String>) : Null<String>
return this.set('source', v);

inline function get_originalLine() : Null<Int>
return this.get('originalLine');

inline function set_originalLine(v:Null<Int>) : Null<Int>
return this.set('originalLine', v);

inline function get_originalColumn() : Null<Int>
return this.get('originalColumn');

inline function set_originalColumn(v:Null<Int>) : Null<Int>
return this.set('originalColumn', v);

inline function get_generatedColumn() : Int
return this.get('generatedColumn');

inline function set_generatedColumn(v:Int) : Int
return this.set('generatedColumn', v);

inline function get_generatedLine() : Int
return this.get('generatedLine');

inline function set_generatedLine(v:Int) : Int
return this.set('generatedLine', v);

inline function get_name() : Null<String>
return this.get('name');

inline function set_name(v:Null<String>) : Null<String>
return this.set('name', v);
}

/**
* Represents each group in source map `mappings` field.
*/
abstract Mapping(Array<Int>) {
static inline var GENERATED_COLUMN = 0;
static inline var SOURCE = 1;
static inline var LINE = 2;
static inline var COLUMN = 3;
static inline var NAME = 4;

/** Zero-based starting column of the line in the generated code */
public var generatedColumn (get,never) : Int;
/** Zero-based index into the `sources` list of source map */
public var source (get,never) : Int;
/** Zero-based starting line in the original source represented */
public var line (get,never) : Int;
/** Zero-based starting column of the line in the source represented */
public var column (get,never) : Int;
/** Zero-based index into the `names` list of source map */
public var name (get,never) : Int;

public inline function new (data:Array<Int>) {
this = data;
}

public inline function getSourcePos (map:Data, generatedLine:Int) : SourcePos {
var pos = new SourcePos();
pos.generatedLine = generatedLine;
pos.generatedColumn = generatedColumn;
if (hasSource()) {
pos.originalLine = line + 1;
pos.originalColumn = column;
pos.source = map.sourceRoot + map.sources[source];
if (hasName()) {
pos.name = map.names[name];
}
}
return pos;
}

public inline function hasSource () : Bool return this.length > SOURCE;
public inline function hasLine () : Bool return this.length > LINE;
public inline function hasColumn () : Bool return this.length > COLUMN;
public inline function hasName () : Bool return this.length > NAME;

public inline function offsetGeneratedColumn (offset:Int) this[GENERATED_COLUMN] += offset;
public inline function offsetSource (offset:Int) this[SOURCE] += offset;
public inline function offsetLine (offset:Int) this[LINE] += offset;
public inline function offsetColumn (offset:Int) this[COLUMN] += offset;
public inline function offsetName (offset:Int) this[NAME] += offset;

inline function get_generatedColumn () return this[GENERATED_COLUMN];
inline function get_source () return this[SOURCE];
inline function get_line () return this[LINE];
inline function get_column () return this[COLUMN];
inline function get_name () return this[NAME];
}
72 changes: 72 additions & 0 deletions format/map/Reader.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package format.map;

import format.map.Data;
using format.map.Vlq;

class Reader {
var input : haxe.io.Input;

public function new() {}
public function read (i : haxe.io.Input) : Data {
this.input = i;
var content = i.readAll().toString();
return parse(content);
}

/**
* Parse raw source map data
* @param json - Raw content of source map file
*/
public function parse (json:String): Data {
var rawData: DataRaw = haxe.Json.parse(json);
if (rawData == null) throw new SourceMapException("Failed to parse source map data.");

var ret = new Data();
ret.version = rawData.version;
ret.file = rawData.file;
ret.sourceRoot = (rawData.sourceRoot == null ? '' : rawData.sourceRoot);
ret.sources = rawData.sources;
ret.sourcesContent = (rawData.sourcesContent == null ? [] : rawData.sourcesContent);
ret.names = rawData.names;


var encoded = rawData.mappings.split(';');
//help some platforms to pre-alloc array
ret.mappings[encoded.length - 1] = null;

var previousSource = 0;
var previousLine = 0;
var previousColumn = 0;
var previousName = 0;

for (l in 0...encoded.length) {
ret.mappings[l] = [];
if (encoded[l].length == 0) continue;

var previousGeneratedColumn = 0;

var segments = encoded[l].split(',');
ret.mappings[l][segments.length - 1] = null;

for (s in 0...segments.length) {
var mapping = new Mapping(segments[s].decode());
ret.mappings[l][s] = mapping;
mapping.offsetGeneratedColumn(previousGeneratedColumn);
if (mapping.hasSource()) {
mapping.offsetSource(previousSource);
mapping.offsetLine(previousLine);
mapping.offsetColumn(previousColumn);
if (mapping.hasName()) {
mapping.offsetName(previousName);
previousName = mapping.name;
}
previousLine = mapping.line;
previousSource = mapping.source;
previousColumn = mapping.column;
}
previousGeneratedColumn = mapping.generatedColumn;
}
}
return ret;
}
}
7 changes: 7 additions & 0 deletions format/map/SourceMapException.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package format.map;

import haxe.Exception;

class SourceMapException extends Exception {

}
46 changes: 46 additions & 0 deletions format/map/Vlq.hx
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package format.map;

using StringTools;

class Vlq {
static inline var SHIFT = 5;
static inline var MASK = (1 << SHIFT) - 1;

/**
* Get a number in range 0...64 (excluding)
* @param charCode - A code of a valid base64 character. It's not verified, so be sure it's a valid character.
* @return Int
*/
static inline function base64Decode (charCode:Int):Int {
if ('a'.code <= charCode) return charCode - 'a'.code + 26; //26 is the position of `a` in base64 alphabet
if ('A'.code <= charCode) return charCode - 'A'.code;
if ('0'.code <= charCode) return charCode - '0'.code + 52; //52 is the position of `0` in base64 alphabet
if (charCode == '+'.code) return 62;
return 63; // `/`
}

static public inline function decode (vlq:String):Array<Int> {
var data = [];
var index = -1;
var lastIndex = vlq.length - 1;
while(index < lastIndex) {
var value = 0;
var shift = 0;
var digit, masked;
do {
if(index >= lastIndex) {
throw new format.map.SourceMapException('Failed to parse vlq: $vlq');
}
digit = base64Decode(vlq.fastCodeAt(++index));
masked = digit & MASK;
value += masked << shift;
shift += SHIFT;
} while(digit != masked);

//the least significant bit in VLQ is used to store a sign
data.push(value & 1 == 1 ? -(value >> 1) : value >> 1);
}

return data;
}
}

0 comments on commit 7a15992

Please sign in to comment.