jssrcmap.pas 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255
  1. { *********************************************************************
  2. This file is part of the Free Component Library (FCL)
  3. Copyright (c) 2018 Mattias Gaertner.
  4. Javascript Source Map
  5. See Source Maps Revision 3:
  6. https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1#
  7. See the file COPYING.FPC, included in this distribution,
  8. for details about the copyright.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  12. **********************************************************************}
  13. unit JSSrcMap;
  14. {$mode objfpc}{$H+}
  15. {$ifdef fpc}
  16. {$define UsePChar}
  17. {$define HasJsonParser}
  18. {$define HasStreams}
  19. {$define HasFS}
  20. {$endif}
  21. {$ifdef pas2js}
  22. {$ifdef nodejs}
  23. {$define HasFS}
  24. {$endif}
  25. {$endif}
  26. interface
  27. uses
  28. {$ifdef pas2js}
  29. JS,
  30. {$ifdef nodejs}
  31. Node.FS,
  32. {$endif}
  33. {$else}
  34. contnrs,
  35. {$endif}
  36. Classes, SysUtils, fpjson
  37. {$ifdef HasJsonParser}
  38. , jsonparser, jsonscanner
  39. {$endif}
  40. ;
  41. const
  42. Base64Chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  43. type
  44. EJSSourceMap = class(Exception);
  45. { TSourceMapSegment }
  46. TSourceMapSegment = class
  47. public
  48. Index: integer; // index in Items
  49. GeneratedLine: integer; // 1-based
  50. GeneratedColumn: integer; // 0-based
  51. SrcFileIndex: integer; // index in FSources
  52. SrcLine: integer;
  53. SrcColumn: integer;
  54. NameIndex: integer; // index in FNames
  55. end;
  56. TSourceMapSrc = class
  57. public
  58. Filename: string; // as added by AddMapping
  59. TranslatedFilename: string; // same as Filename, can be altered, written to JSON
  60. Source: String;
  61. end;
  62. TSourceMapOption = (
  63. smoAddMonotonous, // true = AddMapping GeneratedLine/Col must be behind last add, false = check all adds for duplicate
  64. smoAutoLineStart, // automatically add a first column mapping, repeating last mapping
  65. smoSafetyHeader, // insert ')]}' at start
  66. smoAllowSrcLine0 // don't bark on SrcLine=0
  67. );
  68. TSourceMapOptions = set of TSourceMapOption;
  69. const
  70. DefaultSourceMapOptions = [smoAddMonotonous,smoSafetyHeader];
  71. type
  72. { TSourceMap }
  73. TSourceMap = class
  74. private
  75. type
  76. { TStringToIndex }
  77. TStringToIndex = class
  78. private
  79. FItems: {$ifdef pas2js}TJSObject{$else}TFPHashList{$endif};
  80. public
  81. constructor Create;
  82. destructor Destroy; override;
  83. procedure Clear;
  84. procedure Add(const Value: String; Index: integer);
  85. function FindValue(const Value: String): integer;
  86. end;
  87. private
  88. FGeneratedFilename: string;
  89. FNames: TStrings; // in adding order
  90. FNameToIndex: TStringToIndex; // name to index in FNames
  91. FItems: TFPList; // TSourceMapSegment, in adding order
  92. FOptions: TSourceMapOptions;
  93. FSorted: boolean;
  94. FSourceRoot: string;
  95. FSources: TFPList; // list of TSourceMapSrc, in adding order
  96. FSourceToIndex: TStringToIndex; // srcfile to index in FSources
  97. FVersion: integer;
  98. function GetNames(Index: integer): string;
  99. function GetItems(Index: integer): TSourceMapSegment;
  100. function GetSourceContents(Index: integer): String;
  101. function GetSourceFiles(Index: integer): String;
  102. function GetSourceTranslatedFiles(Index: integer): String;
  103. procedure SetGeneratedFilename(const AValue: string);
  104. procedure SetSorted(const AValue: boolean);
  105. procedure SetSourceContents(Index: integer; const AValue: String);
  106. procedure SetSourceTranslatedFiles(Index: integer; const AValue: String);
  107. procedure Sort;
  108. public
  109. constructor Create(const aGeneratedFilename: string);
  110. destructor Destroy; override;
  111. procedure Clear; virtual;
  112. function AddMapping(
  113. GeneratedLine: integer; // 1-based
  114. GeneratedCol: integer = 0; // 0-based
  115. const SourceFile: string = ''; // can be empty ''
  116. SrcLine: integer = 1; // 1-based
  117. SrcCol: integer = 0; // 0-based
  118. const Name: String = ''): TSourceMapSegment; virtual;
  119. function CreateMappings: String; virtual;
  120. procedure ParseMappings(const Mapping: String); virtual;
  121. function ToJSON: TJSONObject; virtual;
  122. function ToString: string; override;
  123. procedure LoadFromJSON(Obj: TJSONObject); virtual;
  124. procedure SaveToStream(aStream: TFPJSStream); virtual;
  125. {$ifdef HasStreams}
  126. procedure LoadFromStream(aStream: TStream); virtual;
  127. procedure SaveToFile(Filename: string); virtual;
  128. procedure LoadFromFile(Filename: string); virtual;
  129. {$endif}
  130. property GeneratedFilename: string read FGeneratedFilename write SetGeneratedFilename;
  131. function IndexOfName(const Name: string; AddIfNotExists: boolean = false): integer;
  132. function IndexOfSourceFile(const SrcFile: string; AddIfNotExists: boolean = false): integer;
  133. function IndexOfSegmentAt(GeneratedLine, GeneratedCol: integer): integer;
  134. function Count: integer; // segments
  135. property Items[Index: integer]: TSourceMapSegment read GetItems; default; // segments
  136. function SourceCount: integer;
  137. property SourceRoot: string read FSourceRoot write FSourceRoot;
  138. property SourceFiles[Index: integer]: String read GetSourceFiles;
  139. property SourceTranslatedFiles[Index: integer]: String read GetSourceTranslatedFiles
  140. write SetSourceTranslatedFiles;
  141. property SourceContents[Index: integer]: String read GetSourceContents write SetSourceContents;
  142. function NameCount: integer;
  143. property Names[Index: integer]: string read GetNames;
  144. property Version: integer read FVersion; // 3
  145. property Options: TSourceMapOptions read FOptions write FOptions;
  146. property Sorted: boolean read FSorted write SetSorted; // Segments are sorted for GeneratedLine/Col
  147. end;
  148. function DefaultSrcMapHeader: string;
  149. function EncodeBase64VLQ(i: NativeInt): String; // base64 Variable Length Quantity
  150. function DecodeBase64VLQ(const s: string): NativeInt; // base64 Variable Length Quantity
  151. function DecodeBase64VLQ(
  152. {$ifdef UsePChar}var p: PChar{$else}const s: string; var p: integer{$endif}): NativeInt; // base64 Variable Length Quantity
  153. function CompareSegmentWithGeneratedLineCol(
  154. Item1, Item2: {$ifdef pas2js}jsvalue{$else}Pointer{$endif}): Integer;
  155. procedure DebugSrcMapLine(GeneratedLine: integer; var GeneratedLineSrc: String;
  156. SrcMap: TSourceMap; out InfoLine: String);
  157. implementation
  158. function DefaultSrcMapHeader: string;
  159. begin
  160. Result:=')]}'''+LineEnding;
  161. end;
  162. function EncodeBase64VLQ(i: NativeInt): String;
  163. { Convert signed number to base64-VLQ:
  164. Each base64 has 6bit, where the most significant bit is the continuation bit
  165. (1=there is a next base64 character).
  166. The first character contains the sign bit in the last bit (1=negative)
  167. and the 5 least significant bits of the number.
  168. For example:
  169. A = 0 = %000000 => 0
  170. B = 1 = %000001 => -0
  171. C = 2 = %000010 => 1
  172. iF = 34 5 = %100010 %000101 = + 0001 00101 = 100101 = 37
  173. }
  174. procedure RaiseRange;
  175. begin
  176. raise ERangeError.Create('EncodeBase64VLQ');
  177. end;
  178. var
  179. digits: NativeInt;
  180. begin
  181. Result:='';
  182. if i<0 then
  183. begin
  184. i:=-i;
  185. if i>(High(NativeInt)-1) shr 1 then
  186. RaiseRange;
  187. i:=(i shl 1)+1;
  188. end
  189. else
  190. begin
  191. if i>High(NativeInt) shr 1 then
  192. RaiseRange;
  193. i:=i shl 1;
  194. end;
  195. repeat
  196. digits:=i and %11111;
  197. i:=i shr 5;
  198. if i>0 then
  199. inc(digits,%100000); // need another char -> set continuation bit
  200. Result:=Result+Base64Chars[digits+1];
  201. until i=0;
  202. end;
  203. function DecodeBase64VLQ(const s: string): NativeInt;
  204. var
  205. {$ifdef UsePChar}
  206. p: PChar;
  207. {$else}
  208. p: integer;
  209. {$endif}
  210. begin
  211. if s='' then
  212. raise EConvertError.Create('DecodeBase64VLQ empty');
  213. {$ifdef UsePChar}
  214. p:=PChar(s);
  215. Result:=DecodeBase64VLQ(p);
  216. if p-PChar(s)<>length(s) then
  217. raise EConvertError.Create('DecodeBase64VLQ waste');
  218. {$else}
  219. p:=1;
  220. Result:=DecodeBase64VLQ(s,p);
  221. {$endif}
  222. end;
  223. function DecodeBase64VLQ(
  224. {$ifdef UsePChar}var p: PChar{$else}const s: string; var p: integer{$endif}): NativeInt;
  225. { Convert base64-VLQ to signed number,
  226. For the fomat see EncodeBase64VLQ
  227. }
  228. var
  229. {$ifdef UsePChar}
  230. run: PChar;
  231. {$else}
  232. run, l: integer;
  233. {$endif}
  234. procedure RaiseInvalid;
  235. begin
  236. p:=run;
  237. raise ERangeError.Create('DecodeBase64VLQ');
  238. end;
  239. const
  240. MaxShift = {$ifdef pas2js}32{$else}63{$endif}-5; // actually log2(High(NativeInt))-5
  241. var
  242. c: Char;
  243. digit, Shift: Integer;
  244. begin
  245. Result:=0;
  246. Shift:=0;
  247. run:=p;
  248. {$ifdef UsePChar}
  249. {$else}
  250. l:=length(s);
  251. {$endif}
  252. repeat
  253. {$ifdef UsePChar}
  254. c:=run^;
  255. {$else}
  256. if run>l then
  257. RaiseInvalid;
  258. c:=s[run];
  259. {$endif}
  260. case c of
  261. 'A'..'Z': digit:=ord(c)-ord('A');
  262. 'a'..'z': digit:=ord(c)-ord('a')+26;
  263. '0'..'9': digit:=ord(c)-ord('0')+52;
  264. '+': digit:=62;
  265. '/': digit:=63;
  266. else RaiseInvalid;
  267. end;
  268. inc(run);
  269. if Shift>MaxShift then
  270. RaiseInvalid;
  271. inc(Result,(digit and %11111) shl Shift);
  272. inc(Shift,5);
  273. until digit<%100000;
  274. if (Result and 1)>0 then
  275. Result:=-(Result shr 1)
  276. else
  277. Result:=Result shr 1;
  278. p:=run;
  279. end;
  280. function CompareSegmentWithGeneratedLineCol(
  281. Item1, Item2: {$ifdef pas2js}jsvalue{$else}Pointer{$endif}): Integer;
  282. var
  283. Seg1: TSourceMapSegment absolute Item1;
  284. Seg2: TSourceMapSegment absolute Item2;
  285. begin
  286. if Seg1.GeneratedLine<Seg2.GeneratedLine then
  287. Result:=-1
  288. else if Seg1.GeneratedLine>Seg2.GeneratedLine then
  289. Result:=1
  290. else if Seg1.GeneratedColumn<Seg2.GeneratedColumn then
  291. Result:=-1
  292. else if Seg1.GeneratedColumn>Seg2.GeneratedColumn then
  293. Result:=1
  294. // compare Index to keep adding order
  295. else if Seg1.Index<Seg2.Index then
  296. Result:=-1
  297. else if Seg1.Index>Seg2.Index then
  298. Result:=1
  299. else
  300. Result:=0;
  301. end;
  302. procedure DebugSrcMapLine(GeneratedLine: integer; var GeneratedLineSrc: String;
  303. SrcMap: TSourceMap; out InfoLine: String);
  304. var
  305. JS, Origins, Addition: String;
  306. GeneratedCol: integer; // 0-based
  307. i, diff, GenColStep, LastSrcFile, LastSrcLine: Integer;
  308. aSeg: TSourceMapSegment;
  309. begin
  310. InfoLine:='';
  311. JS:=GeneratedLineSrc;
  312. Origins:='';
  313. GeneratedCol:=0;// 0-based
  314. LastSrcFile:=0;
  315. LastSrcLine:=-1;
  316. i:=SrcMap.IndexOfSegmentAt(GeneratedLine,GeneratedCol);
  317. aSeg:=nil;
  318. if i<0 then
  319. begin
  320. // no segment at line start
  321. i:=0;
  322. if (i=SrcMap.Count) then
  323. aSeg:=nil
  324. else
  325. aSeg:=SrcMap[i];
  326. if (aSeg=nil) or (aSeg.GeneratedLine>GeneratedLine) then
  327. begin
  328. // no segment in line
  329. for i:=1 to length(JS) do Origins:=Origins+'?';
  330. GeneratedLineSrc:=JS;
  331. InfoLine:=Origins;
  332. exit;
  333. end
  334. else
  335. begin
  336. // show "?" til start of first segment
  337. for i:=1 to aSeg.GeneratedColumn do Origins:=Origins+'?';
  338. end;
  339. end
  340. else
  341. begin
  342. aSeg:=SrcMap[i];
  343. if i>0 then
  344. LastSrcFile:=SrcMap[i-1].SrcFileIndex;
  345. end;
  346. repeat
  347. Addition:='';
  348. if (aSeg.GeneratedLine=GeneratedLine) and (aSeg.GeneratedColumn=GeneratedCol) then
  349. begin
  350. // segment starts here -> write "|line,col"
  351. Addition:='|';
  352. if LastSrcFile<>aSeg.SrcFileIndex then
  353. begin
  354. Addition:=Addition+{$ifdef HasFS}ExtractFileName{$endif}(SrcMap.SourceFiles[aSeg.SrcFileIndex])+',';
  355. LastSrcFile:=aSeg.SrcFileIndex;
  356. end;
  357. if LastSrcLine<>aSeg.SrcLine then
  358. begin
  359. Addition:=Addition+IntToStr(aSeg.SrcLine)+',';
  360. LastSrcLine:=aSeg.SrcLine;
  361. end;
  362. Addition:=Addition+IntToStr(aSeg.SrcColumn);
  363. Origins:=Origins+Addition;
  364. end;
  365. inc(i);
  366. // skip segments at same GeneratedLine/Col
  367. while (i<SrcMap.Count) do
  368. begin
  369. aSeg:=SrcMap[i];
  370. if (aSeg.GeneratedLine=GeneratedLine) and (aSeg.GeneratedColumn=GeneratedCol) then
  371. inc(i)
  372. else
  373. break;
  374. end;
  375. if (i=SrcMap.Count) then
  376. aSeg:=nil
  377. else
  378. aSeg:=SrcMap[i];
  379. if (aSeg=nil) or (aSeg.GeneratedLine>GeneratedLine) then
  380. begin
  381. // in the last segment
  382. while length(Origins)<length(JS) do
  383. Origins:=Origins+'.';
  384. GeneratedLineSrc:=JS;
  385. InfoLine:=Origins;
  386. exit;
  387. end;
  388. // there is another segment in this line
  389. // -> align JS and Origins
  390. GenColStep:=aSeg.GeneratedColumn-GeneratedCol;
  391. diff:=GenColStep-length(Addition);
  392. if diff<0 then
  393. // for example:
  394. // JS: if(~~e)~~~{
  395. // Origins: |12,3|12,5|12,7
  396. Insert(StringOfChar('~',-diff),JS,length(Origins)-length(Addition)+1+GenColStep)
  397. else
  398. while diff>0 do
  399. begin
  400. Origins:=Origins+'.';
  401. dec(diff);
  402. end;
  403. GeneratedCol:=aSeg.GeneratedColumn;
  404. until false;
  405. end;
  406. { TSourceMap.TStringToIndex }
  407. constructor TSourceMap.TStringToIndex.Create;
  408. begin
  409. {$ifdef pas2js}
  410. FItems:=TJSObject.new;
  411. {$else}
  412. FItems:=TFPHashList.Create;
  413. {$endif}
  414. end;
  415. destructor TSourceMap.TStringToIndex.Destroy;
  416. begin
  417. {$ifdef pas2js}
  418. FItems:=nil;
  419. {$else}
  420. FItems.Clear;
  421. FreeAndNil(FItems);
  422. {$endif}
  423. inherited Destroy;
  424. end;
  425. procedure TSourceMap.TStringToIndex.Clear;
  426. begin
  427. {$ifdef pas2js}
  428. FItems:=TJSObject.new;
  429. {$else}
  430. FItems.Clear;
  431. {$endif}
  432. end;
  433. procedure TSourceMap.TStringToIndex.Add(const Value: String; Index: integer);
  434. begin
  435. {$ifdef pas2js}
  436. FItems['%'+Value]:=Index;
  437. {$else}
  438. // Note: nil=0 means not found in TFPHashList
  439. FItems.Add(Value,{%H-}Pointer(PtrInt(Index+1)));
  440. {$endif}
  441. end;
  442. function TSourceMap.TStringToIndex.FindValue(const Value: String
  443. ): integer;
  444. begin
  445. {$ifdef pas2js}
  446. if FItems.hasOwnProperty('%'+Value) then
  447. Result:=integer(FItems['%'+Value])
  448. else
  449. Result:=-1;
  450. {$else}
  451. // Note: nil=0 means not found in TFPHashList
  452. Result:=integer({%H-}PtrInt(FItems.Find(Value))){%H-}-1;
  453. {$endif}
  454. end;
  455. { TSourceMap }
  456. procedure TSourceMap.SetGeneratedFilename(const AValue: string);
  457. begin
  458. if FGeneratedFilename=AValue then Exit;
  459. FGeneratedFilename:=AValue;
  460. end;
  461. procedure TSourceMap.SetSorted(const AValue: boolean);
  462. begin
  463. if FSorted=AValue then Exit;
  464. if AValue then
  465. Sort
  466. else
  467. FSorted:=false;
  468. end;
  469. procedure TSourceMap.SetSourceContents(Index: integer; const AValue: String);
  470. begin
  471. TSourceMapSrc(FSources[Index]).Source:=AValue;
  472. end;
  473. procedure TSourceMap.SetSourceTranslatedFiles(Index: integer;
  474. const AValue: String);
  475. begin
  476. TSourceMapSrc(FSources[Index]).TranslatedFilename:=AValue;
  477. end;
  478. procedure TSourceMap.Sort;
  479. var
  480. i: Integer;
  481. begin
  482. if FSorted then exit;
  483. FItems.Sort(@CompareSegmentWithGeneratedLineCol);
  484. for i:=0 to Count-1 do
  485. Items[i].Index:=i;
  486. FSorted:=true;
  487. end;
  488. function TSourceMap.GetItems(Index: integer): TSourceMapSegment;
  489. begin
  490. Result:=TSourceMapSegment(FItems[Index]);
  491. end;
  492. function TSourceMap.GetSourceContents(Index: integer): String;
  493. begin
  494. Result:=TSourceMapSrc(FSources[Index]).Source;
  495. end;
  496. function TSourceMap.GetNames(Index: integer): string;
  497. begin
  498. Result:=FNames[Index];
  499. end;
  500. function TSourceMap.GetSourceFiles(Index: integer): String;
  501. begin
  502. Result:=TSourceMapSrc(FSources[Index]).Filename;
  503. end;
  504. function TSourceMap.GetSourceTranslatedFiles(Index: integer): String;
  505. begin
  506. Result:=TSourceMapSrc(FSources[Index]).TranslatedFilename;
  507. end;
  508. constructor TSourceMap.Create(const aGeneratedFilename: string);
  509. begin
  510. FOptions:=DefaultSourceMapOptions;
  511. FVersion:=3;
  512. FNames:=TStringList.Create;
  513. FNameToIndex:=TStringToIndex.Create;
  514. FItems:=TFPList.Create;
  515. FSources:=TFPList.Create;
  516. FSourceToIndex:=TStringToIndex.Create;
  517. GeneratedFilename:=aGeneratedFilename;
  518. FSorted:=true;
  519. end;
  520. destructor TSourceMap.Destroy;
  521. begin
  522. Clear;
  523. FreeAndNil(FSourceToIndex);
  524. FreeAndNil(FSources);
  525. FreeAndNil(FItems);
  526. FreeAndNil(FNameToIndex);
  527. FreeAndNil(FNames);
  528. inherited Destroy;
  529. end;
  530. procedure TSourceMap.Clear;
  531. var
  532. i: Integer;
  533. begin
  534. FGeneratedFilename:='';
  535. FSourceToIndex.Clear;
  536. for i:=0 to FSources.Count-1 do
  537. TObject(FSources[i]).{$ifdef pas2js}Destroy{$else}Free{$endif};
  538. FSources.Clear;
  539. for i:=0 to FItems.Count-1 do
  540. TObject(FItems[i]).{$ifdef pas2js}Destroy{$else}Free{$endif};
  541. FItems.Clear;
  542. FNameToIndex.Clear;
  543. FNames.Clear;
  544. FSourceRoot:='';
  545. FSorted:=true;
  546. end;
  547. function TSourceMap.AddMapping(GeneratedLine: integer; GeneratedCol: integer;
  548. const SourceFile: string; SrcLine: integer; SrcCol: integer;
  549. const Name: String): TSourceMapSegment;
  550. procedure RaiseInvalid(Msg: string);
  551. begin
  552. raise EJSSourceMap.CreateFmt('%s (GeneratedLine=%d GeneratedCol=%d SrcFile="%s" SrcLine=%d SrcCol=%d Name="%s")',
  553. [Msg,GeneratedLine,GeneratedCol,SourceFile,SrcLine,SrcCol,Name]);
  554. end;
  555. var
  556. NodeCnt: Integer;
  557. OtherNode: TSourceMapSegment;
  558. begin
  559. {$IFDEF VerboseSrcMap}
  560. writeln('TSourceMap.AddMapping Gen:Line=',GeneratedLine,',Col=',GeneratedCol,
  561. ' Src:File=',ExtractFileName(SourceFile),',Line=',SrcLine,',Col=',SrcCol,' Name=',Name);
  562. {$ENDIF}
  563. if GeneratedLine<1 then
  564. RaiseInvalid('invalid GeneratedLine');
  565. if GeneratedCol<0 then
  566. RaiseInvalid('invalid GeneratedCol');
  567. if SourceFile='' then
  568. begin
  569. if Count=0 then
  570. RaiseInvalid('missing source file');
  571. if SrcLine<>1 then
  572. RaiseInvalid('invalid SrcLine');
  573. if SrcCol<>0 then
  574. RaiseInvalid('invalid SrcCol');
  575. if Name<>'' then
  576. RaiseInvalid('invalid Name');
  577. end
  578. else
  579. begin
  580. if SrcLine<1 then
  581. begin
  582. if (SrcLine<0) or not (smoAllowSrcLine0 in Options) then
  583. RaiseInvalid('invalid SrcLine');
  584. end;
  585. if SrcCol<0 then
  586. RaiseInvalid('invalid SrcCol');
  587. end;
  588. // Note: same line/col is allowed
  589. NodeCnt:=Count;
  590. if (NodeCnt>0) then
  591. begin
  592. OtherNode:=Items[NodeCnt-1];
  593. if (OtherNode.GeneratedLine>GeneratedLine)
  594. or ((OtherNode.GeneratedLine=GeneratedLine)
  595. and (OtherNode.GeneratedColumn>GeneratedCol)) then
  596. begin
  597. if smoAddMonotonous in FOptions then
  598. RaiseInvalid('GeneratedLine/Col not monotonous')
  599. else
  600. FSorted:=false;
  601. end;
  602. end;
  603. // add
  604. Result:=TSourceMapSegment.Create;
  605. Result.Index:=FItems.Count;
  606. Result.GeneratedLine:=GeneratedLine;
  607. Result.GeneratedColumn:=GeneratedCol;
  608. if SourceFile='' then
  609. Result.SrcFileIndex:=-1
  610. else
  611. Result.SrcFileIndex:=IndexOfSourceFile(SourceFile,true);
  612. Result.SrcLine:=SrcLine;
  613. Result.SrcColumn:=SrcCol;
  614. if Name<>'' then
  615. Result.NameIndex:=IndexOfName(Name,true)
  616. else
  617. Result.NameIndex:=-1;
  618. FItems.Add(Result);
  619. end;
  620. function TSourceMap.CreateMappings: String;
  621. {$ifdef pas2js}
  622. var
  623. buf: TJSArray;
  624. procedure AddStr(const s: string); inline;
  625. begin
  626. buf.push(s);
  627. end;
  628. procedure AddChar(c: char); inline;
  629. begin
  630. buf.push(c);
  631. end;
  632. {$else}
  633. var
  634. buf: TMemoryStream;
  635. procedure AddStr(const s: string);
  636. begin
  637. if s<>'' then
  638. buf.Write(s[1],length(s)*sizeof(char));
  639. end;
  640. procedure AddChar(c: char);
  641. begin
  642. buf.Write(c,sizeof(char));
  643. end;
  644. {$endif}
  645. var
  646. i, LastGeneratedLine, LastGeneratedColumn, j, LastSrcFileIndex, LastSrcLine,
  647. LastSrcColumn, SrcLine, LastNameIndex: Integer;
  648. Item: TSourceMapSegment;
  649. begin
  650. Result:='';
  651. LastGeneratedLine:=1;
  652. LastGeneratedColumn:=0;
  653. LastSrcFileIndex:=0;
  654. LastSrcLine:=0;
  655. LastSrcColumn:=0;
  656. LastNameIndex:=0;
  657. {$ifdef pas2js}
  658. buf:=TJSArray.new;
  659. {$else}
  660. buf:=TMemoryStream.Create;
  661. {$endif}
  662. try
  663. for i:=0 to Count-1 do
  664. begin
  665. Item:=Items[i];
  666. if LastGeneratedLine<Item.GeneratedLine then
  667. begin
  668. // new line
  669. LastGeneratedColumn:=0; // column is reset every generated line
  670. for j:=LastGeneratedLine+1 to Item.GeneratedLine do
  671. begin
  672. AddChar(';');
  673. if (smoAutoLineStart in FOptions)
  674. and ((j<Item.GeneratedLine) or (Item.GeneratedColumn>0)) then
  675. begin
  676. // repeat mapping at start of line
  677. // column 0
  678. AddStr(EncodeBase64VLQ(0-LastGeneratedColumn));
  679. LastGeneratedColumn:=0;
  680. // same src file index
  681. AddStr(EncodeBase64VLQ(0));
  682. // same src line
  683. AddStr(EncodeBase64VLQ(0));
  684. // same src column
  685. AddStr(EncodeBase64VLQ(0));
  686. if j=Item.GeneratedLine then
  687. AddChar(',');
  688. end;
  689. end;
  690. LastGeneratedLine:=Item.GeneratedLine;
  691. end
  692. else if i>0 then
  693. begin
  694. // not the first segment
  695. if (LastGeneratedLine=Item.GeneratedLine)
  696. and (LastGeneratedColumn=Item.GeneratedColumn) then
  697. continue;
  698. AddChar(',');
  699. end;
  700. // column diff
  701. //writeln('TSourceMap.CreateMappings Seg=',i,' Gen:Line=',LastGeneratedLine,',Col=',Item.GeneratedColumn,' Src:File=',Item.SrcFileIndex,',Line=',Item.SrcLine,',Col=',Item.SrcColumn,' Name=',Item.NameIndex);
  702. AddStr(EncodeBase64VLQ(Item.GeneratedColumn-LastGeneratedColumn));
  703. LastGeneratedColumn:=Item.GeneratedColumn;
  704. if Item.SrcFileIndex<0 then
  705. continue; // no source -> segment length 1
  706. // src file index diff
  707. AddStr(EncodeBase64VLQ(Item.SrcFileIndex-LastSrcFileIndex));
  708. LastSrcFileIndex:=Item.SrcFileIndex;
  709. // src line diff
  710. SrcLine:=Item.SrcLine-1; // 0 based in version 3
  711. AddStr(EncodeBase64VLQ(SrcLine-LastSrcLine));
  712. LastSrcLine:=SrcLine;
  713. // src column diff
  714. AddStr(EncodeBase64VLQ(Item.SrcColumn-LastSrcColumn));
  715. LastSrcColumn:=Item.SrcColumn;
  716. // name index
  717. if Item.NameIndex<0 then
  718. continue; // no name -> segment length 4
  719. AddStr(EncodeBase64VLQ(Item.NameIndex-LastNameIndex));
  720. LastNameIndex:=Item.NameIndex;
  721. end;
  722. {$ifdef pas2js}
  723. Result:=buf.join('');
  724. {$else}
  725. SetLength(Result,buf.Size);
  726. if Result<>'' then
  727. Move(buf.Memory^,Result[1],buf.Size);
  728. {$endif}
  729. finally
  730. {$ifdef pas2js}
  731. {$else}
  732. buf.Free;
  733. {$endif}
  734. end;
  735. end;
  736. procedure TSourceMap.ParseMappings(const Mapping: String);
  737. const
  738. MaxInt = High(integer) div 2;
  739. {$ifdef UsePChar}
  740. var
  741. p: PChar;
  742. function Decode: NativeInt; inline;
  743. begin
  744. Result:=DecodeBase64VLQ(p);
  745. end;
  746. procedure E(const Msg: string);
  747. begin
  748. raise EJSSourceMap.CreateFmt(Msg,[PtrUInt(p-PChar(Mapping))+1]);
  749. end;
  750. {$else}
  751. var
  752. p: integer;
  753. function Decode: NativeInt; inline;
  754. begin
  755. Result:=DecodeBase64VLQ(Mapping,p);
  756. end;
  757. procedure E(const Msg: string);
  758. begin
  759. raise EJSSourceMap.CreateFmt(Msg,[p]);
  760. end;
  761. {$endif}
  762. var
  763. GeneratedLine, LastColumn, Column, LastSrcFileIndex, LastSrcLine,
  764. LastSrcColumn, LastNameIndex, SrcFileIndex, SrcLine, SrcColumn,
  765. NameIndex, l: Integer;
  766. ColDiff, SrcFileIndexDiff, SrcLineDiff, SrcColumnDiff,
  767. NameIndexDiff: NativeInt;
  768. Segment: TSourceMapSegment;
  769. begin
  770. l:=length(Mapping);
  771. if l=0 then exit;
  772. p:={$ifdef UsePChar}PChar(Mapping){$else}1{$endif};
  773. GeneratedLine:=1;
  774. LastColumn:=0;
  775. LastSrcFileIndex:=0;
  776. LastSrcLine:=0;
  777. LastSrcColumn:=0;
  778. LastNameIndex:=0;
  779. while {$ifdef UsePChar}true{$else}p<=l{$endif} do
  780. begin
  781. case {$ifdef UsePChar}p^{$else}Mapping[p]{$endif} of
  782. {$ifdef UsePChar}
  783. #0:
  784. if p-PChar(Mapping)=length(Mapping) then
  785. exit
  786. else
  787. E('unexpected #0 at %d');
  788. {$endif}
  789. ',':
  790. begin
  791. // next segment
  792. inc(p);
  793. end;
  794. ';':
  795. begin
  796. // next line
  797. inc(GeneratedLine);
  798. LastColumn:=0;
  799. inc(p);
  800. end;
  801. else
  802. begin
  803. ColDiff:=Decode;
  804. if (ColDiff>MaxInt) or (ColDiff<-MaxInt) then
  805. E('column out of range at %d');
  806. Column:=LastColumn+integer(ColDiff);
  807. if (Column>MaxInt) or (Column<-MaxInt) then
  808. E('column out of range at %d');
  809. LastColumn:=Column;
  810. Segment:=TSourceMapSegment.Create;
  811. Segment.Index:=FItems.Count;
  812. FItems.Add(Segment);
  813. Segment.GeneratedLine:=GeneratedLine;
  814. Segment.GeneratedColumn:=Column;
  815. Segment.SrcFileIndex:=-1;
  816. Segment.NameIndex:=-1;
  817. if {$ifdef UsePChar}not (p^ in [',',';',#0]){$else}(p<=l) and not (Mapping[p] in [',',';']){$endif} then
  818. begin
  819. // src file index
  820. SrcFileIndexDiff:=Decode;
  821. if (SrcFileIndexDiff>MaxInt) or (SrcFileIndexDiff<-MaxInt) then
  822. E('src file index out of range at %d');
  823. SrcFileIndex:=LastSrcFileIndex+integer(SrcFileIndexDiff);
  824. if (SrcFileIndex<0) or (SrcFileIndex>=SourceCount) then
  825. E('src file index out of range at %d');
  826. LastSrcFileIndex:=SrcFileIndex;
  827. Segment.SrcFileIndex:=SrcFileIndex;
  828. // src line
  829. SrcLineDiff:=Decode;
  830. if (SrcLineDiff>MaxInt) or (SrcLineDiff<-MaxInt) then
  831. E('src line out of range at %d');
  832. SrcLine:=LastSrcLine+integer(SrcLineDiff);
  833. if (SrcLine>MaxInt) or (SrcLine<-MaxInt) then
  834. E('src line out of range at %d');
  835. LastSrcLine:=SrcLine;
  836. Segment.SrcLine:=SrcLine+1; // lines are stored 0-based
  837. // src column
  838. SrcColumnDiff:=Decode;
  839. if (SrcColumnDiff>MaxInt) or (SrcColumnDiff<-MaxInt) then
  840. E('src column out of range at %d');
  841. SrcColumn:=LastSrcColumn+integer(SrcColumnDiff);
  842. if (SrcColumn>MaxInt) or (SrcColumn<-MaxInt) then
  843. E('src column out of range at %d');
  844. LastSrcColumn:=SrcColumn;
  845. Segment.SrcColumn:=SrcColumn;
  846. if {$ifdef UsePChar}not (p^ in [',',';',#0]){$else}(p<=l) and not (Mapping[p] in [',',';']){$endif} then
  847. begin
  848. // name index
  849. NameIndexDiff:=Decode;
  850. if (NameIndexDiff>MaxInt) or (NameIndexDiff<-MaxInt) then
  851. E('name index out of range at %d');
  852. NameIndex:=LastNameIndex+integer(NameIndexDiff);
  853. if (NameIndex<0) or (NameIndex>=NameCount) then
  854. E('name index out of range at %d');
  855. LastNameIndex:=NameIndex;
  856. Segment.NameIndex:=NameIndex;
  857. end;
  858. end;
  859. end;
  860. end;
  861. end;
  862. end;
  863. function TSourceMap.ToJSON: TJSONObject;
  864. var
  865. Obj: TJSONObject;
  866. i: Integer;
  867. Arr: TJSONArray;
  868. Mappings: String;
  869. begin
  870. Result:=nil;
  871. Mappings:=CreateMappings;
  872. Obj:=TJSONObject.Create;
  873. try
  874. // "version" - integer
  875. Obj.Add('version',Version);
  876. // "file" - GeneratedFilename
  877. if GeneratedFilename<>'' then
  878. Obj.Add('file',GeneratedFilename);
  879. // "sourceRoot" - SourceRoot
  880. if SourceRoot<>'' then
  881. Obj.Add('sourceRoot',SourceRoot);
  882. // "sources" - array of filenames
  883. Arr:=TJSONArray.Create;
  884. Obj.Add('sources',Arr);
  885. for i:=0 to SourceCount-1 do
  886. Arr.Add(SourceTranslatedFiles[i]);
  887. // "sourcesContent" - array of source content: null or source as string
  888. // only needed if there is a source
  889. i:=SourceCount-1;
  890. while i>=0 do
  891. if SourceContents[i]='' then
  892. dec(i)
  893. else
  894. begin
  895. // there is a source -> add array
  896. Arr:=TJSONArray.Create;
  897. Obj.Add('sourcesContent',Arr);
  898. for i:=0 to SourceCount-1 do
  899. if SourceContents[i]='' then
  900. Arr.Add(TJSONNull.Create)
  901. else
  902. Arr.Add(SourceContents[i]);
  903. break;
  904. end;
  905. // "names" - array of names
  906. Arr:=TJSONArray.Create;
  907. Obj.Add('names',Arr);
  908. for i:=0 to NameCount-1 do
  909. Arr.Add(Names[i]);
  910. // "mappings" - string
  911. Obj.Add('mappings',Mappings);
  912. Result:=Obj;
  913. finally
  914. if Result=nil then
  915. Obj.Free;
  916. end;
  917. end;
  918. function TSourceMap.ToString: string;
  919. var
  920. Obj: TJSONObject;
  921. begin
  922. Obj:=ToJSON;
  923. try
  924. if smoSafetyHeader in Options then
  925. Result:=DefaultSrcMapHeader+Obj.AsJSON
  926. else
  927. Result:=Obj.AsJSON;
  928. finally
  929. Obj.Free;
  930. end;
  931. end;
  932. procedure TSourceMap.LoadFromJSON(Obj: TJSONObject);
  933. var
  934. aVersion, i, j: integer;
  935. Arr: TJSONArray;
  936. Data: TJSONData;
  937. aFilename, aName: String;
  938. aMappings: String;
  939. begin
  940. // Note: does not support sections yet
  941. Clear;
  942. // "version" - integer
  943. aVersion:=Obj.Get('version',0);
  944. if aVersion<>Version then
  945. raise EJSSourceMap.CreateFmt('unsupported version %d',[aVersion]);
  946. // "file" - GeneratedFilename
  947. GeneratedFilename:=String(Obj.Get('file',''));
  948. // "sourceRoot" - SourceRoot
  949. SourceRoot:=Obj.Get('sourceRoot','');
  950. // "sources" - array of filenames
  951. Arr:=nil;
  952. if not Obj.Find('sources',Arr) then
  953. raise EJSSourceMap.Create('missing sources array');
  954. for i:=0 to Arr.Count-1 do
  955. begin
  956. Data:=Arr[i];
  957. if not (Data is TJSONString) then
  958. raise EJSSourceMap.CreateFmt('sources must string, but found %s',[Data.ClassName]);
  959. aFilename:=String(TJSONString(Data).AsString);
  960. j:=IndexOfSourceFile(aFilename,true);
  961. if j<>i then
  962. raise EJSSourceMap.CreateFmt('duplicate source file "%s" at %d',[aFilename,i]);
  963. end;
  964. // optional: "sourcesContent" - array of sources
  965. Arr:=nil;
  966. if Obj.Find('sourcesContent',Arr) then
  967. begin
  968. if Arr.Count<>SourceCount then
  969. raise EJSSourceMap.CreateFmt('number of elements in sources %d mismatch sourcesContent %d',[SourceCount,Arr.Count]);
  970. for i:=0 to Arr.Count-1 do
  971. begin
  972. Data:=Arr[i];
  973. if (Data is TJSONString) then
  974. SourceContents[i]:=String(TJSONString(Data).AsString)
  975. else if Data is TJSONNull then
  976. else
  977. raise EJSSourceMap.CreateFmt('sourcesContent[%d] must be string',[i]);
  978. end;
  979. end;
  980. // optional: "names" - array of strings
  981. Arr:=nil;
  982. if Obj.Find('names',Arr) then
  983. for i:=0 to Arr.Count-1 do
  984. begin
  985. Data:=Arr[i];
  986. if not (Data is TJSONString) then
  987. raise EJSSourceMap.CreateFmt('names must string, but found %s',[Data.ClassName]);
  988. aName:=String(TJSONString(Data).AsString);
  989. j:=IndexOfName(aName,true);
  990. if j<>i then
  991. raise EJSSourceMap.CreateFmt('duplicate name "%s" at %d',[aName,i]);
  992. end;
  993. // "mappings" - string
  994. aMappings:=Obj.Get('mappings','');
  995. ParseMappings(aMappings);
  996. end;
  997. procedure TSourceMap.SaveToStream(aStream: TFPJSStream);
  998. var
  999. Obj: TJSONObject;
  1000. begin
  1001. Obj:=ToJSON;
  1002. try
  1003. if smoSafetyHeader in Options then
  1004. begin
  1005. {$ifdef pas2js}
  1006. aStream.push(DefaultSrcMapHeader);
  1007. {$else}
  1008. aStream.Write(DefaultSrcMapHeader[1],length(DefaultSrcMapHeader));
  1009. {$endif}
  1010. end;
  1011. Obj.DumpJSON(aStream);
  1012. finally
  1013. Obj.Free;
  1014. end;
  1015. end;
  1016. {$ifdef HasStreams}
  1017. procedure TSourceMap.LoadFromStream(aStream: TStream);
  1018. var
  1019. s: string;
  1020. P: TJSONParser;
  1021. Data: TJSONData;
  1022. begin
  1023. s:='';
  1024. SetLength(s,aStream.Size-aStream.Position);
  1025. if s<>'' then
  1026. aStream.Read(s[1],length(s));
  1027. if LeftStr(s,4)=')]}''' then
  1028. Delete(s,1,4)
  1029. else if LeftStr(s,3)=')]}' then
  1030. Delete(s,1,3);
  1031. P:=TJSONParser.Create(s,[joUTF8]);
  1032. try
  1033. Data:=P.Parse;
  1034. if not (Data is TJSONObject) then
  1035. raise EJSSourceMap.Create('source map must be a JSON object');
  1036. LoadFromJSON(TJSONObject(Data));
  1037. finally
  1038. P.Free;
  1039. end;
  1040. end;
  1041. procedure TSourceMap.SaveToFile(Filename: string);
  1042. var
  1043. TheStream: TMemoryStream;
  1044. begin
  1045. TheStream:=TMemoryStream.Create;
  1046. try
  1047. SaveToStream(TheStream);
  1048. TheStream.Position:=0;
  1049. TheStream.SaveToFile(Filename);
  1050. finally
  1051. TheStream.Free;
  1052. end;
  1053. end;
  1054. procedure TSourceMap.LoadFromFile(Filename: string);
  1055. var
  1056. TheStream: TMemoryStream;
  1057. begin
  1058. TheStream:=TMemoryStream.Create;
  1059. try
  1060. TheStream.LoadFromFile(Filename);
  1061. TheStream.Position:=0;
  1062. LoadFromStream(TheStream);
  1063. finally
  1064. TheStream.Free;
  1065. end;
  1066. end;
  1067. {$endif}
  1068. function TSourceMap.IndexOfName(const Name: string; AddIfNotExists: boolean
  1069. ): integer;
  1070. begin
  1071. Result:=FNameToIndex.FindValue(Name);
  1072. if (Result>=0) or not AddIfNotExists then exit;
  1073. Result:=FNames.Count;
  1074. FNames.Add(Name);
  1075. FNameToIndex.Add(Name,Result);
  1076. end;
  1077. function TSourceMap.IndexOfSourceFile(const SrcFile: string;
  1078. AddIfNotExists: boolean): integer;
  1079. var
  1080. Src: TSourceMapSrc;
  1081. begin
  1082. Result:=FSourceToIndex.FindValue(SrcFile);
  1083. if (Result>=0) or not AddIfNotExists then exit;
  1084. Src:=TSourceMapSrc.Create;
  1085. Src.Filename:=SrcFile;
  1086. Src.TranslatedFilename:=SrcFile;
  1087. Result:=FSources.Count;
  1088. FSources.Add(Src);
  1089. FSourceToIndex.Add(SrcFile,Result);
  1090. end;
  1091. function TSourceMap.IndexOfSegmentAt(GeneratedLine, GeneratedCol: integer
  1092. ): integer;
  1093. var
  1094. l, r, m: Integer;
  1095. aSeg: TSourceMapSegment;
  1096. begin
  1097. Sort;
  1098. l:=0;
  1099. r:=Count-1;
  1100. aSeg:=nil;
  1101. while l<=r do
  1102. begin
  1103. m:=(l+r) div 2;
  1104. aSeg:=Items[m];
  1105. if aSeg.GeneratedLine<GeneratedLine then
  1106. l:=m+1
  1107. else if aSeg.GeneratedLine>GeneratedLine then
  1108. r:=m-1
  1109. else if aSeg.GeneratedColumn<GeneratedCol then
  1110. l:=m+1
  1111. else if aSeg.GeneratedColumn>GeneratedCol then
  1112. r:=m-1
  1113. else
  1114. begin
  1115. // exact match found
  1116. Result:=m;
  1117. // -> return the leftmost exact match
  1118. while Result>0 do
  1119. begin
  1120. aSeg:=Items[Result-1];
  1121. if (aSeg.GeneratedLine<>GeneratedLine)
  1122. or (aSeg.GeneratedColumn<>GeneratedCol) then
  1123. exit;
  1124. dec(Result);
  1125. end;
  1126. exit;
  1127. end;
  1128. end;
  1129. // no exact match found
  1130. if aSeg=nil then
  1131. exit(-1);
  1132. // return the next lower. Note: there may be no such segment
  1133. if (aSeg.GeneratedLine>GeneratedLine)
  1134. or ((aSeg.GeneratedLine=GeneratedLine) and (aSeg.GeneratedColumn>GeneratedCol)) then
  1135. dec(m);
  1136. Result:=m;
  1137. end;
  1138. function TSourceMap.Count: integer;
  1139. begin
  1140. Result:=FItems.Count;
  1141. end;
  1142. function TSourceMap.SourceCount: integer;
  1143. begin
  1144. Result:=FSources.Count;
  1145. end;
  1146. function TSourceMap.NameCount: integer;
  1147. begin
  1148. Result:=FNames.Count;
  1149. end;
  1150. end.