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
479 const wxChar
*c
, *last
;
480 const wxChar
*in_str
= input
.c_str();
483 output
.reserve(input
.length());
485 for (c
= in_str
, last
= in_str
; *c
!= wxT('\0'); c
++)
490 output
.append(last
, c
- last
);
491 if ( *++c
== wxT('\0') )
495 const wxChar
*ent_s
= c
;
498 for (; (*c
>= wxT('a') && *c
<= wxT('z')) ||
499 (*c
>= wxT('A') && *c
<= wxT('Z')) ||
500 (*c
>= wxT('0') && *c
<= wxT('9')) ||
501 *c
== wxT('_') || *c
== wxT('#'); c
++) {}
502 entity
.append(ent_s
, c
- ent_s
);
503 if (*c
!= wxT(';')) c
--;
505 entity_char
= GetEntityChar(entity
);
507 output
<< entity_char
;
510 output
.append(ent_s
-1, c
-ent_s
+2);
511 wxLogTrace(wxTRACE_HTML_DEBUG
,
512 wxT("Unrecognized HTML entity: '%s'"),
517 if (*last
!= wxT('\0'))
523 wxChar
wxHtmlEntitiesParser::GetCharForCode(unsigned code
) const
528 wbuf
[0] = (wchar_t)code
;
530 wxMBConv
*conv
= m_conv
? m_conv
: &wxConvLocal
;
531 if (conv
->WC2MB(buf
, wbuf
, 2) == (size_t)-1)
535 return (code
< 256) ? (wxChar
)code
: '?';
540 struct wxHtmlEntityInfo
542 const wxStringCharType
*name
;
546 extern "C" int LINKAGEMODE
wxHtmlEntityCompare(const void *key
, const void *item
)
548 #if wxUSE_UNICODE_UTF8
549 return strcmp((char*)key
, ((wxHtmlEntityInfo
*)item
)->name
);
551 return wxStrcmp((wxChar
*)key
, ((wxHtmlEntityInfo
*)item
)->name
);
555 wxChar
wxHtmlEntitiesParser::GetEntityChar(const wxString
& entity
) const
559 if (entity
[0] == wxT('#'))
561 const wxChar
*ent_s
= entity
.c_str();
562 const wxChar
*format
;
564 if (ent_s
[1] == wxT('x') || ent_s
[1] == wxT('X'))
573 if (wxSscanf(ent_s
, format
, &code
) != 1)
578 // store the literals in wx's internal representation (either char*
579 // in UTF-8 or wchar_t*) for best performance:
580 #define ENTITY(name, code) { wxSTRING_TEXT(name), code }
582 static wxHtmlEntityInfo substitutions
[] = {
583 ENTITY("AElig", 198),
584 ENTITY("Aacute", 193),
585 ENTITY("Acirc", 194),
586 ENTITY("Agrave", 192),
587 ENTITY("Alpha", 913),
588 ENTITY("Aring", 197),
589 ENTITY("Atilde", 195),
592 ENTITY("Ccedil", 199),
594 ENTITY("Dagger", 8225),
595 ENTITY("Delta", 916),
597 ENTITY("Eacute", 201),
598 ENTITY("Ecirc", 202),
599 ENTITY("Egrave", 200),
600 ENTITY("Epsilon", 917),
603 ENTITY("Gamma", 915),
604 ENTITY("Iacute", 205),
605 ENTITY("Icirc", 206),
606 ENTITY("Igrave", 204),
609 ENTITY("Kappa", 922),
610 ENTITY("Lambda", 923),
612 ENTITY("Ntilde", 209),
614 ENTITY("OElig", 338),
615 ENTITY("Oacute", 211),
616 ENTITY("Ocirc", 212),
617 ENTITY("Ograve", 210),
618 ENTITY("Omega", 937),
619 ENTITY("Omicron", 927),
620 ENTITY("Oslash", 216),
621 ENTITY("Otilde", 213),
625 ENTITY("Prime", 8243),
628 ENTITY("Scaron", 352),
629 ENTITY("Sigma", 931),
630 ENTITY("THORN", 222),
632 ENTITY("Theta", 920),
633 ENTITY("Uacute", 218),
634 ENTITY("Ucirc", 219),
635 ENTITY("Ugrave", 217),
636 ENTITY("Upsilon", 933),
639 ENTITY("Yacute", 221),
642 ENTITY("aacute", 225),
643 ENTITY("acirc", 226),
644 ENTITY("acute", 180),
645 ENTITY("aelig", 230),
646 ENTITY("agrave", 224),
647 ENTITY("alefsym", 8501),
648 ENTITY("alpha", 945),
652 ENTITY("aring", 229),
653 ENTITY("asymp", 8776),
654 ENTITY("atilde", 227),
656 ENTITY("bdquo", 8222),
658 ENTITY("brvbar", 166),
659 ENTITY("bull", 8226),
661 ENTITY("ccedil", 231),
662 ENTITY("cedil", 184),
666 ENTITY("clubs", 9827),
667 ENTITY("cong", 8773),
669 ENTITY("crarr", 8629),
671 ENTITY("curren", 164),
672 ENTITY("dArr", 8659),
673 ENTITY("dagger", 8224),
674 ENTITY("darr", 8595),
676 ENTITY("delta", 948),
677 ENTITY("diams", 9830),
678 ENTITY("divide", 247),
679 ENTITY("eacute", 233),
680 ENTITY("ecirc", 234),
681 ENTITY("egrave", 232),
682 ENTITY("empty", 8709),
683 ENTITY("emsp", 8195),
684 ENTITY("ensp", 8194),
685 ENTITY("epsilon", 949),
686 ENTITY("equiv", 8801),
690 ENTITY("euro", 8364),
691 ENTITY("exist", 8707),
693 ENTITY("forall", 8704),
694 ENTITY("frac12", 189),
695 ENTITY("frac14", 188),
696 ENTITY("frac34", 190),
697 ENTITY("frasl", 8260),
698 ENTITY("gamma", 947),
701 ENTITY("hArr", 8660),
702 ENTITY("harr", 8596),
703 ENTITY("hearts", 9829),
704 ENTITY("hellip", 8230),
705 ENTITY("iacute", 237),
706 ENTITY("icirc", 238),
707 ENTITY("iexcl", 161),
708 ENTITY("igrave", 236),
709 ENTITY("image", 8465),
710 ENTITY("infin", 8734),
713 ENTITY("iquest", 191),
714 ENTITY("isin", 8712),
716 ENTITY("kappa", 954),
717 ENTITY("lArr", 8656),
718 ENTITY("lambda", 955),
719 ENTITY("lang", 9001),
720 ENTITY("laquo", 171),
721 ENTITY("larr", 8592),
722 ENTITY("lceil", 8968),
723 ENTITY("ldquo", 8220),
725 ENTITY("lfloor", 8970),
726 ENTITY("lowast", 8727),
729 ENTITY("lsaquo", 8249),
730 ENTITY("lsquo", 8216),
733 ENTITY("mdash", 8212),
734 ENTITY("micro", 181),
735 ENTITY("middot", 183),
736 ENTITY("minus", 8722),
738 ENTITY("nabla", 8711),
740 ENTITY("ndash", 8211),
744 ENTITY("notin", 8713),
745 ENTITY("nsub", 8836),
746 ENTITY("ntilde", 241),
748 ENTITY("oacute", 243),
749 ENTITY("ocirc", 244),
750 ENTITY("oelig", 339),
751 ENTITY("ograve", 242),
752 ENTITY("oline", 8254),
753 ENTITY("omega", 969),
754 ENTITY("omicron", 959),
755 ENTITY("oplus", 8853),
759 ENTITY("oslash", 248),
760 ENTITY("otilde", 245),
761 ENTITY("otimes", 8855),
764 ENTITY("part", 8706),
765 ENTITY("permil", 8240),
766 ENTITY("perp", 8869),
770 ENTITY("plusmn", 177),
771 ENTITY("pound", 163),
772 ENTITY("prime", 8242),
773 ENTITY("prod", 8719),
774 ENTITY("prop", 8733),
777 ENTITY("rArr", 8658),
778 ENTITY("radic", 8730),
779 ENTITY("rang", 9002),
780 ENTITY("raquo", 187),
781 ENTITY("rarr", 8594),
782 ENTITY("rceil", 8969),
783 ENTITY("rdquo", 8221),
784 ENTITY("real", 8476),
786 ENTITY("rfloor", 8971),
789 ENTITY("rsaquo", 8250),
790 ENTITY("rsquo", 8217),
791 ENTITY("sbquo", 8218),
792 ENTITY("scaron", 353),
793 ENTITY("sdot", 8901),
796 ENTITY("sigma", 963),
797 ENTITY("sigmaf", 962),
799 ENTITY("spades", 9824),
801 ENTITY("sube", 8838),
807 ENTITY("supe", 8839),
808 ENTITY("szlig", 223),
810 ENTITY("there4", 8756),
811 ENTITY("theta", 952),
812 ENTITY("thetasym", 977),
813 ENTITY("thinsp", 8201),
814 ENTITY("thorn", 254),
815 ENTITY("tilde", 732),
816 ENTITY("times", 215),
817 ENTITY("trade", 8482),
818 ENTITY("uArr", 8657),
819 ENTITY("uacute", 250),
820 ENTITY("uarr", 8593),
821 ENTITY("ucirc", 251),
822 ENTITY("ugrave", 249),
824 ENTITY("upsih", 978),
825 ENTITY("upsilon", 965),
827 ENTITY("weierp", 8472),
829 ENTITY("yacute", 253),
834 ENTITY("zwnj", 8204),
837 static size_t substitutions_cnt
= 0;
839 if (substitutions_cnt
== 0)
840 while (substitutions
[substitutions_cnt
].code
!= 0)
843 wxHtmlEntityInfo
*info
= NULL
;
845 // bsearch crashes under WinCE for some reason
847 for (i
= 0; i
< substitutions_cnt
; i
++)
849 if (entity
== substitutions
[i
].name
)
851 info
= & substitutions
[i
];
856 info
= (wxHtmlEntityInfo
*) bsearch(entity
.wx_str(), substitutions
,
858 sizeof(wxHtmlEntityInfo
),
859 wxHtmlEntityCompare
);
868 return GetCharForCode(code
);
871 wxFSFile
*wxHtmlParser::OpenURL(wxHtmlURLType
WXUNUSED(type
),
872 const wxString
& url
) const
874 return m_FS
? m_FS
->OpenFile(url
) : NULL
;
879 //-----------------------------------------------------------------------------
880 // wxHtmlParser::ExtractCharsetInformation
881 //-----------------------------------------------------------------------------
883 class wxMetaTagParser
: public wxHtmlParser
886 wxMetaTagParser() { }
888 wxObject
* GetProduct() { return NULL
; }
891 virtual void AddText(const wxString
& WXUNUSED(txt
)) {}
893 DECLARE_NO_COPY_CLASS(wxMetaTagParser
)
896 class wxMetaTagHandler
: public wxHtmlTagHandler
899 wxMetaTagHandler(wxString
*retval
) : wxHtmlTagHandler(), m_retval(retval
) {}
900 wxString
GetSupportedTags() { return wxT("META,BODY"); }
901 bool HandleTag(const wxHtmlTag
& tag
);
906 DECLARE_NO_COPY_CLASS(wxMetaTagHandler
)
909 bool wxMetaTagHandler::HandleTag(const wxHtmlTag
& tag
)
911 if (tag
.GetName() == _T("BODY"))
913 m_Parser
->StopParsing();
917 if (tag
.HasParam(_T("HTTP-EQUIV")) &&
918 tag
.GetParam(_T("HTTP-EQUIV")).IsSameAs(_T("Content-Type"), false) &&
919 tag
.HasParam(_T("CONTENT")))
921 wxString content
= tag
.GetParam(_T("CONTENT")).Lower();
922 if (content
.Left(19) == _T("text/html; charset="))
924 *m_retval
= content
.Mid(19);
925 m_Parser
->StopParsing();
933 wxString
wxHtmlParser::ExtractCharsetInformation(const wxString
& markup
)
936 wxMetaTagParser
*parser
= new wxMetaTagParser();
939 parser
->AddTagHandler(new wxMetaTagHandler(&charset
));
940 parser
->Parse(markup
);
948 wxHtmlParser::SkipCommentTag(wxString::const_iterator
& start
,
949 wxString::const_iterator end
)
951 wxASSERT_MSG( *start
== '<', _T("should be called on the tag start") );
953 wxString::const_iterator p
= start
;
955 // comments begin with "<!--" in HTML 4.0
956 if ( end
- p
< 3 || *++p
!= '!' || *++p
!= '-' || *++p
!= '-' )
958 // not a comment at all
962 // skip the start of the comment tag in any case, if we don't find the
963 // closing tag we should ignore broken markup
966 // comments end with "--[ \t\r\n]*>", i.e. white space is allowed between
967 // comment delimiter and the closing tag character (section 3.2.4 of
968 // http://www.w3.org/TR/html401/)
974 if ( (c
== wxT(' ') || c
== wxT('\n') ||
975 c
== wxT('\r') || c
== wxT('\t')) && dashes
>= 2 )
977 // ignore white space before potential tag end
981 if ( c
== wxT('>') && dashes
>= 2 )
983 // found end of comment