class CSSStyleDeclaration(OrderedDict):
    """Represents a CSS property-value pairs."""

    def __init__(self, style: str = None, parent: 'CSSStyleRule' = None,
                 owner: AbstractNode = None) -> None:
        """Initialize with styles.

        :arg str style: style strings.
        :arg CSSStyleRule parent: css style rule including this decl.
        :arg AbstractNode owner: owner node of this decl.
        """
        self.parentRule = parent
        self._owner = owner
        if style:
            self._parse_str(style)

    def _update_web(self) -> None:
        from wdom.web_node import WdomElement
        if isinstance(self._owner, WdomElement):
            css = self.cssText
            if css:
                self._owner.js_exec('setAttribute', 'style', css)
            else:
                self._owner.js_exec('removeAttribute', 'style')

    def _parse_str(self, style: str) -> None:
        self.clear()
        orig_style = style
        style_str = _style_cleanup_re.sub(r'\1', style.strip())
        if len(style_str) == 0:
            # do nothing, just clear and update
            self._update_web()
            return

        # temporary disable udpating browser for better performance
        self._owner, _owner = None, self._owner
        for decls in style_str.split(';'):
            if ':' not in decls:
                if len(decls) > 0:
                    logger.warning('[skip] unknown style: {}'.format(decls))
                continue
            decl_list = decls.split(':')
            if len(decl_list) != 2:
                raise ValueError(
                    'Invalid style declaration: {0} in {1}'.format(
                        decls, orig_style))
            prop = decl_list[0]
            value = decl_list[1]
            self[prop] = value
        self._owner = _owner
        self._update_web()

    @property
    def cssText(self) -> str:
        """String-representation."""
        text = '; '.join('{0}: {1}'.format(k, v) for k, v in self.items())
        if text:
            text += ';'
        return text

    @property
    def length(self) -> int:
        """Retrun number of included styles."""
        return len(self)

    @property
    def parentRule(self) -> 'CSSStyleRule':
        """Parent CSSStyleRule."""
        return self.__parent

    @parentRule.setter
    def parentRule(self, parent: 'CSSStyleRule') -> None:
        self.__parent = parent

    def getPropertyValue(self, prop: str) -> str:
        """Return value of the css property.

        If the property is not included, return empty string.
        """
        return self.get(prop, '')

    def removeProperty(self, prop: str) -> str:
        """Remove the css property."""
        removed_prop = self.get(prop)
        # removed_prop may be False or '', so need to check it is None
        if removed_prop is not None:
            del self[prop]
        return removed_prop

    def setProperty(self, prop: str, value: str, priority: str = None
                    ) -> None:
        """Set property as the value.

        The third argument ``priority`` is not implemented yet.
        """
        self[prop] = value

    def __getitem__(self, attr: str) -> str:
        return self.get(_normalize_css_property(attr), '')

    def __setitem__(self, attr: str, value: str) -> None:
        super().__setitem__(_normalize_css_property(attr), value)
        self._update_web()

    def __delitem__(self, attr: str) -> None:
        super().__delitem__(_normalize_css_property(attr))
        self._update_web()

    def __getattr__(self, attr: str) -> str:
        if attr.startswith('_') or attr in dir(self):
            return super().__getattr__(attr)  # type: ignore
        return self.get(_normalize_css_property(attr), '')

    def __setattr__(self, attr: str, value: str) -> None:
        if attr.startswith('_') or attr in dir(self):
            super().__setattr__(attr, value)
        else:
            self[_normalize_css_property(attr)] = value

    def __delattr__(self, attr: str) -> None:
        if attr.startswith('_') or attr in dir(self):
            super().__delattr__(attr)
        else:
            self.__delitem__(_normalize_css_property(attr)) class DOMTokenList(MutableSequence[str]):
    """Collection of DOM token strings.

    DOM token is a string which does not contain spases.
    This class is mainly used for class list.
    """

    def __init__(self, owner: Union[Node, Type['HTMLElement']],
                 *args: Union[str, 'DOMTokenList']) -> None:
        """Initialize with owner node (maybe type of node) and initial values.

        :arg owner: Node/Node-class which has this collection.
        :arg args: space-separated string or iterable of tokens.
        """
        self._list = list()  # type: List[str]
        self._owner = owner
        self._append(args)

    def __len__(self) -> int:
        return len(self._list)

    def __contains__(self, item: object) -> bool:
        return item in self._list

    def __iter__(self) -> Iterator[str]:
        for token in self._list:
            yield token

    def _validate_token(self, token: str) -> None:
        if not isinstance(token, str):
            raise TypeError(
                'Token must be str, but {} passed.'.format(type(token)))
        if ' ' in token:
            raise ValueError(
                'Token contains space characters, which are invalid.')

    def _append(self, token: Union[Iterable, str]) -> None:
        if isinstance(token, str):
            for t in token.split(' '):
                self.add(t)
        elif isinstance(token, Iterable):
            for t in token:
                self._append(t)
        elif token is None:
            pass
        else:
            raise TypeError

    def __getitem__(self, index: Union[int, slice]  # type: ignore
                    ) -> Optional[str]:
        if isinstance(index, slice):
            TypeError('slicing is not supported.')
        elif 0 <= index < len(self._list):
            return self._list[index]
        return None

    def __setitem__(self, s, item) -> None:  # type: ignore
        raise NotImplementedError

    def __delitem__(self, index: int) -> None:  # type: ignore
        raise NotImplementedError

    @property
    def length(self) -> int:
        """Get number of DOM token in this list."""
        return self.__len__()

    def add(self, *tokens: str) -> None:
        """Add new tokens to list."""
        from wdom.web_node import WdomElement
        _new_tokens = []
        for token in tokens:
            self._validate_token(token)
            if token and token not in self:
                self._list.append(token)
                _new_tokens.append(token)
        if isinstance(self._owner, WdomElement) and _new_tokens:
            self._owner.js_exec('addClass', _new_tokens)  # type: ignore

    def remove(self, *tokens: str) -> None:
        """Remove tokens from list."""
        from wdom.web_node import WdomElement
        _removed_tokens = []
        for token in tokens:
            self._validate_token(token)
            if token in self:
                self._list.remove(token)
                _removed_tokens.append(token)
        if isinstance(self._owner, WdomElement) and _removed_tokens:
            self._owner.js_exec('removeClass', _removed_tokens)  # type: ignore

    def toggle(self, token: str) -> None:
        """Add or remove token to/from list.

        If token is in this list, the token will be removed. Otherwise add it
        to list.
        """
        self._validate_token(token)
        if token in self:
            self.remove(token)
        else:
            self.add(token)

    def item(self, index: int) -> Optional[str]:
        """Return the token of the ``index``.

        ``index`` must be 0 or positive integer. If index is out of range,
        return None.
        """
        return self[index]

    def insert(self, index: int, item: str) -> None:
        """Not implemented."""
        raise NotImplementedError

    def contains(self, token: str) -> bool:
        """Return if the token is in the list or not."""
        self._validate_token(token)
        return token in self

    def toString(self) -> str:
        """Return string representation of this list.

        Actually it will be a spase-separated tokens.
        """
        return ' '.join(self) class NamedNodeMap(UserDict):
    """Collection of Attr objects."""

    def __init__(self, owner: Node) -> None:
        """Initialize with owner node.

        :arg Node owner: owner node of this object.
        """
        self._owner = owner
        self._dict = OrderedDict()  # type: OrderedDict[str, Attr]

    def __len__(self) -> int:
        return len(self._dict)

    def __contains__(self, item: object) -> bool:
        return item in self._dict

    def __getitem__(self, index: Union[int, str]) -> Optional[Attr]:
        if isinstance(index, int):
            return tuple(self._dict.values())[index]
        return None

    def __setitem__(self, attr: str, item: Attr) -> None:
        self._dict[attr] = item

    def __delitem__(self, attr: str) -> None:
        del self._dict[attr]

    def __iter__(self) -> Iterator[str]:
        for attr in self._dict.keys():
            yield attr

    @property
    def length(self) -> int:
        """Return number of Attrs in this collection."""
        return len(self)

    def getNamedItem(self, name: str) -> Optional[Attr]:
        """Get ``Attr`` object which has ``name``.

        If does not have ``name`` attr, return None.
        """
        return self._dict.get(name, None)

    def setNamedItem(self, item: Attr) -> None:
        """Set ``Attr`` object in this collection."""
        from wdom.web_node import WdomElement
        if not isinstance(item, Attr):
            raise TypeError('item must be an instance of Attr')
        if isinstance(self._owner, WdomElement):
            self._owner.js_exec('setAttribute', item.name,  # type: ignore
                                item.value)
        self._dict[item.name] = item
        item._owner = self._owner

    def removeNamedItem(self, item: Attr) -> Optional[Attr]:
        """Set ``Attr`` object and return it (if exists)."""
        from wdom.web_node import WdomElement
        if not isinstance(item, Attr):
            raise TypeError('item must be an instance of Attr')
        if isinstance(self._owner, WdomElement):
            self._owner.js_exec('removeAttribute', item.name)
        removed_item = self._dict.pop(item.name, None)
        if removed_item:
            removed_item._owner = self._owner
        return removed_item

    def item(self, index: int) -> Optional[Attr]:
        """Return ``index``-th attr node."""
        if 0 <= index < len(self):
            return self._dict[tuple(self._dict.keys())[index]]
        return None

    def toString(self) -> str:
        """Return string representation of collections."""
        return ' '.join(attr.html for attr in self._dict.values()) class Attr:
    """Attribute node.

    In the latest DOM specification, Attr interface does not inherits ``Node``
    interface. (Previously, Attr inherited Node interface.)
    """

    def __init__(self, name: str,
                 value: _AttrValueType = None,
                 owner: Node = None) -> None:
        """Initialize this attribute.

        :arg str name: property name.
        :arg _AttrValueType value: attribute value.
        :arg Node owner: owner node of this attribute (optional).
        """
        self._name = name.lower()
        self._value = value
        self._owner = owner

    @property
    def html(self) -> str:
        """Return string representation of this.

        Used in start tag of HTML representation of the Element node.
        """
        if self._owner and self.name in self._owner._special_attr_boolean:
            return self.name
        else:
            value = self.value
            if isinstance(value, str):
                value = html_.escape(value)
            return '{name}="{value}"'.format(name=self.name, value=value)

    @property
    def name(self) -> str:
        """Name of this attr."""
        return self._name

    @property
    def value(self) -> _AttrValueType:
        """Value of this attr."""
        return self._value or ''

    @value.setter
    def value(self, val: str) -> None:
        self._value = val

    @property
    def isId(self) -> bool:
        """Return True if this Attr is an ID node (name is ``id``)."""
        return self.name.lower() == 'id' class Element(Node, EventTarget, ParentNode, NonDocumentTypeChildNode,
              ChildNode, metaclass=ElementMeta):
    """Element base class."""

    nodeType = Node.ELEMENT_NODE
    nodeValue = None
    _parser_class = ElementParser  # type: Type[ElementParser]
    _element_buffer = WeakSet()  # type: WeakSet[Node]
    _elements_with_id = WeakValueDictionary()  # type: MutableMapping
    _should_escape_text = True
    _special_attr_string = ['id']
    _special_attr_boolean = []  # type: List[str]

    def __init__(self, tag: str='', parent: Node = None,
                 _registered: bool = True, **kwargs: Any) -> None:
        """Initialize.

        :arg str tag: HTML tag of this node.
        :arg Node parent: Parent node of this node.
        :arg bool _registered: Is registered to CustomElementRegistry.
        :arg kwargs: key-value pair of attributes.
        """
        super().__init__(parent=parent)
        self._registered = _registered
        self.tag = tag
        self._element_buffer.add(self)  # used to suport custom elements
        self.attributes = NamedNodeMap(self)
        self.classList = DOMTokenList(self)

        if 'class_' in kwargs:
            kwargs['class'] = kwargs.pop('class_')
        if 'is_' in kwargs:
            kwargs['is'] = kwargs.pop('is_')
        for k, v in kwargs.items():
            self.setAttribute(k, v)

    def _clone_node(self) -> 'Element':
        clone = type(self)(self.tag)
        for attr in self.attributes:
            clone.setAttribute(attr, self.getAttribute(attr))
        # TODO: should clone event listeners???
        return clone

    def _get_attrs_by_string(self) -> str:
        # attrs = ' '.join(attr.html for attr in self.attributes.values())
        attrs = self.attributes.toString()
        classes = self.getAttribute('class')
        if classes:
            attrs = ' '.join((attrs.strip(), 'class="{}"'.format(classes)))
        return attrs.strip()

    @property
    def start_tag(self) -> str:
        """Return HTML start tag."""
        tag = '<' + self.tag
        attrs = self._get_attrs_by_string()
        if attrs:
            tag = ' '.join((tag, attrs))
        return tag + '>'

    @property
    def end_tag(self) -> str:
        """Return HTML end tag."""
        return '</{}>'.format(self.tag)

    def _parse_html(self, html: str) -> DocumentFragment:
        parser = self._parser_class()
        parser.feed(html)
        return parser.root

    def _get_inner_html(self) -> str:
        return ''.join(child.html for child in self.childNodes)

    def _set_inner_html(self, html: str) -> None:
        self._empty()
        self._append_child(self._parse_html(html))

    @property
    def innerHTML(self) -> str:
        """Return HTML representation of child nodes."""
        return self._get_inner_html()

    @innerHTML.setter
    def innerHTML(self, html: str) -> None:
        """Remove all child nodes and set ``html`` as new contents.

        ``html`` is parsed to ``Element`` nodes and set as child nodes.
        """
        self._set_inner_html(html)

    @property
    def html(self) -> str:
        """Return HTML representation of this node."""
        return self.start_tag + self.innerHTML + self.end_tag

    def insertAdjacentHTML(self, position: str, html: str) -> None:
        """Parse ``html`` to DOM and insert to ``position``.

        ``position`` is a case-insensive string, and must be one of
        "beforeBegin", "afterBegin", "beforeEnd", or "afterEnd".
        """
        df = self._parse_html(html)
        pos = position.lower()
        if pos == 'beforebegin':
            self.before(df)
        elif pos == 'afterbegin':
            self.prepend(df)
        elif pos == 'beforeend':
            self.append(df)
        elif pos == 'afterend':
            self.after(df)
        else:
            raise ValueError(
                'The value provided ({}) is not one of "beforeBegin", '
                '"afterBegin", "beforeEnd", or "afterEnd".'.format(position)
            )

    @property
    def outerHTML(self) -> str:
        """Return html representation of this node.

        Equivalent to ``self.html``.
        """
        return self.html

    @property
    def nodeName(self) -> str:  # type: ignore
        """Return tag name (capital case)."""
        return self.tag.upper()

    @property
    def tagName(self) -> str:
        """Return tag name (capital case)."""
        return self.tag.upper()

    @property
    def localName(self) -> str:
        """Return tag name (lower case)."""
        return self.tag.lower()

    @property
    def className(self) -> str:
        """Get/Set class name as/by string."""
        return self.getAttribute('class') or ''  # type: ignore

    @className.setter
    def className(self, new_class: str) -> None:
        if not isinstance(new_class, str):
            raise TypeError('className must be str.')
        self.setAttribute('class', new_class)

    def getAttribute(self, attr: str) -> _AttrValueType:
        """Get attribute of this node as string format.

        If this node does not have ``attr``, return None.
        """
        if attr == 'class':
            if self.classList:
                return self.classList.toString()
            return None
        attr_node = self.getAttributeNode(attr)
        if attr_node is None:
            return None
        return attr_node.value

    def getAttributeNode(self, attr: str) -> Optional[Attr]:
        """Get attribute of this node as Attr format.

        If this node does not have ``attr``, return None.
        """
        return self.attributes.getNamedItem(attr)

    def hasAttribute(self, attr: str) -> bool:
        """Return True if this node has ``attr``."""
        if attr == 'class':
            return bool(self.classList)
        return attr in self.attributes

    def hasAttributes(self) -> bool:
        """Return True if this node has any attributes."""
        return bool(self.attributes) or bool(self.classList)

    def _set_attribute_class(self, value: _AttrValueType) -> None:
        if isinstance(value, str):
            self.classList = DOMTokenList(self, value)
        elif isinstance(value, Iterable):
            self.classList = DOMTokenList(self, *value)
        else:
            raise TypeError(
                'class attribute must be str, '
                'but got {}'.format(type(value))
            )

    def _change_id(self, value: _AttrValueType) -> None:
        if 'id' in self.attributes:
            # remove old reference to self
            self._elements_with_id.pop(self.id, None)
        # register this elements with new id
        if isinstance(value, (int, str)):
            self._elements_with_id[value] = self
        else:
            raise TypeError(
                'id attribute must be int or integer-string.'
            )

    def _set_attribute(self, attr: str, value: _AttrValueType) -> None:
        if attr == 'class':
            self._set_attribute_class(value)
        else:
            if attr == 'id':
                self._change_id(value)
            if not attr == 'draggable':
                attr_cls = Attr
            else:
                attr_cls = DraggableAttr
            new_attr_node = attr_cls(attr, value)
            self.setAttributeNode(new_attr_node)

    def setAttribute(self, attr: str, value: _AttrValueType) -> None:
        """Set ``attr`` and ``value`` in this node."""
        self._set_attribute(attr, value)

    def setAttributeNode(self, attr: Attr) -> None:
        """Set ``Attr`` node as this node's attribute."""
        self.attributes.setNamedItem(attr)

    def _remove_attribute(self, attr: str) -> None:
        if attr == 'class':
            self.classList = DOMTokenList(self)
        else:
            if attr == 'id':
                self._elements_with_id.pop(self.id, None)
            _attr = self.getAttributeNode(attr)
            if _attr:
                self.attributes.removeNamedItem(_attr)

    def removeAttribute(self, attr: str) -> None:
        """Remove ``attr`` from this node."""
        self._remove_attribute(attr)

    def removeAttributeNode(self, attr: Attr) -> Optional[Attr]:
        """Remove ``Attr`` node from this node."""
        return self.attributes.removeNamedItem(attr)

    def getElementsBy(self, cond: Callable[['Element'], bool]) -> NodeList:
        """Get elements under this node which matches condition."""
        return getElementsBy(self, cond)

    def getElementsByTagName(self, tag: str) -> NodeList:
        """Get elements with tag name under this node."""
        return getElementsByTagName(self, tag)

    def getElementsByClassName(self, class_name: str) -> NodeList:
        """Get elements with class name under this node."""
        return getElementsByClassName(self, class_name)

    def querySelector(self, selectors: str) -> Node:
        """Not Implemented."""
        return querySelector(self, selectors)

    def querySelectorAll(self, selectors: str) -> NodeList:
        """Not Implemented."""
        return querySelectorAll(self, selectors) class HTMLElement(Element):
    """Base class for HTMLElement.

    This class extends `Element` class with some HTML specific features.
    """

    _special_attr_string = ['title', 'type']
    _special_attr_boolean = ['hidden']
    _parser_class = HTMLElementParser  # type: Type[ElementParser]

    def __init__(self, *args: Any, style: str=None, **kwargs: Any) -> None:
        super().__init__(*args, **kwargs)
        self.__style = CSSStyleDeclaration(style, owner=self)

    def _get_attrs_by_string(self) -> str:
        attrs = super()._get_attrs_by_string()
        style = self.getAttribute('style')
        if style:
            attrs += ' style="{}"'.format(style)
        return attrs.strip()

    def _clone_node(self) -> 'HTMLElement':
        clone = super()._clone_node()
        clone.style.update(self.style)
        return clone

    @property
    def end_tag(self) -> str:
        """Retrun html end tag.

        If tag is empty tag like <img> or <br>, return empty string.
        """
        if self.tag in HTML_EMPTY:
            return ''
        return super().end_tag

    @property
    def style(self) -> CSSStyleDeclaration:
        """Return style attribute of this node."""
        return self.__style

    @style.setter
    def style(self, style: _AttrValueType) -> None:
        """Set style attribute of this node.

        If argument ``style`` is string, it will be parsed to
        ``CSSStyleDeclaration``.
        """
        if isinstance(style, str):
            self.__style._parse_str(style)
        elif style is None:
            self.__style._parse_str('')
        elif isinstance(style, CSSStyleDeclaration):
            self.__style._owner = None
            if style._owner is not None:
                new_style = CSSStyleDeclaration(owner=self)
                new_style.update(style)
                self.__style = new_style
            else:
                # always making new decl may be better
                style._owner = self
                self.__style = style
        else:
            raise TypeError('Invalid type for style: {}'.format(type(style)))

    @property
    def draggable(self) -> Union[bool, str]:
        """Get ``draggable`` property."""
        if not self.hasAttribute('draggable'):
            return False
        return self.getAttribute('draggable')  # type: ignore

    @draggable.setter
    def draggable(self, value: Union[bool, str]) -> None:
        """Set ``draggable`` property.

        ``value`` is boolean or string.
        """
        if value is False:
            self.removeAttribute('draggable')
        else:
            self.setAttribute('draggable', value)

    def getAttribute(self, attr: str) -> _AttrValueType:  # noqa: D102
        if attr == 'style':
            # if style is neither None nor empty, return None
            # otherwise, return style.cssText
            if self.style:
                return self.style.cssText
            return None
        return super().getAttribute(attr)

    def _set_attribute(self, attr: str, value: _AttrValueType) -> None:
        if attr == 'style':
            self.style = value  # type: ignore
        else:
            super()._set_attribute(attr, value)  # type: ignore

    def _remove_attribute(self, attr: str) -> None:
        if attr == 'style':
            self.style = None  # type: ignore
        else:
            super()._remove_attribute(attr)  # type: ignore class HTMLSelectElement(HTMLElement, FormControlMixin):
    """HTMLSelectElement class (<select></select> tag)."""

    _special_attr_string = ['name', 'size', 'value']
    _special_attr_boolean = ['disabled', 'multiple', 'required']

    def __init__(self, *args: Any, **kwargs: Any) -> None:
        self._selected_options = []  # type: List[str]
        super().__init__(*args, **kwargs)

    def on_event_pre(self, e: Event) -> None:
        """Set values set on browser before calling event listeners."""
        super().on_event_pre(e)
        ct_msg = e.init.get('currentTarget', dict())
        if e.type in ('input', 'change'):
            self._set_attribute('value', ct_msg.get('value'))
            _selected = ct_msg.get('selectedOptions', [])
            self._selected_options.clear()
            for opt in self.options:
                if opt.wdom_id in _selected:
                    self._selected_options.append(opt)
                    opt._set_attribute('selected', True)
                else:
                    opt._remove_attribute('selected')

    @property
    def length(self) -> int:
        """Return number of options in this node."""
        return len(self.options)

    @property
    def options(self) -> NodeList:
        """Return all option nodes in this node."""
        return self.getElementsByTagName('option')

    @property
    def selectedOptions(self) -> NodeList:
        """Return all selected option nodes."""
        return NodeList(list(opt for opt in self.options if opt.selected)) class HTMLOptionElement(HTMLElement, FormControlMixin):  # noqa: D204
    """HTMLOptionElement class (<option></option> tag)."""
    _special_attr_string = ['label', 'value']
    _special_attr_boolean = ['defaultSelected', 'disabled', 'selected'] def getElementsBy(start_node: ParentNode,
                  cond: Callable[['Element'], bool]) -> NodeList:
    """Return list of child elements of start_node which matches ``cond``.

    ``cond`` must be a function which gets a single argument ``Element``,
    and returns boolean. If the node matches requested condition, ``cond``
    should return True.
    This searches all child elements recursively.

    :arg ParentNode start_node:
    :arg cond: Callable[[Element], bool]
    :rtype: NodeList[Element]
    """
    elements = []
    for child in start_node.children:
        if cond(child):
            elements.append(child)
        elements.extend(child.getElementsBy(cond))
    return NodeList(elements) def getElementsByClassName(start_node: ParentNode, class_name: str
                           ) -> NodeList:
    """Get child nodes which has ``class_name`` class attribute."""
    classes = set(class_name.split(' '))
    return getElementsBy(
        start_node,
        lambda node: classes.issubset(set(node.classList))
    ) def getElementsByTagName(start_node: ParentNode, tag: str) -> NodeList:
    """Get child nodes which tag name is ``tag``."""
    _tag = tag.upper()
    return getElementsBy(start_node, lambda node: node.tagName == _tag) def querySelector(start_node: ParentNode, selectors: str) -> AbstractNode:
    """Not Implemented yet.

    Please use `getElementsBy` method instead.
    """
    raise NotImplementedError def querySelectorAll(start_node: ParentNode, selectors: str) -> NodeList:
    """Not Implemented yet.

    Please use `getElementsBy` method instead.
    """
    raise NotImplementedError class Text(CharacterData):
    """Node class to wrap text contents."""

    nodeType = Node.TEXT_NODE
    nodeName = '#text'

    @property
    def html(self) -> str:
        """Return html-escaped string representation of this node."""
        if self.parentNode and self.parentNode._should_escape_text:
            return html.escape(self.data)
        return self.data @property
