sax_html.pp 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  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, j: Integer;
  470. TagInfo: PHTMLElementProps;
  471. begin
  472. // WriteLn('End: ', LocalName, '. Node buffer: ', FNodeBuffer.Count, ' elements');
  473. // Find the matching start tag
  474. i := FNodeBuffer.Count - 1;
  475. while i >= 0 do
  476. begin
  477. NodeInfo := THTMLNodeInfo(FNodeBuffer.Items[i]);
  478. if (NodeInfo.NodeType = ntTag) and
  479. (CompareText(NodeInfo.DOMNode.NodeName, LocalName) = 0) then
  480. begin
  481. // We found the matching start tag
  482. TagInfo := nil;
  483. for j := Low(HTMLElProps) to High(HTMLElProps) do
  484. if CompareText(HTMLElProps[j].Name, LocalName) = 0 then
  485. begin
  486. TagInfo := @HTMLElProps[j];
  487. break;
  488. end;
  489. Inc(i);
  490. while i < FNodeBuffer.Count do
  491. begin
  492. NodeInfo2 := THTMLNodeInfo(FNodeBuffer.Items[i]);
  493. if (NodeInfo2.NodeType = ntWhitespace) and Assigned(TagInfo) and
  494. (not (efPreserveWhitespace in TagInfo^.Flags)) then
  495. // Handle whitespace, which doesn't need to get preserved...
  496. if not (efPCDATAContent in TagInfo^.Flags) then
  497. // No character data allowed within the current element
  498. NodeInfo2.DOMNode.Free
  499. else
  500. begin
  501. // Character data allowed, so normalize it
  502. NodeInfo2.DOMNode.NodeValue := ' ';
  503. NodeInfo.DOMNode.AppendChild(NodeInfo2.DOMNode)
  504. end
  505. else
  506. NodeInfo.DOMNode.AppendChild(NodeInfo2.DOMNode);
  507. NodeInfo2.Free;
  508. FNodeBuffer.Delete(i);
  509. end;
  510. break;
  511. end;
  512. Dec(i);
  513. end;
  514. end;
  515. procedure ReadHTMLFile(var ADoc: THTMLDocument; const AFilename: String);
  516. var
  517. f: TStream;
  518. begin
  519. ADoc := nil;
  520. f := TFileStream.Create(AFilename, fmOpenRead);
  521. try
  522. ReadHTMLFile(ADoc, f);
  523. finally
  524. f.Free;
  525. end;
  526. end;
  527. procedure ReadHTMLFile(var ADoc: THTMLDocument; var f: TStream);
  528. var
  529. Reader: THTMLReader;
  530. Converter: THTMLToDOMConverter;
  531. begin
  532. ADoc := THTMLDocument.Create;
  533. Reader := THTMLReader.Create;
  534. try
  535. Converter := THTMLToDOMConverter.Create(Reader, ADoc);
  536. try
  537. Reader.ParseStream(f);
  538. finally
  539. Converter.Free;
  540. end;
  541. finally
  542. Reader.Free;
  543. end;
  544. end;
  545. procedure ReadHTMLFragment(AParentNode: TDOMNode; const AFilename: String);
  546. var
  547. f: TStream;
  548. begin
  549. f := TFileStream.Create(AFilename, fmOpenRead);
  550. try
  551. ReadHTMLFragment(AParentNode, f);
  552. finally
  553. f.Free;
  554. end;
  555. end;
  556. procedure ReadHTMLFragment(AParentNode: TDOMNode; var f: TStream);
  557. var
  558. Reader: THTMLReader;
  559. Converter: THTMLToDOMConverter;
  560. begin
  561. Reader := THTMLReader.Create;
  562. try
  563. Converter := THTMLToDOMConverter.CreateFragment(Reader, AParentNode);
  564. try
  565. Reader.ParseStream(f);
  566. finally
  567. Converter.Free;
  568. end;
  569. finally
  570. Reader.Free;
  571. end;
  572. end;
  573. end.