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