def customElements(self) -> CustomElementsRegistry:
    """Return customElementsRegistry object."""
    return self._custom_elements class TestCase(unittest.TestCase):
    """Base class for testing wdom modules.

    This class is a sub class of the ``unittest.TestCase``. After all test
    methods, reset wdom's global objects like document, application, and
    elements. If you use ``tearDown`` method, do not forget to call
    ``super().tearDown()``.
    """

    def setUp(self) -> None:
        """Reset WDOM states."""
        super().setUp()
        reset()

    def tearDown(self) -> None:
        """Reset WDOM states."""
        reset()
        super().tearDown()

    def assertIsTrue(self, bl: bool) -> None:
        """Check arg is exactly True, not truthy."""
        self.assertIs(bl, True)

    def assertIsFalse(self, bl: bool) -> None:
        """Check arg is exactly False, not falsy."""
        self.assertIs(bl, False)
import sys
import gc
from unittest import skipIf
from wdom.css import CSSStyleDeclaration
from wdom.element import (
    DOMTokenList, NamedNodeMap, Attr, Element, HTMLElement,
    HTMLSelectElement, HTMLOptionElement,
    getElementsBy, getElementsByClassName, getElementsByTagName,
    querySelector, querySelectorAll,
)
from wdom.node import Text
from wdom.window import customElements
from .base import TestCase
        self.assertEqual(self.map.length, 1)
        self.assertEqual(self.map.getNamedItem('src').value, 'a')
        self.assertIsNone(self.map.getNamedItem('aaa'))
        self.map.removeNamedItem(Attr('aaa'))
        self.assertEqual(self.map.length, 1)
        self.map.removeNamedItem(Attr('src'))
        self.assertEqual(self.map.length, 0)

    def test_item(self):
        self.map.setNamedItem(self.attr)
        self.assertIsNone(self.map.item(1))
        self.assertIs(self.map.item(0), self.attr)


