TypeScript/src/harness/documents.ts
2018-04-24 10:48:55 -07:00

187 lines
7.6 KiB
TypeScript

// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to
// support the eventual conversion of harness into a modular system.
namespace documents {
export class TextDocument {
public readonly meta: Map<string, string>;
public readonly file: string;
public readonly text: string;
private _lineStarts: ReadonlyArray<number> | undefined;
private _testFile: Harness.Compiler.TestFile | undefined;
constructor(file: string, text: string, meta?: Map<string, string>) {
this.file = file;
this.text = text;
this.meta = meta || new Map<string, string>();
}
public get lineStarts(): ReadonlyArray<number> {
return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text));
}
public static fromTestFile(file: Harness.Compiler.TestFile) {
return new TextDocument(
file.unitName,
file.content,
file.fileOptions && Object.keys(file.fileOptions)
.reduce((meta, key) => meta.set(key, file.fileOptions[key]), new Map<string, string>()));
}
public asTestFile() {
return this._testFile || (this._testFile = {
unitName: this.file,
content: this.text,
fileOptions: Array.from(this.meta)
.reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record<string, string>)
});
}
}
export interface RawSourceMap {
version: number;
file: string;
sourceRoot?: string;
sources: string[];
sourcesContent?: string[];
names: string[];
mappings: string;
}
export interface Mapping {
mappingIndex: number;
emittedLine: number;
emittedColumn: number;
sourceIndex: number;
sourceLine: number;
sourceColumn: number;
nameIndex?: number;
}
export class SourceMap {
public readonly raw: RawSourceMap;
public readonly mapFile: string | undefined;
public readonly version: number;
public readonly file: string;
public readonly sourceRoot: string | undefined;
public readonly sources: ReadonlyArray<string> = [];
public readonly sourcesContent: ReadonlyArray<string> | undefined;
public readonly mappings: ReadonlyArray<Mapping> = [];
public readonly names: ReadonlyArray<string> | undefined;
private static readonly _mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g;
private static readonly _sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig;
private static readonly _dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i;
private static readonly _base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
private _emittedLineMappings: Mapping[][] = [];
private _sourceLineMappings: Mapping[][][] = [];
constructor(mapFile: string | undefined, data: string | RawSourceMap) {
this.raw = typeof data === "string" ? JSON.parse(data) as RawSourceMap : data;
this.mapFile = mapFile;
this.version = this.raw.version;
this.file = this.raw.file;
this.sourceRoot = this.raw.sourceRoot;
this.sources = this.raw.sources;
this.sourcesContent = this.raw.sourcesContent;
this.names = this.raw.names;
// populate mappings
const mappings: Mapping[] = [];
let emittedLine = 0;
let emittedColumn = 0;
let sourceIndex = 0;
let sourceLine = 0;
let sourceColumn = 0;
let nameIndex = 0;
let match: RegExpExecArray | null;
while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) {
if (match[1]) {
const segment = SourceMap._decodeVLQ(match[1]);
if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) {
throw new Error("Invalid VLQ");
}
emittedColumn += segment[0];
if (segment.length >= 4) {
sourceIndex += segment[1];
sourceLine += segment[2];
sourceColumn += segment[3];
}
const mapping: Mapping = { mappingIndex: mappings.length, emittedLine, emittedColumn, sourceIndex, sourceLine, sourceColumn };
if (segment.length === 5) {
nameIndex += segment[4];
mapping.nameIndex = nameIndex;
}
mappings.push(mapping);
const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []);
mappingsForEmittedLine.push(mapping);
const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []);
const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []);
mappingsForSourceLine.push(mapping);
}
else if (match[2]) {
emittedLine++;
emittedColumn = 0;
}
else {
throw new Error(`Unrecognized character '${match[0]}'.`);
}
}
this.mappings = mappings;
}
public static getUrl(text: string) {
let match: RegExpExecArray | null;
let lastMatch: RegExpExecArray | undefined;
while (match = SourceMap._sourceMappingURLRegExp.exec(text)) {
lastMatch = match;
}
return lastMatch ? lastMatch[1] : undefined;
}
public static fromUrl(url: string) {
const match = SourceMap._dataURLRegExp.exec(url);
return match ? new SourceMap(/*mapFile*/ undefined, new Buffer(match[1], "base64").toString("utf8")) : undefined;
}
public static fromSource(text: string) {
const url = this.getUrl(text);
return url && this.fromUrl(url);
}
public getMappingsForEmittedLine(emittedLine: number): ReadonlyArray<Mapping> | undefined {
return this._emittedLineMappings[emittedLine];
}
public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): ReadonlyArray<Mapping> | undefined {
const mappingsForSource = this._sourceLineMappings[sourceIndex];
return mappingsForSource && mappingsForSource[sourceLine];
}
private static _decodeVLQ(text: string): number[] {
const vlq: number[] = [];
let shift = 0;
let value = 0;
for (let i = 0; i < text.length; i++) {
const currentByte = SourceMap._base64Chars.indexOf(text.charAt(i));
value += (currentByte & 31) << shift;
if ((currentByte & 32) === 0) {
vlq.push(value & 1 ? -(value >>> 1) : value >>> 1);
shift = 0;
value = 0;
}
else {
shift += 5;
}
}
return vlq;
}
}
}