{ ********************************************************************* This file is part of the Free Component Library (FCL) Copyright (c) 2018 Mattias Gaertner. Javascript Source Map See Source Maps Revision 3: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1# See the file COPYING.FPC, included in this distribution, for details about the copyright. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. **********************************************************************} unit JSSrcMap; {$mode objfpc}{$H+} {$ifdef fpc} {$define UsePChar} {$define HasJsonParser} {$define HasStreams} {$define HasFS} {$endif} {$ifdef pas2js} {$ifdef nodejs} {$define HasFS} {$endif} {$endif} interface uses {$ifdef pas2js} JS, {$ifdef nodejs} Node.FS, {$endif} {$else} contnrs, {$endif} Classes, SysUtils, fpjson {$ifdef HasJsonParser} , jsonparser, jsonscanner {$endif} ; const Base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; type EJSSourceMap = class(Exception); { TSourceMapSegment } TSourceMapSegment = class public Index: integer; // index in Items GeneratedLine: integer; // 1-based GeneratedColumn: integer; // 0-based SrcFileIndex: integer; // index in FSources SrcLine: integer; SrcColumn: integer; NameIndex: integer; // index in FNames end; TSourceMapSrc = class public Filename: string; // as added by AddMapping TranslatedFilename: string; // same as Filename, can be altered, written to JSON Source: String; end; TSourceMapOption = ( smoAddMonotonous, // true = AddMapping GeneratedLine/Col must be behind last add, false = check all adds for duplicate smoAutoLineStart, // automatically add a first column mapping, repeating last mapping smoSafetyHeader, // insert ')]}' at start smoAllowSrcLine0 // don't bark on SrcLine=0 ); TSourceMapOptions = set of TSourceMapOption; const DefaultSourceMapOptions = [smoAddMonotonous,smoSafetyHeader]; type { TSourceMap } TSourceMap = class private type { TStringToIndex } TStringToIndex = class private FItems: {$ifdef pas2js}TJSObject{$else}TFPHashList{$endif}; public constructor Create; destructor Destroy; override; procedure Clear; procedure Add(const Value: String; Index: integer); function FindValue(const Value: String): integer; end; private FGeneratedFilename: string; FNames: TStrings; // in adding order FNameToIndex: TStringToIndex; // name to index in FNames FItems: TFPList; // TSourceMapSegment, in adding order FOptions: TSourceMapOptions; FSorted: boolean; FSourceRoot: string; FSources: TFPList; // list of TSourceMapSrc, in adding order FSourceToIndex: TStringToIndex; // srcfile to index in FSources FVersion: integer; function GetNames(Index: integer): string; function GetItems(Index: integer): TSourceMapSegment; function GetSourceContents(Index: integer): String; function GetSourceFiles(Index: integer): String; function GetSourceTranslatedFiles(Index: integer): String; procedure SetGeneratedFilename(const AValue: string); procedure SetSorted(const AValue: boolean); procedure SetSourceContents(Index: integer; const AValue: String); procedure SetSourceTranslatedFiles(Index: integer; const AValue: String); procedure Sort; public constructor Create(const aGeneratedFilename: string); destructor Destroy; override; procedure Clear; virtual; function AddMapping( GeneratedLine: integer; // 1-based GeneratedCol: integer = 0; // 0-based const SourceFile: string = ''; // can be empty '' SrcLine: integer = 1; // 1-based SrcCol: integer = 0; // 0-based const Name: String = ''): TSourceMapSegment; virtual; function CreateMappings: String; virtual; procedure ParseMappings(const Mapping: String); virtual; function ToJSON: TJSONObject; virtual; function ToString: string; override; procedure LoadFromJSON(Obj: TJSONObject); virtual; procedure SaveToStream(aStream: TFPJSStream); virtual; {$ifdef HasStreams} procedure LoadFromStream(aStream: TStream); virtual; procedure SaveToFile(Filename: string); virtual; procedure LoadFromFile(Filename: string); virtual; {$endif} property GeneratedFilename: string read FGeneratedFilename write SetGeneratedFilename; function IndexOfName(const Name: string; AddIfNotExists: boolean = false): integer; function IndexOfSourceFile(const SrcFile: string; AddIfNotExists: boolean = false): integer; function IndexOfSegmentAt(GeneratedLine, GeneratedCol: integer): integer; function Count: integer; // segments property Items[Index: integer]: TSourceMapSegment read GetItems; default; // segments function SourceCount: integer; property SourceRoot: string read FSourceRoot write FSourceRoot; property SourceFiles[Index: integer]: String read GetSourceFiles; property SourceTranslatedFiles[Index: integer]: String read GetSourceTranslatedFiles write SetSourceTranslatedFiles; property SourceContents[Index: integer]: String read GetSourceContents write SetSourceContents; function NameCount: integer; property Names[Index: integer]: string read GetNames; property Version: integer read FVersion; // 3 property Options: TSourceMapOptions read FOptions write FOptions; property Sorted: boolean read FSorted write SetSorted; // Segments are sorted for GeneratedLine/Col end; function DefaultSrcMapHeader: string; function EncodeBase64VLQ(i: NativeInt): String; // base64 Variable Length Quantity function DecodeBase64VLQ(const s: string): NativeInt; // base64 Variable Length Quantity function DecodeBase64VLQ( {$ifdef UsePChar}var p: PChar{$else}const s: string; var p: integer{$endif}): NativeInt; // base64 Variable Length Quantity function CompareSegmentWithGeneratedLineCol( Item1, Item2: {$ifdef pas2js}jsvalue{$else}Pointer{$endif}): Integer; procedure DebugSrcMapLine(GeneratedLine: integer; var GeneratedLineSrc: String; SrcMap: TSourceMap; out InfoLine: String); implementation function DefaultSrcMapHeader: string; begin Result:=')]}'''+LineEnding; end; function EncodeBase64VLQ(i: NativeInt): String; { Convert signed number to base64-VLQ: Each base64 has 6bit, where the most significant bit is the continuation bit (1=there is a next base64 character). The first character contains the sign bit in the last bit (1=negative) and the 5 least significant bits of the number. For example: A = 0 = %000000 => 0 B = 1 = %000001 => -0 C = 2 = %000010 => 1 iF = 34 5 = %100010 %000101 = + 0001 00101 = 100101 = 37 } procedure RaiseRange; begin raise ERangeError.Create('EncodeBase64VLQ'); end; var digits: NativeInt; begin Result:=''; if i<0 then begin i:=-i; if i>(High(NativeInt)-1) shr 1 then RaiseRange; i:=(i shl 1)+1; end else begin if i>High(NativeInt) shr 1 then RaiseRange; i:=i shl 1; end; repeat digits:=i and %11111; i:=i shr 5; if i>0 then inc(digits,%100000); // need another char -> set continuation bit Result:=Result+Base64Chars[digits+1]; until i=0; end; function DecodeBase64VLQ(const s: string): NativeInt; var {$ifdef UsePChar} p: PChar; {$else} p: integer; {$endif} begin if s='' then raise EConvertError.Create('DecodeBase64VLQ empty'); {$ifdef UsePChar} p:=PChar(s); Result:=DecodeBase64VLQ(p); if p-PChar(s)<>length(s) then raise EConvertError.Create('DecodeBase64VLQ waste'); {$else} p:=1; Result:=DecodeBase64VLQ(s,p); {$endif} end; function DecodeBase64VLQ( {$ifdef UsePChar}var p: PChar{$else}const s: string; var p: integer{$endif}): NativeInt; { Convert base64-VLQ to signed number, For the fomat see EncodeBase64VLQ } var {$ifdef UsePChar} run: PChar; {$else} run, l: integer; {$endif} procedure RaiseInvalid; begin p:=run; raise ERangeError.Create('DecodeBase64VLQ'); end; const MaxShift = {$ifdef pas2js}32{$else}63{$endif}-5; // actually log2(High(NativeInt))-5 var c: Char; digit, Shift: Integer; begin Result:=0; Shift:=0; run:=p; {$ifdef UsePChar} {$else} l:=length(s); {$endif} repeat {$ifdef UsePChar} c:=run^; {$else} if run>l then RaiseInvalid; c:=s[run]; {$endif} case c of 'A'..'Z': digit:=ord(c)-ord('A'); 'a'..'z': digit:=ord(c)-ord('a')+26; '0'..'9': digit:=ord(c)-ord('0')+52; '+': digit:=62; '/': digit:=63; else RaiseInvalid; end; inc(run); if Shift>MaxShift then RaiseInvalid; inc(Result,(digit and %11111) shl Shift); inc(Shift,5); until digit<%100000; if (Result and 1)>0 then Result:=-(Result shr 1) else Result:=Result shr 1; p:=run; end; function CompareSegmentWithGeneratedLineCol( Item1, Item2: {$ifdef pas2js}jsvalue{$else}Pointer{$endif}): Integer; var Seg1: TSourceMapSegment absolute Item1; Seg2: TSourceMapSegment absolute Item2; begin if Seg1.GeneratedLineSeg2.GeneratedLine then Result:=1 else if Seg1.GeneratedColumnSeg2.GeneratedColumn then Result:=1 // compare Index to keep adding order else if Seg1.IndexSeg2.Index then Result:=1 else Result:=0; end; procedure DebugSrcMapLine(GeneratedLine: integer; var GeneratedLineSrc: String; SrcMap: TSourceMap; out InfoLine: String); var JS, Origins, Addition: String; GeneratedCol: integer; // 0-based i, diff, GenColStep, LastSrcFile, LastSrcLine: Integer; aSeg: TSourceMapSegment; begin InfoLine:=''; JS:=GeneratedLineSrc; Origins:=''; GeneratedCol:=0;// 0-based LastSrcFile:=0; LastSrcLine:=-1; i:=SrcMap.IndexOfSegmentAt(GeneratedLine,GeneratedCol); aSeg:=nil; if i<0 then begin // no segment at line start i:=0; if (i=SrcMap.Count) then aSeg:=nil else aSeg:=SrcMap[i]; if (aSeg=nil) or (aSeg.GeneratedLine>GeneratedLine) then begin // no segment in line for i:=1 to length(JS) do Origins:=Origins+'?'; GeneratedLineSrc:=JS; InfoLine:=Origins; exit; end else begin // show "?" til start of first segment for i:=1 to aSeg.GeneratedColumn do Origins:=Origins+'?'; end; end else begin aSeg:=SrcMap[i]; if i>0 then LastSrcFile:=SrcMap[i-1].SrcFileIndex; end; repeat Addition:=''; if (aSeg.GeneratedLine=GeneratedLine) and (aSeg.GeneratedColumn=GeneratedCol) then begin // segment starts here -> write "|line,col" Addition:='|'; if LastSrcFile<>aSeg.SrcFileIndex then begin Addition:=Addition+{$ifdef HasFS}ExtractFileName{$endif}(SrcMap.SourceFiles[aSeg.SrcFileIndex])+','; LastSrcFile:=aSeg.SrcFileIndex; end; if LastSrcLine<>aSeg.SrcLine then begin Addition:=Addition+IntToStr(aSeg.SrcLine)+','; LastSrcLine:=aSeg.SrcLine; end; Addition:=Addition+IntToStr(aSeg.SrcColumn); Origins:=Origins+Addition; end; inc(i); // skip segments at same GeneratedLine/Col while (iGeneratedLine) then begin // in the last segment while length(Origins) align JS and Origins GenColStep:=aSeg.GeneratedColumn-GeneratedCol; diff:=GenColStep-length(Addition); if diff<0 then // for example: // JS: if(~~e)~~~{ // Origins: |12,3|12,5|12,7 Insert(StringOfChar('~',-diff),JS,length(Origins)-length(Addition)+1+GenColStep) else while diff>0 do begin Origins:=Origins+'.'; dec(diff); end; GeneratedCol:=aSeg.GeneratedColumn; until false; end; { TSourceMap.TStringToIndex } constructor TSourceMap.TStringToIndex.Create; begin {$ifdef pas2js} FItems:=TJSObject.new; {$else} FItems:=TFPHashList.Create; {$endif} end; destructor TSourceMap.TStringToIndex.Destroy; begin {$ifdef pas2js} FItems:=nil; {$else} FItems.Clear; FreeAndNil(FItems); {$endif} inherited Destroy; end; procedure TSourceMap.TStringToIndex.Clear; begin {$ifdef pas2js} FItems:=TJSObject.new; {$else} FItems.Clear; {$endif} end; procedure TSourceMap.TStringToIndex.Add(const Value: String; Index: integer); begin {$ifdef pas2js} FItems['%'+Value]:=Index; {$else} // Note: nil=0 means not found in TFPHashList FItems.Add(Value,{%H-}Pointer(PtrInt(Index+1))); {$endif} end; function TSourceMap.TStringToIndex.FindValue(const Value: String ): integer; begin {$ifdef pas2js} if FItems.hasOwnProperty('%'+Value) then Result:=integer(FItems['%'+Value]) else Result:=-1; {$else} // Note: nil=0 means not found in TFPHashList Result:=integer({%H-}PtrInt(FItems.Find(Value))){%H-}-1; {$endif} end; { TSourceMap } procedure TSourceMap.SetGeneratedFilename(const AValue: string); begin if FGeneratedFilename=AValue then Exit; FGeneratedFilename:=AValue; end; procedure TSourceMap.SetSorted(const AValue: boolean); begin if FSorted=AValue then Exit; if AValue then Sort else FSorted:=false; end; procedure TSourceMap.SetSourceContents(Index: integer; const AValue: String); begin TSourceMapSrc(FSources[Index]).Source:=AValue; end; procedure TSourceMap.SetSourceTranslatedFiles(Index: integer; const AValue: String); begin TSourceMapSrc(FSources[Index]).TranslatedFilename:=AValue; end; procedure TSourceMap.Sort; var i: Integer; begin if FSorted then exit; FItems.Sort(@CompareSegmentWithGeneratedLineCol); for i:=0 to Count-1 do Items[i].Index:=i; FSorted:=true; end; function TSourceMap.GetItems(Index: integer): TSourceMapSegment; begin Result:=TSourceMapSegment(FItems[Index]); end; function TSourceMap.GetSourceContents(Index: integer): String; begin Result:=TSourceMapSrc(FSources[Index]).Source; end; function TSourceMap.GetNames(Index: integer): string; begin Result:=FNames[Index]; end; function TSourceMap.GetSourceFiles(Index: integer): String; begin Result:=TSourceMapSrc(FSources[Index]).Filename; end; function TSourceMap.GetSourceTranslatedFiles(Index: integer): String; begin Result:=TSourceMapSrc(FSources[Index]).TranslatedFilename; end; constructor TSourceMap.Create(const aGeneratedFilename: string); begin FOptions:=DefaultSourceMapOptions; FVersion:=3; FNames:=TStringList.Create; FNameToIndex:=TStringToIndex.Create; FItems:=TFPList.Create; FSources:=TFPList.Create; FSourceToIndex:=TStringToIndex.Create; GeneratedFilename:=aGeneratedFilename; FSorted:=true; end; destructor TSourceMap.Destroy; begin Clear; FreeAndNil(FSourceToIndex); FreeAndNil(FSources); FreeAndNil(FItems); FreeAndNil(FNameToIndex); FreeAndNil(FNames); inherited Destroy; end; procedure TSourceMap.Clear; var i: Integer; begin FGeneratedFilename:=''; FSourceToIndex.Clear; for i:=0 to FSources.Count-1 do TObject(FSources[i]).{$ifdef pas2js}Destroy{$else}Free{$endif}; FSources.Clear; for i:=0 to FItems.Count-1 do TObject(FItems[i]).{$ifdef pas2js}Destroy{$else}Free{$endif}; FItems.Clear; FNameToIndex.Clear; FNames.Clear; FSourceRoot:=''; FSorted:=true; end; function TSourceMap.AddMapping(GeneratedLine: integer; GeneratedCol: integer; const SourceFile: string; SrcLine: integer; SrcCol: integer; const Name: String): TSourceMapSegment; procedure RaiseInvalid(Msg: string); begin raise EJSSourceMap.CreateFmt('%s (GeneratedLine=%d GeneratedCol=%d SrcFile="%s" SrcLine=%d SrcCol=%d Name="%s")', [Msg,GeneratedLine,GeneratedCol,SourceFile,SrcLine,SrcCol,Name]); end; var NodeCnt: Integer; OtherNode: TSourceMapSegment; begin {$IFDEF VerboseSrcMap} writeln('TSourceMap.AddMapping Gen:Line=',GeneratedLine,',Col=',GeneratedCol, ' Src:File=',ExtractFileName(SourceFile),',Line=',SrcLine,',Col=',SrcCol,' Name=',Name); {$ENDIF} if GeneratedLine<1 then RaiseInvalid('invalid GeneratedLine'); if GeneratedCol<0 then RaiseInvalid('invalid GeneratedCol'); if SourceFile='' then begin if Count=0 then RaiseInvalid('missing source file'); if SrcLine<>1 then RaiseInvalid('invalid SrcLine'); if SrcCol<>0 then RaiseInvalid('invalid SrcCol'); if Name<>'' then RaiseInvalid('invalid Name'); end else begin if SrcLine<1 then begin if (SrcLine<0) or not (smoAllowSrcLine0 in Options) then RaiseInvalid('invalid SrcLine'); end; if SrcCol<0 then RaiseInvalid('invalid SrcCol'); end; // Note: same line/col is allowed NodeCnt:=Count; if (NodeCnt>0) then begin OtherNode:=Items[NodeCnt-1]; if (OtherNode.GeneratedLine>GeneratedLine) or ((OtherNode.GeneratedLine=GeneratedLine) and (OtherNode.GeneratedColumn>GeneratedCol)) then begin if smoAddMonotonous in FOptions then RaiseInvalid('GeneratedLine/Col not monotonous') else FSorted:=false; end; end; // add Result:=TSourceMapSegment.Create; Result.Index:=FItems.Count; Result.GeneratedLine:=GeneratedLine; Result.GeneratedColumn:=GeneratedCol; if SourceFile='' then Result.SrcFileIndex:=-1 else Result.SrcFileIndex:=IndexOfSourceFile(SourceFile,true); Result.SrcLine:=SrcLine; Result.SrcColumn:=SrcCol; if Name<>'' then Result.NameIndex:=IndexOfName(Name,true) else Result.NameIndex:=-1; FItems.Add(Result); end; function TSourceMap.CreateMappings: String; {$ifdef pas2js} var buf: TJSArray; procedure AddStr(const s: string); inline; begin buf.push(s); end; procedure AddChar(c: char); inline; begin buf.push(c); end; {$else} var buf: TMemoryStream; procedure AddStr(const s: string); begin if s<>'' then buf.Write(s[1],length(s)*sizeof(char)); end; procedure AddChar(c: char); begin buf.Write(c,sizeof(char)); end; {$endif} var i, LastGeneratedLine, LastGeneratedColumn, j, LastSrcFileIndex, LastSrcLine, LastSrcColumn, SrcLine, LastNameIndex: Integer; Item: TSourceMapSegment; begin Result:=''; LastGeneratedLine:=1; LastGeneratedColumn:=0; LastSrcFileIndex:=0; LastSrcLine:=0; LastSrcColumn:=0; LastNameIndex:=0; {$ifdef pas2js} buf:=TJSArray.new; {$else} buf:=TMemoryStream.Create; {$endif} try for i:=0 to Count-1 do begin Item:=Items[i]; if LastGeneratedLine0)) then begin // repeat mapping at start of line // column 0 AddStr(EncodeBase64VLQ(0-LastGeneratedColumn)); LastGeneratedColumn:=0; // same src file index AddStr(EncodeBase64VLQ(0)); // same src line AddStr(EncodeBase64VLQ(0)); // same src column AddStr(EncodeBase64VLQ(0)); if j=Item.GeneratedLine then AddChar(','); end; end; LastGeneratedLine:=Item.GeneratedLine; end else if i>0 then begin // not the first segment if (LastGeneratedLine=Item.GeneratedLine) and (LastGeneratedColumn=Item.GeneratedColumn) then continue; AddChar(','); end; // column diff //writeln('TSourceMap.CreateMappings Seg=',i,' Gen:Line=',LastGeneratedLine,',Col=',Item.GeneratedColumn,' Src:File=',Item.SrcFileIndex,',Line=',Item.SrcLine,',Col=',Item.SrcColumn,' Name=',Item.NameIndex); AddStr(EncodeBase64VLQ(Item.GeneratedColumn-LastGeneratedColumn)); LastGeneratedColumn:=Item.GeneratedColumn; if Item.SrcFileIndex<0 then continue; // no source -> segment length 1 // src file index diff AddStr(EncodeBase64VLQ(Item.SrcFileIndex-LastSrcFileIndex)); LastSrcFileIndex:=Item.SrcFileIndex; // src line diff SrcLine:=Item.SrcLine-1; // 0 based in version 3 AddStr(EncodeBase64VLQ(SrcLine-LastSrcLine)); LastSrcLine:=SrcLine; // src column diff AddStr(EncodeBase64VLQ(Item.SrcColumn-LastSrcColumn)); LastSrcColumn:=Item.SrcColumn; // name index if Item.NameIndex<0 then continue; // no name -> segment length 4 AddStr(EncodeBase64VLQ(Item.NameIndex-LastNameIndex)); LastNameIndex:=Item.NameIndex; end; {$ifdef pas2js} Result:=buf.join(''); {$else} SetLength(Result,buf.Size); if Result<>'' then Move(buf.Memory^,Result[1],buf.Size); {$endif} finally {$ifdef pas2js} {$else} buf.Free; {$endif} end; end; procedure TSourceMap.ParseMappings(const Mapping: String); const MaxInt = High(integer) div 2; {$ifdef UsePChar} var p: PChar; function Decode: NativeInt; inline; begin Result:=DecodeBase64VLQ(p); end; procedure E(const Msg: string); begin raise EJSSourceMap.CreateFmt(Msg,[PtrUInt(p-PChar(Mapping))+1]); end; {$else} var p: integer; function Decode: NativeInt; inline; begin Result:=DecodeBase64VLQ(Mapping,p); end; procedure E(const Msg: string); begin raise EJSSourceMap.CreateFmt(Msg,[p]); end; {$endif} var GeneratedLine, LastColumn, Column, LastSrcFileIndex, LastSrcLine, LastSrcColumn, LastNameIndex, SrcFileIndex, SrcLine, SrcColumn, NameIndex, l: Integer; ColDiff, SrcFileIndexDiff, SrcLineDiff, SrcColumnDiff, NameIndexDiff: NativeInt; Segment: TSourceMapSegment; begin l:=length(Mapping); if l=0 then exit; p:={$ifdef UsePChar}PChar(Mapping){$else}1{$endif}; GeneratedLine:=1; LastColumn:=0; LastSrcFileIndex:=0; LastSrcLine:=0; LastSrcColumn:=0; LastNameIndex:=0; while {$ifdef UsePChar}true{$else}p<=l{$endif} do begin case {$ifdef UsePChar}p^{$else}Mapping[p]{$endif} of {$ifdef UsePChar} #0: if p-PChar(Mapping)=length(Mapping) then exit else E('unexpected #0 at %d'); {$endif} ',': begin // next segment inc(p); end; ';': begin // next line inc(GeneratedLine); LastColumn:=0; inc(p); end; else begin ColDiff:=Decode; if (ColDiff>MaxInt) or (ColDiff<-MaxInt) then E('column out of range at %d'); Column:=LastColumn+integer(ColDiff); if (Column>MaxInt) or (Column<-MaxInt) then E('column out of range at %d'); LastColumn:=Column; Segment:=TSourceMapSegment.Create; Segment.Index:=FItems.Count; FItems.Add(Segment); Segment.GeneratedLine:=GeneratedLine; Segment.GeneratedColumn:=Column; Segment.SrcFileIndex:=-1; Segment.NameIndex:=-1; if {$ifdef UsePChar}not (p^ in [',',';',#0]){$else}(p<=l) and not (Mapping[p] in [',',';']){$endif} then begin // src file index SrcFileIndexDiff:=Decode; if (SrcFileIndexDiff>MaxInt) or (SrcFileIndexDiff<-MaxInt) then E('src file index out of range at %d'); SrcFileIndex:=LastSrcFileIndex+integer(SrcFileIndexDiff); if (SrcFileIndex<0) or (SrcFileIndex>=SourceCount) then E('src file index out of range at %d'); LastSrcFileIndex:=SrcFileIndex; Segment.SrcFileIndex:=SrcFileIndex; // src line SrcLineDiff:=Decode; if (SrcLineDiff>MaxInt) or (SrcLineDiff<-MaxInt) then E('src line out of range at %d'); SrcLine:=LastSrcLine+integer(SrcLineDiff); if (SrcLine>MaxInt) or (SrcLine<-MaxInt) then E('src line out of range at %d'); LastSrcLine:=SrcLine; Segment.SrcLine:=SrcLine+1; // lines are stored 0-based // src column SrcColumnDiff:=Decode; if (SrcColumnDiff>MaxInt) or (SrcColumnDiff<-MaxInt) then E('src column out of range at %d'); SrcColumn:=LastSrcColumn+integer(SrcColumnDiff); if (SrcColumn>MaxInt) or (SrcColumn<-MaxInt) then E('src column out of range at %d'); LastSrcColumn:=SrcColumn; Segment.SrcColumn:=SrcColumn; if {$ifdef UsePChar}not (p^ in [',',';',#0]){$else}(p<=l) and not (Mapping[p] in [',',';']){$endif} then begin // name index NameIndexDiff:=Decode; if (NameIndexDiff>MaxInt) or (NameIndexDiff<-MaxInt) then E('name index out of range at %d'); NameIndex:=LastNameIndex+integer(NameIndexDiff); if (NameIndex<0) or (NameIndex>=NameCount) then E('name index out of range at %d'); LastNameIndex:=NameIndex; Segment.NameIndex:=NameIndex; end; end; end; end; end; end; function TSourceMap.ToJSON: TJSONObject; var Obj: TJSONObject; i: Integer; Arr: TJSONArray; Mappings: String; begin Result:=nil; Mappings:=CreateMappings; Obj:=TJSONObject.Create; try // "version" - integer Obj.Add('version',Version); // "file" - GeneratedFilename if GeneratedFilename<>'' then Obj.Add('file',GeneratedFilename); // "sourceRoot" - SourceRoot if SourceRoot<>'' then Obj.Add('sourceRoot',SourceRoot); // "sources" - array of filenames Arr:=TJSONArray.Create; Obj.Add('sources',Arr); for i:=0 to SourceCount-1 do Arr.Add(SourceTranslatedFiles[i]); // "sourcesContent" - array of source content: null or source as string // only needed if there is a source i:=SourceCount-1; while i>=0 do if SourceContents[i]='' then dec(i) else begin // there is a source -> add array Arr:=TJSONArray.Create; Obj.Add('sourcesContent',Arr); for i:=0 to SourceCount-1 do if SourceContents[i]='' then Arr.Add(TJSONNull.Create) else Arr.Add(SourceContents[i]); break; end; // "names" - array of names Arr:=TJSONArray.Create; Obj.Add('names',Arr); for i:=0 to NameCount-1 do Arr.Add(Names[i]); // "mappings" - string Obj.Add('mappings',Mappings); Result:=Obj; finally if Result=nil then Obj.Free; end; end; function TSourceMap.ToString: string; var Obj: TJSONObject; begin Obj:=ToJSON; try if smoSafetyHeader in Options then Result:=DefaultSrcMapHeader+Obj.AsJSON else Result:=Obj.AsJSON; finally Obj.Free; end; end; procedure TSourceMap.LoadFromJSON(Obj: TJSONObject); var aVersion, i, j: integer; Arr: TJSONArray; Data: TJSONData; aFilename, aName: String; aMappings: String; begin // Note: does not support sections yet Clear; // "version" - integer aVersion:=Obj.Get('version',0); if aVersion<>Version then raise EJSSourceMap.CreateFmt('unsupported version %d',[aVersion]); // "file" - GeneratedFilename GeneratedFilename:=String(Obj.Get('file','')); // "sourceRoot" - SourceRoot SourceRoot:=Obj.Get('sourceRoot',''); // "sources" - array of filenames Arr:=nil; if not Obj.Find('sources',Arr) then raise EJSSourceMap.Create('missing sources array'); for i:=0 to Arr.Count-1 do begin Data:=Arr[i]; if not (Data is TJSONString) then raise EJSSourceMap.CreateFmt('sources must string, but found %s',[Data.ClassName]); aFilename:=String(TJSONString(Data).AsString); j:=IndexOfSourceFile(aFilename,true); if j<>i then raise EJSSourceMap.CreateFmt('duplicate source file "%s" at %d',[aFilename,i]); end; // optional: "sourcesContent" - array of sources Arr:=nil; if Obj.Find('sourcesContent',Arr) then begin if Arr.Count<>SourceCount then raise EJSSourceMap.CreateFmt('number of elements in sources %d mismatch sourcesContent %d',[SourceCount,Arr.Count]); for i:=0 to Arr.Count-1 do begin Data:=Arr[i]; if (Data is TJSONString) then SourceContents[i]:=String(TJSONString(Data).AsString) else if Data is TJSONNull then else raise EJSSourceMap.CreateFmt('sourcesContent[%d] must be string',[i]); end; end; // optional: "names" - array of strings Arr:=nil; if Obj.Find('names',Arr) then for i:=0 to Arr.Count-1 do begin Data:=Arr[i]; if not (Data is TJSONString) then raise EJSSourceMap.CreateFmt('names must string, but found %s',[Data.ClassName]); aName:=String(TJSONString(Data).AsString); j:=IndexOfName(aName,true); if j<>i then raise EJSSourceMap.CreateFmt('duplicate name "%s" at %d',[aName,i]); end; // "mappings" - string aMappings:=Obj.Get('mappings',''); ParseMappings(aMappings); end; procedure TSourceMap.SaveToStream(aStream: TFPJSStream); var Obj: TJSONObject; begin Obj:=ToJSON; try if smoSafetyHeader in Options then begin {$ifdef pas2js} aStream.push(DefaultSrcMapHeader); {$else} aStream.Write(DefaultSrcMapHeader[1],length(DefaultSrcMapHeader)); {$endif} end; Obj.DumpJSON(aStream); finally Obj.Free; end; end; {$ifdef HasStreams} procedure TSourceMap.LoadFromStream(aStream: TStream); var s: string; P: TJSONParser; Data: TJSONData; begin s:=''; SetLength(s,aStream.Size-aStream.Position); if s<>'' then aStream.Read(s[1],length(s)); if LeftStr(s,4)=')]}''' then Delete(s,1,4) else if LeftStr(s,3)=')]}' then Delete(s,1,3); P:=TJSONParser.Create(s,[joUTF8]); try Data:=P.Parse; if not (Data is TJSONObject) then raise EJSSourceMap.Create('source map must be a JSON object'); LoadFromJSON(TJSONObject(Data)); finally P.Free; end; end; procedure TSourceMap.SaveToFile(Filename: string); var TheStream: TMemoryStream; begin TheStream:=TMemoryStream.Create; try SaveToStream(TheStream); TheStream.Position:=0; TheStream.SaveToFile(Filename); finally TheStream.Free; end; end; procedure TSourceMap.LoadFromFile(Filename: string); var TheStream: TMemoryStream; begin TheStream:=TMemoryStream.Create; try TheStream.LoadFromFile(Filename); TheStream.Position:=0; LoadFromStream(TheStream); finally TheStream.Free; end; end; {$endif} function TSourceMap.IndexOfName(const Name: string; AddIfNotExists: boolean ): integer; begin Result:=FNameToIndex.FindValue(Name); if (Result>=0) or not AddIfNotExists then exit; Result:=FNames.Count; FNames.Add(Name); FNameToIndex.Add(Name,Result); end; function TSourceMap.IndexOfSourceFile(const SrcFile: string; AddIfNotExists: boolean): integer; var Src: TSourceMapSrc; begin Result:=FSourceToIndex.FindValue(SrcFile); if (Result>=0) or not AddIfNotExists then exit; Src:=TSourceMapSrc.Create; Src.Filename:=SrcFile; Src.TranslatedFilename:=SrcFile; Result:=FSources.Count; FSources.Add(Src); FSourceToIndex.Add(SrcFile,Result); end; function TSourceMap.IndexOfSegmentAt(GeneratedLine, GeneratedCol: integer ): integer; var l, r, m: Integer; aSeg: TSourceMapSegment; begin Sort; l:=0; r:=Count-1; aSeg:=nil; while l<=r do begin m:=(l+r) div 2; aSeg:=Items[m]; if aSeg.GeneratedLineGeneratedLine then r:=m-1 else if aSeg.GeneratedColumnGeneratedCol then r:=m-1 else begin // exact match found Result:=m; // -> return the leftmost exact match while Result>0 do begin aSeg:=Items[Result-1]; if (aSeg.GeneratedLine<>GeneratedLine) or (aSeg.GeneratedColumn<>GeneratedCol) then exit; dec(Result); end; exit; end; end; // no exact match found if aSeg=nil then exit(-1); // return the next lower. Note: there may be no such segment if (aSeg.GeneratedLine>GeneratedLine) or ((aSeg.GeneratedLine=GeneratedLine) and (aSeg.GeneratedColumn>GeneratedCol)) then dec(m); Result:=m; end; function TSourceMap.Count: integer; begin Result:=FItems.Count; end; function TSourceMap.SourceCount: integer; begin Result:=FSources.Count; end; function TSourceMap.NameCount: integer; begin Result:=FNames.Count; end; end.