class TestElementMeta(TestCase):
    def setUp(self):
        class NewTag(Element):
            _special_attr_string = ['a', 'b']
            _special_attr_boolean = ['c', 'd']
        self.tag = NewTag('a')

    def check_noattr(self, attr):
        self.assertNotIn(attr, self.tag.attributes)
        self.assertFalse(self.tag.hasAttribute(attr))
        self.assertIsNone(self.tag.getAttribute(attr))

    def check_hasattr(self, attr):
        self.assertIn(attr, self.tag.attributes)
        self.assertTrue(self.tag.hasAttribute(attr))

    def test_special_attr_str(self):
        for attr in ['a', 'b', 'c', 'd', 'e']:
            self.check_noattr(attr)
        self.assertEqual(self.tag.a, '')
        self.assertEqual(self.tag.b, '')
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_str_setter(self):
        self.tag.a = 'a'
        self.check_hasattr('a')
        self.assertEqual(self.tag.a, 'a')
        self.assertEqual(self.tag.getAttribute('a'), 'a')
        self.assertEqual(self.tag.html, '<a a="a"></a>')

        self.tag.removeAttribute('a')
        self.check_noattr('a')
        self.assertEqual(self.tag.a, '')
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_str_setattr(self):
        self.tag.setAttribute('b', 'b')
        self.check_hasattr('b')
        self.assertEqual(self.tag.b, 'b')
        self.assertEqual(self.tag.getAttribute('b'), 'b')
        self.assertEqual(self.tag.html, '<a b="b"></a>')

        del self.tag.b
        self.check_noattr('b')
        self.assertEqual(self.tag.b, '')
        self.assertEqual(self.tag.html, '<a></a>')
        # delete again, but not raise error
        del self.tag.b

    def test_special_attr_bool(self):
        for attr in ['a', 'b', 'c', 'd', 'e']:
            self.check_noattr(attr)
        self.assertFalse(self.tag.a)
        self.assertFalse(self.tag.b)
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_bool_setter(self):
        self.tag.c = True
        self.check_hasattr('c')
        self.assertTrue(self.tag.c)
        self.assertEqual(self.tag.html, '<a c></a>')

        self.tag.c = False
        self.check_noattr('c')
        self.assertFalse(self.tag.c)
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_bool_setattr(self):
        self.tag.setAttribute('d', 'd')
        self.check_hasattr('d')
        self.assertTrue(self.tag.d)
        self.assertEqual(self.tag.html, '<a d></a>')

        self.tag.removeAttribute('d')
        self.check_noattr('d')
        self.assertFalse(self.tag.d)
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_bool_setattr_empty(self):
        # In this case, 'd' still exists and True (compatible with JS...)
        self.tag.setAttribute('d', '')
        self.check_hasattr('d')
        self.assertEqual(self.tag.html, '<a d></a>')

        # Now remove 'd'
        self.tag.d = ''
        self.check_noattr('d')
        self.assertFalse(self.tag.d)
        self.assertEqual(self.tag.html, '<a></a>')

    def test_special_attr_bool_setattr_false(self):
        # In this case, 'd' still exists and True (compatible with JS...)
        self.tag.setAttribute('d', False)
        self.check_hasattr('d')
        self.assertEqual(self.tag.html, '<a d></a>')

        # Now remove 'd'
        del self.tag.d
        self.check_noattr('d')
        self.assertFalse(self.tag.d)
        self.assertEqual(self.tag.html, '<a></a>')


class TestGetElements(TestCase):
    def setUp(self):
        self.elm = Element('tag')

        self.c1 = Element('c1')