cldrxml.pas 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634
  1. { Parser of the CLDR collation xml files.
  2. Copyright (c) 2013 by Inoussa OUEDRAOGO
  3. The source code is distributed under the Library GNU
  4. General Public License with the following modification:
  5. - object files and libraries linked into an application may be
  6. distributed without source code.
  7. If you didn't receive a copy of the file COPYING, contact:
  8. Free Software Foundation
  9. 675 Mass Ave
  10. Cambridge, MA 02139
  11. USA
  12. This program is distributed in the hope that it will be useful,
  13. but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  15. }
  16. unit cldrxml;
  17. {$mode objfpc}{$H+}
  18. {$TypedAddress on}
  19. interface
  20. uses
  21. Classes, SysUtils, DOM,
  22. cldrhelper;
  23. procedure ParseInitialDocument(ASequence : POrderedCharacters; ADoc : TDOMDocument);overload;
  24. procedure ParseInitialDocument(ASequence : POrderedCharacters; AFileName : string);overload;
  25. procedure ParseCollationDocument(ADoc : TDOMDocument; ACollation : TCldrCollation);
  26. procedure ParseCollationDocument(const AFileName : string; ACollation : TCldrCollation);
  27. resourcestring
  28. sCaseNothandled = 'This case is not handled : "%s", Position = %d.';
  29. sCodePointExpected = 'Code Point node expected as child at this position "%d".';
  30. sCollationsNodeNotFound = '"collations" node not found.';
  31. sHexAttributeExpected = '"hex" attribute expected at this position "%d".';
  32. sInvalidResetClause = 'Invalid "Reset" clause.';
  33. sNodeNameAssertMessage = 'Expected NodeName "%s", got "%s".';
  34. sRulesNodeNotFound = '"rules" node not found.';
  35. sTextNodeChildExpected = '(Child) text node expected at this position "%d", but got "%s".';
  36. sUniqueChildNodeExpected = 'Unique child node expected at this position "%d".';
  37. sUnknownResetLogicalPosition = 'Unknown reset logical position : "%s".';
  38. implementation
  39. uses
  40. typinfo, XMLRead, XPath, Helper, unicodeset;
  41. const
  42. s_AT = 'at';
  43. s_BEFORE = 'before';
  44. s_CODEPOINT = 'codepoint';
  45. s_COLLATION = 'collation';
  46. s_COLLATIONS = 'collations';
  47. s_CONTEXT = 'context';
  48. s_DEFAULT = 'default';
  49. s_EXTEND = 'extend';
  50. s_HEX = 'hex';
  51. s_POSITION = 'position';
  52. s_RESET = 'reset';
  53. s_RULES = 'rules';
  54. s_STANDART = 'standard';
  55. s_TYPE = 'type';
  56. procedure CheckNodeName(ANode : TDOMNode; const AExpectedName : DOMString);
  57. begin
  58. if (ANode.NodeName <> AExpectedName) then
  59. raise Exception.CreateFmt(sNodeNameAssertMessage,[AExpectedName,ANode.NodeName]);
  60. end;
  61. function CharToReorderWeigthKind(const AChar : Char) : TReorderWeigthKind;inline;
  62. begin
  63. case AChar of
  64. 'p' : Result := TReorderWeigthKind.PriMary;
  65. 's' : Result := TReorderWeigthKind.Secondary;
  66. 't' : Result := TReorderWeigthKind.Tertiary;
  67. 'i' : Result := TReorderWeigthKind.Identity;
  68. else
  69. Result := TReorderWeigthKind.Identity;
  70. end;
  71. end;
  72. function DomString2UnicodeCodePointArray(const AValue : DOMString): TUnicodeCodePointArray;
  73. var
  74. u4str : UCS4String;
  75. k : Integer;
  76. begin
  77. if (Length(AValue) = 0) then
  78. exit(nil);
  79. if (Length(AValue) = 1) then begin
  80. SetLength(Result,1);
  81. Result[0] := Ord(AValue[1])
  82. end else begin
  83. u4str := WideStringToUCS4String(AValue);
  84. k := Length(u4str) - 1; // remove the last #0
  85. SetLength(Result,k);
  86. for k := 0 to k - 1 do
  87. Result[k] := u4str[k];
  88. end;
  89. end;
  90. function TryStrToLogicalReorder(
  91. const AValue : string;
  92. out AResult : TReorderLogicalReset
  93. ) : Boolean;
  94. var
  95. s : string;
  96. i : Integer;
  97. begin
  98. s := StringReplace(AValue,' ','',[rfReplaceAll]);
  99. s := StringReplace(s,'_','',[rfReplaceAll]);
  100. i := GetEnumValue(TypeInfo(TReorderLogicalReset),s);
  101. Result := (i > -1);
  102. if Result then
  103. AResult := TReorderLogicalReset(i);
  104. end;
  105. function ParseStatement(
  106. ARules : TDOMElement;
  107. AStartPosition : Integer;
  108. AStatement : PReorderSequence;
  109. var ANextPos : Integer
  110. ) : Boolean;
  111. var
  112. startPosition : Integer;
  113. statement : PReorderSequence;
  114. elementActualCount : Integer;
  115. list : TDOMNodeList;
  116. inBlock : Boolean;
  117. procedure SkipComments();
  118. begin
  119. while (startPosition < list.Count) do begin
  120. if (list[startPosition].NodeType <> COMMENT_NODE) then
  121. Break;
  122. Inc(startPosition);
  123. end;
  124. end;
  125. function parse_reset() : Integer;
  126. var
  127. n, t : TDOMNode;
  128. s : string;
  129. logicalPos : TReorderLogicalReset;
  130. begin
  131. SkipComments();
  132. n := list[startPosition];
  133. CheckNodeName(n,s_RESET);
  134. if n.HasChildNodes() then begin
  135. n := n.FirstChild;
  136. if (n.NodeType = TEXT_NODE) then begin
  137. statement^.Reset := DomString2UnicodeCodePointArray(Trim(TDOMText(n).Data));
  138. Result := startPosition+1;
  139. end else begin
  140. if not TryStrToLogicalReorder(n.NodeName,logicalPos) then
  141. raise Exception.CreateFmt(sUnknownResetLogicalPosition,[n.NodeName]);
  142. statement^.LogicalPosition := logicalPos;
  143. Result := startPosition+1;
  144. end;
  145. end else if not n.HasChildNodes() then begin
  146. if (list[startPosition+1].NodeName = s_POSITION) then begin
  147. s := list[startPosition+1].Attributes.GetNamedItem(s_AT).NodeValue;
  148. if not TryStrToLogicalReorder(s,logicalPos) then
  149. raise Exception.CreateFmt(sUnknownResetLogicalPosition,[s]);
  150. statement^.LogicalPosition := logicalPos;
  151. Result := startPosition+2;
  152. end else begin
  153. t := list[startPosition+1];
  154. {if (t.NodeType <> TEXT_NODE) then
  155. raise Exception.CreateFmt(sTextNodeChildExpected,[(startPosition+1),(t.NodeName+'('+t.ClassName+')')]);}
  156. if (t.NodeType = TEXT_NODE) then
  157. statement^.Reset := DomString2UnicodeCodePointArray(Trim(TDOMText(t).Data))
  158. else
  159. statement^.Reset := DomString2UnicodeCodePointArray(' ');
  160. Result := startPosition+2;
  161. end;
  162. end;
  163. if (statement^.LogicalPosition = TReorderLogicalReset.None) and
  164. (Length(statement^.Reset) = 0)
  165. then
  166. raise Exception.Create(sInvalidResetClause);
  167. end;
  168. procedure EnsureElementLength(const ALength : Integer);
  169. var
  170. k, d : Integer;
  171. begin
  172. k := Length(statement^.Elements);
  173. if (k < ALength) then begin
  174. k := ALength;
  175. if (k = 0) then begin
  176. k := 50;
  177. end else begin
  178. if (k < 10) then
  179. d := 10
  180. else
  181. d := 2;
  182. k := k * d;
  183. end;
  184. SetLength(statement^.Elements,k);
  185. end;
  186. end;
  187. {procedure AddElement(AText : DOMString; AWeigthKind : TReorderWeigthKind);overload;
  188. var
  189. u4str : UCS4String;
  190. k : Integer;
  191. kp : PReorderUnit;
  192. begin
  193. u4str := WideStringToUCS4String(AText);
  194. EnsureElementLength(elementActualCount+1);
  195. kp := @statement^.Elements[elementActualCount];
  196. k := Length(u4str) - 1{null terminated};
  197. SetLength(kp^.Characters,k);
  198. for k := 0 to k - 1 do
  199. kp^.Characters[k] := u4str[k];
  200. kp^.WeigthKind:= AWeigthKind;
  201. elementActualCount := elementActualCount + 1;
  202. end;}
  203. procedure AddElement(
  204. const AChars : array of UCS4Char;
  205. const AWeigthKind : TReorderWeigthKind;
  206. const AContext : DOMString
  207. );overload;
  208. var
  209. kp : PReorderUnit;
  210. k : Integer;
  211. begin
  212. EnsureElementLength(elementActualCount+1);
  213. kp := @statement^.Elements[elementActualCount];
  214. SetLength(kp^.Characters,Length(AChars));
  215. for k := 0 to Length(AChars) - 1 do
  216. kp^.Characters[k] := AChars[k];
  217. kp^.WeigthKind := AWeigthKind;
  218. elementActualCount := elementActualCount + 1;
  219. if (AContext <> '') then
  220. kp^.Context := DomString2UnicodeCodePointArray(AContext);
  221. end;
  222. procedure ReadChars(
  223. ANode : TDOMNode;
  224. APos : Integer;
  225. var AChars : UCS4String
  226. );
  227. var
  228. t : TDOMNode;
  229. u4str : UCS4String;
  230. s : DOMString;
  231. begin
  232. if not ANode.HasChildNodes() then begin
  233. SetLength(AChars,1);
  234. AChars[0] := Ord(UnicodeChar(' '));
  235. exit;
  236. //raise Exception.CreateFmt(sCodePointExpected + ANode.ClassName,[APos]);
  237. end;
  238. t := ANode.FindNode(s_CODEPOINT);
  239. if (t = nil) then begin
  240. if (ANode.ChildNodes.Count <> 1) then
  241. raise Exception.CreateFmt(sUniqueChildNodeExpected,[APos]);
  242. t := ANode.ChildNodes[0];
  243. if not t.InheritsFrom(TDOMText) then
  244. raise Exception.CreateFmt(sTextNodeChildExpected,[APos,(t.NodeName+'('+t.ClassName+')')]);
  245. s := TDOMText(t).Data;
  246. if (Length(s) = 1) then begin
  247. SetLength(AChars,1);
  248. AChars[0] := Ord(s[1]);
  249. end else begin
  250. u4str := WideStringToUCS4String(s);
  251. AChars := u4str;
  252. SetLength(AChars,Length(AChars)-1);
  253. end;
  254. end else begin
  255. t := t.Attributes.GetNamedItem(s_HEX);
  256. if (t = nil) then
  257. raise Exception.CreateFmt(sHexAttributeExpected,[APos]);
  258. SetLength(AChars,1);
  259. AChars[0] := StrToInt('$'+t.NodeValue);
  260. end
  261. end;
  262. procedure AddPrefixChars(const APrefix : array of UCS4Char; var ADest : TUnicodeCodePointArray);
  263. var
  264. k : Integer;
  265. begin
  266. k := Length(ADest);
  267. SetLength(ADest,(k+Length(APrefix)));
  268. Move(ADest[0],ADest[k+1],(SizeOf(k*ADest[0])));
  269. for k := 0 to k - 1 do
  270. ADest[k] := APrefix[k];
  271. end;
  272. function ReadNextItem(const APos : Integer) : Integer;
  273. var
  274. n, t : TDOMNode;
  275. s, contextStr : DOMString;
  276. w : TReorderWeigthKind;
  277. isSimpleCharTag : Boolean;
  278. simpleCharTag : AnsiChar;
  279. last : PReorderUnit;
  280. u4str : UCS4String;
  281. k : Integer;
  282. begin
  283. contextStr := '';
  284. Result := APos;
  285. n := list[APos];
  286. isSimpleCharTag := (Length(n.NodeName) = 1) and (Ord(n.NodeName[1])<=127);
  287. if isSimpleCharTag then begin
  288. simpleCharTag := AnsiChar(n.NodeName[1]);
  289. if (simpleCharTag = 'x') then begin
  290. inBlock := True;
  291. n := n.FirstChild;
  292. if (n.NodeName = s_CONTEXT) then begin
  293. if n.HasChildNodes() then begin
  294. t := n.FirstChild;
  295. if (t.NodeType = TEXT_NODE) then
  296. contextStr := TDOMText(t).Data;
  297. end;
  298. n := n.NextSibling;
  299. end;
  300. isSimpleCharTag := (Length(n.NodeName) = 1) and (Ord(n.NodeName[1])<=127);
  301. if isSimpleCharTag then
  302. simpleCharTag := AnsiChar(n.NodeName[1]);
  303. end;
  304. end;
  305. if isSimpleCharTag and (simpleCharTag in ['p','s','t','i']) then begin
  306. w := CharToReorderWeigthKind(AnsiChar(n.NodeName[1]));
  307. ReadChars(n,APos,u4str);
  308. AddElement(u4str,w,contextStr);
  309. Result := Result + 1;
  310. if not inBlock then
  311. exit;
  312. last := @statement^.Elements[elementActualCount-1];
  313. n := n.NextSibling;
  314. if (n <> nil) and (n.NodeName = s_EXTEND) then begin
  315. ReadChars(n,APos,u4str);
  316. SetLength(last^.ExpansionChars,Length(u4str));
  317. for k := 0 to Length(u4str) - 1 do
  318. last^.ExpansionChars[k] := u4str[k];
  319. end;
  320. exit;
  321. end;
  322. if (Length(n.NodeName) = 2) and (n.NodeName[2] = 'c') and
  323. (Ord(n.NodeName[1])<=127) and (AnsiChar(n.NodeName[1]) in ['p','s','t','i'])
  324. then begin
  325. w := CharToReorderWeigthKind(AnsiChar(n.NodeName[1]));
  326. ReadChars(n,APos,u4str);
  327. for k := Low(u4str) to High(u4str) do
  328. AddElement(u4str[k],w,contextStr);
  329. Result := Result + 1;
  330. exit;
  331. end;
  332. raise Exception.CreateFmt(sCaseNothandled,[n.NodeName,APos]);
  333. end;
  334. var
  335. i, c : Integer;
  336. n : TDOMNode;
  337. begin
  338. Result := False;
  339. inBlock := False;
  340. elementActualCount := 0;
  341. if (AStartPosition <= 0) then
  342. startPosition := 0
  343. else
  344. startPosition := AStartPosition;
  345. i := startPosition;
  346. list := ARules.ChildNodes;
  347. c := list.Count;
  348. if (c <= i) then
  349. exit;
  350. statement := AStatement;
  351. statement^.Clear();
  352. n := list[i];
  353. i := parse_reset();
  354. while (i < c) do begin
  355. n := list[i];
  356. if (n.NodeName = s_RESET) then
  357. Break;
  358. i := ReadNextItem(i);
  359. end;
  360. SetLength(statement^.Elements,elementActualCount);
  361. Result := (i > startPosition);
  362. if Result then
  363. ANextPos := i;
  364. end;
  365. procedure ParseInitialDocument(ASequence : POrderedCharacters; ADoc : TDOMDocument);
  366. var
  367. n : TDOMNode;
  368. rulesElement : TDOMElement;
  369. i, c, nextPost : Integer;
  370. statement : TReorderSequence;
  371. p : PReorderUnit;
  372. begin
  373. n := ADoc.DocumentElement.FindNode(s_RULES);
  374. if (n = nil) then
  375. raise Exception.Create(sRulesNodeNotFound);
  376. rulesElement := n as TDOMElement;
  377. c := rulesElement.ChildNodes.Count;
  378. ASequence^.Clear();
  379. SetLength(ASequence^.Data,c+100);
  380. nextPost := 0;
  381. i := 0;
  382. while (i < c) do begin
  383. statement.Clear();
  384. if not ParseStatement(rulesElement,i,@statement,nextPost) then
  385. Break;
  386. i := nextPost;
  387. try
  388. ASequence^.ApplyStatement(@statement);
  389. except
  390. on e : Exception do begin
  391. e.Message := Format('%s Position = %d',[e.Message,i]);
  392. raise;
  393. end;
  394. end;
  395. end;
  396. if (ASequence^.ActualLength > 0) then begin
  397. p := @ASequence^.Data[0];
  398. for i := 0 to ASequence^.ActualLength - 1 do begin
  399. p^.Changed := False;
  400. Inc(p);
  401. end;
  402. end;
  403. end;
  404. procedure ParseInitialDocument(ASequence : POrderedCharacters; AFileName : string);
  405. var
  406. doc : TXMLDocument;
  407. begin
  408. ReadXMLFile(doc,AFileName);
  409. try
  410. ParseInitialDocument(ASequence,doc);
  411. finally
  412. doc.Free();
  413. end;
  414. end;
  415. function EvaluateXPathStr(const AExpression : string; AContextNode : TDOMNode): DOMString;
  416. var
  417. xv : TXPathVariable;
  418. begin
  419. xv := EvaluateXPathExpression(AExpression,AContextNode);
  420. try
  421. if (xv <> nil) then
  422. Result := xv.AsText
  423. else
  424. Result := '';
  425. finally
  426. xv.Free();
  427. end;
  428. end;
  429. function ParseDeletion(
  430. const APattern : DOMString;
  431. ASequence : PReorderSequence
  432. ) : Integer;
  433. var
  434. r : array of TReorderUnit;
  435. c, i : Integer;
  436. uset : TUnicodeSet;
  437. it : TUnicodeSet.TIterator;
  438. p : PReorderUnit;
  439. begin
  440. if (APattern = '') then
  441. exit(0);
  442. it := nil;
  443. uset := TUnicodeSet.Create();
  444. try
  445. uset.AddPattern(APattern);
  446. it := uset.CreateIterator();
  447. c := 0;
  448. it.Reset();
  449. while it.MoveNext() do begin
  450. Inc(c);
  451. end;
  452. SetLength(r,c);
  453. p := @r[0];
  454. i := 0;
  455. it.Reset();
  456. while it.MoveNext() do begin
  457. p^.Clear();
  458. p^.WeigthKind := TReorderWeigthKind.Deletion;
  459. p^.Characters := Copy(it.GetCurrent());
  460. Inc(p);
  461. Inc(i);
  462. end;
  463. ASequence^.Clear();
  464. ASequence^.Elements := r;
  465. finally
  466. it.Free();
  467. uset.Free();
  468. end;
  469. SetLength(r,0);
  470. end;
  471. procedure ParseCollationItem(ACollationNode : TDOMElement; AItem : TCldrCollationItem);
  472. var
  473. n : TDOMNode;
  474. rulesElement : TDOMElement;
  475. i, c, nextPos : Integer;
  476. statementList : TReorderSequenceArray;
  477. sal : Integer;//statement actual length
  478. statement : PReorderSequence;
  479. s : DOMString;
  480. begin
  481. AItem.TypeName := ACollationNode.GetAttribute(s_TYPE);
  482. AItem.Base := EvaluateXPathStr('base',ACollationNode);
  483. AItem.Backwards := (EvaluateXPathStr('settings/@backwards',ACollationNode) = 'on');
  484. if AItem.Backwards then
  485. AItem.ChangedFields := AItem.ChangedFields + [TCollationField.BackWard];
  486. SetLength(statementList,15);
  487. sal := 0;
  488. statement := @statementList[0];
  489. s := EvaluateXPathStr('suppress_contractions',ACollationNode);
  490. if (s <> '') then begin
  491. if (ParseDeletion(s,statement) > 0) then begin
  492. Inc(sal);
  493. Inc(statement);
  494. end else begin
  495. statement^.Clear();
  496. end;
  497. end;
  498. n := ACollationNode.FindNode(s_RULES);
  499. if (n <> nil) then begin
  500. rulesElement := n as TDOMElement;
  501. c := rulesElement.ChildNodes.Count;
  502. nextPos := 0;
  503. i := 0;
  504. while (i < c) do begin
  505. statement^.Clear();
  506. if not ParseStatement(rulesElement,i,statement,nextPos) then
  507. Break;
  508. i := nextPos;
  509. Inc(statement);
  510. Inc(sal);
  511. if (sal >= Length(statementList)) then begin
  512. SetLength(statementList,(sal*2));
  513. statement := @statementList[(sal-1)];
  514. end;
  515. end;
  516. end;
  517. SetLength(statementList,sal);
  518. AItem.Rules := statementList;
  519. end;
  520. procedure ParseCollationDocument(ADoc : TDOMDocument; ACollation : TCldrCollation);
  521. var
  522. rulesNodes, n : TDOMNode;
  523. collationsElement, rulesElement : TDOMElement;
  524. i, c : Integer;
  525. item : TCldrCollationItem;
  526. nl : TDOMNodeList;
  527. begin
  528. n := ADoc.DocumentElement.FindNode(s_COLLATIONS);
  529. if (n = nil) then
  530. raise Exception.Create(sCollationsNodeNotFound);
  531. collationsElement := n as TDOMElement;
  532. ACollation.Clear();
  533. ACollation.Language := EvaluateXPathStr('identity/language/@type',ADoc.DocumentElement);
  534. ACollation.Version := EvaluateXPathStr('identity/version/@number',ADoc.DocumentElement);
  535. ACollation.DefaultType := EvaluateXPathStr('collations/default/@type',ADoc.DocumentElement);
  536. if collationsElement.HasChildNodes() then begin
  537. nl := collationsElement.ChildNodes;
  538. c := nl.Count;
  539. item := nil;
  540. try
  541. for i := 0 to c - 1 do begin
  542. n := nl[i];
  543. if (n.NodeName = s_COLLATION) then begin
  544. item := TCldrCollationItem.Create();
  545. ParseCollationItem((n as TDOMElement),item);
  546. ACollation.Add(item);
  547. item := nil;
  548. end
  549. end;
  550. except
  551. FreeAndNil(item);
  552. raise;
  553. end;
  554. end;
  555. end;
  556. function ReadXMLFile(f: TStream) : TXMLDocument;
  557. var
  558. src : TXMLInputSource;
  559. parser: TDOMParser;
  560. begin
  561. src := TXMLInputSource.Create(f);
  562. Result := TXMLDocument.Create;
  563. parser := TDOMParser.Create();
  564. try
  565. parser.Options.IgnoreComments := True;
  566. parser.Parse(src, Result);
  567. finally
  568. src.Free();
  569. parser.Free;
  570. end;
  571. end;
  572. function ReadXMLFile(const AFilename: String) : TXMLDocument;
  573. var
  574. FileStream: TStream;
  575. begin
  576. Result := nil;
  577. FileStream := TFileStream.Create(AFilename, fmOpenRead+fmShareDenyWrite);
  578. try
  579. Result := ReadXMLFile(FileStream);
  580. finally
  581. FileStream.Free;
  582. end;
  583. end;
  584. procedure ParseCollationDocument(const AFileName : string; ACollation : TCldrCollation);
  585. var
  586. doc : TXMLDocument;
  587. begin
  588. doc := ReadXMLFile(AFileName);
  589. try
  590. ParseCollationDocument(doc,ACollation);
  591. ACollation.LocalID := ExtractFileName(ChangeFileExt(AFileName,''));
  592. finally
  593. doc.Free();
  594. end;
  595. end;
  596. end.