@@ -189,21 +189,60 @@ where
189189 current_component : Option < String > ,
190190}
191191
192- /// Returns true if `raw_html`:
193- /// - starts with '<'
194- /// - ends with '>'
195- /// - does not have any '<' or '>' in between.
192+ /// Returns true if `raw_html` appears to be a custom component tag.
196193///
197- /// TODO:
198- /// An string attribute can a ">" character.
194+ /// A valid custom component tag must:
195+ /// - Start with '<'
196+ /// - End with '>'
197+ /// - Have a tag name that either:
198+ /// - Starts with an uppercase letter (A-Z), OR
199+ /// - Starts with a lowercase letter (a-z) and contains at least one dash (-)
200+ ///
201+ /// This validation prevents standard HTML tags like `<div>`, `<span>`, `<p>` from being
202+ /// treated as custom components while allowing custom component names like:
203+ /// - `<MyComponent>` (uppercase start, no dash needed)
204+ /// - `<my-component>` (lowercase start, has dash)
205+ /// - `<My-Component>` (uppercase start, has dash)
206+ ///
207+ /// The function also handles:
208+ /// - Self-closing tags: `<My-Component/>`
209+ /// - Tags with attributes: `<My-Component attr="value">`
210+ /// - Closing tags: `</My-Component>`
211+ ///
212+ /// Invalid HTML like `<Y and Y>` is rejected because "and" is not valid attribute syntax.
199213fn can_be_custom_component ( raw_html : & str ) -> bool {
200- let chars: Vec < _ > = raw_html. trim ( ) . chars ( ) . collect ( ) ;
201- let len = chars. len ( ) ;
202- if len < 3 {
203- return false ;
204- } ;
205- let ( fst, middle, last) = ( chars[ 0 ] , & chars[ 1 ..len - 1 ] , chars[ len - 1 ] ) ;
206- fst == '<' && last == '>' && middle. iter ( ) . all ( |c| c != & '<' && c != & '>' )
214+ lazy_static:: lazy_static! {
215+ // Regex patterns for custom component tags:
216+
217+ // Simple tags: <MyComponent> or </MyComponent> or <my-component> or </my-component>
218+ // ^< - starts with <
219+ // /? - optional / for closing tags
220+ // ([A-Z][A-Za-z0-9-]* - uppercase start with optional alphanumeric and dashes
221+ // |[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*) - OR lowercase start with at least one dash
222+ // >$ - ends with >
223+ static ref SIMPLE_TAG_RE : regex:: Regex = regex:: Regex :: new(
224+ r"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)>$"
225+ ) . unwrap( ) ;
226+
227+ // Self-closing tags: <MyComponent/> or <my-component/>
228+ static ref SELF_CLOSING_RE : regex:: Regex = regex:: Regex :: new(
229+ r"^<([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)/\s*>$"
230+ ) . unwrap( ) ;
231+
232+ // Tags with attributes: <MyComponent attr="value"> or <my-component attr="value"/>
233+ // After the tag name, we must have whitespace followed by content that contains '='
234+ // This rejects things like "<Y and Y>" where there's no '='
235+ // Note: The regex is non-greedy and will match the smallest possible string,
236+ // so escaped characters like < or > in attributes are allowed
237+ static ref WITH_ATTRS_RE : regex:: Regex = regex:: Regex :: new(
238+ r"^</?([A-Z][A-Za-z0-9-]*|[a-z][A-Za-z0-9]*-[A-Za-z0-9-]*)\s+.*?=.*?/?\s*>$"
239+ ) . unwrap( ) ;
240+ }
241+
242+ let s = raw_html. trim ( ) ;
243+
244+ // Try to match with regex patterns
245+ SIMPLE_TAG_RE . is_match ( s) || SELF_CLOSING_RE . is_match ( s) || WITH_ATTRS_RE . is_match ( s)
207246}
208247
209248impl < ' a , ' callback , ' c , I , F > Iterator for Renderer < ' a , ' callback , ' c , I , F >
@@ -348,7 +387,7 @@ where
348387 } )
349388 } else {
350389 Some ( match item {
351- Event :: InlineHtml ( ref x) => RenderEvent {
390+ Event :: InlineHtml ( ref x) if can_be_custom_component ( x ) => RenderEvent {
352391 // FIXME: avoid clone
353392 custom_tag : Some ( x. clone ( ) ) ,
354393 event : item,
@@ -585,3 +624,100 @@ where
585624 } )
586625 }
587626}
627+
628+ #[ cfg( test) ]
629+ mod tests {
630+ use super :: * ;
631+
632+ #[ test]
633+ fn test_can_be_custom_component_uppercase_start ( ) {
634+ // Uppercase start should always be valid
635+ assert ! ( can_be_custom_component( "<MyComponent>" ) ) ;
636+ assert ! ( can_be_custom_component( "<Counter>" ) ) ;
637+ assert ! ( can_be_custom_component( "<DataTable>" ) ) ;
638+ assert ! ( can_be_custom_component( "<MyComponent/>" ) ) ;
639+ assert ! ( can_be_custom_component( "</MyComponent>" ) ) ;
640+ assert ! ( can_be_custom_component( "<MyComponent attr=\" value\" >" ) ) ;
641+ assert ! ( can_be_custom_component( "<My-Component>" ) ) ;
642+ assert ! ( can_be_custom_component( "<MY-COMPONENT>" ) ) ;
643+ }
644+
645+ #[ test]
646+ fn test_can_be_custom_component_lowercase_with_dash ( ) {
647+ // Lowercase start with dash should be valid
648+ assert ! ( can_be_custom_component( "<my-component>" ) ) ;
649+ assert ! ( can_be_custom_component( "<data-table>" ) ) ;
650+ assert ! ( can_be_custom_component( "<custom-counter>" ) ) ;
651+ assert ! ( can_be_custom_component( "<my-component/>" ) ) ;
652+ assert ! ( can_be_custom_component( "</my-component>" ) ) ;
653+ assert ! ( can_be_custom_component( "<my-component attr=\" value\" >" ) ) ;
654+ assert ! ( can_be_custom_component( "<a-b>" ) ) ;
655+ assert ! ( can_be_custom_component( "<my-custom-widget>" ) ) ;
656+ }
657+
658+ #[ test]
659+ fn test_can_be_custom_component_lowercase_no_dash ( ) {
660+ // Lowercase start without dash should be invalid (standard HTML tags)
661+ assert ! ( !can_be_custom_component( "<div>" ) ) ;
662+ assert ! ( !can_be_custom_component( "<span>" ) ) ;
663+ assert ! ( !can_be_custom_component( "<p>" ) ) ;
664+ assert ! ( !can_be_custom_component( "<section>" ) ) ;
665+ assert ! ( !can_be_custom_component( "<article>" ) ) ;
666+ assert ! ( !can_be_custom_component( "<header>" ) ) ;
667+ assert ! ( !can_be_custom_component( "<footer>" ) ) ;
668+ assert ! ( !can_be_custom_component( "<div/>" ) ) ;
669+ assert ! ( !can_be_custom_component( "</div>" ) ) ;
670+ assert ! ( !can_be_custom_component( "<div class=\" test\" >" ) ) ;
671+ }
672+
673+ #[ test]
674+ fn test_can_be_custom_component_edge_cases ( ) {
675+ // Empty or invalid tags
676+ assert ! ( !can_be_custom_component( "<>" ) ) ;
677+ assert ! ( !can_be_custom_component( "</>" ) ) ;
678+ assert ! ( !can_be_custom_component( "" ) ) ;
679+ assert ! ( !can_be_custom_component( "< >" ) ) ;
680+ assert ! ( !can_be_custom_component( "text" ) ) ;
681+
682+ // Missing brackets
683+ assert ! ( !can_be_custom_component( "MyComponent>" ) ) ;
684+ assert ! ( !can_be_custom_component( "<MyComponent" ) ) ;
685+
686+ // Whitespace handling
687+ assert ! ( can_be_custom_component( " <MyComponent> " ) ) ;
688+ assert ! ( can_be_custom_component( " <my-component> " ) ) ;
689+ assert ! ( !can_be_custom_component( " <div> " ) ) ;
690+ }
691+
692+ #[ test]
693+ fn test_can_be_custom_component_with_attributes ( ) {
694+ // With attributes
695+ assert ! ( can_be_custom_component(
696+ "<Counter initial=\" 5\" step=\" 1\" />"
697+ ) ) ;
698+ assert ! ( can_be_custom_component(
699+ "<my-widget data=\" test\" class=\" styled\" />"
700+ ) ) ;
701+ assert ! ( !can_be_custom_component(
702+ "<div class=\" container\" id=\" main\" >"
703+ ) ) ;
704+ }
705+
706+ #[ test]
707+ fn test_can_be_custom_component_self_closing ( ) {
708+ // Self-closing tags
709+ assert ! ( can_be_custom_component( "<MyComponent/>" ) ) ;
710+ assert ! ( can_be_custom_component( "<my-component/>" ) ) ;
711+ assert ! ( !can_be_custom_component( "<div/>" ) ) ;
712+ assert ! ( !can_be_custom_component( "<span/>" ) ) ;
713+ }
714+
715+ #[ test]
716+ fn test_can_be_custom_component_closing_tags ( ) {
717+ // Closing tags
718+ assert ! ( can_be_custom_component( "</MyComponent>" ) ) ;
719+ assert ! ( can_be_custom_component( "</my-component>" ) ) ;
720+ assert ! ( !can_be_custom_component( "</div>" ) ) ;
721+ assert ! ( !can_be_custom_component( "</span>" ) ) ;
722+ }
723+ }
0 commit comments