sax_html.pp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561
  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;
  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. procedure ReaderCharacters(Sender: TObject; const ch: PSAXChar;
  63. Start, Count: Integer);
  64. procedure ReaderIgnorableWhitespace(Sender: TObject; const ch: PSAXChar;
  65. Start, Count: Integer);
  66. procedure ReaderSkippedEntity(Sender: TObject; const Name: SAXString);
  67. procedure ReaderStartElement(Sender: TObject;
  68. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  69. procedure ReaderEndElement(Sender: TObject;
  70. const NamespaceURI, LocalName, RawName: SAXString);
  71. public
  72. constructor Create(AReader: THTMLReader; ADocument: TDOMDocument);
  73. destructor Destroy; override;
  74. end;
  75. implementation
  76. uses HTMLDefs;
  77. const
  78. WhitespaceChars = [#9, #10, #13, ' '];
  79. constructor THTMLReader.Create;
  80. begin
  81. inherited Create;
  82. FScannerContext := scUnknown;
  83. end;
  84. destructor THTMLReader.Destroy;
  85. begin
  86. if FStarted then
  87. DoEndDocument;
  88. inherited Destroy;
  89. end;
  90. procedure THTMLReader.Parse(AInput: TSAXInputSource);
  91. const
  92. MaxBufferSize = 1024;
  93. var
  94. Buffer: array[0..MaxBufferSize - 1] of Char;
  95. BufferSize, BufferPos: Integer;
  96. begin
  97. if not FStarted then
  98. begin
  99. FStarted := True;
  100. DoStartDocument;
  101. end;
  102. FEndOfStream := False;
  103. while True do
  104. begin
  105. // Read data into the input buffer
  106. BufferSize := AInput.Stream.Read(Buffer, MaxBufferSize);
  107. if BufferSize = 0 then
  108. begin
  109. FEndOfStream := True;
  110. break;
  111. end;
  112. BufferPos := 0;
  113. while BufferPos < BufferSize do
  114. case ScannerContext of
  115. scUnknown:
  116. case Buffer[BufferPos] of
  117. #9, #10, #13, ' ':
  118. EnterNewScannerContext(scWhitespace);
  119. '&':
  120. begin
  121. Inc(BufferPos);
  122. EnterNewScannerContext(scEntityReference);
  123. end;
  124. '<':
  125. begin
  126. Inc(BufferPos);
  127. EnterNewScannerContext(scTag);
  128. end;
  129. else
  130. EnterNewScannerContext(scText);
  131. end;
  132. scWhitespace:
  133. case Buffer[BufferPos] of
  134. #9, #10, #13, ' ':
  135. begin
  136. FTokenText := FTokenText + Buffer[BufferPos];
  137. Inc(BufferPos);
  138. end;
  139. '&':
  140. begin
  141. Inc(BufferPos);
  142. EnterNewScannerContext(scEntityReference);
  143. end;
  144. '<':
  145. begin
  146. Inc(BufferPos);
  147. EnterNewScannerContext(scTag);
  148. end;
  149. else
  150. EnterNewScannerContext(scText);
  151. end;
  152. scText:
  153. case Buffer[BufferPos] of
  154. #9, #10, #13, ' ':
  155. EnterNewScannerContext(scWhitespace);
  156. '&':
  157. begin
  158. Inc(BufferPos);
  159. EnterNewScannerContext(scEntityReference);
  160. end;
  161. '<':
  162. begin
  163. Inc(BufferPos);
  164. EnterNewScannerContext(scTag);
  165. end;
  166. else
  167. begin
  168. FTokenText := FTokenText + Buffer[BufferPos];
  169. Inc(BufferPos);
  170. end;
  171. end;
  172. scEntityReference:
  173. if Buffer[BufferPos] = ';' then
  174. begin
  175. Inc(BufferPos);
  176. EnterNewScannerContext(scUnknown);
  177. end else if not (Buffer[BufferPos] in
  178. ['a'..'z', 'A'..'Z', '0'..'9', '#']) then
  179. EnterNewScannerContext(scUnknown)
  180. else
  181. begin
  182. FTokenText := FTokenText + Buffer[BufferPos];
  183. Inc(BufferPos);
  184. end;
  185. scTag:
  186. case Buffer[BufferPos] of
  187. '''', '"':
  188. begin
  189. if FAttrNameRead then
  190. begin
  191. if FCurStringValueDelimiter = #0 then
  192. FCurStringValueDelimiter := Buffer[BufferPos]
  193. else if FCurStringValueDelimiter = Buffer[BufferPos] then
  194. begin
  195. FCurStringValueDelimiter := #0;
  196. FAttrNameRead := False;
  197. end;
  198. end;
  199. FTokenText := FTokenText + Buffer[BufferPos];
  200. Inc(BufferPos);
  201. end;
  202. '=':
  203. begin
  204. FAttrNameRead := True;
  205. FTokenText := FTokenText + Buffer[BufferPos];
  206. Inc(BufferPos);
  207. end;
  208. '>':
  209. begin
  210. Inc(BufferPos);
  211. if FCurStringValueDelimiter = #0 then
  212. EnterNewScannerContext(scUnknown);
  213. end;
  214. else
  215. begin
  216. FTokenText := FTokenText + Buffer[BufferPos];
  217. Inc(BufferPos);
  218. end;
  219. end;
  220. end;
  221. end;
  222. end;
  223. procedure THTMLReader.EnterNewScannerContext(NewContext: THTMLScannerContext);
  224. function SplitTagString(const s: String; var Attr: TSAXAttributes): String;
  225. var
  226. i, j: Integer;
  227. AttrName: String;
  228. ValueDelimiter: Char;
  229. DoIncJ: Boolean;
  230. begin
  231. Attr := nil;
  232. i := Pos(' ', s);
  233. if i <= 0 then
  234. Result := LowerCase(s)
  235. else
  236. begin
  237. Result := LowerCase(Copy(s, 1, i - 1));
  238. Attr := TSAXAttributes.Create;
  239. Inc(i);
  240. while (i <= Length(s)) and (s[i] in WhitespaceChars) do
  241. Inc(i);
  242. SetLength(AttrName, 0);
  243. j := i;
  244. while j <= Length(s) do
  245. if s[j] = '=' then
  246. begin
  247. AttrName := LowerCase(Copy(s, i, j - i));
  248. Inc(j);
  249. if (j < Length(s)) and ((s[j] = '''') or (s[j] = '"')) then
  250. begin
  251. ValueDelimiter := s[j];
  252. Inc(j);
  253. end else
  254. ValueDelimiter := #0;
  255. i := j;
  256. DoIncJ := False;
  257. while j <= Length(s) do
  258. if ValueDelimiter = #0 then
  259. if s[j] in WhitespaceChars then
  260. break
  261. else
  262. Inc(j)
  263. else if s[j] = ValueDelimiter then
  264. begin
  265. DoIncJ := True;
  266. break
  267. end else
  268. Inc(j);
  269. Attr.AddAttribute('', AttrName, '', '', Copy(s, i, j - i));
  270. if DoIncJ then
  271. Inc(j);
  272. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  273. Inc(j);
  274. i := j;
  275. end
  276. else if s[j] in WhitespaceChars then
  277. begin
  278. Attr.AddAttribute('', Copy(s, i, j - i), '', '', '');
  279. Inc(j);
  280. while (j <= Length(s)) and (s[j] in WhitespaceChars) do
  281. Inc(j);
  282. i := j;
  283. end else
  284. Inc(j);
  285. end;
  286. end;
  287. var
  288. Attr: TSAXAttributes;
  289. EntString, TagName: String;
  290. Found: Boolean;
  291. Ent: Char;
  292. i: Integer;
  293. begin
  294. case ScannerContext of
  295. scWhitespace:
  296. DoIgnorableWhitespace(PSAXChar(TokenText), 1, Length(TokenText));
  297. scText:
  298. DoCharacters(PSAXChar(TokenText), 0, Length(TokenText));
  299. scEntityReference:
  300. begin
  301. if ResolveHTMLEntityReference(TokenText, Ent) then
  302. begin
  303. EntString := Ent;
  304. DoCharacters(PSAXChar(EntString), 0, 1);
  305. end else
  306. begin
  307. { Is this a predefined Unicode character entity? We must check this,
  308. as undefined entities must be handled as text, for compatiblity
  309. to popular browsers... }
  310. Found := False;
  311. for i := Low(UnicodeHTMLEntities) to High(UnicodeHTMLEntities) do
  312. if UnicodeHTMLEntities[i] = TokenText then
  313. begin
  314. Found := True;
  315. break;
  316. end;
  317. if Found then
  318. DoSkippedEntity(TokenText)
  319. else
  320. DoCharacters(PSAXChar('&' + TokenText), 0, Length(TokenText) + 1);
  321. end;
  322. end;
  323. scTag:
  324. if Length(TokenText) > 0 then
  325. begin
  326. Attr := nil;
  327. if TokenText[1] = '/' then
  328. begin
  329. DoEndElement('',
  330. SplitTagString(Copy(TokenText, 2, Length(TokenText)), Attr), '');
  331. end else if TokenText[1] <> '!' then
  332. begin
  333. // Do NOT combine to a single line, as Attr is an output value!
  334. TagName := SplitTagString(TokenText, Attr);
  335. DoStartElement('', TagName, '', Attr);
  336. end;
  337. if Assigned(Attr) then
  338. Attr.Free;
  339. end;
  340. end;
  341. FScannerContext := NewContext;
  342. SetLength(FTokenText, 0);
  343. FCurStringValueDelimiter := #0;
  344. FAttrNameRead := False;
  345. end;
  346. { THTMLToDOMConverter }
  347. constructor THTMLToDOMConverter.Create(AReader: THTMLReader; ADocument: TDOMDocument);
  348. begin
  349. inherited Create;
  350. FReader := AReader;
  351. FReader.OnCharacters := @ReaderCharacters;
  352. FReader.OnIgnorableWhitespace := @ReaderIgnorableWhitespace;
  353. FReader.OnSkippedEntity := @ReaderSkippedEntity;
  354. FReader.OnStartElement := @ReaderStartElement;
  355. FReader.OnEndElement := @ReaderEndElement;
  356. FDocument := ADocument;
  357. FElementStack := TList.Create;
  358. FNodeBuffer := TList.Create;
  359. end;
  360. destructor THTMLToDOMConverter.Destroy;
  361. var
  362. i: Integer;
  363. begin
  364. // Theoretically, always exactly one item will remain - the root element:
  365. for i := 0 to FNodeBuffer.Count - 1 do
  366. THTMLNodeInfo(FNodeBuffer[i]).Free;
  367. FNodeBuffer.Free;
  368. FElementStack.Free;
  369. inherited Destroy;
  370. end;
  371. procedure THTMLToDOMConverter.ReaderCharacters(Sender: TObject;
  372. const ch: PSAXChar; Start, Count: Integer);
  373. var
  374. s: SAXString;
  375. NodeInfo: THTMLNodeInfo;
  376. begin
  377. SetLength(s, Count);
  378. Move(ch^, s[1], Count * SizeOf(SAXChar));
  379. NodeInfo := THTMLNodeInfo.Create;
  380. NodeInfo.NodeType := ntText;
  381. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  382. FNodeBuffer.Add(NodeInfo);
  383. end;
  384. procedure THTMLToDOMConverter.ReaderIgnorableWhitespace(Sender: TObject;
  385. const ch: PSAXChar; Start, Count: Integer);
  386. var
  387. s: SAXString;
  388. NodeInfo: THTMLNodeInfo;
  389. begin
  390. SetLength(s, Count);
  391. Move(ch^, s[1], Count * SizeOf(SAXChar));
  392. NodeInfo := THTMLNodeInfo.Create;
  393. NodeInfo.NodeType := ntWhitespace;
  394. NodeInfo.DOMNode := FDocument.CreateTextNode(s);
  395. FNodeBuffer.Add(NodeInfo);
  396. end;
  397. procedure THTMLToDOMConverter.ReaderSkippedEntity(Sender: TObject;
  398. const Name: SAXString);
  399. var
  400. NodeInfo: THTMLNodeInfo;
  401. begin
  402. NodeInfo := THTMLNodeInfo.Create;
  403. NodeInfo.NodeType := ntEntityReference;
  404. NodeInfo.DOMNode := FDocument.CreateEntityReference(Name);
  405. FNodeBuffer.Add(NodeInfo);
  406. end;
  407. procedure THTMLToDOMConverter.ReaderStartElement(Sender: TObject;
  408. const NamespaceURI, LocalName, RawName: SAXString; Attr: TSAXAttributes);
  409. var
  410. NodeInfo: THTMLNodeInfo;
  411. Element: TDOMElement;
  412. i: Integer;
  413. begin
  414. Element := FDocument.CreateElement(LocalName);
  415. if Assigned(Attr) then
  416. begin
  417. // WriteLn('Attribute: ', Attr.GetLength);
  418. for i := 0 to Attr.GetLength - 1 do
  419. begin
  420. // WriteLn('#', i, ': LocalName = ', Attr.GetLocalName(i), ', Value = ', Attr.GetValue(i));
  421. Element[Attr.GetLocalName(i)] := Attr.GetValue(i);
  422. end;
  423. end;
  424. NodeInfo := THTMLNodeInfo.Create;
  425. NodeInfo.NodeType := ntTag;
  426. NodeInfo.DOMNode := Element;
  427. if not Assigned(FDocument.DocumentElement) then
  428. FDocument.AppendChild(NodeInfo.DOMNode);
  429. FNodeBuffer.Add(NodeInfo);
  430. end;
  431. procedure THTMLToDOMConverter.ReaderEndElement(Sender: TObject;
  432. const NamespaceURI, LocalName, RawName: SAXString);
  433. var
  434. NodeInfo, NodeInfo2: THTMLNodeInfo;
  435. i, j: Integer;
  436. TagInfo: PHTMLElementProps;
  437. IsFirst: Boolean;
  438. begin
  439. // WriteLn('End: ', LocalName);
  440. // Find the matching start tag
  441. i := FNodeBuffer.Count - 1;
  442. while i >= 0 do
  443. begin
  444. NodeInfo := THTMLNodeInfo(FNodeBuffer.Items[i]);
  445. if (NodeInfo.NodeType = ntTag) and
  446. (CompareText(NodeInfo.DOMNode.NodeName, LocalName) = 0) then
  447. begin
  448. // We found the matching start tag
  449. TagInfo := nil;
  450. for j := Low(HTMLElProps) to High(HTMLElProps) do
  451. if CompareText(HTMLElProps[j].Name, LocalName) = 0 then
  452. begin
  453. TagInfo := @HTMLElProps[j];
  454. break;
  455. end;
  456. Inc(i);
  457. IsFirst := True;
  458. while i < FNodeBuffer.Count do
  459. begin
  460. NodeInfo2 := THTMLNodeInfo(FNodeBuffer.Items[i]);
  461. if (NodeInfo2.NodeType = ntWhitespace) and Assigned(TagInfo) and
  462. (not (efPreserveWhitespace in TagInfo^.Flags)) then
  463. // Handle whitespace, which doesn't need to get preserved...
  464. if not (efPCDATAContent in TagInfo^.Flags) then
  465. // No character data allowed within the current element
  466. NodeInfo2.DOMNode.Free
  467. else
  468. begin
  469. // Character data allowed, so normalize it
  470. NodeInfo2.DOMNode.NodeValue := ' ';
  471. NodeInfo.DOMNode.AppendChild(NodeInfo2.DOMNode)
  472. end;
  473. NodeInfo2.Free;
  474. FNodeBuffer.Delete(i);
  475. IsFirst := False;
  476. end;
  477. break;
  478. end;
  479. Dec(i);
  480. end;
  481. end;
  482. end.
  483. {
  484. $Log$
  485. Revision 1.3 2002-12-12 20:17:32 sg
  486. * More WideString fixes
  487. Revision 1.2 2002/12/12 13:43:38 michael
  488. + Patches from peter to fix 1.1 compile
  489. Revision 1.1 2002/12/11 21:06:07 sg
  490. * Small cleanups
  491. * Replaced htmldoc unit with dom_html unit
  492. * Added SAX parser framework and SAX HTML parser
  493. }