• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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}