1import * as Harness from "./_namespaces/Harness"; 2import * as ts from "./_namespaces/ts"; 3 4// NOTE: The contents of this file are all exported from the namespace 'documents'. This is to 5// support the eventual conversion of harness into a modular system. 6 7export class TextDocument { 8 public readonly meta: Map<string, string>; 9 public readonly file: string; 10 public readonly text: string; 11 12 private _lineStarts: readonly number[] | undefined; 13 private _testFile: Harness.Compiler.TestFile | undefined; 14 15 constructor(file: string, text: string, meta?: Map<string, string>) { 16 this.file = file; 17 this.text = text; 18 this.meta = meta || new Map<string, string>(); 19 } 20 21 public get lineStarts(): readonly number[] { 22 return this._lineStarts || (this._lineStarts = ts.computeLineStarts(this.text)); 23 } 24 25 public static fromTestFile(file: Harness.Compiler.TestFile) { 26 return new TextDocument( 27 file.unitName, 28 file.content, 29 file.fileOptions && Object.keys(file.fileOptions) 30 .reduce((meta, key) => meta.set(key, file.fileOptions[key]), new Map<string, string>())); 31 } 32 33 public asTestFile() { 34 return this._testFile || (this._testFile = { 35 unitName: this.file, 36 content: this.text, 37 fileOptions: Array.from(this.meta) 38 .reduce((obj, [key, value]) => (obj[key] = value, obj), {} as Record<string, string>) 39 }); 40 } 41} 42 43export interface RawSourceMap { 44 version: number; 45 file: string; 46 sourceRoot?: string; 47 sources: string[]; 48 sourcesContent?: string[]; 49 names: string[]; 50 mappings: string; 51} 52 53export interface Mapping { 54 mappingIndex: number; 55 emittedLine: number; 56 emittedColumn: number; 57 sourceIndex: number; 58 sourceLine: number; 59 sourceColumn: number; 60 nameIndex?: number; 61} 62 63export class SourceMap { 64 public readonly raw: RawSourceMap; 65 public readonly mapFile: string | undefined; 66 public readonly version: number; 67 public readonly file: string; 68 public readonly sourceRoot: string | undefined; 69 public readonly sources: readonly string[] = []; 70 public readonly sourcesContent: readonly string[] | undefined; 71 public readonly mappings: readonly Mapping[] = []; 72 public readonly names: readonly string[] | undefined; 73 74 private static readonly _mappingRegExp = /([A-Za-z0-9+/]+),?|(;)|./g; 75 private static readonly _sourceMappingURLRegExp = /^\/\/[#@]\s*sourceMappingURL\s*=\s*(.*?)\s*$/mig; 76 private static readonly _dataURLRegExp = /^data:application\/json;base64,([a-z0-9+/=]+)$/i; 77 private static readonly _base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 78 79 private _emittedLineMappings: Mapping[][] = []; 80 private _sourceLineMappings: Mapping[][][] = []; 81 82 constructor(mapFile: string | undefined, data: string | RawSourceMap) { 83 this.raw = typeof data === "string" ? JSON.parse(data) as RawSourceMap : data; 84 this.mapFile = mapFile; 85 this.version = this.raw.version; 86 this.file = this.raw.file; 87 this.sourceRoot = this.raw.sourceRoot; 88 this.sources = this.raw.sources; 89 this.sourcesContent = this.raw.sourcesContent; 90 this.names = this.raw.names; 91 92 // populate mappings 93 const mappings: Mapping[] = []; 94 let emittedLine = 0; 95 let emittedColumn = 0; 96 let sourceIndex = 0; 97 let sourceLine = 0; 98 let sourceColumn = 0; 99 let nameIndex = 0; 100 let match: RegExpExecArray | null; 101 while (match = SourceMap._mappingRegExp.exec(this.raw.mappings)) { 102 if (match[1]) { 103 const segment = SourceMap._decodeVLQ(match[1]); 104 if (segment.length !== 1 && segment.length !== 4 && segment.length !== 5) { 105 throw new Error("Invalid VLQ"); 106 } 107 108 emittedColumn += segment[0]; 109 if (segment.length >= 4) { 110 sourceIndex += segment[1]; 111 sourceLine += segment[2]; 112 sourceColumn += segment[3]; 113 } 114 115 const mapping: Mapping = { mappingIndex: mappings.length, emittedLine, emittedColumn, sourceIndex, sourceLine, sourceColumn }; 116 if (segment.length === 5) { 117 nameIndex += segment[4]; 118 mapping.nameIndex = nameIndex; 119 } 120 121 mappings.push(mapping); 122 123 const mappingsForEmittedLine = this._emittedLineMappings[mapping.emittedLine] || (this._emittedLineMappings[mapping.emittedLine] = []); 124 mappingsForEmittedLine.push(mapping); 125 126 const mappingsForSource = this._sourceLineMappings[mapping.sourceIndex] || (this._sourceLineMappings[mapping.sourceIndex] = []); 127 const mappingsForSourceLine = mappingsForSource[mapping.sourceLine] || (mappingsForSource[mapping.sourceLine] = []); 128 mappingsForSourceLine.push(mapping); 129 } 130 else if (match[2]) { 131 emittedLine++; 132 emittedColumn = 0; 133 } 134 else { 135 throw new Error(`Unrecognized character '${match[0]}'.`); 136 } 137 } 138 139 this.mappings = mappings; 140 } 141 142 public static getUrl(text: string) { 143 let match: RegExpExecArray | null; 144 let lastMatch: RegExpExecArray | undefined; 145 while (match = SourceMap._sourceMappingURLRegExp.exec(text)) { 146 lastMatch = match; 147 } 148 return lastMatch ? lastMatch[1] : undefined; 149 } 150 151 public static fromUrl(url: string) { 152 const match = SourceMap._dataURLRegExp.exec(url); 153 return match ? new SourceMap(/*mapFile*/ undefined, ts.sys.base64decode!(match[1])) : undefined; 154 } 155 156 public static fromSource(text: string): SourceMap | undefined { 157 const url = this.getUrl(text); 158 return url === undefined ? undefined : this.fromUrl(url); 159 } 160 161 public getMappingsForEmittedLine(emittedLine: number): readonly Mapping[] | undefined { 162 return this._emittedLineMappings[emittedLine]; 163 } 164 165 public getMappingsForSourceLine(sourceIndex: number, sourceLine: number): readonly Mapping[] | undefined { 166 const mappingsForSource = this._sourceLineMappings[sourceIndex]; 167 return mappingsForSource && mappingsForSource[sourceLine]; 168 } 169 170 private static _decodeVLQ(text: string): number[] { 171 const vlq: number[] = []; 172 let shift = 0; 173 let value = 0; 174 for (let i = 0; i < text.length; i++) { 175 const currentByte = SourceMap._base64Chars.indexOf(text.charAt(i)); 176 value += (currentByte & 31) << shift; 177 if ((currentByte & 32) === 0) { 178 vlq.push(value & 1 ? -(value >>> 1) : value >>> 1); 179 shift = 0; 180 value = 0; 181 } 182 else { 183 shift += 5; 184 } 185 } 186 return vlq; 187 } 188}