sax_html.pp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670
  1. {
  2. $Id$
  3. This file is part of the Free Component Library
  4. HTML parser with SAX-like interface
  5. Copyright (c) 2000-2002 by
  6. Areca Systems GmbH / Sebastian Guenther, [email protected]
  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. {
  14. Known problems:
  15. * The whitespace handling does only work for processing the DOM tree.
  16. Storing the DOM tree to a XML file will result in a quite ugly file.
  17. (This probably has got much better with recent versions, which do
  18. decent whitespace converting, but it's not tested really good.)
  19. * Entity references in attribute values don't get parsed.
  20. }
  21. unit SAX_HTML;
  22. interface
  23. uses SysUtils, Classes, SAX, DOM, DOM_HTML;
  24. type
  25. { THTMLReader: The HTML reader class }
  26. THTMLScannerContext = (
  27. scUnknown,
  28. scWhitespace, // within whitespace
  29. scText, // within text
  30. scEntityReference, // within entity reference ("&...;")
  31. scTag); // within a start tag or end tag
  32. THTMLReader = class(TSAXReader)
  33. private
  34. FStarted: Boolean;
  35. FEndOfStream: Boolean;
  36. FScannerContext: THTMLScannerContext;
  37. FTokenText: SAXString;
  38. FCurStringValueDelimiter: Char;
  39. FAttrNameRead: Boolean;
  40. protected
  41. procedure EnterNewScannerContext(NewContext: THTMLScannerContext);
  42. public
  43. constructor Create;
  44. destructor Destroy; override;
  45. procedure Parse(AInput: TSAXInputSource); override; overload;
  46. property EndOfStream: Boolean read FEndOfStream;
  47. property ScannerContext: THTMLScannerContext read FScannerContext;
  48. property TokenText: SAXString read FTokenText;
  49. end;
  50. { THTMLToDOMConverter }
  51. THTMLNodeType = (ntWhitespace, ntText, ntEntityReference, ntTag);
  52. THTMLNodeInfo = class
  53. NodeType: THTMLNodeType;
  54. DOMNode: TDOMNode;
  55. end;
  56. THTMLToDOMConverter = class
  57. private
  58. FReader: THTMLReader;
  59. FDocument: TDOMDocument;
  60. FElementStack: TList;
  61. FNodeBuffer: TList;
  62. IsFragmentMode, FragmentRootSet: Boolean;
  63. FragmentRoot: TDOMNode;
  64. procedure ReaderCharacters(Sender: TObject; const ch: PSAXChar;
  65. Start, Count: Integer);
  66. procedure ReaderIgnorableWhitespace(Sender: TObject; const ch: PSAXChar;
  67. Start, Count: Integer);
  68. procedure ReaderSkippedEntity(Sender: TObject; const Name: SAXString);
  69. procedure ReaderStartElement(Sender: TObject;
  70. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  71. procedure ReaderEndElement(Sender: TObject;
  72. const NamespaceURI, LocalName, RawName: SAXString);
  73. public
  74. constructor Create(AReader: THTMLReader; ADocument: TDOMDocument);
  75. constructor CreateFragment(AReader: THTMLReader; AFragmentRoot: TDOMNode);
  76. destructor Destroy; override;
  77. end;
  78. // Helper functions; these ones are HTML equivalents of ReadXML[File|Fragment]
  79. procedure ReadHTMLFile(var ADoc: THTMLDocument; const AFilename: String);
  80. procedure ReadHTMLFile(var ADoc: THTMLDocument; var f: TStream);
  81. procedure ReadHTMLFragment(AParentNode: TDOMNode; const AFilename: String);
  82. procedure ReadHTMLFragment(AParentNode: TDOMNode; var f: TStream);
  83. implementation
  84. uses HTMLDefs;
  85. const
  86. WhitespaceChars = [#9, #10, #13, ' '];
  87. constructor THTMLReader.Create;
  88. begin
  89. inherited Create;
  90. FScannerContext := scUnknown;
  91. end;
  92. destructor THTMLReader.Destroy;
  93. begin
  94. if FStarted then
  95. DoEndDocument;
  96. inherited Destroy;
  97. end;
  98. procedure THTMLReader.Parse(AInput: TSAXInputSource);
  99. const
  100. MaxBufferSize = 1024;
  101. var
  102. Buffer: array[0..MaxBufferSize - 1] of Char;
  103. BufferSize, BufferPos: Integer;
  104. begin
  105. if not FStarted then
  106. begin
  107. FStarted := True;
  108. DoStartDocument;
  109. end;
  110. FEndOfStream := False;
  111. while True do
  112. begin
  113. // Read data into the input buffer
  114. BufferSize := AInput.Stream.Read(Buffer, MaxBufferSize);
  115. if BufferSize = 0 then
  116. begin
  117. FEndOfStream := True;
  118. break;
  119. end;
  120. BufferPos := 0;
  121. while BufferPos < BufferSize do
  122. case ScannerContext of
  123. scUnknown:
  124. case Buffer[BufferPos] of
  125. #9, #10, #13, ' ':
  126. EnterNewScannerContext(scWhitespace);
  127. '&':
  128. begin
  129. Inc(BufferPos);
  130. EnterNewScannerContext(scEntityReference);
  131. end;
  132. '<':
  133. begin
  134. Inc(BufferPos);
  135. EnterNewScannerContext(scTag);
  136. end;
  137. else
  138. EnterNewScannerContext(scText);
  139. end;
  140. scWhitespace:
  141. case Buffer[BufferPos] of
  142. #9, #10, #13, ' ':
  143. begin
  144. FTokenText := FTokenText + Buffer[BufferPos];
  145. Inc(BufferPos);
  146. end;
  147. '&':
  148. begin
  149. Inc(BufferPos);
  150. EnterNewScannerContext(scEntityReference);
  151. end;
  152. '<':
  153. begin
  154. Inc(BufferPos);
  155. EnterNewScannerContext(scTag);
  156. end;
  157. else
  158. EnterNewScannerContext(scText);
  159. end;
  160. scText:
  161. case Buffer[BufferPos] of
  162. #9, #10, #13, ' ':
  163. EnterNewScannerContext(scWhitespace);
  164. '&':
  165. begin
  166. Inc(BufferPos);
  167. EnterNewScannerContext(scEntityReference);
  168. end;
  169. '<':
  170. begin
  171. Inc(BufferPos);
  172. EnterNewScannerContext(scTag);
  173. end;
  174. else
  175. begin
  176. FTokenText := FTokenText + Buffer[BufferPos];
  177. Inc(BufferPos);
  178. end;
  179. end;
  180. scEntityReference:
  181. if Buffer[BufferPos] = ';' then
  182. begin
  183. Inc(BufferPos);
  184. EnterNewScannerContext(scUnknown);
  185. end else if not (Buffer[BufferPos] in
  186. ['a'..'z', 'A'..'Z', '0'..'9', '#']) then
  187. EnterNewScannerContext(scUnknown)
  188. else
  189. begin
  190. FTokenText := FTokenText + Buffer[BufferPos];
  191. Inc(BufferPos);
  192. end;
  193. scTag:
  194. case Buffer[BufferPos] of
  195. '''', '"':
  196. begin
  197. if FAttrNameRead then
  198. begin
  199. if FCurStringValueDelimiter = #0 then
  200. FCurStringValueDelimiter := Buffer[BufferPos]
  201. else if FCurStringValueDelimiter = Buffer[BufferPos] then
  202. begin
  203. FCurStringValueDelimiter := #0;
  204. FAttrNameRead := False;
  205. end;
  206. end;
  207. FTokenText := FTokenText + Buffer[BufferPos];
  208. Inc(BufferPos);
  209. end;
  210. '=':
  211. begin
  212. FAttrNameRead := True;
  213. FTokenText := FTokenText + Buffer[BufferPos];
  214. Inc(BufferPos);
  215. end;
  216. '>':
  217. begin
  218. Inc(BufferPos);
  219. if FCurStringValueDelimiter = #0 then
  220. EnterNewScannerContext(scUnknown);
  221. end;
  222. else
  223. begin
  224. FTokenText := FTokenText + Buffer[BufferPos];
  225. Inc(BufferPos);
  226. end;
  227. end;
  228. end;
  229. end;
  230. end;
  231. procedure THTMLReader.EnterNewScannerContext(NewContext: THTMLScannerContext);
  232. function SplitTagString(const s: String; var Attr: TSAXAttributes): String;
  233. var
  234. i, j: Integer;
  235. AttrName: String;
  236. ValueDelimiter: Char;
  237. DoIncJ: Boolean;
  238. begin
  239. Attr := nil;
  240. i := Pos(' ', s);
  241. if i <= 0 then
  242. Result := LowerCase(s)
  243. else
  244. begin
  245. Result := LowerCase(Copy(s, 1, i - 1));
  246. Attr := TSAXAttributes.Create;
  247. Inc(i);
  248. while (i <= Length(s)) and (s[i] in WhitespaceChars) do
  249. Inc(i);
  250. SetLength(AttrName, 0);
  251. j := i;
  252. while j <= Length(s) do
  253. if s[j] = '=' then
  254. begin
  255. AttrName := LowerCase(Copy(s, i, j - i));
  256. Inc(j);
  257. if (j < Length(s)) and ((s[j] = '''') or (s[j] = '"')) then
  258. begin
  259. ValueDelimiter := s[j];
  260. Inc(j);
  261. end else
  262. ValueDelimiter := #0;
  263. i := j;
  264. DoIncJ := False;
  265. while j <= Length(s) do
  266. if ValueDelimiter = #0 then
  267. if s[j] in WhitespaceChars then
  268. break
  269. else
  270. Inc(j)
  271. else if s[j] = ValueDelimiter then
  272. begin
  273. DoIncJ := True;
  274. break
  275. end else
  276. Inc(j);
  277. Attr.AddAttribute('', AttrName, '', '', Copy(s, i, j - i));
  278. if DoIncJ then
  279. Inc(j);
  280. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  281. Inc(j);
  282. i := j;
  283. end
  284. else if s[j] in WhitespaceChars then
  285. begin
  286. Attr.AddAttribute('', Copy(s, i, j - i), '', '', '');
  287. Inc(j);
  288. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  289. Inc(j);
  290. i := j;
  291. end else
  292. Inc(j);
  293. end;
  294. end;
  295. var
  296. Attr: TSAXAttributes;
  297. EntString, TagName: String;
  298. Found: Boolean;
  299. Ent: Char;
  300. i: Integer;
  301. begin
  302. case ScannerContext of
  303. scWhitespace:
  304. DoIgnorableWhitespace(PSAXChar(TokenText), 1, Length(TokenText));
  305. scText:
  306. DoCharacters(PSAXChar(TokenText), 0, Length(TokenText));
  307. scEntityReference:
  308. begin
  309. if ResolveHTMLEntityReference(TokenText, Ent) then
  310. begin
  311. EntString := Ent;
  312. DoCharacters(PSAXChar(EntString), 0, 1);
  313. end else
  314. begin
  315. { Is this a predefined Unicode character entity? We must check this,
  316. as undefined entities must be handled as text, for compatiblity
  317. to popular browsers... }
  318. Found := False;
  319. for i := Low(UnicodeHTMLEntities) to High(UnicodeHTMLEntities) do
  320. if UnicodeHTMLEntities[i] = TokenText then
  321. begin
  322. Found := True;
  323. break;
  324. end;
  325. if Found then
  326. DoSkippedEntity(TokenText)
  327. else
  328. DoCharacters(PSAXChar('&' + TokenText), 0, Length(TokenText) + 1);
  329. end;
  330. end;
  331. scTag:
  332. if Length(TokenText) > 0 then
  333. begin
  334. Attr := nil;
  335. if TokenText[1] = '/' then
  336. begin
  337. DoEndElement('',
  338. SplitTagString(Copy(TokenText, 2, Length(TokenText)), Attr), '');
  339. end else if TokenText[1] <> '!' then
  340. begin
  341. // Do NOT combine to a single line, as Attr is an output value!
  342. TagName := SplitTagString(TokenText, Attr);
  343. DoStartElement('', TagName, '', Attr);
  344. end;
  345. if Assigned(Attr) then
  346. Attr.Free;
  347. end;
  348. end;
  349. FScannerContext := NewContext;
  350. SetLength(FTokenText, 0);
  351. FCurStringValueDelimiter := #0;
  352. FAttrNameRead := False;
  353. end;
  354. { THTMLToDOMConverter }
  355. constructor THTMLToDOMConverter.Create(AReader: THTMLReader;
  356. ADocument: TDOMDocument);
  357. begin
  358. inherited Create;
  359. FReader := AReader;
  360. FReader.OnCharacters := @ReaderCharacters;
  361. FReader.OnIgnorableWhitespace := @ReaderIgnorableWhitespace;
  362. FReader.OnSkippedEntity := @ReaderSkippedEntity;
  363. FReader.OnStartElement := @ReaderStartElement;
  364. FReader.OnEndElement := @ReaderEndElement;
  365. FDocument := ADocument;
  366. FElementStack := TList.Create;
  367. FNodeBuffer := TList.Create;
  368. end;
  369. constructor THTMLToDOMConverter.CreateFragment(AReader: THTMLReader;
  370. AFragmentRoot: TDOMNode);
  371. begin
  372. inherited Create;
  373. FReader := AReader;
  374. FReader.OnCharacters := @ReaderCharacters;
  375. FReader.OnIgnorableWhitespace := @ReaderIgnorableWhitespace;
  376. FReader.OnSkippedEntity := @ReaderSkippedEntity;
  377. FReader.OnStartElement := @ReaderStartElement;
  378. FReader.OnEndElement := @ReaderEndElement;
  379. FDocument := AFragmentRoot.OwnerDocument;
  380. FElementStack := TList.Create;
  381. FNodeBuffer := TList.Create;
  382. FragmentRoot := AFragmentRoot;
  383. IsFragmentMode := True;
  384. end;
  385. destructor THTMLToDOMConverter.Destroy;
  386. var
  387. i: Integer;
  388. begin
  389. // Theoretically, always exactly one item will remain - the root element:
  390. for i := 0 to FNodeBuffer.Count - 1 do
  391. THTMLNodeInfo(FNodeBuffer[i]).Free;
  392. FNodeBuffer.Free;
  393. FElementStack.Free;
  394. inherited Destroy;
  395. end;
  396. procedure THTMLToDOMConverter.ReaderCharacters(Sender: TObject;
  397. const ch: PSAXChar; Start, Count: Integer);
  398. var
  399. s: SAXString;
  400. NodeInfo: THTMLNodeInfo;
  401. begin
  402. SetLength(s, Count);
  403. Move(ch^, s[1], Count * SizeOf(SAXChar));
  404. NodeInfo := THTMLNodeInfo.Create;
  405. NodeInfo.NodeType := ntText;
  406. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  407. FNodeBuffer.Add(NodeInfo);
  408. end;
  409. procedure THTMLToDOMConverter.ReaderIgnorableWhitespace(Sender: TObject;
  410. const ch: PSAXChar; Start, Count: Integer);
  411. var
  412. s: SAXString;
  413. NodeInfo: THTMLNodeInfo;
  414. begin
  415. SetLength(s, Count);
  416. Move(ch^, s[1], Count * SizeOf(SAXChar));
  417. NodeInfo := THTMLNodeInfo.Create;
  418. NodeInfo.NodeType := ntWhitespace;
  419. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  420. FNodeBuffer.Add(NodeInfo);
  421. end;
  422. procedure THTMLToDOMConverter.ReaderSkippedEntity(Sender: TObject;
  423. const Name: SAXString);
  424. var
  425. NodeInfo: THTMLNodeInfo;
  426. begin
  427. NodeInfo := THTMLNodeInfo.Create;
  428. NodeInfo.NodeType := ntEntityReference;
  429. NodeInfo.DOMNode := FDocument.CreateEntityReference(Name);
  430. FNodeBuffer.Add(NodeInfo);
  431. end;
  432. procedure THTMLToDOMConverter.ReaderStartElement(Sender: TObject;
  433. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  434. var
  435. NodeInfo: THTMLNodeInfo;
  436. Element: TDOMElement;
  437. i: Integer;
  438. begin
  439. // WriteLn('Start: ', LocalName, '. Node buffer before: ', FNodeBuffer.Count, ' elements');
  440. Element := FDocument.CreateElement(LocalName);
  441. if Assigned(Attr) then
  442. begin
  443. // WriteLn('Attribute: ', Attr.GetLength);
  444. for i := 0 to Attr.GetLength - 1 do
  445. begin
  446. // WriteLn('#', i, ': LocalName = ', Attr.GetLocalName(i), ', Value = ', Attr.GetValue(i));
  447. Element[Attr.GetLocalName(i)] := Attr.GetValue(i);
  448. end;
  449. end;
  450. NodeInfo := THTMLNodeInfo.Create;
  451. NodeInfo.NodeType := ntTag;
  452. NodeInfo.DOMNode := Element;
  453. if IsFragmentMode then
  454. begin
  455. if not FragmentRootSet then
  456. begin
  457. FragmentRoot.AppendChild(Element);
  458. FragmentRootSet := True;
  459. end;
  460. end else
  461. if not Assigned(FDocument.DocumentElement) then
  462. FDocument.AppendChild(Element);
  463. FNodeBuffer.Add(NodeInfo);
  464. // WriteLn('Start: ', LocalName, '. Node buffer after: ', FNodeBuffer.Count, ' elements');
  465. end;
  466. procedure THTMLToDOMConverter.ReaderEndElement(Sender: TObject;
  467. const NamespaceURI, LocalName, RawName: SAXString);
  468. var
  469. NodeInfo, NodeInfo2: THTMLNodeInfo;
  470. i, j: Integer;
  471. TagInfo: PHTMLElementProps;
  472. begin
  473. // WriteLn('End: ', LocalName, '. Node buffer: ', FNodeBuffer.Count, ' elements');
  474. // Find the matching start tag
  475. i := FNodeBuffer.Count - 1;
  476. while i >= 0 do
  477. begin
  478. NodeInfo := THTMLNodeInfo(FNodeBuffer.Items[i]);
  479. if (NodeInfo.NodeType = ntTag) and
  480. (CompareText(NodeInfo.DOMNode.NodeName, LocalName) = 0) then
  481. begin
  482. // We found the matching start tag
  483. TagInfo := nil;
  484. for j := Low(HTMLElProps) to High(HTMLElProps) do
  485. if CompareText(HTMLElProps[j].Name, LocalName) = 0 then
  486. begin
  487. TagInfo := @HTMLElProps[j];
  488. break;
  489. end;
  490. Inc(i);
  491. while i < FNodeBuffer.Count do
  492. begin
  493. NodeInfo2 := THTMLNodeInfo(FNodeBuffer.Items[i]);
  494. if (NodeInfo2.NodeType = ntWhitespace) and Assigned(TagInfo) and
  495. (not (efPreserveWhitespace in TagInfo^.Flags)) then
  496. // Handle whitespace, which doesn't need to get preserved...
  497. if not (efPCDATAContent in TagInfo^.Flags) then
  498. // No character data allowed within the current element
  499. NodeInfo2.DOMNode.Free
  500. else
  501. begin
  502. // Character data allowed, so normalize it
  503. NodeInfo2.DOMNode.NodeValue := ' ';
  504. NodeInfo.DOMNode.AppendChild(NodeInfo2.DOMNode)
  505. end
  506. else
  507. NodeInfo.DOMNode.AppendChild(NodeInfo2.DOMNode);
  508. NodeInfo2.Free;
  509. FNodeBuffer.Delete(i);
  510. end;
  511. break;
  512. end;
  513. Dec(i);
  514. end;
  515. end;
  516. procedure ReadHTMLFile(var ADoc: THTMLDocument; const AFilename: String);
  517. var
  518. f: TStream;
  519. begin
  520. ADoc := nil;
  521. f := TFileStream.Create(AFilename, fmOpenRead);
  522. try
  523. ReadHTMLFile(ADoc, f);
  524. finally
  525. f.Free;
  526. end;
  527. end;
  528. procedure ReadHTMLFile(var ADoc: THTMLDocument; var f: TStream);
  529. var
  530. Reader: THTMLReader;
  531. Converter: THTMLToDOMConverter;
  532. begin
  533. ADoc := THTMLDocument.Create;
  534. Reader := THTMLReader.Create;
  535. try
  536. Converter := THTMLToDOMConverter.Create(Reader, ADoc);
  537. try
  538. Reader.ParseStream(f);
  539. finally
  540. Converter.Free;
  541. end;
  542. finally
  543. Reader.Free;
  544. end;
  545. end;
  546. procedure ReadHTMLFragment(AParentNode: TDOMNode; const AFilename: String);
  547. var
  548. f: TStream;
  549. begin
  550. f := TFileStream.Create(AFilename, fmOpenRead);
  551. try
  552. ReadHTMLFragment(AParentNode, f);
  553. finally
  554. f.Free;
  555. end;
  556. end;
  557. procedure ReadHTMLFragment(AParentNode: TDOMNode; var f: TStream);
  558. var
  559. Reader: THTMLReader;
  560. Converter: THTMLToDOMConverter;
  561. begin
  562. Reader := THTMLReader.Create;
  563. try
  564. Converter := THTMLToDOMConverter.CreateFragment(Reader, AParentNode);
  565. try
  566. Reader.ParseStream(f);
  567. finally
  568. Converter.Free;
  569. end;
  570. finally
  571. Reader.Free;
  572. end;
  573. end;
  574. end.
  575. {
  576. $Log$
  577. Revision 1.5 2003-03-16 22:38:09 sg
  578. * Added fragment parsing functions
  579. Revision 1.4 2002/12/14 19:18:21 sg
  580. * Improved whitespace handling (although it's still not perfect in all
  581. cases)
  582. Revision 1.3 2002/12/12 20:17:32 sg
  583. * More WideString fixes
  584. Revision 1.2 2002/12/12 13:43:38 michael
  585. + Patches from peter to fix 1.1 compile
  586. Revision 1.1 2002/12/11 21:06:07 sg
  587. * Small cleanups
  588. * Replaced htmldoc unit with dom_html unit
  589. * Added SAX parser framework and SAX HTML parser
  590. }