// 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; public readonly file: string; public readonly text: string; private _lineStarts: readonly number[] | undefined; private _testFile: Harness.Compiler.TestFile | undefined; constructor(file: string, text: string, meta?: Map) { this.file = file; this.text = text; this.meta = meta || new Map(); } public get lineStarts(): readonly 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())); } 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) }); } } 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: readonly string[] = []; public readonly sourcesContent: readonly string[] | undefined; public readonly mappings: readonly Mapping[] = []; public readonly names: readonly 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, ts.sys.base64decode!(match[1])) : undefined; } public static fromSource(text: string): SourceMap | undefined { const url = this.getUrl(text); return url === undefined ? undefined : this.fromUrl(url); } public getMappingsForEmittedLine(emittedLine: number): readonly Mapping[] | undefined { return this._emittedLineMappings[emittedLine]; } public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): readonly 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; } } }