@@ -934,6 +934,11 @@ def field(self, node, label):
934934 label_attrs = label if isinstance (label , dict ) else {'text' : label }
935935 label = label_attrs .get ('text' , '' )
936936 kind = node .tag if node .tag in {'textarea' , 'select' } else node .attrs .get ('type' , 'text' )
937+ # A search box in a form that is no site search (search_form) is the
938+ # form's text field; rules on input[type=search] no longer match it.
939+ if kind == 'search' :
940+ self .finding ('field-type' , 'search field kept as a text field: ' + (node .attrs .get ('name' ) or node .attrs .get ('id' ) or '' ))
941+ kind = 'text'
937942 if kind not in {'text' , 'email' , 'tel' , 'url' , 'number' , 'textarea' , 'select' , 'checkbox' , 'radio' }:
938943 self .finding ('unmapped-field' , kind )
939944 return None
@@ -965,6 +970,80 @@ def field(self, node, label):
965970 self .finding ('field-default' , 'Preserve textarea default content: ' + name )
966971 return {'name' : 'h2wp/field' , 'attributes' : result }
967972
973+ def search_form (self , node ):
974+ """core/search for a site search, else None: a GET to the front page
975+ with one box named `s`, the way WordPress searches (a WordPress export
976+ keeps its theme's form). No mail form: WordPress answers it with its
977+ search results. Its hidden inputs are the search's own parameters
978+ (`post_type=post`: notes only). An icon or text beside the box keeps
979+ the form's classes on an element around the search, so the design
980+ lays them out as it did."""
981+ if (node .attrs .get ('method' ) or 'get' ).strip ().lower () != 'get' :
982+ return None
983+ # The front page: `/`, a path to it, or the site's own address as an
984+ # export writes it (home_url(): any host, no path).
985+ action , home = (node .attrs .get ('action' ) or '' ).strip (), self .resolve_quiet ('/' )
986+ address = urlsplit (action )
987+ if action != '/' and not (address .scheme in ('http' , 'https' ) and address .path in ('' , '/' ) and not address .query ) \
988+ and not (action and home .startswith ('page:' ) and self .resolve_quiet (action ).split ('#' )[0 ].split ('?' )[0 ] == home ):
989+ return None
990+ kind = lambda n : (n .attrs .get ('type' ) or ('submit' if n .tag == 'button' else 'text' )).lower ()
991+ controls = [n for n in walk (node ) if n .tag in ('input' , 'select' , 'textarea' , 'button' )]
992+ box = [n for n in controls if n .tag == 'input' and kind (n ) in ('search' , 'text' ) and n .attrs .get ('name' ) == 's' ]
993+ hidden = [n for n in controls if n .tag == 'input' and kind (n ) == 'hidden' ]
994+ submit = [n for n in controls if (n .tag == 'button' and kind (n ) == 'submit' ) or (n .tag == 'input' and kind (n ) in ('submit' , 'image' ))]
995+ query = {n .attrs .get ('name' ) or '' : n .attrs .get ('value' ) or '' for n in hidden }
996+ if len (box ) != 1 or len (submit ) > 1 or len (box ) + len (hidden ) + len (submit ) != len (controls ) or len (query ) != len (hidden ) \
997+ or any (not re .fullmatch (r'[a-z_][a-z0-9_-]{0,31}' , k , re .I ) or k == 's' or len (v ) > 200 for k , v in query .items ()):
998+ return None
999+ field = box [0 ]
1000+ labels = [n for n in walk (node ) if n .tag == 'label' ]
1001+ named = self .labels .get (field .attrs .get ('id' )) if field .attrs .get ('id' ) else None
1002+ words = norm (plain (labels [0 ])) if labels else norm ((named or {}).get ('text' ) or '' )
1003+ attrs = {'label' : words or norm (field .attrs .get ('aria-label' ) or '' ) or norm (field .attrs .get ('placeholder' ) or '' ) or 'Search' ,
1004+ 'showLabel' : bool (labels ), 'buttonPosition' : 'no-button' }
1005+ if field .attrs .get ('placeholder' ):
1006+ attrs ['placeholder' ] = field .attrs ['placeholder' ]
1007+ if submit :
1008+ button = submit [0 ]
1009+ text = norm (plain (button )) if button .tag == 'button' else norm (button .attrs .get ('value' ) or '' )
1010+ attrs .update ({'buttonPosition' : 'button-outside' , 'buttonText' : text or 'Search' })
1011+ if button .tag == 'button' and not text and any (n .tag == 'svg' for n in walk (button )):
1012+ attrs ['buttonUseIcon' ] = True
1013+ self .finding ('search-button' , 'core/search draws its own button (wp-block-search__button): the source button\' s markup and classes "' + (button .attrs .get ('class' ) or '' ) + '" are not carried; review its style' )
1014+ if labels :
1015+ self .finding ('search-label' , 'core/search draws its own label (wp-block-search__label): the source label\' s classes "' + (labels [0 ].attrs .get ('class' ) or '' ) + '" are not carried; review its style' )
1016+ if query :
1017+ attrs ['query' ] = query
1018+ own , extra = self .attrs (node , ('action' , 'method' , 'role' ))
1019+ if extra :
1020+ self .finding ('search-attributes' , 'core/search keeps only the form\' s classes and id: ' + json .dumps (extra , sort_keys = True ))
1021+ parts = {id (n ) for n in box + hidden + submit + labels }
1022+ decoration = [c for c in node .children if (isinstance (c , str ) and c .strip ()) or (isinstance (c , Node ) and id (c ) not in parts )]
1023+ search = {'name' : 'core/search' , 'attributes' : attrs }
1024+ if not decoration :
1025+ search ['attributes' ].update (own )
1026+ return search
1027+ if any (not any (c is n for c in node .children ) for n in box + hidden + submit + labels ):
1028+ # The box sits inside the form's own markup: the search stands for
1029+ # the whole form, and that markup is not kept.
1030+ self .finding ('search-markup' , 'core/search stands for the whole form; the markup around its box is not kept: ' + ', ' .join (sorted ({c .tag for c in decoration if isinstance (c , Node )})))
1031+ search ['attributes' ].update (own )
1032+ return search
1033+ # The box was the form's own item: the search form takes its place and
1034+ # its room (a box that fills the pill fills it still; with no room to
1035+ # spare it keeps its size).
1036+ search ['attributes' ]['className' ] = self .style_class ('flex:1 1 auto;min-width:0' )
1037+ inner = []
1038+ for child in node .children :
1039+ if isinstance (child , Node ) and child is field :
1040+ inner .append (search )
1041+ elif (isinstance (child , str ) and child .strip ()) or (isinstance (child , Node ) and id (child ) not in parts ):
1042+ block = self .block (child )
1043+ if block :
1044+ inner .append (block )
1045+ return {'name' : 'h2wp/element' , 'attributes' : {** own , 'tagName' : 'div' }, 'innerBlocks' : inner }
1046+
9681047 def simple_attrs (self , node , extra = ()):
9691048 """class/id/style only (no findings); None when other attributes exist."""
9701049 if set (node .attrs ) - {'class' , 'id' , 'style' , * extra }:
@@ -1613,6 +1692,9 @@ def block_default(self, node, texts=None):
16131692 self .finding ('unmapped-element' , tag )
16141693 return None
16151694 if tag == 'form' :
1695+ search = self .search_form (node )
1696+ if search :
1697+ return search
16161698 attrs , extra = self .attrs (node , ('action' , 'method' , 'data-spa-success' , 'data-spa-validate' ))
16171699 # The feedback the source app showed on a successful submit
16181700 # (stage -1 records it; see prerender-spa.py), shown by the
0 commit comments