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