1 /////////////////////////////////////////////////////////////////////////////
2 // Name: src/html/htmlpars.cpp
3 // Purpose: wxHtmlParser class (generic parser)
4 // Author: Vaclav Slavik
6 // Copyright: (c) 1999 Vaclav Slavik
7 // Licence: wxWindows licence
8 /////////////////////////////////////////////////////////////////////////////
10 #include "wx/wxprec.h"
16 #if wxUSE_HTML && wxUSE_STREAMS
19 #include "wx/dynarray.h"
23 #include "wx/wxcrtvararg.h"
26 #include "wx/tokenzr.h"
27 #include "wx/wfstream.h"
29 #include "wx/fontmap.h"
30 #include "wx/html/htmldefs.h"
31 #include "wx/html/htmlpars.h"
32 #include "wx/arrimpl.cpp"
35 #include "wx/msw/wince/missing.h" // for bsearch()
38 // DLL options compatibility check:
39 WX_CHECK_BUILD_OPTIONS("wxHTML")
41 const wxChar
*wxTRACE_HTML_DEBUG
= _T("htmldebug");
43 //-----------------------------------------------------------------------------
44 // wxHtmlParser helpers
45 //-----------------------------------------------------------------------------
50 wxHtmlTextPiece(int pos
, int lng
) : m_pos(pos
), m_lng(lng
) {}
54 WX_DECLARE_OBJARRAY(wxHtmlTextPiece
, wxHtmlTextPieces
);
55 WX_DEFINE_OBJARRAY(wxHtmlTextPieces
)
57 class wxHtmlParserState
62 wxHtmlTextPieces
*m_textPieces
;
65 wxHtmlParserState
*m_nextState
;
68 //-----------------------------------------------------------------------------
70 //-----------------------------------------------------------------------------
72 IMPLEMENT_ABSTRACT_CLASS(wxHtmlParser
,wxObject
)
74 wxHtmlParser::wxHtmlParser()
75 : wxObject(), m_HandlersHash(wxKEY_STRING
),
76 m_FS(NULL
), m_HandlersStack(NULL
)
78 m_entitiesParser
= new wxHtmlEntitiesParser
;
86 wxHtmlParser::~wxHtmlParser()
88 while (RestoreState()) {}
93 wxList
& tmp
= *m_HandlersStack
;
94 wxList::iterator it
, en
;
95 for( it
= tmp
.begin(), en
= tmp
.end(); it
!= en
; ++it
)
96 delete (wxHashTable
*)*it
;
99 delete m_HandlersStack
;
100 m_HandlersHash
.Clear();
101 WX_CLEAR_LIST(wxList
, m_HandlersList
);
102 delete m_entitiesParser
;
105 wxObject
* wxHtmlParser::Parse(const wxString
& source
)
109 wxObject
*result
= GetProduct();
114 void wxHtmlParser::InitParser(const wxString
& source
)
117 m_stopParsing
= false;
120 void wxHtmlParser::DoneParser()
125 void wxHtmlParser::SetSource(const wxString
& src
)
134 void wxHtmlParser::CreateDOMTree()
136 wxHtmlTagsCache
cache(m_Source
);
137 m_TextPieces
= new wxHtmlTextPieces
;
138 CreateDOMSubTree(NULL
, 0, m_Source
.length(), &cache
);
142 extern bool wxIsCDATAElement(const wxChar
*tag
);
144 void wxHtmlParser::CreateDOMSubTree(wxHtmlTag
*cur
,
145 int begin_pos
, int end_pos
,
146 wxHtmlTagsCache
*cache
)
148 if (end_pos
<= begin_pos
) return;
152 int textBeginning
= begin_pos
;
154 // If the tag contains CDATA text, we include the text between beginning
155 // and ending tag verbosely. Setting i=end_pos will skip to the very
156 // end of this function where text piece is added, bypassing any child
157 // tags parsing (CDATA element can't have child elements by definition):
158 if (cur
!= NULL
&& wxIsCDATAElement(cur
->GetName().c_str()))
165 c
= m_Source
.GetChar(i
);
169 // add text to m_TextPieces:
170 if (i
- textBeginning
> 0)
172 wxHtmlTextPiece(textBeginning
, i
- textBeginning
));
174 // if it is a comment, skip it:
175 wxString::const_iterator iter
= m_Source
.begin() + i
;
176 if ( SkipCommentTag(iter
, m_Source
.end()) )
179 i
= iter
- m_Source
.begin() + 1; // skip closing '>' too
182 // add another tag to the tree:
183 else if (i
< end_pos
-1 && m_Source
.GetChar(i
+1) != wxT('/'))
187 chd
= new wxHtmlTag(cur
, m_Source
,
188 i
, end_pos
, cache
, m_entitiesParser
);
191 chd
= new wxHtmlTag(NULL
, m_Source
,
192 i
, end_pos
, cache
, m_entitiesParser
);
195 // if this is the first tag to be created make the root
196 // m_Tags point to it:
201 // if there is already a root tag add this tag as
203 chd
->m_Prev
= m_Tags
->GetLastSibling();
204 chd
->m_Prev
->m_Next
= chd
;
208 if (chd
->HasEnding())
210 CreateDOMSubTree(chd
,
211 chd
->GetBeginPos(), chd
->GetEndPos1(),
213 i
= chd
->GetEndPos2();
216 i
= chd
->GetBeginPos();
221 // ... or skip ending tag:
224 while (i
< end_pos
&& m_Source
.GetChar(i
) != wxT('>')) i
++;
231 // add remaining text to m_TextPieces:
232 if (end_pos
- textBeginning
> 0)
234 wxHtmlTextPiece(textBeginning
, end_pos
- textBeginning
));
237 void wxHtmlParser::DestroyDOMTree()
243 t2
= t1
->GetNextSibling();
247 m_Tags
= m_CurTag
= NULL
;
253 void wxHtmlParser::DoParsing()
257 DoParsing(0, m_Source
.length());
260 void wxHtmlParser::DoParsing(int begin_pos
, int end_pos
)
262 if (end_pos
<= begin_pos
) return;
264 wxHtmlTextPieces
& pieces
= *m_TextPieces
;
265 size_t piecesCnt
= pieces
.GetCount();
267 while (begin_pos
< end_pos
)
269 while (m_CurTag
&& m_CurTag
->GetBeginPos() < begin_pos
)
270 m_CurTag
= m_CurTag
->GetNextTag();
271 while (m_CurTextPiece
< piecesCnt
&&
272 pieces
[m_CurTextPiece
].m_pos
< begin_pos
)
275 if (m_CurTextPiece
< piecesCnt
&&
277 pieces
[m_CurTextPiece
].m_pos
< m_CurTag
->GetBeginPos()))
280 AddText(GetEntitiesParser()->Parse(
281 m_Source
.Mid(pieces
[m_CurTextPiece
].m_pos
,
282 pieces
[m_CurTextPiece
].m_lng
)));
283 begin_pos
= pieces
[m_CurTextPiece
].m_pos
+
284 pieces
[m_CurTextPiece
].m_lng
;
289 if (m_CurTag
->HasEnding())
290 begin_pos
= m_CurTag
->GetEndPos2();
292 begin_pos
= m_CurTag
->GetBeginPos();
293 wxHtmlTag
*t
= m_CurTag
;
294 m_CurTag
= m_CurTag
->GetNextTag();
303 void wxHtmlParser::AddTag(const wxHtmlTag
& tag
)
308 h
= (wxHtmlTagHandler
*) m_HandlersHash
.Get(tag
.GetName());
311 inner
= h
->HandleTag(tag
);
318 DoParsing(tag
.GetBeginPos(), tag
.GetEndPos1());
322 void wxHtmlParser::AddTagHandler(wxHtmlTagHandler
*handler
)
324 wxString
s(handler
->GetSupportedTags());
325 wxStringTokenizer
tokenizer(s
, wxT(", "));
327 while (tokenizer
.HasMoreTokens())
328 m_HandlersHash
.Put(tokenizer
.GetNextToken(), handler
);
330 if (m_HandlersList
.IndexOf(handler
) == wxNOT_FOUND
)
331 m_HandlersList
.Append(handler
);
333 handler
->SetParser(this);
336 void wxHtmlParser::PushTagHandler(wxHtmlTagHandler
*handler
, const wxString
& tags
)
338 wxStringTokenizer
tokenizer(tags
, wxT(", "));
341 if (m_HandlersStack
== NULL
)
343 m_HandlersStack
= new wxList
;
346 m_HandlersStack
->Insert((wxObject
*)new wxHashTable(m_HandlersHash
));
348 while (tokenizer
.HasMoreTokens())
350 key
= tokenizer
.GetNextToken();
351 m_HandlersHash
.Delete(key
);
352 m_HandlersHash
.Put(key
, handler
);
356 void wxHtmlParser::PopTagHandler()
358 wxList::compatibility_iterator first
;
360 if ( !m_HandlersStack
||
362 !(first
= m_HandlersStack
->GetFirst())
364 ((first
= m_HandlersStack
->GetFirst()) == NULL
)
365 #endif // wxUSE_STL/!wxUSE_STL
368 wxLogWarning(_("Warning: attempt to remove HTML tag handler from empty stack."));
371 m_HandlersHash
= *((wxHashTable
*) first
->GetData());
372 delete (wxHashTable
*) first
->GetData();
373 m_HandlersStack
->Erase(first
);
376 void wxHtmlParser::SetSourceAndSaveState(const wxString
& src
)
378 wxHtmlParserState
*s
= new wxHtmlParserState
;
380 s
->m_curTag
= m_CurTag
;
382 s
->m_textPieces
= m_TextPieces
;
383 s
->m_curTextPiece
= m_CurTextPiece
;
384 s
->m_source
= m_Source
;
386 s
->m_nextState
= m_SavedStates
;
393 m_Source
= wxEmptyString
;
398 bool wxHtmlParser::RestoreState()
400 if (!m_SavedStates
) return false;
404 wxHtmlParserState
*s
= m_SavedStates
;
405 m_SavedStates
= s
->m_nextState
;
407 m_CurTag
= s
->m_curTag
;
409 m_TextPieces
= s
->m_textPieces
;
410 m_CurTextPiece
= s
->m_curTextPiece
;
411 m_Source
= s
->m_source
;
417 wxString
wxHtmlParser::GetInnerSource(const wxHtmlTag
& tag
)
419 return GetSource()->Mid(tag
.GetBeginPos(),
420 tag
.GetEndPos1() - tag
.GetBeginPos());
423 //-----------------------------------------------------------------------------
425 //-----------------------------------------------------------------------------
427 IMPLEMENT_ABSTRACT_CLASS(wxHtmlTagHandler
,wxObject
)
429 void wxHtmlTagHandler::ParseInnerSource(const wxString
& source
)
431 // It is safe to temporarily change the source being parsed,
432 // provided we restore the state back after parsing
433 m_Parser
->SetSourceAndSaveState(source
);
434 m_Parser
->DoParsing();
435 m_Parser
->RestoreState();
439 //-----------------------------------------------------------------------------
440 // wxHtmlEntitiesParser
441 //-----------------------------------------------------------------------------
443 IMPLEMENT_DYNAMIC_CLASS(wxHtmlEntitiesParser
,wxObject
)
445 wxHtmlEntitiesParser::wxHtmlEntitiesParser()
446 #if wxUSE_WCHAR_T && !wxUSE_UNICODE
447 : m_conv(NULL
), m_encoding(wxFONTENCODING_SYSTEM
)
452 wxHtmlEntitiesParser::~wxHtmlEntitiesParser()
454 #if wxUSE_WCHAR_T && !wxUSE_UNICODE
459 void wxHtmlEntitiesParser::SetEncoding(wxFontEncoding encoding
)
461 #if wxUSE_WCHAR_T && !wxUSE_UNICODE
462 if (encoding
== m_encoding
)
467 m_encoding
= encoding
;
468 if (m_encoding
== wxFONTENCODING_SYSTEM
)
471 m_conv
= new wxCSConv(wxFontMapper::GetEncodingName(m_encoding
));
477 wxString
wxHtmlEntitiesParser::Parse(const wxString
& input
) const
481 const wxString::const_iterator
end(input
.end());
482 wxString::const_iterator
c(input
.begin());
483 wxString::const_iterator
last(c
);
485 for ( ; c
< end
; ++c
)
489 if ( output
.empty() )
490 output
.reserve(input
.length());
493 output
.append(last
, c
);
498 const wxString::const_iterator ent_s
= c
;
502 ((*c
>= wxT('a') && *c
<= wxT('z')) ||
503 (*c
>= wxT('A') && *c
<= wxT('Z')) ||
504 (*c
>= wxT('0') && *c
<= wxT('9')) ||
505 *c
== wxT('_') || *c
== wxT('#')); ++c
) {}
506 entity
.append(ent_s
, c
);
507 if (c
== end
|| *c
!= wxT(';')) --c
;
509 entity_char
= GetEntityChar(entity
);
511 output
<< entity_char
;
514 output
.append(ent_s
-1, c
+1);
515 wxLogTrace(wxTRACE_HTML_DEBUG
,
516 "Unrecognized HTML entity: '%s'",
521 if ( last
== input
.begin() ) // common case: no entity
524 output
.append(last
, end
);
529 wxChar
wxHtmlEntitiesParser::GetCharForCode(unsigned code
) const
534 wbuf
[0] = (wchar_t)code
;
536 wxMBConv
*conv
= m_conv
? m_conv
: &wxConvLocal
;
537 if (conv
->WC2MB(buf
, wbuf
, 2) == (size_t)-1)
541 return (code
< 256) ? (wxChar
)code
: '?';
546 struct wxHtmlEntityInfo
548 const wxStringCharType
*name
;
552 extern "C" int LINKAGEMODE
wxHtmlEntityCompare(const void *key
, const void *item
)
554 #if wxUSE_UNICODE_UTF8
555 return strcmp((char*)key
, ((wxHtmlEntityInfo
*)item
)->name
);
557 return wxStrcmp((wxChar
*)key
, ((wxHtmlEntityInfo
*)item
)->name
);
561 wxChar
wxHtmlEntitiesParser::GetEntityChar(const wxString
& entity
) const
565 if (entity
[0] == wxT('#'))
567 // NB: parsed value is a number, so it's OK to use wx_str(), internal
568 // representation is the same for numbers
569 const wxStringCharType
*ent_s
= entity
.wx_str();
570 const wxStringCharType
*format
;
572 if (ent_s
[1] == wxSTRING_TEXT('x') || ent_s
[1] == wxSTRING_TEXT('X'))
574 format
= wxSTRING_TEXT("%x");
578 format
= wxSTRING_TEXT("%u");
581 if (wxSscanf(ent_s
, format
, &code
) != 1)
586 // store the literals in wx's internal representation (either char*
587 // in UTF-8 or wchar_t*) for best performance:
588 #define ENTITY(name, code) { wxSTRING_TEXT(name), code }
590 static wxHtmlEntityInfo substitutions
[] = {
591 ENTITY("AElig", 198),
592 ENTITY("Aacute", 193),
593 ENTITY("Acirc", 194),
594 ENTITY("Agrave", 192),
595 ENTITY("Alpha", 913),
596 ENTITY("Aring", 197),
597 ENTITY("Atilde", 195),
600 ENTITY("Ccedil", 199),
602 ENTITY("Dagger", 8225),
603 ENTITY("Delta", 916),
605 ENTITY("Eacute", 201),
606 ENTITY("Ecirc", 202),
607 ENTITY("Egrave", 200),
608 ENTITY("Epsilon", 917),
611 ENTITY("Gamma", 915),
612 ENTITY("Iacute", 205),
613 ENTITY("Icirc", 206),
614 ENTITY("Igrave", 204),
617 ENTITY("Kappa", 922),
618 ENTITY("Lambda", 923),
620 ENTITY("Ntilde", 209),
622 ENTITY("OElig", 338),
623 ENTITY("Oacute", 211),
624 ENTITY("Ocirc", 212),
625 ENTITY("Ograve", 210),
626 ENTITY("Omega", 937),
627 ENTITY("Omicron", 927),
628 ENTITY("Oslash", 216),
629 ENTITY("Otilde", 213),
633 ENTITY("Prime", 8243),
636 ENTITY("Scaron", 352),
637 ENTITY("Sigma", 931),
638 ENTITY("THORN", 222),
640 ENTITY("Theta", 920),
641 ENTITY("Uacute", 218),
642 ENTITY("Ucirc", 219),
643 ENTITY("Ugrave", 217),
644 ENTITY("Upsilon", 933),
647 ENTITY("Yacute", 221),
650 ENTITY("aacute", 225),
651 ENTITY("acirc", 226),
652 ENTITY("acute", 180),
653 ENTITY("aelig", 230),
654 ENTITY("agrave", 224),
655 ENTITY("alefsym", 8501),
656 ENTITY("alpha", 945),
660 ENTITY("aring", 229),
661 ENTITY("asymp", 8776),
662 ENTITY("atilde", 227),
664 ENTITY("bdquo", 8222),
666 ENTITY("brvbar", 166),
667 ENTITY("bull", 8226),
669 ENTITY("ccedil", 231),
670 ENTITY("cedil", 184),
674 ENTITY("clubs", 9827),
675 ENTITY("cong", 8773),
677 ENTITY("crarr", 8629),
679 ENTITY("curren", 164),
680 ENTITY("dArr", 8659),
681 ENTITY("dagger", 8224),
682 ENTITY("darr", 8595),
684 ENTITY("delta", 948),
685 ENTITY("diams", 9830),
686 ENTITY("divide", 247),
687 ENTITY("eacute", 233),
688 ENTITY("ecirc", 234),
689 ENTITY("egrave", 232),
690 ENTITY("empty", 8709),
691 ENTITY("emsp", 8195),
692 ENTITY("ensp", 8194),
693 ENTITY("epsilon", 949),
694 ENTITY("equiv", 8801),
698 ENTITY("euro", 8364),
699 ENTITY("exist", 8707),
701 ENTITY("forall", 8704),
702 ENTITY("frac12", 189),
703 ENTITY("frac14", 188),
704 ENTITY("frac34", 190),
705 ENTITY("frasl", 8260),
706 ENTITY("gamma", 947),
709 ENTITY("hArr", 8660),
710 ENTITY("harr", 8596),
711 ENTITY("hearts", 9829),
712 ENTITY("hellip", 8230),
713 ENTITY("iacute", 237),
714 ENTITY("icirc", 238),
715 ENTITY("iexcl", 161),
716 ENTITY("igrave", 236),
717 ENTITY("image", 8465),
718 ENTITY("infin", 8734),
721 ENTITY("iquest", 191),
722 ENTITY("isin", 8712),
724 ENTITY("kappa", 954),
725 ENTITY("lArr", 8656),
726 ENTITY("lambda", 955),
727 ENTITY("lang", 9001),
728 ENTITY("laquo", 171),
729 ENTITY("larr", 8592),
730 ENTITY("lceil", 8968),
731 ENTITY("ldquo", 8220),
733 ENTITY("lfloor", 8970),
734 ENTITY("lowast", 8727),
737 ENTITY("lsaquo", 8249),
738 ENTITY("lsquo", 8216),
741 ENTITY("mdash", 8212),
742 ENTITY("micro", 181),
743 ENTITY("middot", 183),
744 ENTITY("minus", 8722),
746 ENTITY("nabla", 8711),
748 ENTITY("ndash", 8211),
752 ENTITY("notin", 8713),
753 ENTITY("nsub", 8836),
754 ENTITY("ntilde", 241),
756 ENTITY("oacute", 243),
757 ENTITY("ocirc", 244),
758 ENTITY("oelig", 339),
759 ENTITY("ograve", 242),
760 ENTITY("oline", 8254),
761 ENTITY("omega", 969),
762 ENTITY("omicron", 959),
763 ENTITY("oplus", 8853),
767 ENTITY("oslash", 248),
768 ENTITY("otilde", 245),
769 ENTITY("otimes", 8855),
772 ENTITY("part", 8706),
773 ENTITY("permil", 8240),
774 ENTITY("perp", 8869),
778 ENTITY("plusmn", 177),
779 ENTITY("pound", 163),
780 ENTITY("prime", 8242),
781 ENTITY("prod", 8719),
782 ENTITY("prop", 8733),
785 ENTITY("rArr", 8658),
786 ENTITY("radic", 8730),
787 ENTITY("rang", 9002),
788 ENTITY("raquo", 187),
789 ENTITY("rarr", 8594),
790 ENTITY("rceil", 8969),
791 ENTITY("rdquo", 8221),
792 ENTITY("real", 8476),
794 ENTITY("rfloor", 8971),
797 ENTITY("rsaquo", 8250),
798 ENTITY("rsquo", 8217),
799 ENTITY("sbquo", 8218),
800 ENTITY("scaron", 353),
801 ENTITY("sdot", 8901),
804 ENTITY("sigma", 963),
805 ENTITY("sigmaf", 962),
807 ENTITY("spades", 9824),
809 ENTITY("sube", 8838),
815 ENTITY("supe", 8839),
816 ENTITY("szlig", 223),
818 ENTITY("there4", 8756),
819 ENTITY("theta", 952),
820 ENTITY("thetasym", 977),
821 ENTITY("thinsp", 8201),
822 ENTITY("thorn", 254),
823 ENTITY("tilde", 732),
824 ENTITY("times", 215),
825 ENTITY("trade", 8482),
826 ENTITY("uArr", 8657),
827 ENTITY("uacute", 250),
828 ENTITY("uarr", 8593),
829 ENTITY("ucirc", 251),
830 ENTITY("ugrave", 249),
832 ENTITY("upsih", 978),
833 ENTITY("upsilon", 965),
835 ENTITY("weierp", 8472),
837 ENTITY("yacute", 253),
842 ENTITY("zwnj", 8204),
845 static size_t substitutions_cnt
= 0;
847 if (substitutions_cnt
== 0)
848 while (substitutions
[substitutions_cnt
].code
!= 0)
851 wxHtmlEntityInfo
*info
= NULL
;
853 // bsearch crashes under WinCE for some reason
855 for (i
= 0; i
< substitutions_cnt
; i
++)
857 if (entity
== substitutions
[i
].name
)
859 info
= & substitutions
[i
];
864 info
= (wxHtmlEntityInfo
*) bsearch(entity
.wx_str(), substitutions
,
866 sizeof(wxHtmlEntityInfo
),
867 wxHtmlEntityCompare
);
876 return GetCharForCode(code
);
879 wxFSFile
*wxHtmlParser::OpenURL(wxHtmlURLType
WXUNUSED(type
),
880 const wxString
& url
) const
882 return m_FS
? m_FS
->OpenFile(url
) : NULL
;
887 //-----------------------------------------------------------------------------
888 // wxHtmlParser::ExtractCharsetInformation
889 //-----------------------------------------------------------------------------
891 class wxMetaTagParser
: public wxHtmlParser
894 wxMetaTagParser() { }
896 wxObject
* GetProduct() { return NULL
; }
899 virtual void AddText(const wxString
& WXUNUSED(txt
)) {}
901 DECLARE_NO_COPY_CLASS(wxMetaTagParser
)
904 class wxMetaTagHandler
: public wxHtmlTagHandler
907 wxMetaTagHandler(wxString
*retval
) : wxHtmlTagHandler(), m_retval(retval
) {}
908 wxString
GetSupportedTags() { return wxT("META,BODY"); }
909 bool HandleTag(const wxHtmlTag
& tag
);
914 DECLARE_NO_COPY_CLASS(wxMetaTagHandler
)
917 bool wxMetaTagHandler::HandleTag(const wxHtmlTag
& tag
)
919 if (tag
.GetName() == _T("BODY"))
921 m_Parser
->StopParsing();
925 if (tag
.HasParam(_T("HTTP-EQUIV")) &&
926 tag
.GetParam(_T("HTTP-EQUIV")).IsSameAs(_T("Content-Type"), false) &&
927 tag
.HasParam(_T("CONTENT")))
929 wxString content
= tag
.GetParam(_T("CONTENT")).Lower();
930 if (content
.Left(19) == _T("text/html; charset="))
932 *m_retval
= content
.Mid(19);
933 m_Parser
->StopParsing();
941 wxString
wxHtmlParser::ExtractCharsetInformation(const wxString
& markup
)
944 wxMetaTagParser
*parser
= new wxMetaTagParser();
947 parser
->AddTagHandler(new wxMetaTagHandler(&charset
));
948 parser
->Parse(markup
);
956 wxHtmlParser::SkipCommentTag(wxString::const_iterator
& start
,
957 wxString::const_iterator end
)
959 wxASSERT_MSG( *start
== '<', _T("should be called on the tag start") );
961 wxString::const_iterator p
= start
;
963 // comments begin with "<!--" in HTML 4.0
964 if ( p
> end
- 3 || *++p
!= '!' || *++p
!= '-' || *++p
!= '-' )
966 // not a comment at all
970 // skip the start of the comment tag in any case, if we don't find the
971 // closing tag we should ignore broken markup
974 // comments end with "--[ \t\r\n]*>", i.e. white space is allowed between
975 // comment delimiter and the closing tag character (section 3.2.4 of
976 // http://www.w3.org/TR/html401/)
982 if ( (c
== wxT(' ') || c
== wxT('\n') ||
983 c
== wxT('\r') || c
== wxT('\t')) && dashes
>= 2 )
985 // ignore white space before potential tag end
989 if ( c
== wxT('>') && dashes
>= 2 )
991 // found end of comment
1005 #endif // wxUSE_HTML