├── .github └── workflows │ ├── pytest.yaml │ └── shiny.yaml ├── .gitignore ├── .here ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.md ├── htmltools ├── __init__.py ├── _core.py ├── _jsx.py ├── _util.py ├── _versions.py ├── lib │ ├── react-dom │ │ └── react-dom.production.min.js │ └── react │ │ └── react.production.min.js ├── libtest │ ├── dep2 │ │ ├── td2.css │ │ └── td2.js │ └── testdep │ │ ├── testdep.css │ │ └── testdep.js ├── py.typed ├── svg.py └── tags.py ├── pyproject.toml ├── pyrightconfig.json ├── scripts ├── generate_tags.py └── getdeps.R └── tests ├── __init__.py ├── __snapshots__ └── test_deps.ambr ├── assets └── testdep-nested │ ├── css │ └── my-styles.css │ └── js │ └── my-js.js ├── test_consolidate_attrs.py ├── test_deps.py ├── test_html_document.py ├── test_is.py ├── test_jsx_tags.py ├── test_tags.py ├── test_tags_context.py └── test_util.py /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | release: 9 | types: [published] 10 | 11 | jobs: 12 | build: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | python-version: ["3.12", "3.11", "3.10", "3.9"] 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | fail-fast: false 19 | defaults: 20 | run: 21 | shell: bash 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v5 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python -m pip install --upgrade pip 32 | pip install -e ".[dev,test]" 33 | - name: Install 34 | run: | 35 | make install 36 | - name: Run unit tests 37 | run: | 38 | make test 39 | - name: pyright, flake8, black and isort 40 | run: | 41 | make check 42 | deploy: 43 | name: "Deploy to PyPI" 44 | runs-on: ubuntu-latest 45 | if: github.event_name == 'release' 46 | needs: [build] 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: "Set up Python 3.10" 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: "3.10" 53 | - name: Install dependencies 54 | run: | 55 | python -m pip install --upgrade pip 56 | pip install -e ".[dev,test]" 57 | - name: "Build Package" 58 | run: | 59 | make dist 60 | 61 | # test deploy ---- 62 | - name: "Test Deploy to PyPI" 63 | uses: pypa/gh-action-pypi-publish@release/v1 64 | if: startsWith(github.event.release.name, 'TEST') 65 | with: 66 | user: __token__ 67 | password: ${{ secrets.PYPI_TEST_API_TOKEN }} 68 | repository_url: https://test.pypi.org/legacy/ 69 | 70 | ## prod deploy ---- 71 | - name: "Deploy to PyPI" 72 | uses: pypa/gh-action-pypi-publish@release/v1 73 | if: startsWith(github.event.release.name, 'htmltools') 74 | with: 75 | user: __token__ 76 | password: ${{ secrets.PYPI_API_TOKEN }} 77 | -------------------------------------------------------------------------------- /.github/workflows/shiny.yaml: -------------------------------------------------------------------------------- 1 | name: Bleeding Edge Shiny 2 | 3 | on: 4 | push: 5 | branches: "shiny-**" 6 | pull_request: 7 | 8 | jobs: 9 | htmltools-pr: 10 | runs-on: ubuntu-latest 11 | defaults: 12 | run: 13 | shell: bash 14 | 15 | steps: 16 | - name: Checkout py-shiny@main 17 | uses: actions/checkout@v4 18 | with: 19 | repository: posit-dev/py-shiny 20 | ref: main 21 | fetch-depth: 0 # Required for shiny version 22 | - name: Setup py-shiny@main 23 | uses: posit-dev/py-shiny/.github/py-shiny/setup@main 24 | with: 25 | python-version: "3.12" 26 | 27 | - name: Checkout dev branch of py-htmltools 28 | uses: actions/checkout@v4 29 | with: 30 | path: _dev/htmltools 31 | 32 | - name: Install dev py-htmltools htmltools dependencies 33 | run: | 34 | cd _dev/htmltools 35 | pip uninstall -y htmltools 36 | pip install -e ".[dev,test]" 37 | make install 38 | 39 | - name: Check py-shiny@main 40 | uses: posit-dev/py-shiny/.github/py-shiny/check@main 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | 58 | # Flask stuff: 59 | instance/ 60 | .webassets-cache 61 | 62 | # Scrapy stuff: 63 | .scrapy 64 | 65 | # Sphinx documentation 66 | docs/_build/ 67 | 68 | # PyBuilder 69 | target/ 70 | 71 | # Jupyter Notebook 72 | .ipynb_checkpoints 73 | 74 | # pyenv 75 | .python-version 76 | 77 | # celery beat schedule file 78 | celerybeat-schedule 79 | 80 | # SageMath parsed files 81 | *.sage.py 82 | 83 | # dotenv 84 | .env 85 | 86 | # virtualenv 87 | .venv 88 | venv/ 89 | ENV/ 90 | 91 | # Spyder project settings 92 | .spyderproject 93 | .spyproject 94 | 95 | # Rope project settings 96 | .ropeproject 97 | 98 | # mkdocs documentation 99 | /site 100 | 101 | # mypy 102 | .mypy_cache/ 103 | 104 | typings/ 105 | -------------------------------------------------------------------------------- /.here: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/posit-dev/py-htmltools/ddbb4993672489c3ed62bf80867ebff20a42162c/.here -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black 3 | rev: "24.2.0" 4 | hooks: 5 | - id: black 6 | language_version: python3 7 | exclude: ^tests/snapshots/ 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.trimTrailingWhitespace": true, 3 | "files.insertFinalNewline": true, 4 | "python.formatting.provider": "black", 5 | "python.linting.flake8Enabled": true, 6 | "editor.tabSize": 2, 7 | "files.encoding": "utf8", 8 | "files.eol": "\n", 9 | "[python]": { 10 | "editor.formatOnSave": true, 11 | "editor.tabSize": 4, 12 | "editor.codeActionsOnSave": { 13 | "source.organizeImports": "explicit" 14 | }, 15 | }, 16 | "isort.args":["--profile", "black"], 17 | "editor.rulers": [ 18 | 88 19 | ], 20 | "files.exclude": { 21 | "**/__pycache__": true, 22 | "build/**": true 23 | }, 24 | "autoDocstring.guessTypes": false, 25 | "search.exclude": { 26 | "build/**": true 27 | }, 28 | } 29 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log for htmltools (for Python) 2 | 3 | All notable changes to htmltools for Python will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.6.0] 2024-10-29 9 | 10 | ### Breaking changes 11 | 12 | * `HTML` no longer inherits from `str`. It now inherits from `collections.UserString`. This was done to avoid confusion between `str` and `HTML` objects. (#86) 13 | 14 | * `TagList` no longer inherits from `list`. It now inherits from `collections.UserList`. This was done to avoid confusion between `list` and `TagList` objects. (#97) 15 | 16 | * `Tag` and `TagList`'s method `.get_html_string()` now both return `str` instead of `HTML`. (#86) 17 | 18 | * Strings added to `HTML` objects, now return `HTML` objects. E.g. `HTML_value + str_value` and `str_value + HTML_value` both return `HTML` objects. To maintain a `str` result, call `str()` on your `HTML` objects before adding them to other strings values. (#86) 19 | 20 | * Items added to `TagList` objects, now return `TagList` objects. E.g. `TagList_value + arr_value` and `arr_value + TagList_value` both return new `TagList` objects. To maintain a `list` result, call `list()` on your `TagList` objects before combining them to other list objects. (#97) 21 | 22 | ### New features 23 | 24 | * Exported `ReprHtml` protocol class. If an object has a `_repr_html_` method, then it is of instance `ReprHtml`. (#86) 25 | 26 | * Exported `is_tag_node()` and `is_tag_child()` functions that utilize `typing.TypeIs` to narrow `TagNode` and `TagChild` type variables, respectively. (#86) 27 | 28 | * Exported `consolidate_attrs(*args, **kwargs)` function. This function will combine the `TagAttrs` (supplied in `*args`) with `TagAttrValues` (supplied in `**kwargs`) into a single `TagAttrs` object. In addition, it will also return all `*args` that are not dictionary as a list of unaltered `TagChild` objects. (#86) 29 | 30 | * The `Tag` method `.add_style(style=)` added support for `HTML` objects in addition to `str` values. (#86) 31 | 32 | ### Bug fixes 33 | 34 | * Fixed an issue with `HTMLTextDocument()` returning extracted `HTMLDependency()`s in a non-determistic order. (#95) 35 | 36 | ## [0.5.3] 2024-07-18 37 | 38 | * HTML tags in docstrings are now escaped. (#90) 39 | 40 | ## [0.5.2] 2024-05-23 41 | 42 | * The `HTMLDependency.copy()` method can now correctly copy folders in depenendencies that both include directories and have `all_files=True`. (#87) 43 | 44 | ## [0.5.1] 2023-12-18 45 | 46 | * `Tag` objects can now be used as context managers, as in `with tags.div():`. When used this way, then inside the `with` block, `sys.displayhook` is replaced with a function which adds items as children to the `Tag`. This is meant to be used with Shiny Express, Quarto, or Jupyter. (#76) 47 | 48 | * Added a function `wrap_displayhook_handler()`. This alliows displayhooks to delegate their logic for handling various kinds of objects (like `Tag` objects and objects with a `_repr_html()`) to this function. (#77) 49 | 50 | 51 | ## [0.5.0] 2023-12-07 52 | 53 | * Objects with a `_repr_html_` method can now appear as children of `Tag`/`TagList` objects. (#74) 54 | 55 | * Changed the type annotation of `_add_ws` from `bool` to `TagAttrValue`. This makes it easier to write functions which call `Tag` functions and pass along `**kwargs`. (#67) 56 | 57 | * Changed the type annotation of `collapse_` from `str` to `str | float | None`. This makes it easier to write calls to `css()` pass along `**kwargs`. (#68) 58 | 59 | * Enhanced the type definition of `TagAttrs` to include `TagAttrDict`, the type of a `Tag`'s `attrs` property. (#55) 60 | 61 | * For `HTMLTextDocument` objects, deduplicate HTML dependencies. (#72) 62 | 63 | * Switched from `setup.cfg` and `setup.py` to `pyproject.toml`. (#73) 64 | 65 | 66 | ## [0.4.1] 2023-10-30 67 | 68 | * Fixed deserialization of JSON HTML dependencies when they contained newline characters. (#65) 69 | 70 | 71 | ## [0.4.0] 2023-10-30 72 | 73 | * Added `HTMLTextDocument` class, which takes as input a string representation of an HTML document. (#61) 74 | 75 | * Added `htmltools.html_dependency_render_mode`. If this is set to `"json"`, then `HTMLDependency` objects will be rendered as JSON inside of ` 324 | """ 325 | 326 | def create_tag( 327 | *args: TagNode, 328 | **kwargs: TagAttrValue, 329 | ) -> JSXTag: 330 | return JSXTag(name, *args, allowedProps=allowedProps, **kwargs) 331 | 332 | create_tag.__name__ = name 333 | 334 | return create_tag 335 | 336 | 337 | # -------------------------------------------------------- 338 | # jsx expressions 339 | # -------------------------------------------------------- 340 | class jsx(str): 341 | """ 342 | Mark a string as a JSX expression. 343 | 344 | Example 345 | ------- 346 | >>> Foo = JSXTag("Foo") 347 | >>> Foo(prop = "A string", jsxProp = jsx("() => console.log('here')")) 348 | 358 | """ 359 | 360 | def __new__(cls, *args: str) -> "jsx": 361 | return super().__new__( # pyright: ignore[reportGeneralTypeIssues] 362 | cls, "\n".join(args) 363 | ) 364 | 365 | # jsx() + jsx() should return jsx() 366 | def __add__(self, other: "str | jsx") -> str: 367 | res = str.__add__(self, other) 368 | return jsx(res) if isinstance(other, jsx) else res 369 | 370 | 371 | def _lib_dependency(pkg: str, script: ScriptItem) -> HTMLDependency: 372 | return HTMLDependency( 373 | name=pkg, 374 | version=versions[pkg], 375 | source={"package": "htmltools", "subdir": "lib/" + pkg}, 376 | script=script, 377 | ) 378 | -------------------------------------------------------------------------------- /htmltools/_util.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import hashlib 4 | import importlib 5 | import os 6 | import re 7 | import tempfile 8 | from contextlib import closing 9 | from http.server import SimpleHTTPRequestHandler 10 | from socket import socket 11 | from socketserver import TCPServer 12 | from threading import Thread 13 | from typing import Any, Hashable, Iterable, NamedTuple, Optional, TypeVar, Union 14 | 15 | T = TypeVar("T") 16 | 17 | HashableT = TypeVar("HashableT", bound=Hashable) 18 | 19 | __all__ = ( 20 | "css", 21 | "html_escape", 22 | ) 23 | 24 | 25 | def css( 26 | collapse_: str | float | None = "", 27 | **kwargs: str | float | None, 28 | ) -> Optional[str]: 29 | """ 30 | CSS string helper 31 | 32 | Convenience function for building CSS style declarations (i.e. the string that goes 33 | into a style attribute, or the parts that go inside curly braces in a full 34 | stylesheet). 35 | 36 | Parameters 37 | ---------- 38 | collapse_ 39 | String to use to collapse properties into a single string; likely ``""`` (the 40 | default) for style attributes, and either ``"\n"`` or ``None`` for style blocks. 41 | **kwargs 42 | Named style properties, where the name is the property name and the argument is 43 | the property value. 44 | 45 | Returns 46 | ------- 47 | : 48 | A string of CSS style declarations, or ``None`` if no properties were given. 49 | 50 | Example 51 | ------- 52 | >>> from htmltools import css 53 | >>> css(font_size = "12px", backgroundColor = "red") 54 | 'font-size:12px;background-color:red;' 55 | 56 | Note 57 | ---- 58 | CSS uses '-' (minus) as a separator character in property names, which isn't allowed 59 | in Python's keyword arguments. This function allows you to use '_' (underscore) as a 60 | separator and/or camelCase notation instead. 61 | """ 62 | # Note that _add_ws is marked as a TagAttrValue for the sake of static type 63 | # checking, but it must in fact be a bool. This is due to limitations in 64 | # Python's type system when passing along **kwargs. 65 | # Similar to https://github.com/posit-dev/py-htmltools/pull/67 66 | if not isinstance(collapse_, str): 67 | raise TypeError("`collapse_` must be of type `str`") 68 | 69 | res = "" 70 | for k, v in kwargs.items(): 71 | if v is None: 72 | continue 73 | v = " ".join(v) if isinstance(v, list) else str(v) 74 | k = re.sub("_", "-", re.sub("([A-Z])", "-\\1", k).lower()) 75 | res += k + ":" + v + ";" + collapse_ 76 | return None if res == "" else res 77 | 78 | 79 | # Flatten a arbitrarily nested list and remove None. Does not alter input object. 80 | def flatten(x: Iterable[Union[T, None]]) -> list[T]: 81 | result: list[T] = [] 82 | _flatten_recurse(x, result) 83 | return result 84 | 85 | 86 | # Having this separate function and passing along `result` is faster than defining 87 | # a closure inside of `flatten()` (and not passing `result`). 88 | def _flatten_recurse(x: Iterable[T | None], result: list[T]) -> None: 89 | from ._core import TagList 90 | 91 | for item in x: 92 | if isinstance(item, (list, tuple, TagList)): 93 | # Don't yet know how to specify recursive generic types, so we'll tell 94 | # the type checker to ignore this line. 95 | _flatten_recurse( 96 | item, # pyright: ignore[reportUnknownArgumentType] 97 | result, # pyright: ignore[reportArgumentType] 98 | ) 99 | elif item is not None: 100 | result.append(item) 101 | 102 | 103 | # similar to unique() in R (set() doesn't preserve order) 104 | def unique(x: list[HashableT]) -> list[HashableT]: 105 | # This implementation requires Python 3.7+. Starting with that version, dict 106 | # order is guaranteed to be the same as insertion order. 107 | return list(dict.fromkeys(x)) 108 | 109 | 110 | HTML_ESCAPE_TABLE = { 111 | "&": "&", 112 | ">": ">", 113 | "<": "<", 114 | } 115 | 116 | HTML_ATTRS_ESCAPE_TABLE = { 117 | **HTML_ESCAPE_TABLE, 118 | '"': """, 119 | "'": "'", 120 | "\r": " ", 121 | "\n": " ", 122 | } 123 | 124 | 125 | def html_escape(text: str, attr: bool = False) -> str: 126 | table = HTML_ATTRS_ESCAPE_TABLE if attr else HTML_ESCAPE_TABLE 127 | if not re.search("|".join(table), text): 128 | return text 129 | for key, value in table.items(): 130 | text = text.replace(key, value) 131 | return text 132 | 133 | 134 | # Backwards compatibility with faicons 0.2.1 135 | _html_escape = html_escape 136 | 137 | 138 | # similar to base::system.file() 139 | def package_dir(package: str) -> str: 140 | with tempfile.TemporaryDirectory(): 141 | pkg_file = importlib.import_module(".", package=package).__file__ 142 | if pkg_file is None: 143 | raise ImportError(f"Couldn't load package {package}") 144 | return os.path.dirname(pkg_file) 145 | 146 | 147 | # Backwards compatibility with shinywidgets 0.1.4 148 | _package_dir = package_dir 149 | 150 | 151 | def hash_deterministic(s: str) -> str: 152 | """ 153 | Returns a deterministic hash of the given string. 154 | """ 155 | return hashlib.sha1(s.encode("utf-8")).hexdigest() 156 | 157 | 158 | class _HttpServerInfo(NamedTuple): 159 | port: int 160 | thread: Thread 161 | 162 | 163 | _http_servers: dict[str, _HttpServerInfo] = {} 164 | 165 | 166 | def ensure_http_server(path: str) -> int: 167 | server = _http_servers.get(path) 168 | if server: 169 | return server.port 170 | 171 | _http_servers[path] = start_http_server(path) 172 | return _http_servers[path].port 173 | 174 | 175 | def start_http_server(path: str) -> _HttpServerInfo: 176 | port: int = get_open_port() 177 | th: Thread = Thread(target=http_server, args=(port, path), daemon=True) 178 | th.start() 179 | return _HttpServerInfo(port=port, thread=th) 180 | 181 | 182 | def http_server(port: int, path: str): 183 | class Handler(SimpleHTTPRequestHandler): 184 | def __init__(self, *args: Any, **kwargs: Any): 185 | super().__init__(*args, directory=path, **kwargs) 186 | 187 | def log_message( 188 | self, format, *args # pyright: ignore[reportMissingParameterType] 189 | ): 190 | pass 191 | 192 | with TCPServer(("", port), Handler) as httpd: 193 | httpd.serve_forever() 194 | 195 | 196 | def get_open_port() -> int: 197 | with closing(socket()) as sock: 198 | sock.bind(("", 0)) 199 | return sock.getsockname()[1] 200 | -------------------------------------------------------------------------------- /htmltools/_versions.py: -------------------------------------------------------------------------------- 1 | versions = {"react": "17.0.2", "react-dom": "17.0.2"} 2 | -------------------------------------------------------------------------------- /htmltools/lib/react/react.production.min.js: -------------------------------------------------------------------------------- 1 | /** @license React v17.0.2 2 | * react.production.min.js 3 | * 4 | * Copyright (c) Facebook, Inc. and its affiliates. 5 | * 6 | * This source code is licensed under the MIT license found in the 7 | * LICENSE file in the root directory of this source tree. 8 | */ 9 | (function(){'use strict';(function(c,x){"object"===typeof exports&&"undefined"!==typeof module?x(exports):"function"===typeof define&&define.amd?define(["exports"],x):(c=c||self,x(c.React={}))})(this,function(c){function x(a){if(null===a||"object"!==typeof a)return null;a=Y&&a[Y]||a["@@iterator"];return"function"===typeof a?a:null}function y(a){for(var b="https://reactjs.org/docs/error-decoder.html?invariant="+a,e=1;e>>1,f=a[c];if(void 0!==f&&0E(g,e))void 0!==h&&0>E(h,g)?(a[c]=h,a[k]=e,c=k):(a[c]=g,a[d]=e,c=d);else if(void 0!==h&&0>E(h,e))a[c]=h,a[k]=e,c=k;else break a}}return b}return null}function E(a,b){var e=a.sortIndex-b.sortIndex;return 0!==e?e:a.id-b.id}function P(a){for(var b=p(r);null!==b;){if(null===b.callback)F(r);else if(b.startTime<=a)F(r),b.sortIndex=b.expirationTime,O(q,b);else break;b=p(r)}} 16 | function Q(a){z=!1;P(a);if(!u)if(null!==p(q))u=!0,A(R);else{var b=p(r);null!==b&&G(Q,b.startTime-a)}}function R(a,b){u=!1;z&&(z=!1,S());H=!0;var e=g;try{P(b);for(m=p(q);null!==m&&(!(m.expirationTime>b)||a&&!T());){var c=m.callback;if("function"===typeof c){m.callback=null;g=m.priorityLevel;var f=c(m.expirationTime<=b);b=t();"function"===typeof f?m.callback=f:m===p(q)&&F(q);P(b)}else F(q);m=p(q)}if(null!==m)var d=!0;else{var n=p(r);null!==n&&G(Q,n.startTime-b);d=!1}return d}finally{m=null,g=e,H=!1}} 17 | var w=60103,ha=60106;c.Fragment=60107;c.StrictMode=60108;c.Profiler=60114;var ka=60109,la=60110,ma=60112;c.Suspense=60113;var na=60115,oa=60116;if("function"===typeof Symbol&&Symbol.for){var d=Symbol.for;w=d("react.element");ha=d("react.portal");c.Fragment=d("react.fragment");c.StrictMode=d("react.strict_mode");c.Profiler=d("react.profiler");ka=d("react.provider");la=d("react.context");ma=d("react.forward_ref");c.Suspense=d("react.suspense");na=d("react.memo");oa=d("react.lazy")}var Y="function"=== 18 | typeof Symbol&&Symbol.iterator,ya=Object.prototype.hasOwnProperty,U=Object.assign||function(a,b){if(null==a)throw new TypeError("Object.assign target cannot be null or undefined");for(var e=Object(a),c=1;c=ta};d=function(){};V=function(a){0>a||125d?(a.sortIndex= 25 | c,O(r,a),null===p(q)&&a===p(r)&&(z?S():z=!0,G(Q,c-d))):(a.sortIndex=e,O(q,a),u||H||(u=!0,A(R)));return a},unstable_cancelCallback:function(a){a.callback=null},unstable_wrapCallback:function(a){var b=g;return function(){var c=g;g=b;try{return a.apply(this,arguments)}finally{g=c}}},unstable_getCurrentPriorityLevel:function(){return g},get unstable_shouldYield(){return T},unstable_requestPaint:d,unstable_continueExecution:function(){u||H||(u=!0,A(R))},unstable_pauseExecution:function(){},unstable_getFirstCallbackNode:function(){return p(q)}, 26 | get unstable_now(){return t},get unstable_forceFrameRate(){return V},unstable_Profiling:null},SchedulerTracing:{__proto__:null,__interactionsRef:null,__subscriberRef:null,unstable_clear:function(a){return a()},unstable_getCurrent:function(){return null},unstable_getThreadID:function(){return++Ea},unstable_trace:function(a,b,c){return c()},unstable_wrap:function(a){return a},unstable_subscribe:function(a){},unstable_unsubscribe:function(a){}}};c.Children={map:D,forEach:function(a,b,c){D(a,function(){b.apply(this, 27 | arguments)},c)},count:function(a){var b=0;D(a,function(){b++});return b},toArray:function(a){return D(a,function(a){return a})||[]},only:function(a){if(!M(a))throw Error(y(143));return a}};c.Component=v;c.PureComponent=K;c.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=d;c.cloneElement=function(a,b,c){if(null===a||void 0===a)throw Error(y(267,a));var d=U({},a.props),e=a.key,g=a.ref,n=a._owner;if(null!=b){void 0!==b.ref&&(g=b.ref,n=L.current);void 0!==b.key&&(e=""+b.key);if(a.type&&a.type.defaultProps)var k= 28 | a.type.defaultProps;for(h in b)ea.call(b,h)&&!fa.hasOwnProperty(h)&&(d[h]=void 0===b[h]&&void 0!==k?k[h]:b[h])}var h=arguments.length-2;if(1===h)d.children=c;else if(1 Tag: 14 | """ 15 | Create a `` tag. 16 | 17 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/a 18 | 19 | Parameters 20 | ---------- 21 | *args 22 | Child elements to this tag. 23 | _add_ws 24 | A bool indicating whether whitespace should be added around this tag. 25 | **kwargs 26 | Attributes to this tag. 27 | 28 | Returns 29 | ------- 30 | : 31 | A :class:`~htmltools.Tag` object. 32 | 33 | See Also 34 | -------- 35 | * :class:`~htmltools.Tag` 36 | """ 37 | 38 | return Tag("a", *args, _add_ws=_add_ws, **kwargs) 39 | 40 | 41 | def animate(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 42 | """ 43 | Create a `` tag. 44 | 45 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animate 46 | 47 | Parameters 48 | ---------- 49 | *args 50 | Child elements to this tag. 51 | _add_ws 52 | A bool indicating whether whitespace should be added around this tag. 53 | **kwargs 54 | Attributes to this tag. 55 | 56 | Returns 57 | ------- 58 | : 59 | A :class:`~htmltools.Tag` object. 60 | 61 | See Also 62 | -------- 63 | * :class:`~htmltools.Tag` 64 | """ 65 | 66 | return Tag("animate", *args, _add_ws=_add_ws, **kwargs) 67 | 68 | 69 | def animateMotion(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 70 | """ 71 | Create a `` tag. 72 | 73 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateMotion 74 | 75 | Parameters 76 | ---------- 77 | *args 78 | Child elements to this tag. 79 | _add_ws 80 | A bool indicating whether whitespace should be added around this tag. 81 | **kwargs 82 | Attributes to this tag. 83 | 84 | Returns 85 | ------- 86 | : 87 | A :class:`~htmltools.Tag` object. 88 | 89 | See Also 90 | -------- 91 | * :class:`~htmltools.Tag` 92 | """ 93 | 94 | return Tag("animateMotion", *args, _add_ws=_add_ws, **kwargs) 95 | 96 | 97 | def animateTransform(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 98 | """ 99 | Create a `` tag. 100 | 101 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/animateTransform 102 | 103 | Parameters 104 | ---------- 105 | *args 106 | Child elements to this tag. 107 | _add_ws 108 | A bool indicating whether whitespace should be added around this tag. 109 | **kwargs 110 | Attributes to this tag. 111 | 112 | Returns 113 | ------- 114 | : 115 | A :class:`~htmltools.Tag` object. 116 | 117 | See Also 118 | -------- 119 | * :class:`~htmltools.Tag` 120 | """ 121 | 122 | return Tag("animateTransform", *args, _add_ws=_add_ws, **kwargs) 123 | 124 | 125 | def circle(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 126 | """ 127 | Create a `` tag. 128 | 129 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle 130 | 131 | Parameters 132 | ---------- 133 | *args 134 | Child elements to this tag. 135 | _add_ws 136 | A bool indicating whether whitespace should be added around this tag. 137 | **kwargs 138 | Attributes to this tag. 139 | 140 | Returns 141 | ------- 142 | : 143 | A :class:`~htmltools.Tag` object. 144 | 145 | See Also 146 | -------- 147 | * :class:`~htmltools.Tag` 148 | """ 149 | 150 | return Tag("circle", *args, _add_ws=_add_ws, **kwargs) 151 | 152 | 153 | def clipPath(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 154 | """ 155 | Create a `` tag. 156 | 157 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/clipPath 158 | 159 | Parameters 160 | ---------- 161 | *args 162 | Child elements to this tag. 163 | _add_ws 164 | A bool indicating whether whitespace should be added around this tag. 165 | **kwargs 166 | Attributes to this tag. 167 | 168 | Returns 169 | ------- 170 | : 171 | A :class:`~htmltools.Tag` object. 172 | 173 | See Also 174 | -------- 175 | * :class:`~htmltools.Tag` 176 | """ 177 | 178 | return Tag("clipPath", *args, _add_ws=_add_ws, **kwargs) 179 | 180 | 181 | def defs(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 182 | """ 183 | Create a `` tag. 184 | 185 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/defs 186 | 187 | Parameters 188 | ---------- 189 | *args 190 | Child elements to this tag. 191 | _add_ws 192 | A bool indicating whether whitespace should be added around this tag. 193 | **kwargs 194 | Attributes to this tag. 195 | 196 | Returns 197 | ------- 198 | : 199 | A :class:`~htmltools.Tag` object. 200 | 201 | See Also 202 | -------- 203 | * :class:`~htmltools.Tag` 204 | """ 205 | 206 | return Tag("defs", *args, _add_ws=_add_ws, **kwargs) 207 | 208 | 209 | def desc(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 210 | """ 211 | Create a `` tag. 212 | 213 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc 214 | 215 | Parameters 216 | ---------- 217 | *args 218 | Child elements to this tag. 219 | _add_ws 220 | A bool indicating whether whitespace should be added around this tag. 221 | **kwargs 222 | Attributes to this tag. 223 | 224 | Returns 225 | ------- 226 | : 227 | A :class:`~htmltools.Tag` object. 228 | 229 | See Also 230 | -------- 231 | * :class:`~htmltools.Tag` 232 | """ 233 | 234 | return Tag("desc", *args, _add_ws=_add_ws, **kwargs) 235 | 236 | 237 | def discard(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 238 | """ 239 | Create a `` tag. 240 | 241 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/discard 242 | 243 | Parameters 244 | ---------- 245 | *args 246 | Child elements to this tag. 247 | _add_ws 248 | A bool indicating whether whitespace should be added around this tag. 249 | **kwargs 250 | Attributes to this tag. 251 | 252 | Returns 253 | ------- 254 | : 255 | A :class:`~htmltools.Tag` object. 256 | 257 | See Also 258 | -------- 259 | * :class:`~htmltools.Tag` 260 | """ 261 | 262 | return Tag("discard", *args, _add_ws=_add_ws, **kwargs) 263 | 264 | 265 | def ellipse(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 266 | """ 267 | Create a `` tag. 268 | 269 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse 270 | 271 | Parameters 272 | ---------- 273 | *args 274 | Child elements to this tag. 275 | _add_ws 276 | A bool indicating whether whitespace should be added around this tag. 277 | **kwargs 278 | Attributes to this tag. 279 | 280 | Returns 281 | ------- 282 | : 283 | A :class:`~htmltools.Tag` object. 284 | 285 | See Also 286 | -------- 287 | * :class:`~htmltools.Tag` 288 | """ 289 | 290 | return Tag("ellipse", *args, _add_ws=_add_ws, **kwargs) 291 | 292 | 293 | def feBlend(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 294 | """ 295 | Create a `` tag. 296 | 297 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feBlend 298 | 299 | Parameters 300 | ---------- 301 | *args 302 | Child elements to this tag. 303 | _add_ws 304 | A bool indicating whether whitespace should be added around this tag. 305 | **kwargs 306 | Attributes to this tag. 307 | 308 | Returns 309 | ------- 310 | : 311 | A :class:`~htmltools.Tag` object. 312 | 313 | See Also 314 | -------- 315 | * :class:`~htmltools.Tag` 316 | """ 317 | 318 | return Tag("feBlend", *args, _add_ws=_add_ws, **kwargs) 319 | 320 | 321 | def feColorMatrix(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 322 | """ 323 | Create a `` tag. 324 | 325 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feColorMatrix 326 | 327 | Parameters 328 | ---------- 329 | *args 330 | Child elements to this tag. 331 | _add_ws 332 | A bool indicating whether whitespace should be added around this tag. 333 | **kwargs 334 | Attributes to this tag. 335 | 336 | Returns 337 | ------- 338 | : 339 | A :class:`~htmltools.Tag` object. 340 | 341 | See Also 342 | -------- 343 | * :class:`~htmltools.Tag` 344 | """ 345 | 346 | return Tag("feColorMatrix", *args, _add_ws=_add_ws, **kwargs) 347 | 348 | 349 | def feComponentTransfer(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 350 | """ 351 | Create a `` tag. 352 | 353 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComponentTransfer 354 | 355 | Parameters 356 | ---------- 357 | *args 358 | Child elements to this tag. 359 | _add_ws 360 | A bool indicating whether whitespace should be added around this tag. 361 | **kwargs 362 | Attributes to this tag. 363 | 364 | Returns 365 | ------- 366 | : 367 | A :class:`~htmltools.Tag` object. 368 | 369 | See Also 370 | -------- 371 | * :class:`~htmltools.Tag` 372 | """ 373 | 374 | return Tag("feComponentTransfer", *args, _add_ws=_add_ws, **kwargs) 375 | 376 | 377 | def feComposite(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 378 | """ 379 | Create a `` tag. 380 | 381 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feComposite 382 | 383 | Parameters 384 | ---------- 385 | *args 386 | Child elements to this tag. 387 | _add_ws 388 | A bool indicating whether whitespace should be added around this tag. 389 | **kwargs 390 | Attributes to this tag. 391 | 392 | Returns 393 | ------- 394 | : 395 | A :class:`~htmltools.Tag` object. 396 | 397 | See Also 398 | -------- 399 | * :class:`~htmltools.Tag` 400 | """ 401 | 402 | return Tag("feComposite", *args, _add_ws=_add_ws, **kwargs) 403 | 404 | 405 | def feConvolveMatrix(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 406 | """ 407 | Create a `` tag. 408 | 409 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feConvolveMatrix 410 | 411 | Parameters 412 | ---------- 413 | *args 414 | Child elements to this tag. 415 | _add_ws 416 | A bool indicating whether whitespace should be added around this tag. 417 | **kwargs 418 | Attributes to this tag. 419 | 420 | Returns 421 | ------- 422 | : 423 | A :class:`~htmltools.Tag` object. 424 | 425 | See Also 426 | -------- 427 | * :class:`~htmltools.Tag` 428 | """ 429 | 430 | return Tag("feConvolveMatrix", *args, _add_ws=_add_ws, **kwargs) 431 | 432 | 433 | def feDiffuseLighting(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 434 | """ 435 | Create a `` tag. 436 | 437 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDiffuseLighting 438 | 439 | Parameters 440 | ---------- 441 | *args 442 | Child elements to this tag. 443 | _add_ws 444 | A bool indicating whether whitespace should be added around this tag. 445 | **kwargs 446 | Attributes to this tag. 447 | 448 | Returns 449 | ------- 450 | : 451 | A :class:`~htmltools.Tag` object. 452 | 453 | See Also 454 | -------- 455 | * :class:`~htmltools.Tag` 456 | """ 457 | 458 | return Tag("feDiffuseLighting", *args, _add_ws=_add_ws, **kwargs) 459 | 460 | 461 | def feDisplacementMap(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 462 | """ 463 | Create a `` tag. 464 | 465 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDisplacementMap 466 | 467 | Parameters 468 | ---------- 469 | *args 470 | Child elements to this tag. 471 | _add_ws 472 | A bool indicating whether whitespace should be added around this tag. 473 | **kwargs 474 | Attributes to this tag. 475 | 476 | Returns 477 | ------- 478 | : 479 | A :class:`~htmltools.Tag` object. 480 | 481 | See Also 482 | -------- 483 | * :class:`~htmltools.Tag` 484 | """ 485 | 486 | return Tag("feDisplacementMap", *args, _add_ws=_add_ws, **kwargs) 487 | 488 | 489 | def feDistantLight(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 490 | """ 491 | Create a `` tag. 492 | 493 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDistantLight 494 | 495 | Parameters 496 | ---------- 497 | *args 498 | Child elements to this tag. 499 | _add_ws 500 | A bool indicating whether whitespace should be added around this tag. 501 | **kwargs 502 | Attributes to this tag. 503 | 504 | Returns 505 | ------- 506 | : 507 | A :class:`~htmltools.Tag` object. 508 | 509 | See Also 510 | -------- 511 | * :class:`~htmltools.Tag` 512 | """ 513 | 514 | return Tag("feDistantLight", *args, _add_ws=_add_ws, **kwargs) 515 | 516 | 517 | def feDropShadow(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 518 | """ 519 | Create a `` tag. 520 | 521 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feDropShadow 522 | 523 | Parameters 524 | ---------- 525 | *args 526 | Child elements to this tag. 527 | _add_ws 528 | A bool indicating whether whitespace should be added around this tag. 529 | **kwargs 530 | Attributes to this tag. 531 | 532 | Returns 533 | ------- 534 | : 535 | A :class:`~htmltools.Tag` object. 536 | 537 | See Also 538 | -------- 539 | * :class:`~htmltools.Tag` 540 | """ 541 | 542 | return Tag("feDropShadow", *args, _add_ws=_add_ws, **kwargs) 543 | 544 | 545 | def feFlood(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 546 | """ 547 | Create a `` tag. 548 | 549 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFlood 550 | 551 | Parameters 552 | ---------- 553 | *args 554 | Child elements to this tag. 555 | _add_ws 556 | A bool indicating whether whitespace should be added around this tag. 557 | **kwargs 558 | Attributes to this tag. 559 | 560 | Returns 561 | ------- 562 | : 563 | A :class:`~htmltools.Tag` object. 564 | 565 | See Also 566 | -------- 567 | * :class:`~htmltools.Tag` 568 | """ 569 | 570 | return Tag("feFlood", *args, _add_ws=_add_ws, **kwargs) 571 | 572 | 573 | def feFuncA(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 574 | """ 575 | Create a `` tag. 576 | 577 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncA 578 | 579 | Parameters 580 | ---------- 581 | *args 582 | Child elements to this tag. 583 | _add_ws 584 | A bool indicating whether whitespace should be added around this tag. 585 | **kwargs 586 | Attributes to this tag. 587 | 588 | Returns 589 | ------- 590 | : 591 | A :class:`~htmltools.Tag` object. 592 | 593 | See Also 594 | -------- 595 | * :class:`~htmltools.Tag` 596 | """ 597 | 598 | return Tag("feFuncA", *args, _add_ws=_add_ws, **kwargs) 599 | 600 | 601 | def feFuncB(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 602 | """ 603 | Create a `` tag. 604 | 605 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncB 606 | 607 | Parameters 608 | ---------- 609 | *args 610 | Child elements to this tag. 611 | _add_ws 612 | A bool indicating whether whitespace should be added around this tag. 613 | **kwargs 614 | Attributes to this tag. 615 | 616 | Returns 617 | ------- 618 | : 619 | A :class:`~htmltools.Tag` object. 620 | 621 | See Also 622 | -------- 623 | * :class:`~htmltools.Tag` 624 | """ 625 | 626 | return Tag("feFuncB", *args, _add_ws=_add_ws, **kwargs) 627 | 628 | 629 | def feFuncG(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 630 | """ 631 | Create a `` tag. 632 | 633 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncG 634 | 635 | Parameters 636 | ---------- 637 | *args 638 | Child elements to this tag. 639 | _add_ws 640 | A bool indicating whether whitespace should be added around this tag. 641 | **kwargs 642 | Attributes to this tag. 643 | 644 | Returns 645 | ------- 646 | : 647 | A :class:`~htmltools.Tag` object. 648 | 649 | See Also 650 | -------- 651 | * :class:`~htmltools.Tag` 652 | """ 653 | 654 | return Tag("feFuncG", *args, _add_ws=_add_ws, **kwargs) 655 | 656 | 657 | def feFuncR(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 658 | """ 659 | Create a `` tag. 660 | 661 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feFuncR 662 | 663 | Parameters 664 | ---------- 665 | *args 666 | Child elements to this tag. 667 | _add_ws 668 | A bool indicating whether whitespace should be added around this tag. 669 | **kwargs 670 | Attributes to this tag. 671 | 672 | Returns 673 | ------- 674 | : 675 | A :class:`~htmltools.Tag` object. 676 | 677 | See Also 678 | -------- 679 | * :class:`~htmltools.Tag` 680 | """ 681 | 682 | return Tag("feFuncR", *args, _add_ws=_add_ws, **kwargs) 683 | 684 | 685 | def feGaussianBlur(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 686 | """ 687 | Create a `` tag. 688 | 689 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feGaussianBlur 690 | 691 | Parameters 692 | ---------- 693 | *args 694 | Child elements to this tag. 695 | _add_ws 696 | A bool indicating whether whitespace should be added around this tag. 697 | **kwargs 698 | Attributes to this tag. 699 | 700 | Returns 701 | ------- 702 | : 703 | A :class:`~htmltools.Tag` object. 704 | 705 | See Also 706 | -------- 707 | * :class:`~htmltools.Tag` 708 | """ 709 | 710 | return Tag("feGaussianBlur", *args, _add_ws=_add_ws, **kwargs) 711 | 712 | 713 | def feImage(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 714 | """ 715 | Create a `` tag. 716 | 717 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feImage 718 | 719 | Parameters 720 | ---------- 721 | *args 722 | Child elements to this tag. 723 | _add_ws 724 | A bool indicating whether whitespace should be added around this tag. 725 | **kwargs 726 | Attributes to this tag. 727 | 728 | Returns 729 | ------- 730 | : 731 | A :class:`~htmltools.Tag` object. 732 | 733 | See Also 734 | -------- 735 | * :class:`~htmltools.Tag` 736 | """ 737 | 738 | return Tag("feImage", *args, _add_ws=_add_ws, **kwargs) 739 | 740 | 741 | def feMerge(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 742 | """ 743 | Create a `` tag. 744 | 745 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMerge 746 | 747 | Parameters 748 | ---------- 749 | *args 750 | Child elements to this tag. 751 | _add_ws 752 | A bool indicating whether whitespace should be added around this tag. 753 | **kwargs 754 | Attributes to this tag. 755 | 756 | Returns 757 | ------- 758 | : 759 | A :class:`~htmltools.Tag` object. 760 | 761 | See Also 762 | -------- 763 | * :class:`~htmltools.Tag` 764 | """ 765 | 766 | return Tag("feMerge", *args, _add_ws=_add_ws, **kwargs) 767 | 768 | 769 | def feMergeNode(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 770 | """ 771 | Create a `` tag. 772 | 773 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMergeNode 774 | 775 | Parameters 776 | ---------- 777 | *args 778 | Child elements to this tag. 779 | _add_ws 780 | A bool indicating whether whitespace should be added around this tag. 781 | **kwargs 782 | Attributes to this tag. 783 | 784 | Returns 785 | ------- 786 | : 787 | A :class:`~htmltools.Tag` object. 788 | 789 | See Also 790 | -------- 791 | * :class:`~htmltools.Tag` 792 | """ 793 | 794 | return Tag("feMergeNode", *args, _add_ws=_add_ws, **kwargs) 795 | 796 | 797 | def feMorphology(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 798 | """ 799 | Create a `` tag. 800 | 801 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feMorphology 802 | 803 | Parameters 804 | ---------- 805 | *args 806 | Child elements to this tag. 807 | _add_ws 808 | A bool indicating whether whitespace should be added around this tag. 809 | **kwargs 810 | Attributes to this tag. 811 | 812 | Returns 813 | ------- 814 | : 815 | A :class:`~htmltools.Tag` object. 816 | 817 | See Also 818 | -------- 819 | * :class:`~htmltools.Tag` 820 | """ 821 | 822 | return Tag("feMorphology", *args, _add_ws=_add_ws, **kwargs) 823 | 824 | 825 | def feOffset(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 826 | """ 827 | Create a `` tag. 828 | 829 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feOffset 830 | 831 | Parameters 832 | ---------- 833 | *args 834 | Child elements to this tag. 835 | _add_ws 836 | A bool indicating whether whitespace should be added around this tag. 837 | **kwargs 838 | Attributes to this tag. 839 | 840 | Returns 841 | ------- 842 | : 843 | A :class:`~htmltools.Tag` object. 844 | 845 | See Also 846 | -------- 847 | * :class:`~htmltools.Tag` 848 | """ 849 | 850 | return Tag("feOffset", *args, _add_ws=_add_ws, **kwargs) 851 | 852 | 853 | def fePointLight(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 854 | """ 855 | Create a `` tag. 856 | 857 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/fePointLight 858 | 859 | Parameters 860 | ---------- 861 | *args 862 | Child elements to this tag. 863 | _add_ws 864 | A bool indicating whether whitespace should be added around this tag. 865 | **kwargs 866 | Attributes to this tag. 867 | 868 | Returns 869 | ------- 870 | : 871 | A :class:`~htmltools.Tag` object. 872 | 873 | See Also 874 | -------- 875 | * :class:`~htmltools.Tag` 876 | """ 877 | 878 | return Tag("fePointLight", *args, _add_ws=_add_ws, **kwargs) 879 | 880 | 881 | def feSpecularLighting(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 882 | """ 883 | Create a `` tag. 884 | 885 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feSpecularLighting 886 | 887 | Parameters 888 | ---------- 889 | *args 890 | Child elements to this tag. 891 | _add_ws 892 | A bool indicating whether whitespace should be added around this tag. 893 | **kwargs 894 | Attributes to this tag. 895 | 896 | Returns 897 | ------- 898 | : 899 | A :class:`~htmltools.Tag` object. 900 | 901 | See Also 902 | -------- 903 | * :class:`~htmltools.Tag` 904 | """ 905 | 906 | return Tag("feSpecularLighting", *args, _add_ws=_add_ws, **kwargs) 907 | 908 | 909 | def feSpotLight(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 910 | """ 911 | Create a `` tag. 912 | 913 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feSpotLight 914 | 915 | Parameters 916 | ---------- 917 | *args 918 | Child elements to this tag. 919 | _add_ws 920 | A bool indicating whether whitespace should be added around this tag. 921 | **kwargs 922 | Attributes to this tag. 923 | 924 | Returns 925 | ------- 926 | : 927 | A :class:`~htmltools.Tag` object. 928 | 929 | See Also 930 | -------- 931 | * :class:`~htmltools.Tag` 932 | """ 933 | 934 | return Tag("feSpotLight", *args, _add_ws=_add_ws, **kwargs) 935 | 936 | 937 | def feTile(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 938 | """ 939 | Create a `` tag. 940 | 941 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feTile 942 | 943 | Parameters 944 | ---------- 945 | *args 946 | Child elements to this tag. 947 | _add_ws 948 | A bool indicating whether whitespace should be added around this tag. 949 | **kwargs 950 | Attributes to this tag. 951 | 952 | Returns 953 | ------- 954 | : 955 | A :class:`~htmltools.Tag` object. 956 | 957 | See Also 958 | -------- 959 | * :class:`~htmltools.Tag` 960 | """ 961 | 962 | return Tag("feTile", *args, _add_ws=_add_ws, **kwargs) 963 | 964 | 965 | def feTurbulence(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 966 | """ 967 | Create a `` tag. 968 | 969 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/feTurbulence 970 | 971 | Parameters 972 | ---------- 973 | *args 974 | Child elements to this tag. 975 | _add_ws 976 | A bool indicating whether whitespace should be added around this tag. 977 | **kwargs 978 | Attributes to this tag. 979 | 980 | Returns 981 | ------- 982 | : 983 | A :class:`~htmltools.Tag` object. 984 | 985 | See Also 986 | -------- 987 | * :class:`~htmltools.Tag` 988 | """ 989 | 990 | return Tag("feTurbulence", *args, _add_ws=_add_ws, **kwargs) 991 | 992 | 993 | def filter(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 994 | """ 995 | Create a `` tag. 996 | 997 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/filter 998 | 999 | Parameters 1000 | ---------- 1001 | *args 1002 | Child elements to this tag. 1003 | _add_ws 1004 | A bool indicating whether whitespace should be added around this tag. 1005 | **kwargs 1006 | Attributes to this tag. 1007 | 1008 | Returns 1009 | ------- 1010 | : 1011 | A :class:`~htmltools.Tag` object. 1012 | 1013 | See Also 1014 | -------- 1015 | * :class:`~htmltools.Tag` 1016 | """ 1017 | 1018 | return Tag("filter", *args, _add_ws=_add_ws, **kwargs) 1019 | 1020 | 1021 | def foreignObject(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1022 | """ 1023 | Create a `` tag. 1024 | 1025 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject 1026 | 1027 | Parameters 1028 | ---------- 1029 | *args 1030 | Child elements to this tag. 1031 | _add_ws 1032 | A bool indicating whether whitespace should be added around this tag. 1033 | **kwargs 1034 | Attributes to this tag. 1035 | 1036 | Returns 1037 | ------- 1038 | : 1039 | A :class:`~htmltools.Tag` object. 1040 | 1041 | See Also 1042 | -------- 1043 | * :class:`~htmltools.Tag` 1044 | """ 1045 | 1046 | return Tag("foreignObject", *args, _add_ws=_add_ws, **kwargs) 1047 | 1048 | 1049 | def g(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1050 | """ 1051 | Create a `` tag. 1052 | 1053 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/g 1054 | 1055 | Parameters 1056 | ---------- 1057 | *args 1058 | Child elements to this tag. 1059 | _add_ws 1060 | A bool indicating whether whitespace should be added around this tag. 1061 | **kwargs 1062 | Attributes to this tag. 1063 | 1064 | Returns 1065 | ------- 1066 | : 1067 | A :class:`~htmltools.Tag` object. 1068 | 1069 | See Also 1070 | -------- 1071 | * :class:`~htmltools.Tag` 1072 | """ 1073 | 1074 | return Tag("g", *args, _add_ws=_add_ws, **kwargs) 1075 | 1076 | 1077 | def hatch(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1078 | """ 1079 | Create a `` tag. 1080 | 1081 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/hatch 1082 | 1083 | Parameters 1084 | ---------- 1085 | *args 1086 | Child elements to this tag. 1087 | _add_ws 1088 | A bool indicating whether whitespace should be added around this tag. 1089 | **kwargs 1090 | Attributes to this tag. 1091 | 1092 | Returns 1093 | ------- 1094 | : 1095 | A :class:`~htmltools.Tag` object. 1096 | 1097 | See Also 1098 | -------- 1099 | * :class:`~htmltools.Tag` 1100 | """ 1101 | 1102 | return Tag("hatch", *args, _add_ws=_add_ws, **kwargs) 1103 | 1104 | 1105 | def hatchpath(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1106 | """ 1107 | Create a `` tag. 1108 | 1109 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/hatchpath 1110 | 1111 | Parameters 1112 | ---------- 1113 | *args 1114 | Child elements to this tag. 1115 | _add_ws 1116 | A bool indicating whether whitespace should be added around this tag. 1117 | **kwargs 1118 | Attributes to this tag. 1119 | 1120 | Returns 1121 | ------- 1122 | : 1123 | A :class:`~htmltools.Tag` object. 1124 | 1125 | See Also 1126 | -------- 1127 | * :class:`~htmltools.Tag` 1128 | """ 1129 | 1130 | return Tag("hatchpath", *args, _add_ws=_add_ws, **kwargs) 1131 | 1132 | 1133 | def image(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1134 | """ 1135 | Create a `` tag. 1136 | 1137 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image 1138 | 1139 | Parameters 1140 | ---------- 1141 | *args 1142 | Child elements to this tag. 1143 | _add_ws 1144 | A bool indicating whether whitespace should be added around this tag. 1145 | **kwargs 1146 | Attributes to this tag. 1147 | 1148 | Returns 1149 | ------- 1150 | : 1151 | A :class:`~htmltools.Tag` object. 1152 | 1153 | See Also 1154 | -------- 1155 | * :class:`~htmltools.Tag` 1156 | """ 1157 | 1158 | return Tag("image", *args, _add_ws=_add_ws, **kwargs) 1159 | 1160 | 1161 | def line(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1162 | """ 1163 | Create a `` tag. 1164 | 1165 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/line 1166 | 1167 | Parameters 1168 | ---------- 1169 | *args 1170 | Child elements to this tag. 1171 | _add_ws 1172 | A bool indicating whether whitespace should be added around this tag. 1173 | **kwargs 1174 | Attributes to this tag. 1175 | 1176 | Returns 1177 | ------- 1178 | : 1179 | A :class:`~htmltools.Tag` object. 1180 | 1181 | See Also 1182 | -------- 1183 | * :class:`~htmltools.Tag` 1184 | """ 1185 | 1186 | return Tag("line", *args, _add_ws=_add_ws, **kwargs) 1187 | 1188 | 1189 | def linearGradient(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1190 | """ 1191 | Create a `` tag. 1192 | 1193 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/linearGradient 1194 | 1195 | Parameters 1196 | ---------- 1197 | *args 1198 | Child elements to this tag. 1199 | _add_ws 1200 | A bool indicating whether whitespace should be added around this tag. 1201 | **kwargs 1202 | Attributes to this tag. 1203 | 1204 | Returns 1205 | ------- 1206 | : 1207 | A :class:`~htmltools.Tag` object. 1208 | 1209 | See Also 1210 | -------- 1211 | * :class:`~htmltools.Tag` 1212 | """ 1213 | 1214 | return Tag("linearGradient", *args, _add_ws=_add_ws, **kwargs) 1215 | 1216 | 1217 | def marker(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1218 | """ 1219 | Create a `` tag. 1220 | 1221 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker 1222 | 1223 | Parameters 1224 | ---------- 1225 | *args 1226 | Child elements to this tag. 1227 | _add_ws 1228 | A bool indicating whether whitespace should be added around this tag. 1229 | **kwargs 1230 | Attributes to this tag. 1231 | 1232 | Returns 1233 | ------- 1234 | : 1235 | A :class:`~htmltools.Tag` object. 1236 | 1237 | See Also 1238 | -------- 1239 | * :class:`~htmltools.Tag` 1240 | """ 1241 | 1242 | return Tag("marker", *args, _add_ws=_add_ws, **kwargs) 1243 | 1244 | 1245 | def mask(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1246 | """ 1247 | Create a `` tag. 1248 | 1249 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mask 1250 | 1251 | Parameters 1252 | ---------- 1253 | *args 1254 | Child elements to this tag. 1255 | _add_ws 1256 | A bool indicating whether whitespace should be added around this tag. 1257 | **kwargs 1258 | Attributes to this tag. 1259 | 1260 | Returns 1261 | ------- 1262 | : 1263 | A :class:`~htmltools.Tag` object. 1264 | 1265 | See Also 1266 | -------- 1267 | * :class:`~htmltools.Tag` 1268 | """ 1269 | 1270 | return Tag("mask", *args, _add_ws=_add_ws, **kwargs) 1271 | 1272 | 1273 | def metadata(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1274 | """ 1275 | Create a `` tag. 1276 | 1277 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/metadata 1278 | 1279 | Parameters 1280 | ---------- 1281 | *args 1282 | Child elements to this tag. 1283 | _add_ws 1284 | A bool indicating whether whitespace should be added around this tag. 1285 | **kwargs 1286 | Attributes to this tag. 1287 | 1288 | Returns 1289 | ------- 1290 | : 1291 | A :class:`~htmltools.Tag` object. 1292 | 1293 | See Also 1294 | -------- 1295 | * :class:`~htmltools.Tag` 1296 | """ 1297 | 1298 | return Tag("metadata", *args, _add_ws=_add_ws, **kwargs) 1299 | 1300 | 1301 | def mpath(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1302 | """ 1303 | Create a `` tag. 1304 | 1305 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/mpath 1306 | 1307 | Parameters 1308 | ---------- 1309 | *args 1310 | Child elements to this tag. 1311 | _add_ws 1312 | A bool indicating whether whitespace should be added around this tag. 1313 | **kwargs 1314 | Attributes to this tag. 1315 | 1316 | Returns 1317 | ------- 1318 | : 1319 | A :class:`~htmltools.Tag` object. 1320 | 1321 | See Also 1322 | -------- 1323 | * :class:`~htmltools.Tag` 1324 | """ 1325 | 1326 | return Tag("mpath", *args, _add_ws=_add_ws, **kwargs) 1327 | 1328 | 1329 | def path(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1330 | """ 1331 | Create a `` tag. 1332 | 1333 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/path 1334 | 1335 | Parameters 1336 | ---------- 1337 | *args 1338 | Child elements to this tag. 1339 | _add_ws 1340 | A bool indicating whether whitespace should be added around this tag. 1341 | **kwargs 1342 | Attributes to this tag. 1343 | 1344 | Returns 1345 | ------- 1346 | : 1347 | A :class:`~htmltools.Tag` object. 1348 | 1349 | See Also 1350 | -------- 1351 | * :class:`~htmltools.Tag` 1352 | """ 1353 | 1354 | return Tag("path", *args, _add_ws=_add_ws, **kwargs) 1355 | 1356 | 1357 | def pattern(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1358 | """ 1359 | Create a `` tag. 1360 | 1361 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/pattern 1362 | 1363 | Parameters 1364 | ---------- 1365 | *args 1366 | Child elements to this tag. 1367 | _add_ws 1368 | A bool indicating whether whitespace should be added around this tag. 1369 | **kwargs 1370 | Attributes to this tag. 1371 | 1372 | Returns 1373 | ------- 1374 | : 1375 | A :class:`~htmltools.Tag` object. 1376 | 1377 | See Also 1378 | -------- 1379 | * :class:`~htmltools.Tag` 1380 | """ 1381 | 1382 | return Tag("pattern", *args, _add_ws=_add_ws, **kwargs) 1383 | 1384 | 1385 | def polygon(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1386 | """ 1387 | Create a `` tag. 1388 | 1389 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polygon 1390 | 1391 | Parameters 1392 | ---------- 1393 | *args 1394 | Child elements to this tag. 1395 | _add_ws 1396 | A bool indicating whether whitespace should be added around this tag. 1397 | **kwargs 1398 | Attributes to this tag. 1399 | 1400 | Returns 1401 | ------- 1402 | : 1403 | A :class:`~htmltools.Tag` object. 1404 | 1405 | See Also 1406 | -------- 1407 | * :class:`~htmltools.Tag` 1408 | """ 1409 | 1410 | return Tag("polygon", *args, _add_ws=_add_ws, **kwargs) 1411 | 1412 | 1413 | def polyline(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1414 | """ 1415 | Create a `` tag. 1416 | 1417 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/polyline 1418 | 1419 | Parameters 1420 | ---------- 1421 | *args 1422 | Child elements to this tag. 1423 | _add_ws 1424 | A bool indicating whether whitespace should be added around this tag. 1425 | **kwargs 1426 | Attributes to this tag. 1427 | 1428 | Returns 1429 | ------- 1430 | : 1431 | A :class:`~htmltools.Tag` object. 1432 | 1433 | See Also 1434 | -------- 1435 | * :class:`~htmltools.Tag` 1436 | """ 1437 | 1438 | return Tag("polyline", *args, _add_ws=_add_ws, **kwargs) 1439 | 1440 | 1441 | def radialGradient(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1442 | """ 1443 | Create a `` tag. 1444 | 1445 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/radialGradient 1446 | 1447 | Parameters 1448 | ---------- 1449 | *args 1450 | Child elements to this tag. 1451 | _add_ws 1452 | A bool indicating whether whitespace should be added around this tag. 1453 | **kwargs 1454 | Attributes to this tag. 1455 | 1456 | Returns 1457 | ------- 1458 | : 1459 | A :class:`~htmltools.Tag` object. 1460 | 1461 | See Also 1462 | -------- 1463 | * :class:`~htmltools.Tag` 1464 | """ 1465 | 1466 | return Tag("radialGradient", *args, _add_ws=_add_ws, **kwargs) 1467 | 1468 | 1469 | def rect(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1470 | """ 1471 | Create a `` tag. 1472 | 1473 | Creates the `` SVG element. Learn more at https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect 1474 | 1475 | Parameters 1476 | ---------- 1477 | *args 1478 | Child elements to this tag. 1479 | _add_ws 1480 | A bool indicating whether whitespace should be added around this tag. 1481 | **kwargs 1482 | Attributes to this tag. 1483 | 1484 | Returns 1485 | ------- 1486 | : 1487 | A :class:`~htmltools.Tag` object. 1488 | 1489 | See Also 1490 | -------- 1491 | * :class:`~htmltools.Tag` 1492 | """ 1493 | 1494 | return Tag("rect", *args, _add_ws=_add_ws, **kwargs) 1495 | 1496 | 1497 | def script(*args: TagChild | TagAttrs, _add_ws: TagAttrValue = True, **kwargs: TagAttrValue) -> Tag: 1498 | """ 1499 | Create a ` 9 | 10 | 11 | 12 |
foo
13 | bar 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
foo
26 | bar 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
39 |
foo
40 | bar 41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 |
foo
54 | bar 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
foo
68 | bar 69 |
70 | 71 | 72 | ''' 73 | # --- 74 | -------------------------------------------------------------------------------- /tests/assets/testdep-nested/css/my-styles.css: -------------------------------------------------------------------------------- 1 | .foo {color: red} 2 | -------------------------------------------------------------------------------- /tests/assets/testdep-nested/js/my-js.js: -------------------------------------------------------------------------------- 1 | function() {} 2 | -------------------------------------------------------------------------------- /tests/test_consolidate_attrs.py: -------------------------------------------------------------------------------- 1 | from htmltools import HTML, consolidate_attrs 2 | 3 | 4 | def test_consolidate_attrs(): 5 | 6 | (attrs, children) = consolidate_attrs( 7 | {"class": "&c1"}, 8 | 0, 9 | # This tests `__radd__` method of `HTML` class 10 | {"id": "foo", "class_": HTML("&c2")}, 11 | [1, [2]], 12 | 3, 13 | class_=HTML("&c3"), 14 | other_attr="other", 15 | ) 16 | 17 | assert attrs == {"id": "foo", "class": "&c1 &c2 &c3", "other-attr": "other"} 18 | assert children == [0, [1, [2]], 3] 19 | -------------------------------------------------------------------------------- /tests/test_deps.py: -------------------------------------------------------------------------------- 1 | import tempfile 2 | import textwrap 3 | from pathlib import Path 4 | 5 | from htmltools import HTMLDependency, HTMLDocument, TagList, div, tags 6 | 7 | 8 | def test_init_errors(): 9 | HTMLDependency("a", "1.1", source=None) 10 | 11 | try: 12 | HTMLDependency("a", "1.1", source=4) # type: ignore 13 | except TypeError as e: 14 | assert "to be a dict" in str(e) 15 | 16 | try: 17 | HTMLDependency("a", "1.1", source={"not": "valid"}) # type: ignore 18 | except TypeError as e: 19 | assert "to have either" in str(e) 20 | 21 | 22 | def test_dep_resolution(): 23 | a1_1 = HTMLDependency("a", "1.1", source={"subdir": "foo"}, script={"src": "a1.js"}) 24 | a1_2 = HTMLDependency( 25 | "a", "1.2", source={"href": "http://a.test.com"}, script={"src": "a2.js"} 26 | ) 27 | a1_2_1 = HTMLDependency( 28 | "a", "1.2.1", source={"subdir": "foo"}, script={"src": "a3.js"} 29 | ) 30 | b1_9 = HTMLDependency("b", "1.9", source={"subdir": "foo"}, script={"src": "b1.js"}) 31 | b1_10 = HTMLDependency( 32 | "b", "1.10", source={"href": "http://b.test.com"}, script={"src": "b2.js"} 33 | ) 34 | c1_0 = HTMLDependency("c", "1.0", source={"subdir": "foo"}, script={"src": "c1.js"}) 35 | test = TagList(a1_1, b1_9, b1_10, a1_2, a1_2_1, b1_9, b1_10, c1_0) 36 | assert HTMLDocument(test).render(lib_prefix=None)["html"] == textwrap.dedent( 37 | """\ 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | """ 49 | ) 50 | 51 | assert HTMLDocument(test).render(lib_prefix="libfoo")["html"] == textwrap.dedent( 52 | """\ 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | """ 64 | ) 65 | 66 | 67 | # Test out renderTags and findDependencies when tags are inline 68 | def test_inline_deps(snapshot): 69 | a1_1 = HTMLDependency("a", "1.1", source={"subdir": "foo"}, script={"src": "a1.js"}) 70 | a1_2 = HTMLDependency("a", "1.2", source={"subdir": "foo"}, script={"src": "a2.js"}) 71 | tests = [ 72 | TagList(a1_1, div("foo"), "bar"), 73 | TagList(a1_1, div("foo"), a1_2, "bar"), 74 | div(a1_1, div("foo"), "bar"), 75 | TagList([a1_1, div("foo")], "bar"), 76 | div([a1_1, div("foo")], "bar"), 77 | ] 78 | html_ = "\n\n".join( 79 | [HTMLDocument(t).render(lib_prefix=None)["html"] for t in tests] 80 | ) 81 | assert html_ == snapshot 82 | 83 | 84 | def test_append_deps(): 85 | a1_1 = HTMLDependency("a", "1.1", source={"subdir": "foo"}, script={"src": "a1.js"}) 86 | a1_2 = HTMLDependency( 87 | "a", "1.2", source={"href": "http://foo.com"}, script={"src": "a2.js"} 88 | ) 89 | b1_0 = HTMLDependency("b", "1.0", source={"subdir": "foo"}, script={"src": "b1.js"}) 90 | 91 | expected_result = textwrap.dedent( 92 | """\ 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 |
103 | 104 | """ 105 | ) 106 | 107 | x = div(a1_1, b1_0) 108 | x.append(a1_2) 109 | assert HTMLDocument(x).render(lib_prefix=None)["html"] == expected_result 110 | 111 | y = div(a1_1) 112 | y.append([a1_2, b1_0]) 113 | assert HTMLDocument(y).render(lib_prefix=None)["html"] == expected_result 114 | 115 | z = div() 116 | z.append([a1_1, b1_0]) 117 | z.append(a1_2) 118 | assert HTMLDocument(z).render(lib_prefix=None)["html"] == expected_result 119 | 120 | 121 | def test_script_input(): 122 | def fake_dep(**kwargs): 123 | return HTMLDependency( 124 | "a", "1.0", source={"package": None, "subdir": "srcpath"}, **kwargs 125 | ) 126 | 127 | dep1 = fake_dep( 128 | script={"src": "js/foo bar.js"}, stylesheet={"href": "css/bar foo.css"} 129 | ) 130 | dep2 = fake_dep( 131 | script=[{"src": "js/foo bar.js"}], stylesheet=[{"href": "css/bar foo.css"}] 132 | ) 133 | assert dep1 == dep2 134 | # Make sure repeated calls to as_html() repeatedly encode 135 | test = TagList([dep1, dep2]) 136 | for _ in range(2): 137 | assert HTMLDocument(test).render()["html"] == textwrap.dedent( 138 | """\ 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | """ 149 | ) 150 | 151 | 152 | def test_head_output(): 153 | a = HTMLDependency( 154 | "a", 155 | "1.0", 156 | source={"subdir": "foo"}, 157 | head=tags.script("1 && 1"), 158 | ) 159 | assert a.as_html_tags().get_html_string() == "" 160 | 161 | b = HTMLDependency( 162 | "a", 163 | "1.0", 164 | source={"subdir": "foo"}, 165 | head="", 166 | ) 167 | assert b.as_html_tags().get_html_string() == "" 168 | 169 | assert ( 170 | b.serialize_to_script_json(indent=4).get_html_string() 171 | == """""" 183 | ) 184 | 185 | 186 | def test_meta_output(): 187 | a = HTMLDependency( 188 | "a", 189 | "1.0", 190 | source={"subdir": "foo"}, 191 | script={"src": "a1.js"}, 192 | meta={"name": "viewport", "content": "width=device-width, initial-scale=1"}, 193 | ) 194 | 195 | b = HTMLDependency( 196 | "b", 197 | "2.0", 198 | source={"subdir": "foo"}, 199 | meta=[{"name": "x", "content": "x-value"}, {"name": "y", "content": "y-value"}], 200 | ) 201 | 202 | assert str(a.as_html_tags()) == textwrap.dedent( 203 | """\ 204 | 205 | """ 206 | ) 207 | assert str(b.as_html_tags()) == textwrap.dedent( 208 | """\ 209 | 210 | """ 211 | ) 212 | 213 | # Combine the two in an HTMLDocument and render; all meta tags should show up. 214 | combined_html = HTMLDocument(TagList(a, b)).render()["html"] 215 | assert ( 216 | combined_html.find( 217 | '' 218 | ) 219 | != -1 220 | ) 221 | assert combined_html.find('') != -1 222 | assert combined_html.find('') != -1 223 | 224 | 225 | def test_source_dep_as_dict(): 226 | a = HTMLDependency( 227 | "a", 228 | "1.0", 229 | source={"package": "htmltools", "subdir": "bar"}, 230 | script={"src": "a1.js"}, 231 | meta={"name": "viewport", "content": "width=device-width, initial-scale=1"}, 232 | ) 233 | assert a.as_dict() == { 234 | "name": "a", 235 | "version": "1.0", 236 | "script": [{"src": "lib/a-1.0/a1.js"}], 237 | "stylesheet": [], 238 | "meta": [ 239 | {"name": "viewport", "content": "width=device-width, initial-scale=1"} 240 | ], 241 | "head": None, 242 | } 243 | 244 | b = HTMLDependency( 245 | "b", 246 | "2.0", 247 | source={"package": "htmltools", "subdir": "bar"}, 248 | stylesheet=[{"href": "b1.css"}, {"href": "b2.css"}], 249 | head=tags.script("1 && 1"), 250 | ) 251 | assert b.as_dict(lib_prefix=None) == { 252 | "name": "b", 253 | "version": "2.0", 254 | "script": [], 255 | "stylesheet": [ 256 | {"href": "b-2.0/b1.css", "rel": "stylesheet"}, 257 | {"href": "b-2.0/b2.css", "rel": "stylesheet"}, 258 | ], 259 | "meta": [], 260 | "head": "", 261 | } 262 | 263 | 264 | def test_url_dep_as_dict(): 265 | u = HTMLDependency( 266 | "u", 267 | "1.1", 268 | source={"href": "https://example.com"}, 269 | script={"src": "u1.js"}, 270 | stylesheet={"href": "u1.css"}, 271 | meta={"name": "viewport", "content": "width=device-width, initial-scale=1"}, 272 | ) 273 | assert u.as_dict() == { 274 | "name": "u", 275 | "version": "1.1", 276 | "script": [{"src": "https://example.com/u1.js"}], 277 | "stylesheet": [{"href": "https://example.com/u1.css", "rel": "stylesheet"}], 278 | "meta": [ 279 | {"name": "viewport", "content": "width=device-width, initial-scale=1"} 280 | ], 281 | "head": None, 282 | } 283 | 284 | v = HTMLDependency( 285 | "v", 286 | "2.1", 287 | source={"href": "https://test.com/subdir/trailing/slash/"}, 288 | stylesheet=[{"href": "v1.css"}, {"href": "v2.css"}], 289 | head=tags.script("1 && 1"), 290 | ) 291 | assert v.as_dict(lib_prefix=None) == { 292 | "name": "v", 293 | "version": "2.1", 294 | "script": [], 295 | "stylesheet": [ 296 | { 297 | "href": "https://test.com/subdir/trailing/slash/v1.css", 298 | "rel": "stylesheet", 299 | }, 300 | { 301 | "href": "https://test.com/subdir/trailing/slash/v2.css", 302 | "rel": "stylesheet", 303 | }, 304 | ], 305 | "meta": [], 306 | "head": "", 307 | } 308 | 309 | 310 | def test_copy_to(): 311 | with tempfile.TemporaryDirectory() as tmpdir1: 312 | dep1 = HTMLDependency( 313 | "w", 314 | "1.0", 315 | source={"package": "htmltools", "subdir": "libtest/testdep"}, 316 | all_files=True, 317 | ) 318 | dep1.copy_to(tmpdir1) 319 | assert (Path(tmpdir1) / "w-1.0" / "testdep.css").exists() 320 | assert (Path(tmpdir1) / "w-1.0" / "testdep.js").exists() 321 | 322 | with tempfile.TemporaryDirectory() as tmpdir2: 323 | dep2 = HTMLDependency( 324 | "w", 325 | "1.0", 326 | source={"package": "htmltools", "subdir": "libtest/testdep"}, 327 | all_files=False, 328 | ) 329 | dep2.copy_to(tmpdir2) 330 | assert not (Path(tmpdir2) / "w-1.0" / "testdep.css").exists() 331 | assert not (Path(tmpdir2) / "w-1.0" / "testdep.js").exists() 332 | 333 | with tempfile.TemporaryDirectory() as tmpdir3: 334 | dep3 = HTMLDependency( 335 | "w", 336 | "1.0", 337 | source={"package": "htmltools", "subdir": "libtest/testdep"}, 338 | stylesheet={"href": "testdep.css"}, 339 | all_files=False, 340 | ) 341 | dep3.copy_to(tmpdir3) 342 | assert (Path(tmpdir3) / "w-1.0" / "testdep.css").exists() 343 | assert not (Path(tmpdir3) / "w-1.0" / "testdep.js").exists() 344 | 345 | with tempfile.TemporaryDirectory() as tmpdir4: 346 | dep4 = HTMLDependency( 347 | "w", 348 | "1.0", 349 | source={"package": "htmltools", "subdir": "libtest/testdep"}, 350 | stylesheet={"href": "testdep.css"}, 351 | script={"src": "testdep.js"}, 352 | all_files=False, 353 | ) 354 | dep4.copy_to(tmpdir4) 355 | assert (Path(tmpdir4) / "w-1.0" / "testdep.css").exists() 356 | assert (Path(tmpdir4) / "w-1.0" / "testdep.js").exists() 357 | 358 | with tempfile.TemporaryDirectory() as tmpdir5: 359 | path_testdep_nested = Path(__file__).parent / "assets" / "testdep-nested" 360 | dep5 = HTMLDependency( 361 | "w", 362 | "1.0", 363 | source={"subdir": str(path_testdep_nested)}, 364 | all_files=True, 365 | ) 366 | dep5.copy_to(tmpdir5) 367 | assert (Path(tmpdir5) / "w-1.0" / "css" / "my-styles.css").exists() 368 | assert (Path(tmpdir5) / "w-1.0" / "js" / "my-js.js").exists() 369 | -------------------------------------------------------------------------------- /tests/test_html_document.py: -------------------------------------------------------------------------------- 1 | import os 2 | import textwrap 3 | from tempfile import TemporaryDirectory 4 | from typing import Union 5 | 6 | import htmltools as ht 7 | from htmltools import ( 8 | HTMLDependency, 9 | HTMLDocument, 10 | Tag, 11 | TagList, 12 | div, 13 | head_content, 14 | span, 15 | tags, 16 | ) 17 | 18 | 19 | def saved_html(x: Union[Tag, HTMLDocument]) -> str: 20 | with TemporaryDirectory() as tmpdir: 21 | f = os.path.join(tmpdir, "index.html") 22 | x.save_html(f) 23 | return open(f, "r").read() 24 | 25 | 26 | def test_html_document_html_input(): 27 | # HTMLDocument with an tag. 28 | doc = HTMLDocument( 29 | tags.html( 30 | tags.head(tags.title("Title")), 31 | tags.body(div("Body content"), head_content("abcd")), 32 | myattr="value", 33 | ), 34 | lang="en", 35 | ) 36 | assert doc.render()["html"] == textwrap.dedent( 37 | """\ 38 | 39 | 40 | 41 | 42 | Title 43 | 44 | abcd 45 | 46 | 47 |
Body content
48 | 49 | """ 50 | ) 51 | 52 | doc = HTMLDocument( 53 | tags.html( 54 | head_content("abcd"), 55 | tags.head(tags.title("Title")), 56 | tags.body(div("Body content")), 57 | myattr="value", 58 | ), 59 | lang="en", 60 | ) 61 | assert doc.render()["html"] == textwrap.dedent( 62 | """\ 63 | 64 | 65 | 66 | 67 | Title 68 | 69 | abcd 70 | 71 | 72 |
Body content
73 | 74 | """ 75 | ) 76 | 77 | doc = HTMLDocument( 78 | tags.html( 79 | head_content("abcd"), 80 | tags.body(div("Body content")), 81 | myattr="value", 82 | ), 83 | lang="en", 84 | ) 85 | assert doc.render()["html"] == textwrap.dedent( 86 | """\ 87 | 88 | 89 | 90 | 91 | 92 | abcd 93 | 94 | 95 |
Body content
96 | 97 | """ 98 | ) 99 | 100 | doc = HTMLDocument( 101 | tags.html( 102 | div("Body content"), 103 | myattr="value", 104 | ), 105 | lang="en", 106 | ) 107 | assert doc.render()["html"] == textwrap.dedent( 108 | """\ 109 | 110 | 111 | 112 | 113 | 114 |
Body content
115 | """ 116 | ) 117 | 118 | # HTMLDocument with a tag. 119 | doc = HTMLDocument( 120 | tags.body(div("Body content"), head_content("abcd")), 121 | lang="en", 122 | ) 123 | assert doc.render()["html"] == textwrap.dedent( 124 | """\ 125 | 126 | 127 | 128 | 129 | 130 | abcd 131 | 132 | 133 |
Body content
134 | 135 | """ 136 | ) 137 | 138 | # HTMLDocument with a TagList. 139 | doc = HTMLDocument( 140 | TagList(div("Body content"), head_content("abcd")), 141 | lang="en", 142 | ) 143 | assert doc.render()["html"] == textwrap.dedent( 144 | """\ 145 | 146 | 147 | 148 | 149 | 150 | abcd 151 | 152 | 153 |
Body content
154 | 155 | """ 156 | ) 157 | 158 | 159 | def test_html_document_head_hoisting(): 160 | doc = HTMLDocument( 161 | div( 162 | "Hello, ", 163 | head_content( 164 | tags.script("alert('1')"), 165 | tags.style("span {color: red;}"), 166 | ), 167 | span("world", head_content(tags.script("alert('2')"))), 168 | head_content(tags.script("alert('2')")), 169 | ) 170 | ) 171 | 172 | assert doc.render()["html"] == textwrap.dedent( 173 | """\ 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 |
185 | Hello, world 186 |
187 | 188 | """ 189 | ) 190 | # In this case, render()["html"] and save_html() should produce the same thing. 191 | # That's not always true, like when there are html dependencies. 192 | assert doc.render()["html"] == saved_html(doc) 193 | 194 | 195 | def test_tagify_first(): 196 | # A Tagifiable object which returns a Tag with an HTMLDependency when `tagify()` is 197 | # called. 198 | class DelayedDep: 199 | dep = HTMLDependency( 200 | "testdep", 201 | "1.0", 202 | source={"package": "htmltools", "subdir": "libtest/testdep"}, 203 | script={"src": "testdep.js"}, 204 | stylesheet={"href": "testdep.css"}, 205 | ) 206 | 207 | def tagify(self): 208 | return div("delayed dependency", self.dep) 209 | 210 | x = TagList(div("Hello", DelayedDep()), "world") 211 | 212 | assert x.get_dependencies() == [] 213 | assert x.tagify().get_dependencies() == [DelayedDep.dep] 214 | 215 | result = x.tagify().render() 216 | assert result["dependencies"] == [DelayedDep.dep] 217 | 218 | result = HTMLDocument(x).render(lib_prefix=None) 219 | assert result["dependencies"] == [DelayedDep.dep] 220 | assert result["html"].find(' 21 | 22 | 23 | 24 | 25 | 37 | 38 | """ 39 | % (react_ver, react_dom_ver, react_ver, react_dom_ver) 40 | ) 41 | 42 | # Only the "top-level" tag gets wrapped in 50 | 51 | 52 | 53 | 54 | 69 | 70 | """ 71 | % (react_ver, react_dom_ver, react_ver, react_dom_ver) 72 | ) 73 | 74 | x = Foo( 75 | span(), 76 | "childtext", 77 | jsx("`childexpression`"), 78 | Foo(), 79 | [Foo(), Bar()], 80 | TagList(Foo(), Bar()), 81 | span(Foo(span()), Bar()), 82 | int=1, 83 | float=2.0, 84 | bool=True, 85 | None_=None, 86 | string="string", 87 | list=[1, 2, 3], 88 | ) 89 | assert str(x) == textwrap.dedent( 90 | """\ 91 | """ 121 | ) 122 | 123 | x = Foo( 124 | "Hello", 125 | span("world"), 126 | dict={"a": 1, "b": 2}, 127 | jsxTag=Bar(), 128 | style=css(color="red"), 129 | ) 130 | assert str(x) == textwrap.dedent( 131 | """\ 132 | """ 151 | ) 152 | 153 | x = Foo( 154 | htmlTag=[div(), div(foo=1)], 155 | ) 156 | assert str(x) == textwrap.dedent( 157 | """\ 158 | """ 172 | ) 173 | 174 | x = Foo( 175 | div(style=css(color="red")), 176 | func=jsx("() => console.log('foo')"), 177 | style={"margin": "1rem"}, 178 | ) 179 | assert str(x) == textwrap.dedent( 180 | """\ 181 | """ 197 | ) 198 | 199 | 200 | def test_jsx_tagifiable_children(): 201 | # Test case where children are Tagifiable but not Tag or JsxTag objects. 202 | Foo = jsx_tag_create("Foo") 203 | dep = HTMLDependency("a", "1.1", source={"subdir": "foo"}, script={"src": "a1.js"}) 204 | dep2 = HTMLDependency("b", "1.1", source={"subdir": "foo"}, script={"src": "b1.js"}) 205 | 206 | class TagifiableDep: 207 | def tagify(self): 208 | return dep 209 | 210 | class TagifiableDep2: 211 | def tagify(self): 212 | return span("Hello", Foo("world"), dep2) 213 | 214 | x = Foo(div(TagifiableDep(), TagifiableDep2())) 215 | # Make sure that calling render() doesn't alter the object in place and result in 216 | # output that changes from run to run. 217 | assert x.tagify() == x.tagify() 218 | assert HTMLDocument(x).render() == HTMLDocument(x).render() 219 | 220 | # Make sure that the dependency (which is added to the tree when MyTag.tagify() is 221 | # called) is properly registered. This makes sure that the JSX tag is getting the 222 | # dynamically-generated dependencies. 223 | assert dep in HTMLDocument(x).render()["dependencies"] 224 | assert dep2 in HTMLDocument(x).render()["dependencies"] 225 | assert HTMLDocument(x).render()["html"].find('') 226 | assert HTMLDocument(x).render()["html"].find('') 227 | 228 | assert str(x) == textwrap.dedent( 229 | """\ 230 | """ 255 | ) 256 | 257 | 258 | def test_jsx_tag_normalize_attr(): 259 | Foo = jsx_tag_create("Foo") 260 | x = Foo(class_="class_", x__="x__", x_="x_", x="x") 261 | assert x.attrs == {"class": "class_", "x-": "x__", "x": "x"} 262 | 263 | x = Foo(clAsS_="clAsS_", X__="X__") 264 | assert x.attrs == {"clAsS": "clAsS_", "X-": "X__"} 265 | 266 | x = Foo(clAsS_2="clAsS_2") 267 | assert x.attrs == {"clAsS-2": "clAsS_2"} 268 | 269 | 270 | def test_jsx_tag_attrs_update(): 271 | Foo = jsx_tag_create("Foo") 272 | 273 | # Update with dict 274 | x = Foo(a=1) 275 | x.attrs.update({"b": 2, "c": "C"}) 276 | assert x.attrs == {"a": 1, "b": 2, "c": "C"} 277 | 278 | # Update with kwargs 279 | x = Foo(a=1) 280 | x.attrs.update(b=2, c="C") 281 | assert x.attrs == {"a": 1, "b": 2, "c": "C"} 282 | 283 | # Update with dict and kwargs 284 | x = Foo(a=1) 285 | x.attrs.update({"b": 2}, c="C") 286 | assert x.attrs == {"a": 1, "b": 2, "c": "C"} 287 | -------------------------------------------------------------------------------- /tests/test_tags.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import os 3 | import textwrap 4 | from tempfile import TemporaryDirectory 5 | from typing import Any, Callable, Union, cast 6 | 7 | import pytest 8 | 9 | from htmltools import ( 10 | HTML, 11 | HTMLDependency, 12 | HTMLDocument, 13 | MetadataNode, 14 | Tag, 15 | TagFunction, 16 | TagList, 17 | TagNode, 18 | a, 19 | div, 20 | h1, 21 | h2, 22 | head_content, 23 | span, 24 | tags, 25 | ) 26 | 27 | 28 | def cast_tag(x: Any) -> Tag: 29 | assert isinstance(x, Tag) 30 | return x 31 | 32 | 33 | def expect_html(x: Any, expected: str): 34 | assert str(x) == expected 35 | 36 | 37 | def saved_html(x: Union[Tag, HTMLDocument], **kwargs) -> str: 38 | with TemporaryDirectory() as tmpdir: 39 | f = os.path.join(tmpdir, "index.html") 40 | x.save_html(f, **kwargs) 41 | return open(f, "r").read() 42 | 43 | 44 | def test_basic_tag_api(): 45 | children = [ 46 | h1("hello"), 47 | h2("world"), 48 | "text", 49 | 1, 50 | 2.1, 51 | None, 52 | ["list", ["here"]], 53 | ] 54 | props = dict(class_="foo", for_="bar", id="baz", bool="") 55 | x1 = div(*children, _add_ws=div().add_ws, **props) 56 | x2 = div() 57 | x2.append(*children) 58 | x2.attrs.update(**props) 59 | assert x1 == x2 60 | assert x1.attrs["id"] == "baz" 61 | assert x1.attrs["bool"] == "" 62 | assert str(x1) == textwrap.dedent( 63 | """\ 64 |
65 |

hello

66 |

world

67 | text12.1listhere 68 |
""" 69 | ) 70 | assert x1.attrs["class"] == "foo" 71 | x1.add_class("bar") 72 | assert x1.attrs["class"] == "foo bar" 73 | x1.add_class("baz", prepend=True) 74 | assert x1.attrs["class"] == "baz foo bar" 75 | assert ( 76 | x1.has_class("baz") 77 | and x1.has_class("foo") 78 | and x1.has_class("bar") 79 | and not x1.has_class("missing") 80 | ) 81 | # Add odd white space 82 | x1.attrs["class"] = " " + str(x1.attrs["class"]) + " " 83 | x1.remove_class(" foo") # leading space 84 | assert x1.has_class("bar") and not x1.has_class("foo") and x1.has_class("baz") 85 | x1.remove_class("baz ") # trailing space 86 | assert x1.attrs["class"] == "bar" 87 | x1.remove_class(" bar ") # lots of white space 88 | assert "class" not in x1.attrs.keys() 89 | 90 | x3 = TagList() 91 | x3.append(a()) 92 | x3.insert(0, span()) 93 | expect_html(x3, "
") 94 | 95 | x4 = div() 96 | 97 | x4.add_style(None) # type: ignore[reportGeneralTypeIssues] 98 | x4.add_style(False) # type: ignore[reportGeneralTypeIssues] 99 | assert "style" not in x4.attrs.keys() 100 | try: 101 | x4.add_style("color: red") 102 | raise AssertionError("Expected ValueError for missing semicolon") 103 | except ValueError as e: 104 | assert "must end with a semicolon" in str(e) 105 | 106 | x4.add_style("color: red;") 107 | x4.add_style("color: green;") 108 | x4.add_style("color: blue;", prepend=True) 109 | assert x4.attrs["style"] == "color: blue; color: red; color: green;" 110 | 111 | x5 = div() 112 | x5.add_style("color: &purple;") 113 | assert isinstance(x5.attrs["style"], str) 114 | assert x5.attrs["style"] == "color: &purple;" 115 | x5.add_style(HTML("color: &green;")) 116 | assert isinstance(x5.attrs["style"], HTML) 117 | assert x5.attrs["style"] == HTML("color: &purple; color: &green;") 118 | 119 | x6 = div() 120 | x6.add_style("color: &red;") 121 | assert isinstance(x6.attrs["style"], str) 122 | assert x6.attrs["style"] == "color: &red;" 123 | x6.add_style(HTML("color: &green;"), prepend=True) 124 | assert isinstance(x6.attrs["style"], HTML) 125 | assert x6.attrs["style"] == HTML("color: &green; color: &red;") 126 | assert isinstance(x6.attrs["style"], HTML) 127 | x6.add_style(HTML("color: &blue;")) 128 | assert x6.attrs["style"] == HTML("color: &green; color: &red; color: &blue;") 129 | 130 | 131 | def test_tag_list_dict(): 132 | # Dictionaries allowed at top level 133 | x1 = div("a", {"b": "B"}, "c") 134 | assert x1.attrs == {"b": "B"} 135 | assert str(x1) == '
\n ac\n
' 136 | 137 | # List args can't contain dictionaries 138 | with pytest.raises(TypeError): 139 | div(["a", {"b": "B"}], "c") # type: ignore 140 | 141 | # Nested list args can't contain dictionaries 142 | with pytest.raises(TypeError): 143 | div(["a", ["b", {"c": "C"}]], "d") # type: ignore 144 | 145 | # `children` can't contain dictionaries 146 | with pytest.raises(TypeError): 147 | div({"class": "foo"}, children=[{"class_": "bar"}], class_="baz") # type: ignore 148 | 149 | 150 | def test_tag_attrs_update(): 151 | # Update with dict 152 | x = div(a=1) 153 | x.attrs.update({"b": "2", "c": "C"}) 154 | assert x.attrs == {"a": "1", "b": "2", "c": "C"} 155 | 156 | # Update with kwargs 157 | x = div(a=1) 158 | x.attrs.update(b=2, c="C") 159 | assert x.attrs == {"a": "1", "b": "2", "c": "C"} 160 | 161 | # Update with dict and kwargs 162 | x = div(a=1) 163 | x.attrs.update({"b": "2"}, c="C") 164 | assert x.attrs == {"a": "1", "b": "2", "c": "C"} 165 | 166 | 167 | def test_tag_multiple_repeated_attrs(): 168 | foo_attrs = div({"class": "foo"}).attrs 169 | bar_attrs = div({"class": "bar"}).attrs 170 | x = div({"class": "foo", "class_": "bar"}, class_="baz") 171 | y = div(foo_attrs, {"class_": "bar"}, class_="baz") 172 | z = div({"class": "foo"}, bar_attrs, class_="baz") 173 | assert x.attrs == {"class": "foo bar baz"} 174 | assert y.attrs == {"class": "foo bar baz"} 175 | assert z.attrs == {"class": "foo bar baz"} 176 | x.attrs.update({"class": "bap", "class_": "bas"}, class_="bat") 177 | assert x.attrs == {"class": "bap bas bat"} 178 | 179 | 180 | def test_non_escaped_text_is_escaped_when_added_to_html(): 181 | x = HTML("&") + " &" 182 | x_str = str(x) 183 | assert isinstance(x, HTML) 184 | assert x_str == "& &" 185 | 186 | 187 | def test_html_equals_html(): 188 | x = "

a top level

\n" 189 | a = HTML(x) 190 | b = HTML(x) 191 | assert a == b 192 | assert a == x 193 | assert x == b 194 | assert x == x # for completeness 195 | 196 | 197 | def test_html_adds_str_or_html(): 198 | # first = "first" 199 | # second = "second" 200 | # firstsecond = first + second 201 | 202 | amp = "&" 203 | esc_amp = "&" 204 | 205 | none = amp + amp 206 | first_html = HTML(amp) + amp 207 | second_html = amp + HTML(amp) 208 | 209 | both_html = HTML(amp) + HTML(amp) 210 | 211 | assert TagList(none).get_html_string() == f"{esc_amp}{esc_amp}" 212 | assert isinstance(none, str) 213 | 214 | assert TagList(first_html).get_html_string() == f"{amp}{esc_amp}" 215 | assert isinstance(first_html, HTML) 216 | 217 | assert TagList(second_html).get_html_string() == f"{esc_amp}{amp}" 218 | assert isinstance(second_html, HTML) 219 | 220 | assert TagList(both_html).get_html_string() == f"{amp}{amp}" 221 | assert isinstance(both_html, HTML) 222 | 223 | 224 | def test_tag_shallow_copy(): 225 | dep = HTMLDependency( 226 | "a", "1.1", source={"package": None, "subdir": "foo"}, script={"src": "a1.js"} 227 | ) 228 | x = div(tags.i("hello", prop="value"), "world", dep, class_="myclass") 229 | y = copy.copy(x) 230 | cast_tag(y.children[0]).children[0] = "HELLO" 231 | cast_tag(y.children[0]).attrs["prop"] = "VALUE" 232 | y.children[1] = "WORLD" 233 | y.attrs["class"] = "MYCLASS" 234 | cast(HTMLDependency, y.children[2]).name = "A" 235 | 236 | # With a shallow copy(), the .attrs and .children are shallow copies, but if a 237 | # child is modified in place, then the the original child is modified as well. 238 | assert x is not y 239 | assert x.attrs == {"class": "myclass"} 240 | assert x.children is not y.children 241 | # If a mutable child is modified in place, both x and y see the changes. 242 | assert x.children[0] is y.children[0] 243 | assert cast_tag(x.children[0]).children[0] == "HELLO" 244 | # Immutable children can't be changed in place. 245 | assert x.children[1] is not y.children[1] 246 | assert x.children[1] == "world" 247 | assert x.children[1] is not y.children[1] 248 | # An HTMLDependency is mutable, so it is modified in place. 249 | assert cast(HTMLDependency, x.children[2]).name == "A" 250 | assert cast(HTMLDependency, y.children[2]).name == "A" 251 | assert x.children[2] is y.children[2] 252 | 253 | 254 | def test_tagify_deep_copy(): 255 | # Each call to .tagify() should do a shallow copy, but since it recurses, the result 256 | # is a deep copy. 257 | dep = HTMLDependency( 258 | "a", "1.1", source={"package": None, "subdir": "foo"}, script={"src": "a1.js"} 259 | ) 260 | x = div(tags.i("hello", prop="value"), "world", dep, class_="myclass") 261 | 262 | y = x.tagify() 263 | cast_tag(y.children[0]).children[0] = "HELLO" 264 | cast_tag(y.children[0]).attrs["prop"] = "VALUE" 265 | y.children[1] = "WORLD" 266 | y.attrs["class"] = "MYCLASS" 267 | cast(HTMLDependency, y.children[2]).name = "A" 268 | 269 | assert x.attrs == {"class": "myclass"} 270 | assert y.attrs == {"class": "MYCLASS"} 271 | assert cast_tag(x.children[0]).attrs == {"prop": "value"} 272 | assert cast_tag(y.children[0]).attrs == {"prop": "VALUE"} 273 | assert cast_tag(x.children[0]).children[0] == "hello" 274 | assert cast_tag(y.children[0]).children[0] == "HELLO" 275 | assert x.children[1] == "world" 276 | assert y.children[1] == "WORLD" 277 | assert cast(HTMLDependency, x.children[2]).name == "a" 278 | assert cast(HTMLDependency, y.children[2]).name == "A" 279 | assert x.children[2] is not y.children[2] 280 | 281 | 282 | def test_tag_writing(): 283 | expect_html(TagList("hi"), "hi") 284 | expect_html(TagList("one", "two", TagList("three")), "onetwothree") 285 | expect_html(tags.b("one"), "one") 286 | expect_html(tags.b("one", "two"), "onetwo") 287 | expect_html(TagList(["one"]), "one") 288 | expect_html(TagList([TagList("one")]), "one") 289 | expect_html(TagList(tags.br(), "one"), "
one") 290 | assert ( 291 | str(tags.b("one", "two", span("foo", "bar", span("baz")))) 292 | == "onetwofoobarbaz" 293 | ) 294 | expect_html(tags.area(), "") 295 | 296 | 297 | def test_tag_inline(): 298 | # Empty inline/block tags 299 | expect_html(div(), "
") 300 | expect_html(span(), "") 301 | 302 | # Inline/block tags with one item 303 | expect_html(div("a"), "
a
") 304 | expect_html(span("a"), "a") 305 | 306 | # Inline tags with two children 307 | expect_html( 308 | span("a", "b"), 309 | "ab", 310 | ) 311 | expect_html( 312 | span(span("a"), "b"), 313 | "ab", 314 | ) 315 | expect_html( 316 | span("a", span("b")), 317 | "ab", 318 | ) 319 | expect_html( 320 | span(span("a"), span("b")), 321 | "ab", 322 | ) 323 | 324 | # Block tags with two inline children 325 | expect_html( 326 | div("a", "b"), 327 | textwrap.dedent( 328 | """\ 329 |
330 | ab 331 |
""" 332 | ), 333 | ) 334 | expect_html( 335 | div(span("a"), "b"), 336 | textwrap.dedent( 337 | """\ 338 |
339 | ab 340 |
""" 341 | ), 342 | ) 343 | expect_html( 344 | div("a", span("b")), 345 | textwrap.dedent( 346 | """\ 347 |
348 | ab 349 |
""" 350 | ), 351 | ) 352 | expect_html( 353 | div(span("a"), span("b")), 354 | textwrap.dedent( 355 | """\ 356 |
357 | ab 358 |
""" 359 | ), 360 | ) 361 | 362 | # Block tags with one block and one inline child 363 | expect_html( 364 | div("a", div("b")), 365 | textwrap.dedent( 366 | """\ 367 |
368 | a 369 |
b
370 |
""" 371 | ), 372 | ) 373 | expect_html( 374 | div(span("a"), div("b")), 375 | textwrap.dedent( 376 | """\ 377 |
378 | a 379 |
b
380 |
""" 381 | ), 382 | ) 383 | expect_html( 384 | div(div("a"), "b"), 385 | textwrap.dedent( 386 | """\ 387 |
388 |
a
389 | b 390 |
""" 391 | ), 392 | ) 393 | expect_html( 394 | div(div("a"), span("b")), 395 | textwrap.dedent( 396 | """\ 397 |
398 |
a
399 | b 400 |
""" 401 | ), 402 | ) 403 | 404 | # Block tag with two block children 405 | expect_html( 406 | div(div("a"), div("b")), 407 | textwrap.dedent( 408 | """\ 409 |
410 |
a
411 |
b
412 |
""" 413 | ), 414 | ) 415 | 416 | # Block tag with three children; mix of inline and block 417 | expect_html( 418 | div(span("a"), span("b"), div("c")), 419 | textwrap.dedent( 420 | """\ 421 |
422 | ab 423 |
c
424 |
""" 425 | ), 426 | ) 427 | expect_html( 428 | div(span("a"), "b", div("c")), 429 | textwrap.dedent( 430 | """\ 431 |
432 | ab 433 |
c
434 |
""" 435 | ), 436 | ) 437 | expect_html( 438 | div(div("a"), "b", span("c")), 439 | textwrap.dedent( 440 | """\ 441 |
442 |
a
443 | bc 444 |
""" 445 | ), 446 | ) 447 | 448 | # More complex nesting 449 | expect_html( 450 | div(span(tags.b("a")), span(tags.b("b"))), 451 | textwrap.dedent( 452 | """\ 453 |
454 | ab 455 |
""" 456 | ), 457 | ) 458 | expect_html( 459 | span(span(tags.b("a")), span(tags.b("b")), span("c")), 460 | textwrap.dedent( 461 | """\ 462 | abc""" 463 | ), 464 | ) 465 | expect_html( 466 | div(div(span("a")), span(tags.b("b"))), 467 | textwrap.dedent( 468 | """\ 469 |
470 |
471 | a 472 |
473 | b 474 |
""" 475 | ), 476 | ) 477 | 478 | 479 | def test_tag_list_ws(): 480 | x = TagList(span("a"), "b") 481 | expect_html(x.get_html_string(), "ab") 482 | expect_html(x.get_html_string(add_ws=True), "ab") 483 | expect_html(x.get_html_string(indent=1, add_ws=True), " ab") 484 | 485 | x = TagList(div("a"), "b") 486 | expect_html(x.get_html_string(), "
a
\nb") 487 | expect_html(x.get_html_string(add_ws=True), "
a
\nb") 488 | expect_html(x.get_html_string(indent=2, add_ws=True), "
a
\n b") 489 | 490 | x = TagList("a", "b", div("c", "d"), span("e", "f"), span("g", "h")) 491 | expect_html( 492 | x.get_html_string(), 493 | textwrap.dedent( 494 | """\ 495 | ab 496 |
497 | cd 498 |
499 | efgh""" 500 | ), 501 | ) 502 | expect_html( 503 | x.get_html_string(add_ws=True), 504 | textwrap.dedent( 505 | """\ 506 | ab 507 |
508 | cd 509 |
510 | efgh""" 511 | ), 512 | ) 513 | expect_html( 514 | x.get_html_string(indent=1, add_ws=True), 515 | """ ab 516 |
517 | cd 518 |
519 | efgh""", 520 | ) 521 | 522 | 523 | # This set of tests is commented out because we're not currently enforcing any 524 | # particular behavior for invalid inline/block nesting. 525 | # def test_tag_inline_invalid(): 526 | # # This set of tests is for invalid inline/block nesting. Normally, block tags can 527 | # # contain inline or block tags, but inline tags can only contain inline tags. 528 | # # 529 | # # These tests cover the invalid cases where inline tags contain block tags. 530 | # expect_html( 531 | # span(div("a")), 532 | # "ab", 533 | # ) 534 | # expect_html( 535 | # span(div("a"), "b"), 536 | # "ab", 537 | # ) 538 | # expect_html( 539 | # span("a", div("b")), 540 | # "ab", 541 | # ) 542 | # expect_html( 543 | # span(div("a"), div("b")), 544 | # "ab", 545 | # ) 546 | 547 | 548 | def test_tag_repr(): 549 | assert repr(div()) == str(div()) 550 | assert repr(div("foo", "bar", id="id")) == str(div("foo", "bar", id="id")) 551 | assert repr(div(id="id", class_="cls", foo="bar")) == str( 552 | div(id="id", class_="cls", foo="bar") 553 | ) 554 | 555 | 556 | def test_tag_escaping(): 557 | # Regular text is escaped 558 | expect_html(div(""), "
<a&b>
") 559 | # Children wrapped in HTML() isn't escaped 560 | expect_html(div(HTML("")), "
") 561 | # Attributes are HTML escaped 562 | expect_html(div("text", class_=""), '
text
') 563 | expect_html(div("text", class_="'ab'"), '
text
') 564 | # Attributes support `HTML()` values 565 | expect_html(div("text", class_=HTML("")), '
text
') 566 | 567 | # script and style tags are not escaped 568 | assert str(tags.script("a && b > 3")) == "" 569 | assert str(tags.script("a && b", "x > 3")) == "" 570 | assert str(tags.script("a && b\nx > 3")) == "" 571 | assert str(tags.style("a && b > 3")) == "" 572 | 573 | 574 | def test_html_save(): 575 | assert saved_html(div()) == textwrap.dedent( 576 | """\ 577 | 578 | 579 | 580 | 581 | 582 | 583 |
584 | 585 | """ 586 | ) 587 | 588 | dep = HTMLDependency( 589 | "foo", 590 | "1.0", 591 | source={"package": "htmltools", "subdir": "libtest"}, 592 | stylesheet={"href": "testdep/testdep.css"}, 593 | script={"src": "testdep/testdep.js"}, 594 | ) 595 | assert saved_html(div("foo", dep), libdir=None) == textwrap.dedent( 596 | """\ 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 |
foo
607 | 608 | """ 609 | ) 610 | 611 | doc = HTMLDocument( 612 | div("foo", dep), tags.meta(name="description", content="test"), lang="en" 613 | ) 614 | assert saved_html(doc) == textwrap.dedent( 615 | """\ 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 |
foo
626 | 627 | 628 | """ 629 | ) 630 | 631 | 632 | def test_tag_str(): 633 | # Make sure Tag.__str__() doesn't return an HTML string 634 | # (since, in that case, it'll render as HTML, which is suprising) 635 | x = str(span()) 636 | y = repr(span()) 637 | assert isinstance(x, str) and not isinstance(x, HTML) 638 | assert isinstance(y, str) and not isinstance(y, HTML) 639 | 640 | # Same for TagList.__str__ 641 | x = str(TagList("foo")) 642 | y = str(TagList("foo")) 643 | assert isinstance(x, str) and not isinstance(x, HTML) 644 | assert isinstance(y, str) and not isinstance(y, HTML) 645 | 646 | x = str(HTML("foo&bar")) 647 | y = repr(HTML("foo&bar")) 648 | assert isinstance(x, str) and not isinstance(x, HTML) 649 | assert isinstance(y, str) and not isinstance(y, HTML) 650 | 651 | 652 | # Walk a Tag tree, and apply a function to each node. The node in the tree will be 653 | # replaced with the value returned from `fn()`. If the function alters a node, then it 654 | # will be reflected in the original object that `.walk_mutate()` was called on. 655 | # 656 | # Note that if we were to export this function (perhaps in a class method), some other 657 | # possible variants are: 658 | # * Instead of one `fn`, take `pre` and `post` functions. 659 | # * Allow functions that return `TagChild`, and then flatten/convert those to `TagNode`. 660 | # * Provide a `_walk` function that doesn't mutate the tree. It would return `None`, and 661 | # `fn` should return `None`. This could be useful when `fn` just collects things from 662 | # the tree. 663 | def _walk_mutate(x: TagNode, fn: Callable[[TagNode], TagNode]) -> TagNode: 664 | x = fn(x) 665 | if isinstance(x, Tag): 666 | for i, child in enumerate(x.children): 667 | x.children[i] = _walk_mutate(child, fn) 668 | elif isinstance(x, list): 669 | for i, child in enumerate(x): 670 | x[i] = _walk_mutate(child, fn) # pyright: ignore[reportArgumentType] 671 | return x 672 | 673 | 674 | def test_tag_walk(): 675 | # walk() alters the tree in place, and also returns the altered object. 676 | x = div("hello ", tags.i("world")) 677 | y = div("The value of x is: ", x) 678 | 679 | def alter(x: TagNode) -> TagNode: 680 | if isinstance(x, str): 681 | return x.upper() 682 | elif isinstance(x, Tag): 683 | x.attrs["a"] = "foo" 684 | if x.name == "i": 685 | x.name = "b" 686 | 687 | return x 688 | 689 | res = _walk_mutate(x, alter) 690 | 691 | assert y.children[1] is x 692 | assert x is res 693 | 694 | assert x.attrs.get("a") == "foo" 695 | assert x.children[0] == "HELLO " 696 | assert cast_tag(x.children[1]).name == "b" 697 | assert cast_tag(x.children[1]).attrs.get("a") == "foo" 698 | assert cast_tag(x.children[1]).children[0] == "WORLD" 699 | 700 | 701 | def test_taglist_constructor(): 702 | 703 | # From docs.python.org/3/library/collections.html#collections.UserList: 704 | # > Subclasses of UserList are expected to offer a constructor which can be called 705 | # > with either no arguments or one argument. List operations which return a new 706 | # > sequence attempt to create an instance of the actual implementation class. To do 707 | # > so, it assumes that the constructor can be called with a single parameter, which 708 | # > is a sequence object used as a data source. 709 | 710 | x = TagList() 711 | assert isinstance(x, TagList) 712 | assert len(x) == 0 713 | assert x.get_html_string() == "" 714 | 715 | x = TagList("foo") 716 | assert isinstance(x, TagList) 717 | assert len(x) == 1 718 | assert x.get_html_string() == "foo" 719 | 720 | x = TagList(["foo", "bar"]) 721 | assert isinstance(x, TagList) 722 | assert len(x) == 2 723 | assert x.get_html_string() == "foobar" 724 | 725 | # Also support multiple inputs 726 | x = TagList("foo", "bar") 727 | assert isinstance(x, TagList) 728 | assert len(x) == 2 729 | assert x.get_html_string() == "foobar" 730 | 731 | 732 | def test_taglist_add(): 733 | 734 | # Similar to `HTML(UserString)`, a `TagList(UserList)` should be the result when 735 | # adding to anything else. 736 | 737 | empty_arr = [] 738 | int_arr = [1] 739 | tl_foo = TagList("foo") 740 | tl_bar = TagList("bar") 741 | 742 | def assert_tag_list(x: TagList, contents: list[str]) -> None: 743 | assert isinstance(x, TagList) 744 | assert len(x) == len(contents) 745 | for i, content_item in enumerate(contents): 746 | assert x[i] == content_item 747 | 748 | # Make sure the TagLists are not altered over time 749 | assert len(empty_arr) == 0 750 | assert len(int_arr) == 1 751 | assert len(tl_foo) == 1 752 | assert len(tl_bar) == 1 753 | assert int_arr[0] == 1 754 | assert tl_foo[0] == "foo" 755 | assert tl_bar[0] == "bar" 756 | 757 | assert_tag_list(empty_arr + tl_foo, ["foo"]) 758 | assert_tag_list(tl_foo + empty_arr, ["foo"]) 759 | assert_tag_list(int_arr + tl_foo, ["1", "foo"]) 760 | assert_tag_list(tl_foo + int_arr, ["foo", "1"]) 761 | assert_tag_list(tl_foo + tl_bar, ["foo", "bar"]) 762 | assert_tag_list(tl_foo + "bar", ["foo", "bar"]) 763 | assert_tag_list("foo" + tl_bar, ["foo", "bar"]) 764 | 765 | 766 | def test_taglist_methods(): 767 | # Testing methods from https://docs.python.org/3/library/stdtypes.html#common-sequence-operations 768 | # 769 | # Operation | Result | Notes 770 | # --------- | ------ | ----- 771 | # x in s | True if an item of s is equal to x, else False | (1) 772 | # x not in s | False if an item of s is equal to x, else True | (1) 773 | # s + t | the concatenation of s and t | (6)(7) 774 | # s * n or n * s | equivalent to adding s to itself n times | (2)(7) 775 | # s[i] | ith item of s, origin 0 | (3) 776 | # s[i:j] | slice of s from i to j | (3)(4) 777 | # s[i:j:k] | slice of s from i to j with step k | (3)(5) 778 | # len(s) | length of s 779 | # min(s) | smallest item of s 780 | # max(s) | largest item of s 781 | # s.index(x[, i[, j]]) | index of the first occurrence of x in s (at or after index i and before index j) | (8) 782 | # s.count(x) | total number of occurrences of x in s 783 | 784 | x = TagList("foo", "bar", "foo", "baz") 785 | y = TagList("a", "b", "c") 786 | 787 | assert "bar" in x 788 | assert "qux" not in x 789 | 790 | add = x + y 791 | assert isinstance(add, TagList) 792 | assert list(add) == ["foo", "bar", "foo", "baz", "a", "b", "c"] 793 | 794 | mul = x * 2 795 | assert isinstance(mul, TagList) 796 | assert list(mul) == ["foo", "bar", "foo", "baz", "foo", "bar", "foo", "baz"] 797 | 798 | assert x[1] == "bar" 799 | assert x[1:3] == TagList("bar", "foo") 800 | assert mul[1:6:2] == TagList("bar", "baz", "bar") 801 | 802 | assert len(x) == 4 803 | 804 | assert min(x) == "bar" # pyright: ignore[reportArgumentType] 805 | assert max(x) == "foo" # pyright: ignore[reportArgumentType] 806 | 807 | assert x.index("foo") == 0 808 | assert x.index("foo", 1) == 2 809 | with pytest.raises(ValueError): 810 | x.index("foo", 1, 1) 811 | 812 | assert x.count("foo") == 2 813 | assert mul.count("foo") == 4 814 | 815 | 816 | def test_taglist_extend(): 817 | x = TagList("foo") 818 | y = ["bar", "baz"] 819 | x.extend(y) 820 | assert isinstance(x, TagList) 821 | assert list(x) == ["foo", "bar", "baz"] 822 | assert y == ["bar", "baz"] 823 | 824 | x = TagList("foo") 825 | y = TagList("bar", "baz") 826 | x.extend(y) 827 | assert isinstance(x, TagList) 828 | assert list(x) == ["foo", "bar", "baz"] 829 | assert list(y) == ["bar", "baz"] 830 | 831 | x = TagList("foo") 832 | y = "bar" 833 | x.extend(y) 834 | assert list(x) == ["foo", "bar"] 835 | assert y == "bar" 836 | 837 | x = TagList("foo") 838 | x.extend(TagList("bar")) 839 | assert list(x) == ["foo", "bar"] 840 | 841 | 842 | def test_taglist_flatten(): 843 | x = div(1, TagList(2, TagList(span(3), 4))) 844 | assert list(x.children) == ["1", "2", span("3"), "4"] 845 | 846 | x = TagList(1, TagList(2, TagList(span(3), 4))) 847 | assert list(x) == ["1", "2", span("3"), "4"] 848 | 849 | 850 | def test_taglist_insert(): 851 | x = TagList(1, 2, 3, 4) 852 | x.insert(2, "new") 853 | assert list(x) == ["1", "2", "new", "3", "4"] 854 | 855 | x = TagList(1, 2, 3, 4) 856 | x.insert(2, TagList("new")) 857 | assert list(x) == ["1", "2", "new", "3", "4"] 858 | 859 | x = TagList(1, 2, 3, 4) 860 | x.insert(2, TagList("new", "new2")) 861 | assert list(x) == ["1", "2", "new", "new2", "3", "4"] 862 | 863 | 864 | def test_taglist_tagifiable(): 865 | # When a TagList contains a Tagifiable object which returns a TagList, make sure it 866 | # gets flattened into the original TagList. 867 | class Foo: 868 | def __init__(self, *args) -> None: 869 | self._content = TagList(*args) 870 | 871 | def tagify(self) -> TagList: 872 | return self._content.tagify() 873 | 874 | x = TagList(1, Foo(), 2) 875 | assert list(x.tagify()) == ["1", "2"] 876 | 877 | x = TagList(1, Foo("foo"), 2) 878 | assert list(x.tagify()) == ["1", "foo", "2"] 879 | 880 | x = TagList(1, Foo("foo", span("bar")), 2) 881 | assert list(x.tagify()) == ["1", "foo", span("bar"), "2"] 882 | 883 | # Recursive tagify() 884 | x = TagList(1, Foo("foo", Foo("bar")), 2) 885 | assert list(x.tagify()) == ["1", "foo", "bar", "2"] 886 | 887 | # Make sure it works for Tag objects as well. 888 | x = div(1, Foo("foo", Foo("bar")), 2) 889 | assert list(x.tagify().children) == ["1", "foo", "bar", "2"] 890 | 891 | 892 | def test_attr_vals(): 893 | attrs = { 894 | "none": None, 895 | "false": False, 896 | "true": True, 897 | "str": "a", 898 | "int": 1, 899 | "float": 1.2, 900 | } 901 | test = TagList(div(**attrs), div(class_="foo").add_class("bar")) 902 | 903 | assert ( 904 | str(test) 905 | == """
906 |
""" 907 | ) 908 | 909 | 910 | def test_tag_normalize_attr(): 911 | x = div(class_="class_", x__="x__", x_="x_", x="x") 912 | assert x.attrs == {"class": "class_", "x-": "x__", "x": "x_ x"} 913 | 914 | x = div(foo_bar="baz") 915 | assert x.attrs == {"foo-bar": "baz"} 916 | 917 | 918 | def test_metadata_nodes_gone(): 919 | # Make sure MetadataNodes don't result in a blank line. 920 | assert str(div(span("Body content"), head_content("abc"))) == textwrap.dedent( 921 | """\ 922 |
923 | Body content 924 |
""" 925 | ) 926 | 927 | assert ( 928 | str(TagList(span("Body content"), MetadataNode())) 929 | == "Body content" 930 | ) 931 | 932 | 933 | def test_repr_html(): 934 | # Make sure that objects with a __repr_html__ method are rendered as HTML. 935 | class Foo: 936 | def _repr_html_(self) -> str: 937 | return "Foo" 938 | 939 | f = Foo() 940 | assert str(TagList(f)) == "Foo" 941 | assert str(span(f)) == "Foo" 942 | assert str(div(f)) == textwrap.dedent( 943 | """\ 944 |
945 | Foo 946 |
""" 947 | ) 948 | 949 | 950 | def test_types(): 951 | # When a type checker like pyright is run on this file, this line will make sure 952 | # that a Tag function like `div()` matches the signature of the TagFunction Protocol 953 | # class. 954 | f: TagFunction = div # noqa: F841 955 | -------------------------------------------------------------------------------- /tests/test_tags_context.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from typing import cast 3 | 4 | import pytest 5 | 6 | from htmltools import Tag, TagList, tags 7 | 8 | 9 | def test_tag_context_manager(): 10 | # This tag is here to set sys.displayhook to something that collects the contents 11 | # inside its `with` statement. We're interested in checking the children of 12 | # wrapper_tag. 13 | wrapper_tag = tags.body() 14 | with wrapper_tag: 15 | with tags.div(): 16 | # Each line inside has sys.displayhook called on it, so we can check that 17 | # that function was replaced correctly. Note that normally when these 18 | # context managers are used, the evaluation environment (like Shiny Express, 19 | # or Jupyter) will automatically call sys.displayhook on each line. 20 | sys.displayhook("Hello, ") 21 | sys.displayhook(tags.i("big")) 22 | with tags.b(): 23 | sys.displayhook("world") 24 | sys.displayhook("!") 25 | 26 | res = wrapper_tag.children[0] 27 | assert str(res) == "
\n Hello, bigworld!\n
" 28 | 29 | # Make sure that TagChildren are properly added to the parent. 30 | with tags.span(): 31 | sys.displayhook(["a", 1, tags.b("bold")]) 32 | sys.displayhook(TagList("c", 2, tags.i("italic"))) 33 | sys.displayhook(3) 34 | 35 | res = cast(Tag, wrapper_tag.children[1]) 36 | assert str(res) == "a1boldc2italic3" 37 | # Make sure the list and TagList were flattened when added to the parent, just 38 | # like if they were passed to `span([...], TagList(...))`. 39 | assert len(res.children) == 7 40 | 41 | 42 | def test_tag_context_manager_type_validate(): 43 | # Make sure Tag context managers validate types of inputs 44 | # Pass in objects that aren't valid TagChildren 45 | with pytest.raises(TypeError): 46 | with tags.span(): 47 | # Pass in a set, which is not a valid TagChild 48 | sys.displayhook({1, 2, 3}) 49 | 50 | with pytest.raises(TypeError): 51 | with tags.span(): 52 | # Can't pass in a dictionary -- this is a TagAttrs object, but not TagChild. 53 | sys.displayhook({"class": "foo", "id": "bar"}) 54 | 55 | with pytest.raises(TypeError): 56 | with tags.span(): 57 | sys.displayhook("A") 58 | # Pass in a module object, which is not a valid TagChild 59 | sys.displayhook(tags) 60 | 61 | 62 | if __name__ == "__main__": 63 | with tags.div(): 64 | sys.displayhook({1, 2, 3}) 65 | -------------------------------------------------------------------------------- /tests/test_util.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Union 2 | 3 | import pytest 4 | 5 | from htmltools import TagList, css, div, span 6 | from htmltools._util import flatten 7 | 8 | 9 | def test_css_type(): 10 | for val in (True, False): 11 | css_args: Dict[str, Union[str, None]] = { 12 | "font_size": "12px" if val else None, 13 | "background_color": "red", 14 | } 15 | expected_val = ( 16 | "font-size:12px;background-color:red;" if val else "background-color:red;" 17 | ) 18 | # If `css(collapse_=)` does not accept the same type as `**kwargs: str | float | None`, 19 | # then this will produce a type error with pyright (for anyone spreading kwargs). 20 | assert css(**css_args) == expected_val 21 | 22 | pytest.raises(TypeError, css, collapse_=1, font_size="12px") 23 | 24 | 25 | def test_flatten(): 26 | assert flatten([[]]) == [] 27 | assert flatten([1, [2], ["3", [4, None, 5, div(div()), div()]]]) == [ 28 | 1, 29 | 2, 30 | "3", 31 | 4, 32 | 5, 33 | div(div()), 34 | div(), 35 | ] 36 | 37 | # Make sure that _flatten() doesn't alter original objects in place. 38 | x = [4, None, 5, div(div()), div()] 39 | y = ["3", x] 40 | assert flatten([1, [2], y]) == [1, 2, "3", 4, 5, div(div()), div()] 41 | assert x == [4, None, 5, div(div()), div()] 42 | assert y == ["3", [4, None, 5, div(div()), div()]] 43 | 44 | # Tuples 45 | x1: list[Any] = [1, [2], ("3", [4, None, 5, div(div()), div()])] 46 | assert flatten(x1) == [ 47 | 1, 48 | 2, 49 | "3", 50 | 4, 51 | 5, 52 | div(div()), 53 | div(), 54 | ] 55 | 56 | # Flattening TagList. Note that the TagList itself converts its numeric children to 57 | # strings, so 1 and 2 become "1" and "2". 58 | x2: list[Any] = [0, TagList(1, 2, div(), TagList(span(div()), span())), (3, 4)] 59 | assert list(flatten(x2)) == [0, "1", "2", div(), span(div()), span(), 3, 4] 60 | assert flatten([1, [TagList("2"), 3], 4]) == [1, "2", 3, 4] 61 | --------------------------------------------------------------------------------