sax_html.pp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648
  1. {
  2. This file is part of the Free Component Library
  3. HTML parser with SAX-like interface
  4. Copyright (c) 2000-2002 by
  5. Areca Systems GmbH / Sebastian Guenther, [email protected]
  6. See the file COPYING.FPC, included in this distribution,
  7. for details about the copyright.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  11. **********************************************************************}
  12. {
  13. Known problems:
  14. * The whitespace handling does only work for processing the DOM tree.
  15. Storing the DOM tree to a XML file will result in a quite ugly file.
  16. (This probably has got much better with recent versions, which do
  17. decent whitespace converting, but it's not tested really good.)
  18. * Entity references in attribute values don't get parsed.
  19. }
  20. unit SAX_HTML;
  21. interface
  22. uses SysUtils, Classes, SAX, DOM, DOM_HTML;
  23. type
  24. { THTMLReader: The HTML reader class }
  25. THTMLScannerContext = (
  26. scUnknown,
  27. scWhitespace, // within whitespace
  28. scText, // within text
  29. scEntityReference, // within entity reference ("&...;")
  30. scTag); // within a start tag or end tag
  31. THTMLReader = class(TSAXReader)
  32. private
  33. FStarted: Boolean;
  34. FEndOfStream: Boolean;
  35. FScannerContext: THTMLScannerContext;
  36. FTokenText: SAXString;
  37. FCurStringValueDelimiter: Char;
  38. FAttrNameRead: Boolean;
  39. protected
  40. procedure EnterNewScannerContext(NewContext: THTMLScannerContext);
  41. public
  42. constructor Create;
  43. destructor Destroy; override;
  44. procedure Parse(AInput: TSAXInputSource); override; overload;
  45. property EndOfStream: Boolean read FEndOfStream;
  46. property ScannerContext: THTMLScannerContext read FScannerContext;
  47. property TokenText: SAXString read FTokenText;
  48. end;
  49. { THTMLToDOMConverter }
  50. THTMLNodeType = (ntWhitespace, ntText, ntEntityReference, ntTag);
  51. THTMLNodeInfo = class
  52. NodeType: THTMLNodeType;
  53. DOMNode: TDOMNode;
  54. end;
  55. THTMLToDOMConverter = class
  56. private
  57. FReader: THTMLReader;
  58. FDocument: TDOMDocument;
  59. FElementStack: TList;
  60. FNodeBuffer: TList;
  61. IsFragmentMode, FragmentRootSet: Boolean;
  62. FragmentRoot: TDOMNode;
  63. procedure ReaderCharacters(Sender: TObject; const ch: PSAXChar;
  64. Start, Count: Integer);
  65. procedure ReaderIgnorableWhitespace(Sender: TObject; const ch: PSAXChar;
  66. Start, Count: Integer);
  67. procedure ReaderSkippedEntity(Sender: TObject; const Name: SAXString);
  68. procedure ReaderStartElement(Sender: TObject;
  69. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  70. procedure ReaderEndElement(Sender: TObject;
  71. const NamespaceURI, LocalName, RawName: SAXString);
  72. public
  73. constructor Create(AReader: THTMLReader; ADocument: TDOMDocument);
  74. constructor CreateFragment(AReader: THTMLReader; AFragmentRoot: TDOMNode);
  75. destructor Destroy; override;
  76. end;
  77. // Helper functions; these ones are HTML equivalents of ReadXML[File|Fragment]
  78. procedure ReadHTMLFile(var ADoc: THTMLDocument; const AFilename: String);
  79. procedure ReadHTMLFile(var ADoc: THTMLDocument; var f: TStream);
  80. procedure ReadHTMLFragment(AParentNode: TDOMNode; const AFilename: String);
  81. procedure ReadHTMLFragment(AParentNode: TDOMNode; var f: TStream);
  82. implementation
  83. uses HTMLDefs;
  84. const
  85. WhitespaceChars = [#9, #10, #13, ' '];
  86. constructor THTMLReader.Create;
  87. begin
  88. inherited Create;
  89. FScannerContext := scUnknown;
  90. end;
  91. destructor THTMLReader.Destroy;
  92. begin
  93. if FStarted then
  94. DoEndDocument;
  95. inherited Destroy;
  96. end;
  97. procedure THTMLReader.Parse(AInput: TSAXInputSource);
  98. const
  99. MaxBufferSize = 1024;
  100. var
  101. Buffer: array[0..MaxBufferSize - 1] of Char;
  102. BufferSize, BufferPos: Integer;
  103. begin
  104. if not FStarted then
  105. begin
  106. FStarted := True;
  107. DoStartDocument;
  108. end;
  109. FEndOfStream := False;
  110. while True do
  111. begin
  112. // Read data into the input buffer
  113. BufferSize := AInput.Stream.Read(Buffer, MaxBufferSize);
  114. if BufferSize = 0 then
  115. begin
  116. FEndOfStream := True;
  117. break;
  118. end;
  119. BufferPos := 0;
  120. while BufferPos < BufferSize do
  121. case ScannerContext of
  122. scUnknown:
  123. case Buffer[BufferPos] of
  124. #9, #10, #13, ' ':
  125. EnterNewScannerContext(scWhitespace);
  126. '&':
  127. begin
  128. Inc(BufferPos);
  129. EnterNewScannerContext(scEntityReference);
  130. end;
  131. '<':
  132. begin
  133. Inc(BufferPos);
  134. EnterNewScannerContext(scTag);
  135. end;
  136. else
  137. EnterNewScannerContext(scText);
  138. end;
  139. scWhitespace:
  140. case Buffer[BufferPos] of
  141. #9, #10, #13, ' ':
  142. begin
  143. FTokenText := FTokenText + Buffer[BufferPos];
  144. Inc(BufferPos);
  145. end;
  146. '&':
  147. begin
  148. Inc(BufferPos);
  149. EnterNewScannerContext(scEntityReference);
  150. end;
  151. '<':
  152. begin
  153. Inc(BufferPos);
  154. EnterNewScannerContext(scTag);
  155. end;
  156. else
  157. EnterNewScannerContext(scText);
  158. end;
  159. scText:
  160. case Buffer[BufferPos] of
  161. #9, #10, #13, ' ':
  162. EnterNewScannerContext(scWhitespace);
  163. '&':
  164. begin
  165. Inc(BufferPos);
  166. EnterNewScannerContext(scEntityReference);
  167. end;
  168. '<':
  169. begin
  170. Inc(BufferPos);
  171. EnterNewScannerContext(scTag);
  172. end;
  173. else
  174. begin
  175. FTokenText := FTokenText + Buffer[BufferPos];
  176. Inc(BufferPos);
  177. end;
  178. end;
  179. scEntityReference:
  180. if Buffer[BufferPos] = ';' then
  181. begin
  182. Inc(BufferPos);
  183. EnterNewScannerContext(scUnknown);
  184. end else if not (Buffer[BufferPos] in
  185. ['a'..'z', 'A'..'Z', '0'..'9', '#']) then
  186. EnterNewScannerContext(scUnknown)
  187. else
  188. begin
  189. FTokenText := FTokenText + Buffer[BufferPos];
  190. Inc(BufferPos);
  191. end;
  192. scTag:
  193. case Buffer[BufferPos] of
  194. '''', '"':
  195. begin
  196. if FAttrNameRead then
  197. begin
  198. if FCurStringValueDelimiter = #0 then
  199. FCurStringValueDelimiter := Buffer[BufferPos]
  200. else if FCurStringValueDelimiter = Buffer[BufferPos] then
  201. begin
  202. FCurStringValueDelimiter := #0;
  203. FAttrNameRead := False;
  204. end;
  205. end;
  206. FTokenText := FTokenText + Buffer[BufferPos];
  207. Inc(BufferPos);
  208. end;
  209. '=':
  210. begin
  211. FAttrNameRead := True;
  212. FTokenText := FTokenText + Buffer[BufferPos];
  213. Inc(BufferPos);
  214. end;
  215. '>':
  216. begin
  217. Inc(BufferPos);
  218. if FCurStringValueDelimiter = #0 then
  219. EnterNewScannerContext(scUnknown);
  220. end;
  221. else
  222. begin
  223. FTokenText := FTokenText + Buffer[BufferPos];
  224. Inc(BufferPos);
  225. end;
  226. end;
  227. end;
  228. end;
  229. end;
  230. procedure THTMLReader.EnterNewScannerContext(NewContext: THTMLScannerContext);
  231. function SplitTagString(const s: String; var Attr: TSAXAttributes): String;
  232. var
  233. i, j: Integer;
  234. AttrName: String;
  235. ValueDelimiter: Char;
  236. DoIncJ: Boolean;
  237. begin
  238. Attr := nil;
  239. i := Pos(' ', s);
  240. if i <= 0 then
  241. Result := LowerCase(s)
  242. else
  243. begin
  244. Result := LowerCase(Copy(s, 1, i - 1));
  245. Attr := TSAXAttributes.Create;
  246. Inc(i);
  247. while (i <= Length(s)) and (s[i] in WhitespaceChars) do
  248. Inc(i);
  249. SetLength(AttrName, 0);
  250. j := i;
  251. while j <= Length(s) do
  252. if s[j] = '=' then
  253. begin
  254. AttrName := LowerCase(Copy(s, i, j - i));
  255. Inc(j);
  256. if (j < Length(s)) and ((s[j] = '''') or (s[j] = '"')) then
  257. begin
  258. ValueDelimiter := s[j];
  259. Inc(j);
  260. end else
  261. ValueDelimiter := #0;
  262. i := j;
  263. DoIncJ := False;
  264. while j <= Length(s) do
  265. if ValueDelimiter = #0 then
  266. if s[j] in WhitespaceChars then
  267. break
  268. else
  269. Inc(j)
  270. else if s[j] = ValueDelimiter then
  271. begin
  272. DoIncJ := True;
  273. break
  274. end else
  275. Inc(j);
  276. Attr.AddAttribute('', AttrName, '', '', Copy(s, i, j - i));
  277. if DoIncJ then
  278. Inc(j);
  279. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  280. Inc(j);
  281. i := j;
  282. end
  283. else if s[j] in WhitespaceChars then
  284. begin
  285. Attr.AddAttribute('', Copy(s, i, j - i), '', '', '');
  286. Inc(j);
  287. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  288. Inc(j);
  289. i := j;
  290. end else
  291. Inc(j);
  292. end;
  293. end;
  294. var
  295. Attr: TSAXAttributes;
  296. EntString, TagName: String;
  297. Found: Boolean;
  298. Ent: Char;
  299. i: Integer;
  300. begin
  301. case ScannerContext of
  302. scWhitespace:
  303. DoIgnorableWhitespace(PSAXChar(TokenText), 1, Length(TokenText));
  304. scText:
  305. DoCharacters(PSAXChar(TokenText), 0, Length(TokenText));
  306. scEntityReference:
  307. begin
  308. if ResolveHTMLEntityReference(TokenText, Ent) then
  309. begin
  310. EntString := Ent;
  311. DoCharacters(PSAXChar(EntString), 0, 1);
  312. end else
  313. begin
  314. { Is this a predefined Unicode character entity? We must check this,
  315. as undefined entities must be handled as text, for compatiblity
  316. to popular browsers... }
  317. Found := False;
  318. for i := Low(UnicodeHTMLEntities) to High(UnicodeHTMLEntities) do
  319. if UnicodeHTMLEntities[i] = TokenText then
  320. begin
  321. Found := True;
  322. break;
  323. end;
  324. if Found then
  325. DoSkippedEntity(TokenText)
  326. else
  327. DoCharacters(PSAXChar('&' + TokenText), 0, Length(TokenText) + 1);
  328. end;
  329. end;
  330. scTag:
  331. if Length(TokenText) > 0 then
  332. begin
  333. Attr := nil;
  334. if TokenText[1] = '/' then
  335. begin
  336. DoEndElement('',
  337. SplitTagString(Copy(TokenText, 2, Length(TokenText)), Attr), '');
  338. end else if TokenText[1] <> '!' then
  339. begin
  340. // Do NOT combine to a single line, as Attr is an output value!
  341. TagName := SplitTagString(TokenText, Attr);
  342. DoStartElement('', TagName, '', Attr);
  343. end;
  344. if Assigned(Attr) then
  345. Attr.Free;
  346. end;
  347. end;
  348. FScannerContext := NewContext;
  349. SetLength(FTokenText, 0);
  350. FCurStringValueDelimiter := #0;
  351. FAttrNameRead := False;
  352. end;
  353. { THTMLToDOMConverter }
  354. constructor THTMLToDOMConverter.Create(AReader: THTMLReader;
  355. ADocument: TDOMDocument);
  356. begin
  357. inherited Create;
  358. FReader := AReader;
  359. FReader.OnCharacters := @ReaderCharacters;
  360. FReader.OnIgnorableWhitespace := @ReaderIgnorableWhitespace;
  361. FReader.OnSkippedEntity := @ReaderSkippedEntity;
  362. FReader.OnStartElement := @ReaderStartElement;
  363. FReader.OnEndElement := @ReaderEndElement;
  364. FDocument := ADocument;
  365. FElementStack := TList.Create;
  366. FNodeBuffer := TList.Create;
  367. end;
  368. constructor THTMLToDOMConverter.CreateFragment(AReader: THTMLReader;
  369. AFragmentRoot: TDOMNode);
  370. begin
  371. inherited Create;
  372. FReader := AReader;
  373. FReader.OnCharacters := @ReaderCharacters;
  374. FReader.OnIgnorableWhitespace := @ReaderIgnorableWhitespace;
  375. FReader.OnSkippedEntity := @ReaderSkippedEntity;
  376. FReader.OnStartElement := @ReaderStartElement;
  377. FReader.OnEndElement := @ReaderEndElement;
  378. FDocument := AFragmentRoot.OwnerDocument;
  379. FElementStack := TList.Create;
  380. FNodeBuffer := TList.Create;
  381. FragmentRoot := AFragmentRoot;
  382. IsFragmentMode := True;
  383. end;
  384. destructor THTMLToDOMConverter.Destroy;
  385. var
  386. i: Integer;
  387. begin
  388. // Theoretically, always exactly one item will remain - the root element:
  389. for i := 0 to FNodeBuffer.Count - 1 do
  390. THTMLNodeInfo(FNodeBuffer[i]).Free;
  391. FNodeBuffer.Free;
  392. FElementStack.Free;
  393. inherited Destroy;
  394. end;
  395. procedure THTMLToDOMConverter.ReaderCharacters(Sender: TObject;
  396. const ch: PSAXChar; Start, Count: Integer);
  397. var
  398. s: SAXString;
  399. NodeInfo: THTMLNodeInfo;
  400. begin
  401. SetLength(s, Count);
  402. Move(ch^, s[1], Count * SizeOf(SAXChar));
  403. NodeInfo := THTMLNodeInfo.Create;
  404. NodeInfo.NodeType := ntText;
  405. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  406. FNodeBuffer.Add(NodeInfo);
  407. end;
  408. procedure THTMLToDOMConverter.ReaderIgnorableWhitespace(Sender: TObject;
  409. const ch: PSAXChar; Start, Count: Integer);
  410. var
  411. s: SAXString;
  412. NodeInfo: THTMLNodeInfo;
  413. begin
  414. SetLength(s, Count);
  415. Move(ch^, s[1], Count * SizeOf(SAXChar));
  416. NodeInfo := THTMLNodeInfo.Create;
  417. NodeInfo.NodeType := ntWhitespace;
  418. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  419. FNodeBuffer.Add(NodeInfo);
  420. end;
  421. procedure THTMLToDOMConverter.ReaderSkippedEntity(Sender: TObject;
  422. const Name: SAXString);
  423. var
  424. NodeInfo: THTMLNodeInfo;
  425. begin
  426. NodeInfo := THTMLNodeInfo.Create;
  427. NodeInfo.NodeType := ntEntityReference;
  428. NodeInfo.DOMNode := FDocument.CreateEntityReference(Name);
  429. FNodeBuffer.Add(NodeInfo);
  430. end;
  431. procedure THTMLToDOMConverter.ReaderStartElement(Sender: TObject;
  432. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  433. var
  434. NodeInfo: THTMLNodeInfo;
  435. Element: TDOMElement;
  436. i: Integer;
  437. begin
  438. // WriteLn('Start: ', LocalName, '. Node buffer before: ', FNodeBuffer.Count, ' elements');
  439. Element := FDocument.CreateElement(LocalName);
  440. if Assigned(Attr) then
  441. begin
  442. // WriteLn('Attribute: ', Attr.GetLength);
  443. for i := 0 to Attr.GetLength - 1 do
  444. begin
  445. // WriteLn('#', i, ': LocalName = ', Attr.GetLocalName(i), ', Value = ', Attr.GetValue(i));
  446. Element[Attr.GetLocalName(i)] := Attr.GetValue(i);
  447. end;
  448. end;
  449. NodeInfo := THTMLNodeInfo.Create;
  450. NodeInfo.NodeType := ntTag;
  451. NodeInfo.DOMNode := Element;
  452. if IsFragmentMode then
  453. begin
  454. if not FragmentRootSet then
  455. begin
  456. FragmentRoot.AppendChild(Element);
  457. FragmentRootSet := True;
  458. end;
  459. end else
  460. if not Assigned(FDocument.DocumentElement) then
  461. FDocument.AppendChild(Element);
  462. FNodeBuffer.Add(NodeInfo);
  463. // WriteLn('Start: ', LocalName, '. Node buffer after: ', FNodeBuffer.Count, ' elements');
  464. end;
  465. procedure THTMLToDOMConverter.ReaderEndElement(Sender: TObject;
  466. const NamespaceURI, LocalName, RawName: SAXString);
  467. var
  468. NodeInfo, NodeInfo2: THTMLNodeInfo;
  469. i : Integer;
  470. j : THTMLElementTag;
  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(THTMLElementTag) to High(THTMLElementTag) do
  485. if CompareText(HTMLElementProps[j].Name, LocalName) = 0 then
  486. begin
  487. TagInfo := @HTMLElementProps[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.