[
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\nbuy_me_a_coffee: andereoo\n"
  },
  {
    "path": ".gitignore",
    "content": "build/\ntkinterweb/__pycache__/\ntkinterweb/resources/\ntkinterweb.egg-info\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "version: \"2\"\n\nbuild:\n  os: \"ubuntu-22.04\"\n  tools:\n    python: \"3.10\"\n\npython:\n  install:\n    - requirements: docs/requirements.txt\n\nsphinx:\n  configuration: docs/source/conf.py"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2021-2025 Andrew Clarke\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "MANIFEST.in",
    "content": "recursive-include tkinterweb/resources *\r\n"
  },
  {
    "path": "README.md",
    "content": "![PyPi Downloads](https://static.pepy.tech/badge/tkinterweb/month)\n![MIT Licence](https://img.shields.io/pypi/l/tkinterweb) \n![Python 3](https://img.shields.io/pypi/pyversions/tkinterweb)\n![Made in Canada](https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%A6%20made%20in%20Canada-grey)\n\n<div style=\"margin-bottom: 0px\"><a href=\"https://www.buymeacoffee.com/andereoo\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-violet.png\" alt=\"Buy Me A Coffee\" style=\"height: 36px; width: 146px;\" ></a></div>\n\n<p align=\"center\"><img src=\"./docs/source/_static/banner.png\" style=\"width: 425px\"/></p>\n\n**<p align=\"center\">Fast and lightweight web browser, rich text, and app design widgets for Tkinter.</p>**\n\n## Overview\n**TkinterWeb adds HTML and CSS rendering capabilities to Tkinter widgets.**\n\nCommon use cases include displaying help files, documentation, and other HTML content, rendering images (including SVG), building rich-text editors, designing apps with HTML templates, and creating more modern-looking interfaces, with advanced styling and even round buttons!\n\nAll [major operating systems](https://tkinterweb.readthedocs.io/en/latest/compatibility.html#a-note-on-tkhtml-binaries) running Python 3.2+ are supported. \n\n## Usage\n\n**TkinterWeb provides:**\n* A [frame widget](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html) to display and edit websites, help files, RSS feeds, and any other styled HTML in Tkinter.\n* A [label widget](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html#tkinterweb.HtmlLabel) that can display styled HTML.\n* A [text widget](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html#tkinterweb.HtmlText) that allows the user to edit styled HTML.\n\n**TkinterWeb can be used in any Tkinter application to display and edit websites, help pages, documentation, and much more! Here is an example:**\n```\nimport tkinter as tk\nfrom tkinterweb import HtmlFrame # import the HtmlFrame widget\n\nroot = tk.Tk() # create the Tkinter window\nframe = HtmlFrame(root, messages_enabled=True) # create the HtmlFrame widget\nframe.load_website(\"https://tkinterweb.readthedocs.io/en/latest/index.html\") # load a website\nframe.pack(fill=\"both\", expand=True) # attach the HtmlFrame widget to the window\nroot.mainloop()\n```\n![TkinterWeb](/images/tkinterweb-demo.png)\n\nSee [Getting Started](https://tkinterweb.readthedocs.io/en/latest/usage.html) for more tips and tricks.\n\n## Installation\nTo install TkinterWeb, simply type `pip install tkinterweb[recommended]` in the command prompt or terminal. That's it!\n\nOr, you can also choose from the following extras: `pip install tkinterweb[html,images,svg,javascript,requests]`. You can also use `pip install tkinterweb[full]` to install all optional dependencies or ``pip install tkinterweb`` to install the bare minimum.\n\n## Dependencies\n\n**TkinterWeb offers bindings and extensions to a modified version of the Tkhtml3 widget from [http://tkhtml.tcl.tk](https://web.archive.org/web/20250219233338/http://tkhtml.tcl.tk/). Tkinter and the [TkinterWeb-Tkhtml](https://pypi.org/project/tkinterweb-tkhtml/) package are required.**\n\nI also **strongly** recommended installing the following:\n* [TkinterWeb-Tkhtml-Extras](https://pypi.org/project/tkinterweb-tkhtml-extras/) (for better HTML/CSS support and bug fixes)\n* [PIL](https://pillow.readthedocs.io/) (for better image support)\n* [PIL.ImageTk](https://pillow.readthedocs.io/en/stable/reference/ImageTk.html)\n\nYou can also choose from the following list for extra functionality:\n* [Brotli](https://github.com/google/brotli) (for faster page loads on some sites)\n* [PythonMonkey](http://pythonmonkey.io/) (for basic JavaScript support)\n* [CairoSVG](https://cairosvg.org/) or [PyGObject](https://pygobject.gnome.org/) (for SVG support)\n\nPip will automatically install dependencies when installing TkinterWeb. PIL.ImageTk should be automatically installed with PIL but might need to installed separately on some systems.\n\n## API Documentation\n\n> [!WARNING]\n> The API changed significantly in version 4.0.0. See [the changelog](https://tkinterweb.readthedocs.io/en/latest/upgrading.html) for details.\n\n**Documentation and additional information on built-in classes can be found in the corresponding API reference pages:**\n* [`tkinterweb.Demo`](https://tkinterweb.readthedocs.io/en/latest/usage.html#installation)\n* [`tkinterweb.HtmlFrame`](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html)\n* [`tkinterweb.HtmlLabel`](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html#tkinterweb.HtmlLabel)\n* [`tkinterweb.HtmlText`](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html#tkinterweb.HtmlText)\n* [`tkinterweb.HtmlParse`](https://tkinterweb.readthedocs.io/en/latest/api/htmlframe.html#tkinterweb.HtmlParse)\n* [`tkinterweb.TkinterWeb`](https://tkinterweb.readthedocs.io/en/latest/api/tkinterweb.html)\n* [`tkinterweb.Notebook`](https://tkinterweb.readthedocs.io/en/latest/api/notebook.html) (a Tkhtml-compatible drop-in replacement for `ttk.Notebook`)\n\n## FAQs\nSee [Frequently Asked Questions](https://tkinterweb.readthedocs.io/en/latest/faq.html).\n\n## Webpage Compatability\n**HTML & CSS:**\n* TkinterWeb supports HTML 4.01 and CSS 2.1. A full list of supported CSS declarations can be found at [http://tkhtml.tcl.tk/support.html](https://web.archive.org/web/20250325123206/http://tkhtml.tcl.tk/support.html).\n* Most CSS pseudo-elements, such as `:hover` and `:active` are also supported. \n* On 64-bit Windows and Linux, if the [TkinterWeb-Tkhtml-Extras](https://pypi.org/project/tkinterweb-tkhtml-extras/) is installed, HTML5 tags and some extra CSS properties (including `border-radius` and `overflow-x`) and cursors are also supported. To use these features on all other platforms, you will simply need to compile Tkhtml yourself. Visit and clone https://github.com/Andereoo/TkinterWeb-Tkhtml. Then run `python compile.py --install`.\n\n**JavaScript:**\n* Javascript only partly supported at the moment.\n   * To use JavaScript, PythonMonkey must be installed.\n* It is also possible for the user to connect their own JavaScript interpreter or manipulate the document through Python.\n* See [Using JavaScript](https://tkinterweb.readthedocs.io/en/latest/javascript.html) for more information and [DOM Manipulation with TkinterWeb](https://tkinterweb.readthedocs.io/en/latest/dom.html) for information on manipulating the document through Python.\n\n**Images:**\n* TkinterWeb supports nearly 50 different image types through PIL.\n* In order to load Scalable Vector Graphic images, CairoSVG, both PyCairo and PyGObject, or both PyCairo and Rsvg must also be installed. \n\n## Support & Donations\n**☕ If you’d like to support ongoing development and maintenance, please consider supporting this project by [buying me a coffee](https://buymeacoffee.com/andereoo). Any amount is hugely appreciated!**\n\nThis project is released under the [MIT License](./LICENSE.md) and is free to use, including for commercial purposes.\n\nIf you use this project in a commercial product or derive financial benefit from it, please kindly consider supporting its development with a donation. This helps cover maintenance time and ongoing improvements, which in turn will improve your own software!\n\n## Contributing\n**The best ways to contribute to this project are by submitting a [bug report](https://github.com/Andereoo/TkinterWeb/issues/new) to report bugs or suggest new features, or by submitting a [pull request](https://github.com/Andereoo/TkinterWeb/pulls) to offer fixes. Your help makes TkinterWeb become more stable and full-featured!**\n\nPlease check the [FAQs](https://tkinterweb.readthedocs.io/en/latest/faq.html) and [closed bugs](https://github.com/Andereoo/TkinterWeb/issues?q=is%3Aissue) before submitting a bug report to see if your question as already been answered.\n\n## Credits\n**TkinterWeb is powered by the [Tkhtml project](https://web.archive.org/web/20250219233338/http://tkhtml.tcl.tk/).**\n\nSpecial thanks to [Christopher Chavez](https://github.com/chrstphrchvz), [Zamy846692](https://github.com/Zamy846692), [Jośe Fernando Moyano](https://github.com/jofemodo), [Bumshakalaka](https://github.com/Bumshakalaka), [Trov5](https://github.com/TRVRStash), [Mark Mayo](https://github.com/marksmayo), [Jaedson Silva](https://github.com/jaedsonpys), [Nick Moore](https://github.com/nickzoic), [Leonardo Saurwein](https://github.com/Sau1707), and [Hbregalad](https://github.com/hbregalad) for their code suggestions and pull requests.\n\nSpecial thanks to [Christopher Chavez](https://github.com/chrstphrchvz), Jan Nijtmans, and everyone else in the tcl-core mailing list for the help making border rounding work on Windows and MacOSX, and to [Zamy846692](https://github.com/Zamy846692) for spearheading experimental Tkhtml development.\n\nThanks to the [TkinterHtml package](https://bitbucket.org/aivarannamaa/tkinterhtml) for providing the bindings on which this project is based and the [BRL-CAD project](https://github.com/BRL-CAD/brlcad) for providing modifications for Tkhtml on 64-bit Windows.\n\nA huge thanks to everyone else who supported this project by reporting bugs and providing suggestions!\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = source\nBUILDDIR      = build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=source\r\nset BUILDDIR=build\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.http://sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/requirements.txt",
    "content": "sphinx==7.1.2\nsphinx-rtd-theme==1.3.0rc1\nsphinx_design==0.6.1\ntkinterweb-tkhtml >= 2.0.0\nPillow >= 10.0.0\n"
  },
  {
    "path": "docs/source/_static/custom.css",
    "content": "/* This file is used to make tweak the documentation and make it CSS2 compatible.*/\n\n.wy-nav-side {\n  overflow: hidden !important;\n}\n\n.wy-side-nav-search > div.version {\n  color: #cacaca !important;\n}\n\n.wy-side-nav-search, .wy-nav-top, .note .admonition-title, .wy-menu-vertical a:active {\n  background-color: rgb(77, 122, 77) !important;\n}\n\n.wy-nav-content a {\n  color: rgb(77, 122, 77);\n}\n\n.wy-nav-content a:hover {\n  color: rgb(96, 156, 96);\n}\n\ncode.literal,\nspan.literal {\n  color: rgb(94, 94, 94) !important;\n}\n\n.rst-content code.xref,\n.rst-content tt.xref,\na .rst-content code,\na .rst-content tt {\n  color: initial !important\n}\n\n.note {\n  background-color: rgb(230, 238, 230) !important;\n}\n\n#rtd-search-form input {\n  width: 80% !important;\n  color: black !important;\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.wy-breadcrumbs .icon-home:before {\n  content: \"Home\" !important;\n  font-family: Lato, proxima-nova, Helvetica Neue, Arial, sans-serif !important;\n}\n\n.wy-breadcrumbs .wy-breadcrumbs-aside {\n  display: none;\n}\n\n.wy-side-nav-search {\n  padding-left: 0 !important;\n  padding-right: 0 !important;\n}\n\n.wy-breadcrumbs .breadcrumb-item {\n  display: inline !important;\n}\n\n.highlight-python,\n.highlight-console,\n.highlight-default {\n  border: 1px solid #e1e4e5 !important;\n  margin: 1px 0 24px !important;\n  background: #f8f8f8 !important;\n  width: 100% !important;\n}\n\n.highlight pre {\n  white-space: pre !important;\n  margin: 0 !important;\n  padding: 12px !important;\n  display: block !important;\n}\n\n.highlight {\n  border: none !important;\n  margin: 0 !important;\n}\n\n.sig-object {\n  display: table !important;\n  margin: 6px 0 !important;\n  margin-top: 6px !important;\n  font-size: 90% !important;\n  line-height: normal !important;\n  background: rgb(230, 238, 230) !important;\n  color: rgb(77, 122, 77) !important;\n  border-top: 3px solid rgb(101, 145, 101) !important;\n  padding: 6px !important;\n  position: relative !important;\n}\n\n.sig-object .property,\n.sig-object .sig-param,\n.sig-object .sig-paren {\n  font-size: 90% !important;\n  line-height: normal !important;\n  color: rgb(77, 122, 77) !important;\n}\n\n.sig-object .sig-name,\n.sig-object .sig-prename {\n  font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace !important;\n  color: #000 !important;\n}\n\ndd .sig-object {\n  margin-bottom: 6px !important;\n  border: none !important;\n  border-left-width: medium !important;\n  border-left-style: none !important;\n  border-left-color: currentcolor !important;\n  border-left: 3px solid #ccc !important;\n  background: #f0f0f0 !important;\n  color: #555 !important;\n}\n\ndd .sig-name {\n  font-family: SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, Courier New, Courier, monospace !important;\n  color: #000 !important;\n}\n\ndd .property,\ndd .sig-param,\ndd .sig-paren {\n  color: inherit !important\n}\n\n.rst-footer-buttons {\n  display: table !important;\n  width: 100% !important;\n}\n\n.rst-footer-buttons .float-left {\n  display: table-cell !important;\n}\n\n.rst-footer-buttons .float-right {\n  display: table-cell !important;\n}\n\n.fa-arrow-circle-right:before {\n  content: \" →\" !important;\n}\n\n.fa-arrow-circle-left:before {\n  content: \"← \" !important;\n}\n\n.btn {\n  border-color: #e1e4e5 !important;\n  padding: 6px 12px !important;\n  border-bottom: 2px solid #ccc !important;\n  box-shadow: none !important;\n  transition: none !important;\n}\n\n.btn:active {\n  border-top: 2px solid #ccc !important;\n  padding: 5px 12px 7px !important;\n  border-bottom-width: 1px !important;\n}\n\n.wy-side-nav-search > a {\n  padding: 0 !important;\n  margin: 0 !important;\n}\n\n.wy-side-nav-search > a:hover {\n  background: transparent !important;\n}\n\n.wy-side-nav-search > a img.logo {\n  max-width: 250px !important;\n  margin: 0 !important;\n}\n\n.sd-card-header {\n  display: block;\n}\n\ndetails.sd-dropdown .sd-summary-content p {\n  cursor: text;\n}\n\ndetails.sd-dropdown summary.sd-summary-title{\n  padding: .5em .6em .5em 1em !important;\n}\n\n.sd-mb-3, .sd-my-3 {\n  margin-bottom: 16px !important;\n}\n\n.sd-card-header, .sd-card {\n  border-color: #dcdcdc;\n  border-radius: 4px;\n}\n\n.sd-card-body {\n  padding: 0px 17px !important;\n}\n\n.rst-content .section ul, .rst-content .toctree-wrapper ul, .rst-content section ul, .wy-plain-list-disc, article ul {\n  margin: 12px 0px !important;\n}\n\n.sd-card-body > p {\n  margin: 12px 0px !important;\n}"
  },
  {
    "path": "docs/source/api/extensions.rst",
    "content": "TkinterWeb Extensions\n=====================\n\nThe following objects are extensions to the :class:`~tkinterweb.TkinterWeb` widget and are largely internal. You will likely never need to access them, but they are described here just in case.\n\nThe methods described in this page may change at any time without warning. If you are relying on anything here, please let me know so I know to keep compatibility.\n\n.. autoclass:: tkinterweb.extensions.SelectionManager\n   :members:\n\n.. autoclass:: tkinterweb.extensions.CaretManager\n   :members:\n\n.. autoclass:: tkinterweb.extensions.EventManager\n   :members:\n\n.. autoclass:: tkinterweb.extensions.WidgetManager\n   :members:\n\n.. autoclass:: tkinterweb.extensions.SearchManager\n   :members:"
  },
  {
    "path": "docs/source/api/htmldocument.rst",
    "content": "Document Object Model Documentation\n===================================\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <../upgrading>` for details.\n\nThe methods described in this page make it easy to modify the appearance and content of a loaded document and manage interaction with the document. For the most part, this page mirrors the core JavaScript DOM API.\n\n.. autoclass:: tkinterweb.dom.HTMLDocument\n   :members:\n\n.. autoclass:: tkinterweb.dom.HTMLElement\n   :members:\n\nThe following JavaScript event properties are also supported: ``onchange``, ``onclick``, ``oncontextmenu``, ``ondblclick``, ``onload``, ``onmousedown``, ``onmouseenter``, ``onmouseleave``, ``onmousemove``, ``onmouseout``, ``onmouseover``, and ``onmouseup``.\n\n.. autoclass:: tkinterweb.dom.HTMLCollection\n   :members:\n\n.. autoclass:: tkinterweb.dom.CSSStyleDeclaration\n   :members:\n\n.. autoclass:: tkinterweb.dom.DOMRect\n   :members:\n\nSpecial thanks to `Zamy846692 <https://github.com/Zamy846692>`_ for the help making this happen!\n"
  },
  {
    "path": "docs/source/api/htmlframe.rst",
    "content": "HTML Widgets Documentation\n==========================\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <../upgrading>` for details.\n\nThe :class:`~tkinterweb.HtmlFrame` widget is a Tkinter frame that provides additional functionality to the :class:`~tkinterweb.TkinterWeb` widget by adding automatic scrollbars, error handling, and many convenience methods into one embeddable and easy to use widget.\n\nThe :class:`~tkinterweb.HtmlFrame` widget is also capable managing other Tkinter widgets, making it easy to combine Tkinter widgets and HTML elements.\n\n\n.. autoclass:: tkinterweb.HtmlFrame\n   :members:\n\nThis widget also emits the following Tkinter virtual events that can be bound to:\n\n* ``<<DownloadingResource>>``/:py:attr:`utilities.DOWNLOADING_RESOURCE_EVENT`: Generated whenever a new resource is being downloaded.\n* ``<<DoneLoading>>``/:py:attr:`utilities.DONE_LOADING_EVENT`: Generated whenever all outstanding resources have been downloaded. This is generally a good indicator as to when the website is done loading, but may be generated multiple times while loading a page.\n* ``<<DOMContentLoaded>>``/:py:attr:`utilities.DOM_CONTENT_LOADED_EVENT`: Generated once the page content has loaded. The page may not be done loading, but at this point it is possible to interact with the DOM.\n* ``<<UrlChanged>>``/:py:attr:`utilities.URL_CHANGED_EVENT`: Generated whenever the widget's url changes or redirects. Use :attr:`.HtmlFrame.current_url` to get the url.\n* ``<<IconChanged>>``/:py:attr:`utilities.ICON_CHANGED_EVENT`: Generated whenever the icon of a webpage changes. Use :attr:`.HtmlFrame.icon` to get the icon.\n* ``<<TitleChanged>>``/:py:attr:`utilities.TITLE_CHANGED_EVENT`: Generated whenever the title of a website or file has changed. Use :attr:`.HtmlFrame.title` to get the title.\n* ``<<Modified>>```/:py:attr:`utilities.FIELD_CHANGED_EVENT`: Generated whenever the content of an interactive element changes.\n\n.. autoclass:: tkinterweb.HtmlLabel\n   :members:\n\n.. autoclass:: tkinterweb.HtmlText\n   :members:\n\n.. autoclass:: tkinterweb.HtmlParse\n   :members:"
  },
  {
    "path": "docs/source/api/jsengine.rst",
    "content": "JavaScript Engine Documentation\n===============================\n\n\nThe methods described in this page make it easy to interact with the JavaScript Engine.\n\n.. autoclass:: tkinterweb.js.JSEngine\n   :members:\n"
  },
  {
    "path": "docs/source/api/notebook.rst",
    "content": "Notebook Documentation\n=======================\n\nThe TkinterWeb :class:`~tkinterweb.Notebook` widget should be used in place of :py:class:`ttk.Notebook`, which is incompatable with Tkhtml on 64-bit Windows and crashes when selecting tabs. See https://docs.python.org/3/library/tkinter.ttk.html#notebook for the full API.\n\n.. autoclass:: tkinterweb.Notebook\n   :members:\n\nThis widget also emits the following Tkinter virtual events that can be bound to:\n\n* ``<<NotebookTabChanged>>``: Generated whenever the selected tab changes.\n"
  },
  {
    "path": "docs/source/api/tkinterweb.rst",
    "content": "Internals Documentation\n=======================\n\n.. toctree ::\n   :maxdepth: 2\n\n   tkinterweb_api\n   extensions"
  },
  {
    "path": "docs/source/api/tkinterweb_api.rst",
    "content": "Bindings Documentation\n======================\n\n.. note::\n   This API has changed significantly recently. See :doc:`the changelog <../upgrading>` for details.\n\n\nThe following objects offer the core bindings to the Tkhtml3 HTML widget and are largely internal. You will likely never need to access them, but they are described here just in case. \n\nRefer to the `Tkhtml Documentation <https://web.archive.org/web/20250219233338/http://tkhtml.tcl.tk/>`_ for more details on some of the commands.\n\n.. autoclass:: tkinterweb.TkinterWeb\n   :members:\n\n.. autoclass:: tkinterweb.TkHtmlParsedURI\n   :members:"
  },
  {
    "path": "docs/source/api.rst",
    "content": "API Reference\n==============\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <upgrading>` for details.\n\n.. toctree::\n   :maxdepth: 2\n   :caption: Available Classes\n\n   api/htmlframe\n   api/htmldocument\n   api/jsengine\n   api/notebook\n\n-------------------\n\n.. toctree::\n   :maxdepth: 3\n\n   api/tkinterweb"
  },
  {
    "path": "docs/source/caret.rst",
    "content": "Making Documents Editable\n=========================\n\n.. note::\n    Caret browsing support is new in version 4.8. The :class:`~tkinterweb.HtmlText` widget was made editable in version 4.15. Make sure you are using the latest version of TkinterWeb.\n\nOverview\n--------\n\n**TkinterWeb can be used to create a rich text or HTML editor.**\n\nThe :class:`~tkinterweb.HtmlText` widget provides a simple HTML editor that can be extended to adapt to the needs of your application.\n\nTkinterWeb also provides a useful API for developers to create their own HTML-based what-you-see-is-what-you-get editor.\n\nThese features are new. Please reach out to report a bug, suggest an improvement, or seek support.\n\nSetup\n------\n\nTo enable caret browsing mode, add ``yourhtmlframe.configure(caret_browsing_enabled=True)`` to your script or add the parameter ``caret_browsing_enabled=True`` when creating your :class:`~tkinterweb.HtmlFrame` or :class:`~tkinterweb.HtmlLabel` widget.\n\nWhen enabled, a caret will appear once the user clicks on text in the document. Use the methods described below to handle keypresses, or instead use the :class:`~tkinterweb.HtmlText` widget which handles most cases on its own.\n\nHow-to\n------\n\nSimply create your :class:`~tkinterweb.HtmlText` and start editing!\n\n.. code-block:: python\n\n    from tkinterweb import HtmlText\n    yourhtmlframe = HtmlText(root, messages_enabled=True)\n\nYou can also load html, files, and websites. For instance, to create an editable page with a heading, an orange block, and a list, you could use the following:\n\n.. code-block:: python\n\n    yourhtmlframe = HtmlText(root, messages_enabled=True)\n    yourhtmlframe.load_html(\"\"\"<h2>Hello, world!</h2>\n    <div style='background: orange; border-radius: 10px; padding: 10px'>\n        <div>Tkinter is so cool.</div>\n    </div>\n    <ul>\n        <li>TkinterWeb is also cool</li>\n        <li>Python is also cool</li>\n    </ul>\"\"\")\n\n.. image:: ./_static/text_widget.png\n\nIt's that easy! \n\nYou can insert and edit hyperlinks, images, and much more. Click on a hyperlink while pressing the Ctrl key to navigate to it. Like the :class:`~tkinterweb.HtmlFrame` widget, the :class:`~tkinterweb.HtmlText` widget is also scrollable out of the box!\n\nYou can also use :meth:`.HtmlText.insert` and :meth:`.HtmlText.delete` to easily modify the document.\n\nCustomization\n-------------\n\nEverything described below applies to all HTML widgets with caret browsing enabled.\n\nUse :meth:`.HtmlFrame.get_caret_position` to get the caret's position. The element returned will always be a text node.\n\n.. tip::\n     You can use the methods outlined in the `HTMLElement documentation <api/htmldocument.html#tkinterweb.dom.HTMLElement>`_ to get the element's parent if needed. From here you can insert new elements, change the text and much more!\n\nUse :meth:`.HtmlFrame.shift_caret_left` or :meth:`.HtmlFrame.shift_caret_right` to shift the caret left or right.\n\nThe following is a simple example showing how to handle keypresses to insert letters and numbers:\n\n.. code-block:: python\n    \n    def on_keypress(event):\n        # Get the caret's position\n        caret_position = yourhtmlframe.get_caret_position()\n        if caret_position and event.char:\n            element, text, index = caret_position\n            \n            # Add the key's character to the element's text\n            newtext = text[:index] + event.char + text[index:]\n\n            # Set the element's text\n            element.textContent = newtext\n\n            # Shift the caret right\n            yourhtmlframe.shift_caret_right()\n\n    yourhtmlframe.bind(\"<Key>\", on_keypress)\n\nThis works on all HTML widgets.\n\n.. warning::\n    If using the :class:`~tkinterweb.HtmlText` widget, binding to ``<Key>`` will remove all default key bindings. Either bind to individual keys as needed or use ``yourhtmlframe.bind(\"<Key>\", on_keypress, add=\"+\")``, but keep in mind then both bindings will fire.\n\n.. note::\n    Most HTML elements collapse spaces. To insert a space into the document's text, it is usually best to use a non-breaking space (``\"\\xa0\"`` or ``\"&nbsp;\"``).\n\nUse :meth:`.HtmlFrame.set_caret_position` to set the caret's position if you know the element and index you want to place the caret at.\n\nSome extra logic will be needed to handle other types of keypresses. See the :class:`~tkinterweb.HtmlText` source code for inspiration.\n\n.. tip::\n    When handling backspaces at the start of a node or deletions at the end of a node, it is sometimes useful to find the previous or following text nodes, respectively. \n    \n    You can get the preceeding or following text nodes by using :meth:`.HtmlFrame.shift_caret_left` or :meth:`.HtmlFrame.shift_caret_right` followed by :meth:`.HtmlFrame.get_caret_position`.\n\nUse :meth:`.HtmlFrame.get_selection_position` to get the position of any selected text and :meth:`.HtmlFrame.clear_selection` to clear the selection. \n\nYou may need to set the caret's position after modifying the document.\n\n.. tip::\n    :meth:`.HtmlFrame.set_caret_position` will raise an error if the element provided has been removed or is empty. \n    \n    If you need to remove or empty the elements returned by :meth:`.HtmlFrame.get_selection_position` or :meth:`.HtmlFrame.get_caret_position`, you can also get the selection or caret's position relative to the page text content using :meth:`HtmlFrame.get_selection_position(return_elements=False) <.HtmlFrame.get_selection_position>` and :meth:`HtmlFrame.get_caret_position(return_element=False) <.HtmlFrame.get_caret_position>`, respectively.\n\n    You can then set the selection or caret's position as usual, providing only indexes (i.e. ``yourhtmlframe.set_selection_position(start_index=5, end_index=10)``.\n\n\nThe following code can be used as a starting point on handling backspaces when text is selected:\n\n.. code-block:: python\n\n    def on_backspace(event):\n        # Get the selection's position and deselect all selected text\n        selection = yourhtmlframe.get_selection_position()\n\n        if selection:\n            start, end, middle = selection\n            start_element, start_element_text, start_element_index = start\n            end_element, end_element_text, end_element_index = end\n\n            # Deselect all selected text\n            d.clear_selection()\n\n            # Cut out the selection\n            start_element.textContent = start_element_text[:start_element_index] + start_element_text[end_element_index:]\n\n            if start_element != end_element:\n                # Delete the end element\n                end_element.remove()\n\n                # Remove each element that is fully selected, and its parent if it is now empty\n                for element in middle:\n                    parent = element.parentElement\n                    element.remove()\n                    if len(parent.children) == 0:\n                        parent.remove()\n\n            # Set the caret's position\n            yourhtmlframe.set_caret_position(start_element, start_element_index)\n\n    yourhtmlframe.bind(\"<BackSpace>\", on_backspace)\n\nYou can use :meth:`.HtmlFrame.set_selection_position` to set the selection if needed.\n\n-------------------\n\nSee the `HtmlFrame documentation <api/htmlframe.html#tkinterweb.HtmlFrame.get_caret_position>`_ for a complete list of supported methods.\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/compatibility.rst",
    "content": "System and Webpage Compatibility\n================================\n\nSystem compatibility\n--------------------\n\n**TkinterWeb supports all platforms but only ships with precompiled Tkhtml binaries for the most common platforms:**\n\n* x86_64 Windows, Linux, and macOS\n* i686 Windows and Linux\n* ARM64 Macos and Linux\n* ARMv71 Linux\n\nIf your system is unsupported, compile and install Tkhtml by visiting and cloning https://github.com/Andereoo/TkinterWeb-Tkhtml. Then run ``python compile.py --install``.\n\nAlternatively, you can install Tkhtml system-wide (i.e. through your system package manager) and then add the parameter :attr:`use_prebuilt_tkhtml=False` when creating your :class:`~tkinterweb.HtmlFrame`, :class:`~tkinterweb.HtmlLabel`, or :class:`~tkinterweb.HtmlText` widget to use the system's Tkhtml. Keep in mind that some features will no longer work.\n\nIf you are encountering issues, feel free to submit a bug report or feature request.\n\nThe experimental Tkhtml version is not provided as a pre-built binary but can be compiled from the source code at https://github.com/Andereoo/TkinterWeb-Tkhtml/tree/experimental. This version has better cross-platform compatibility, is printable, and introduces support for some new CSS3 properties!\n\nWebpage compatibility\n---------------------\n\n**HTML & CSS:**\n\n* TkinterWeb supports HTML 4.01 and CSS 2.1. A full list of supported CSS declarations can be found at `http://tkhtml.tcl.tk/support.html <https://web.archive.org/web/20250325123206/http://tkhtml.tcl.tk/support.html>`_.\n* Most CSS pseudo-elements, such as ``:hover`` and ``:active`` are also supported.\n* On 64-bit Windows and Linux, if the `TkinterWeb-Tkhtml-Extras <https://pypi.org/project/tkinterweb-tkhtml-extras/>`_ package is installed, HTML5 tags and some extra CSS properties (including ``border-radius`` and ``overflow-x``) and cursors are also supported. To use these features on all other platforms, you will simply need to compile Tkhtml yourself. Visit and clone https://github.com/Andereoo/TkinterWeb-Tkhtml. Then run ``python compile.py --install``.\n\n**JavaScript:**\n\n* JavaScript partly supported at the moment. See :doc:`javascript` for more information.\n\n  * To use JavaScript, `PythonMonkey <http://pythonmonkey.io/>`_  must be installed.\n\n* It is also possible for the user to connect their own JavaScript interpreter or manipulate the document through Python. See :doc:`javascript` and :doc:`dom` for more information.\n\n**Images:**\n\n* TkinterWeb supports nearly 50 different image types through `PIL <https://pillow.readthedocs.io/>`_.\n\n* In order to load Scalable Vector Graphic images, `CairoSVG <https://cairosvg.org/>`_, `PyGObject <https://pygobject.gnome.org/>`_, or both :py:mod:`PyCairo` and :py:mod:`Rsvg` must also be installed. \n\n-------------------\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/conf.py",
    "content": "# Configuration file for the Sphinx documentation builder.\n\n# -- Project information\n\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname((os.path.realpath(__file__))))))\n\nfrom tkinterweb import __title__, __copyright__, __author__, __version__\n\nproject = __title__\ncopyright = __copyright__\nauthor = __author__\n\nrelease = \".\".join(__version__.split(\".\")[:2])\nversion = __version__\n\n# -- General configuration\n\nextensions = [\n    'sphinx.ext.duration',\n    'sphinx.ext.doctest',\n    'sphinx.ext.autodoc',\n    'sphinx.ext.autosummary',\n    'sphinx.ext.intersphinx',\n    \"sphinx_design\",\n]\n\nintersphinx_mapping = {\n    'python': ('https://docs.python.org/3/', None),\n    'sphinx': ('https://www.sphinx-doc.org/en/master/', None),\n}\n\nhtml_logo = \"_static/logo.png\"\nhtml_theme_options = {\"logo_only\": True}\ntemplates_path = ['_templates']\n\nhtml_static_path = ['_static']\nhtml_css_files = ['custom.css']\n\n# -- Options for HTML output\n\nautodoc_member_order = 'bysource'\n\nhtml_theme = 'sphinx_rtd_theme' # May switch to agogo or alabaster or python_docs_theme\n\n# -- Options for EPUB output\nepub_show_urls = 'footnote'\n\ndef skip_member(app, what, name, obj, skip, options):\n    if name == \"destroy\" and what == \"class\":\n        return True\n    return skip\n\ndef setup(app):\n    app.connect(\"autodoc-skip-member\", skip_member)"
  },
  {
    "path": "docs/source/dom.rst",
    "content": "Manipulating the Page\n=====================\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <upgrading>` for details.\n\nOverview\n--------\n\n**TkinterWeb provides a handful of functions that allow for manipulation of the webpage. They are fashioned after common JavaScript functions.**\n\n\nHow-to\n--------\n\nTo manipulate the Document Object Model, use the :attr:`~tkinterweb.HtmlFrame.document` property of your :class:`~tkinterweb.HtmlFrame` or :class:`~tkinterweb.HtmlLabel` widget. For example, to create a heading with blue text inside of an element with the id \"container\", one can use the following:\n\n.. code-block:: python\n\n    yourhtmlframe = tkinterweb.HtmlFrame(root, messages_enabled=True)\n    yourhtmlframe.load_html(\"<div id='container'><p>Test</p></div>\")\n    container = yourhtmlframe.document.getElementById(\"container\")\n    new_header = yourhtmlframe.document.createElement(\"h1\")\n    new_header.textContent = \"Hello, world!\"\n    new_header.style.color = \"blue\"\n    container.appendChild(new_header)\n\n\n.. _binding-to-an-element:\n\nBinding to an element\n---------------------\n\nTo manage bindings on HTML elements, simply use :meth:`~tkinterweb.dom.HTMLElement.bind` and :meth:`~tkinterweb.dom.HTMLElement.unbind` (new in version 4.9):\n\n.. code-block:: python\n\n    container = yourhtmlframe.document.getElementById(\"container\")\n\n    def callback(event):\n        print(\"Woah this is cool!\")\n\n    container.bind(\"<Button-3>\", callback)\n\n-------------------\n\nSee the :doc:`api/htmldocument` for a complete list of supported commands.\n\nSee :doc:`javascript` for information on manipulating the DOM through JavaScript.\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/faq.rst",
    "content": "Frequently Asked Questions\n==========================\n\nHow do I load websites or files?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Use the :meth:`~tkinterweb.HtmlFrame.load_website` or :meth:`~tkinterweb.HtmlFrame.load_file` commands. Alternatively, use the :meth:`~tkinterweb.HtmlFrame.load_url` command to load any generic url, but keep in mind that the url must be properly formatted, because the url scheme will not be automatically applied. As always, check out the :doc:`api/htmlframe` for more information.\n\nHow do I manage clicks and use custom bindings?\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* The :attr:`on_link_click` configuration option can be used to assign a custom function to link clicks. Likewise :attr:`on_form_submit` can be used to handle form submissions. See the :doc:`api/htmlframe` for more information.\n* Like any other Tkinter widget, mouse and keyboard events can be bound to the :class:`~tkinterweb.HtmlFrame` widget and associated HTML elements. See the :doc:`usage` page for more information.\n \nTkinterWeb is crashing\n~~~~~~~~~~~~~~~~~~~~~~\n\n* That is defenitely not normal. Make sure your are using the most up-to-date TkinterWeb version and have crash protection enabled.\n* If you are using a :py:class:`ttk.Notebook` in your app, see the question below.\n* If all else fails, `file a bug report <https://github.com/Andereoo/TkinterWeb/issues/new>`_. Post your operating system, Python version, and TkinterWeb version, as well as any error codes or instructions for reproducing the crash.\n\nI'm having issues when using shrink or the HtmlLabel widget\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* See :doc:`shrink` for more information.\n\nMy app crashes when I open a tab with an HtmlFrame in it\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* Tkhtml (the underlying HTML engine) and the :py:class:`ttk.Notebook` widget aren't compatable on 64-bit Windows.\n* This is a known issue. Fixing this is beyond the scope of this project, but working around it is easy.\n* Instead of using :py:class:`ttk.Notebook`, use :class:`tkinterweb.Notebook`. This is a wrapper around ttk.Notebook that is designed to be a drop-in replacement for the :py:class:`ttk.Notebook` widget. It should look and behave exactly like a :py:class:`ttk.Notebook` widget, but without the crashes. See `bug #19 <https://github.com/Andereoo/TkinterWeb/issues/19>`_ for more information.\n* Please note that after adding a widget to the Notebook (eg. ``mynotebook.add(mywidget)``) there is no need to call :py:func:`~tkinterweb.Widget.pack` or :py:func:`~tkinterweb.Widget.grid` the widget. This may raise errors. TkinterWeb's Notebook widget handles all this on its own.\n\nI get a ModuleNotFoundError after compiling my code\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n* When compiling your code, you might get an error popup saying ``ModuleNotFoundError: The files required to run TkinterWeb could not be found``\n* Your app might also fail quietly if TkinterWeb's dependencies are not installed\n* This occurs when your Python script bundler isn't finding all the files nessessary for running TkinterWeb. You need to force it to get all of TkinterWeb's files and dependencies.\n* On PyInstaller: make sure you are using the latest version of PyInstaller by running ``pip install --upgrade pyinstaller pyinstaller-hooks-contrib``. Otherwise, you can also add the flags ``--collect-all tkinterweb --collect-all tkinterweb_tkhtml --collect-all tkinterweb_tkhtml_extras`` when bundling your app.\n* On py2app / py2exe: Add ``'packages': ['tkinterweb', 'tkinterweb_tkhtml', 'tkinterweb_tkhtml_extras']`` to the ``OPTIONS`` variable in your setup file.\n\n-------------------\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_.\n"
  },
  {
    "path": "docs/source/geometry.rst",
    "content": "Embedding Widgets\n=================\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <upgrading>` for details.\n\nOverview\n--------\n\nBy default, Tkinter provides three geometry managers: pack, place, and grid. While these geometry managers are very powerful, achieving certain layouts, especially with scrolling, can be very difficult.\n\n**TkinterWeb provides a system for attaching Tkinter widgets onto the window, and handles layouts, images, selection, scrolling, and much more for you.**\n\nHow-to\n------\n\nTo place a Tkinter widget inside an HTML document, add the ``data=[yourwidget]`` attribute to an ``<object>`` element. For example, to add a button under some italic text, one could do:\n\n.. code-block:: python\n\n    yourframe = tkinterweb.HtmlFrame(root, messages_enabled=True)\n    yourbutton = tkinter.Button(yourframe, text=\"Hello, world!\")\n    source_html = f\"<i>This is some text</i><br><object data={yourbutton}></object>\"\n    yourframe.load_html(source_html) # or use add_html to add onto the existing document\n\n**Ensure your HtmlFrame widget was created before the widget you are embedding, or else the widget might not be visible.**\n\n.. tip::\n\n    Add the ``allowstyling`` attribute to automatically change the widget's background color, text color, and font to match the containing HTML element. Use ``allowstyling=\"deep\"`` to also style subwidgets (new in version 4.9).\n\n    Add the ``handledelete`` attribute to automatically call :meth:`~tkinter.Widget.destroy` on the widget when it is removed from the page (i.e. if another webpage is loaded).\n\n.. note::\n    \n    By default, scrolling over an embedded widget will scroll the page if the widget or subwidgets do not handle scrolling themselves (new in version 4.9). You can override this behaviour by adding the ``allowscrolling`` or ``allowscrolling=false`` attribute. \n\nWidget position and sizing can be modified using CSS styling on the widget's associated ``<object>`` element.\n\nSee :doc:`dom` (new in version 3.25) for more details.\n\nTo get the element containing your widget, either use :meth:`.HtmlFrame.widget_to_element`.\n\nWidget handling\n---------------\n\nYou can also set, remove, or change the widget in any element later (new in version 4.2):\n\n.. code-block:: python\n\n    yourbutton = tkinter.Button(yourframe, text=\"Hello, world!\")\n    ...\n    yourelement = yourframe.document.getElementById(\"#container\") # get the element to fill\n    yourelement.widget = yourbutton # set the element's widget\n\nThe widget can be removed from the element via ``yourelement.widget = None``.\n\n-------------------\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/index.rst",
    "content": "Welcome to TkinterWeb!\n======================\n\n.. image:: https://static.pepy.tech/badge/tkinterweb/month\n    :target: https://pepy.tech/project/tkinterweb\n    :alt: PyPi Downloads\n\n.. image:: https://img.shields.io/pypi/l/tkinterweb\n    :target: https://pypi.org/project/tkinterweb/\n    :alt: MIT Licence\n\n.. image:: https://img.shields.io/pypi/pyversions/tkinterweb\n    :target: https://pypi.org/project/tkinterweb/\n    :alt: Python 3\n\n.. image:: https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%A6%20made%20in%20Canada-grey\n    :target: https://pypi.org/project/tkinterweb/\n    :alt: Made in Canada\n\n\n**TkinterWeb** is a Python library that adds HTML and CSS rendering capabilities to Tkinter widgets.\n\nCommon use cases include displaying help files, documentation, and other HTML content, rendering images (including SVG), building rich-text editors, designing apps with HTML templates, and creating more modern-looking interfaces, with advanced styling and even round buttons!\n\nGetting started\n---------------\n\nTkinterWeb provides a `frame widget <./api/htmlframe.html>`_, a `label widget <./api/htmlframe.html#tkinterweb.HtmlLabel>`_, and a `text widget <./api/htmlframe.html#tkinterweb.HtmlText>`_. \n\nCheck out the :doc:`usage` section to learn how to get started and discover tips and tricks, :doc:`faq` for frequently asked questions, and the :doc:`api` to explore all of the widgets and functions at your disposal!\n\nLove this project?\n------------------\n\nYou can help this project by submitting a `bug report <https://github.com/Andereoo/TkinterWeb/issues/new>`_ to report bugs or suggest new features, or by submitting a `pull request <https://github.com/Andereoo/TkinterWeb/pulls>`_ to offer fixes. Your help makes TkinterWeb become more stable and full-featured!\n\n☕ Or, if you’d like to support ongoing development and maintenance, please consider supporting this project by `buying me a coffee <https://buymeacoffee.com/andereoo>`_. Any amount is hugely appreciated!\n\nThis project is released under the `MIT License <https://github.com/Andereoo/TkinterWeb/blob/main/LICENSE.md>`_ and is free to use, including for commercial purposes.\n\nIf you use this project in a commercial product or derive financial benefit from it, please kindly consider supporting its development with a donation. This helps cover maintenance time and ongoing improvements, which in turn will improve your own software!\n\n.. raw:: html\n   \n   <a href=\"https://www.buymeacoffee.com/andereoo\" target=\"_blank\"><img src=\"https://cdn.buymeacoffee.com/buttons/v2/default-violet.png\" alt=\"Buy Me A Coffee\" style=\"height: 35px !important;width: 140px !important;\" ></a>\n\nPages\n-----\n\n.. toctree::\n   :maxdepth: 1\n\n   usage\n   geometry\n   dom\n   caret\n   shrink\n   javascript\n   faq\n   compatibility\n   upgrading\n   api\n"
  },
  {
    "path": "docs/source/javascript.rst",
    "content": "Using JavaScript\n================\n\n.. note::\n    JavaScript support is new in version 4.1. Embedded Python support is new in version 4.19. Make sure you are using the latest version of TkinterWeb.\n\nOverview\n--------\n\n**Scripting support makes it easy to embed JavaScript or Python code in your document.**\n\nJavaScript is fully supported through Mozilla's SpiderMonkey engine, but not all DOM commands are supported.  See the :doc:`api/htmldocument` for an exhaustive list of supported DOM commands.\n\nSetup\n------\n\nTo enable JavaScript support in TkinterWeb, first install `PythonMonkey <http://pythonmonkey.io/>`_ using pip:\n\n.. code-block:: console\n\n   $ pip install pythonmonkey\n\nSkip this step if you are embedding Python code in your document.\n\nOr when installing TkinterWeb, use:\n\n.. code-block:: console\n\n   $ pip install tkinterweb[javascript]\n\nThen add ``yourhtmlframe.configure(javascript_enabled=True)`` to your script or add the parameter ``javascript_enabled=True`` when creating your :class:`~tkinterweb.HtmlFrame`, :class:`~tkinterweb.HtmlLabel`, or :class:`~tkinterweb.HtmlText` widget.\n\n.. note::\n    If using Windows, ensure you are using an up-to-date Python version. In some Python versions prior to version 3.13, Python will crash when loading PythonMonkey.\n\n**Only enable JavaScript in documents with code you know and trust.**\n\nHow-to\n------\n\nTo change the color and text of a ``<p>`` element when clicked, you could use the following:\n\n.. code-block:: python\n    \n    yourhtmlframe = tkinterweb.HtmlFrame(root, messages_enabled=True, javascript_enabled=True)\n    yourhtmlframe.load_html(\"\"\"\n        <script>\n        function changeColor(element) {\n            element.style.color = \"blue\"\n            element.textContent = \"I've been clicked!\"\n        }\n        </script>\n        <div id='container'><p onclick=\"changeColor(this)\">Hello, world!</p></div>\n        \"\"\")\n\nAdd the ``defer`` attribute to the relevant ``<script>`` element if you want it to run after the page loads. Otherwise, the script will be executed as soon as it is encountered in the document.\n\nThe following JavaScript event attributes are supported: ``onchange``, ``onload``, ``onclick``, ``oncontextmenu``, ``ondblclick``, ``onmousedown``, ``onmouseenter``, ``onmouseleave``, ``onmousemove``, ``onmouseout``, ``onmouseover``, and ``onmouseup``.\n\n.. tip::\n\n    As of version 4.25, it is highly recommended to enable debug messages (i.e. via ``HtmlFrame(root, messages_enabled=True, ...)``) when testing new scripts. Otherwise, if a script fails it will fail silently.\n\nEmbedding Python in your document\n---------------------------------\n\nTo run embedded scripts as Python code instead of JavaScript, simply use the parameters ``javascript_enabled=True`` and ``javascript_backend=\"python\"`` when creating your HTML widget. Ensure you are running code you trust.\n\nLike normal JavaScript code, by default scripts can access the ``document`` property and inline event callbacks can also access the ``this`` property. You will need to register new objects if you want the document's scripts to be able to access other functions, classes or variables.\n\nThat's it!\n\nRegistering new objects\n-----------------------\n\nTo register new objects, use :meth:`.JSEngine.register`. This gives the document's scripts access to Python objects. This, for instance, can be used to implement a ``window`` API or to add a callback for the JavaScript ``alert()`` function:\n\n.. code-block:: python\n\n    yourhtmlframe = tkinterweb.HtmlFrame(root, messages_enabled=True, javascript_enabled=True)\n    def open_alert_window(text):\n        ## Do stuff\n    yourhtmlframe.javascript.register(\"alert\", open_alert_window)\n    yourhtmlframe.load_html(\"<script>alert('Hello, world!')</script><p>Hello, world!</p>\")\n\nUsing your own interpreter\n--------------------------\n\nAlternatively, you can register your own callback for ``<script>`` elements using the :attr:`on_script` parameter:\n\n.. code-block:: python\n\n    yourhtmlframe = tkinterweb.HtmlFrame(root, messages_enabled=True)\n    def handle_scripts(attributes, tagcontents):\n        ## Do stuff\n    yourhtmlframe.configure(javascript_enabled=True, on_script=handle_scripts)\n    yourhtmlframe.load_html(\"<div id='container'><script>// Do stuff</script><p>Test</p></div>\")\n\n\nYou can also use the :attr:`on_element_script` parameter to handle event scripts (i.e. handle an element's ``onclick`` attribute). The element's corresponding Tkhtml node, relevant event, and code to execute will be passed as parameters.\n\nIf needed you can always then create an :class:`~tkinterweb.dom.HTMLElement` instance from a Tkhtml node:\n\n.. code-block:: python\n    \n    from tkinterweb.dom import HTMLElement\n    ...\n    yourhtmlelement = HTMLElement(yourhtmlframe.document, yourtkhtmlnode)\n\n-------------------\n\nIt is also possible to interact with the document through Python instead. See :doc:`dom`.\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/shrink.rst",
    "content": "Creating a Label Widget\n=======================\n\n.. note::\n    This API changed in version 4.17. See :doc:`the changelog <upgrading>` for details.\n\nOverview\n--------\n\n**Shrink makes HTML widgets behave like label widgets, automatically resizing to match their content.**\n\nHow-to\n------\n\nUse the :class:`.HtmlLabel` widget or add the parameter ``shrink=True`` to the :class:`.HtmlFrame` widget.\n\nYour widget will now shrink to match its content!\n\n.. tip::\n    Use the :class:`.HtmlLabel` widget if you want an HTML widget that looks and behaves like a ttk Label. Use the :class:`.HtmlFrame` widget if you want full control.\n\nTips and tricks\n---------------\n\nWord wrapping\n~~~~~~~~~~~~~\n\nBy default, word wrapping is disabled when shrink is enabled. This forces text to keep inline, which is generally expected from label-like widgets, and prevents a number of bugs that cause the widget to shake or wrap when it shouldn't.\n\n.. note::\n\n    Full shrink word wrapping support is currently only rolled out to 64-bit Windows and Linux users. Ensure you installed TkinterWeb via ``pip install tkinterweb[recommended]`` or ``pip install tkinterweb[full]`` to prevent bugs when using shrink.\n\n    If you are encountering issues on an unsupported platform, either submit a feature request or compile and install Tkhtml 3.1 by visiting and cloning https://github.com/Andereoo/TkinterWeb-Tkhtml. Then run ``python compile.py --install``.\n\nIf you need word wrapping, you can re-enable it by using ``HtmlFrame(..., textwrap=True)``. \n\nIt is a known issue that when word wrapping is enabled and the widget is shrunk it will often not re-expand. You will need to signal to the geometry manager that the widget should expand, or use the experimental ``HtmlFrame.unshrink = True`` (not recommended).\n\nHeight and width\n~~~~~~~~~~~~~~~~\n\nSince the purpose of shrink is to automatically resize the widget according to its content, height and width have little effect.\n\nIf you need to set the height and width, simply disable shrink.\n\nScrollbars\n~~~~~~~~~~\n\nScrollbars are disabled by default when shrink is enabled. Use ``HtmlFrame(..., vertical_scrollbar=\"auto\", horizontal_scrollbar=\"auto\")`` to enable them.\n\n-------------------\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_."
  },
  {
    "path": "docs/source/upgrading.rst",
    "content": "Changelog\n=========\n\n**The API changed significantly in version 4.**\n\n.. dropdown:: Key Changes\n    :open:\n    :color: primary\n\n    * Faster load speed\n    * A more intuitive API\n    * Support for experimental Tkhtml features, such as page printing\n    * Widget behaviour and API is now more closely aligned with standard Tkinter widgets\n    * Many DOM improvements. The DOM API now more closely mirrors its JavaScript counterpart.\n    * Dozens of new configuration options, including access to more settings and the ability to link a JavaScript interpreter\n\n    * Added basic JavaScript support (new in version 4.1)\n    * Improved embedded widget handling (new in version 4.2)\n    * Cross-platform SVG and ``border-radius`` support (new in version 4.4)\n    * Support for Tcl 9 (new in version 4.5)\n    * Caret browsing functionality (new in version 4.8)\n    * Improved thread safety (new in version 4.9)\n    * Ability to bind to HTML elements (new in version 4.10)\n    * Added an HTML-based text widget (new in version 4.15)\n\n.. dropdown:: Removed\n\n    Version 4.0:\n\n    * ``HtmlFrame.get_zoom()`` - use ``HtmlFrame.cget(\"zoom\")``\n    * ``HtmlFrame.set_zoom()`` - use ``HtmlFrame.configure(zoom=)``\n    * ``HtmlFrame.get_fontscale()`` - use ``HtmlFrame.cget(\"fontscale\")``\n    * ``HtmlFrame.set_fontscale()`` - use ``HtmlFrame.configure(fontscale=)``\n    * ``HtmlFrame.get_parsemode()`` - use ``HtmlFrame.cget(\"parsemode\")``\n    * ``HtmlFrame.set_parsemode()`` - use ``HtmlFrame.configure(parsemode=)``\n    * ``HtmlFrame.set_message_func()`` - use ``HtmlFrame.configure(message_func=)``\n    * ``HtmlFrame.set_broken_webpage_message()`` - use ``HtmlFrame.configure(on_navigate_fail=)``. Note that :attr:`on_navigate_fail` requires a function instead.\n    * ``HtmlFrame.set_maximum_thread_count()`` - use ``HtmlFrame.configure(threading_enabled=)``\n    * ``HtmlFrame.set_recursive_hover_depth()`` - use ``HtmlFrame.html.recursive_hover_depth=``\n    * ``HtmlFrame.add_visited_links()`` - use ``HtmlFrame.configure(visited_links=)``\n    * ``HtmlFrame.clear_visited_links()`` - use ``HtmlFrame.configure(visited_links=)``\n    * ``HtmlFrame.enable_stylesheets()`` - use ``HtmlFrame.configure(stylesheets_enabled=)``\n    * ``HtmlFrame.enable_images()`` - use ``HtmlFrame.configure(images_enabled=)``\n    * ``HtmlFrame.enable_forms()`` - use ``HtmlFrame.configure(forms_enabled=)``\n    * ``HtmlFrame.enable_objects()`` - use ``HtmlFrame.configure(objects_enabled=)``\n    * ``HtmlFrame.enable_caches()`` - use ``HtmlFrame.configure(caches_enabled=)``\n    * ``HtmlFrame.enable_dark_theme()`` - use ``HtmlFrame.configure(dark_theme_enabled=, image_inversion_enabled=)``\n    * ``HtmlFrame.on_image_setup()`` - use ``HtmlFrame.configure(on_resource_setup=)``\n    * ``HtmlFrame.on_downloading_resource()`` - bind to  ``<<DownloadingResource>>``/:py:attr:`utilities.DOWNLOADING_RESOURCE_EVENT`\n    * ``HtmlFrame.on_done_loading()`` - bind to ``<<DoneLoading>>``/:py:attr:`utilities.DONE_LOADING_EVENT`\n    * ``HtmlFrame.on_url_change()`` - bind to ``<<UrlChanged>>``/:py:attr:`utilities.URL_CHANGED_EVENT and use :attr:`.HtmlFrame.current_url`\n    * ``HtmlFrame.on_icon_change()`` - bind to ``<<IconChanged>>``/:py:attr:`utilities.ICON_CHANGED_EVENT` and use :attr:`.HtmlFrame.title`\n    * ``HtmlFrame.on_title_change()`` - bind to ``<<TitleChanged>>``/:py:attr:`utilities.TITLE_CHANGED_EVENT` and use :attr:`.HtmlFrame.title`\n    * ``HtmlFrame.on_form_submit()`` - use ``HtmlFrame.configure(on_form_submit=)``\n    * ``HtmlFrame.on_link_click()`` - use ``HtmlFrame.configure(on_link_click=)``\n    * ``HtmlFrame.yview_toelement()`` - use :meth:`.HTMLElement.scrollIntoView`\n    * ``HtmlFrame.get_currently_hovered_node_text()`` - :meth:`.HtmlFrame.get_currently_hovered_element`\n    * ``HtmlFrame.get_currently_hovered_node_tag()`` - :meth:`.HtmlFrame.get_currently_hovered_element`\n    * ``HtmlFrame.get_currently_hovered_node_attribute()`` - :meth:`.HtmlFrame.get_currently_hovered_element`\n    * ``HtmlFrame.get_current_link()`` - use :meth:`.HtmlFrame.get_currently_hovered_element`\n\n    * The ``widgetid`` attribute no longer embeds widgets. Use ``<object data=name_of_your_widget></object>`` or :attr:`.HTMLElement.widget` instead. This improves load speeds and allows for widget style handling.\n\n    Version 4.2:\n\n    * ``TkinterWeb.replace_widget()``\n    * ``TkinterWeb.replace_element()``\n    * ``TkinterWeb.remove_widget()``\n\n    Version 4.8\n\n    * ``HtmlFrame.replace_widget()`` (deprecated in version 4.0) - use :meth:`.HtmlFrame.widget_to_element` and :attr:`.HTMLElement.widget`\n    * ``HtmlFrame.replace_element()`` (deprecated in version 4.0) - use :attr:`.HTMLElement.widget`\n    * ``HtmlFrame.remove_widget()`` (deprecated in version 4.0) - use :meth:`.HTMLElement.remove`\n\n    Version 4.14:\n\n    * The ``style`` configuration option no longer sets the CSS style of :class:`.HtmlLabel` widgets. See `bug #145 <https://github.com/Andereoo/TkinterWeb/issues/145>`_.\n\n.. dropdown:: Deprecated\n\n    Version 4.11:\n\n    * ``TkinterWeb.update_tags()`` - use :meth:`.SelectionManager.update_tags`\n    * ``TkinterWeb.select_all()`` - use :meth:`.SelectionManager.select_all`\n    * ``TkinterWeb.clear_selection()`` - use :meth:`.SelectionManager.clear_selection`\n    * ``TkinterWeb.update_selection()`` - use :meth:`.SelectionManager.update_selection`\n    * ``TkinterWeb.get_selection()`` - use :meth:`.SelectionManager.get_selection`\n    * ``TkinterWeb.copy_selection()`` - use :meth:`.SelectionManager.copy_selection`\n    * ``TkinterWeb.allocate_image_name()`` - use :meth:`.ImageManager.allocate_image_name`\n    * ``TkinterWeb.handle_node_replacement()`` - use :meth:`.WidgetManager.handle_node_replacement`\n    * ``TkinterWeb.map_node()`` - use :meth:`.WidgetManager.map_node`\n    * ``TkinterWeb.find_text()`` - use :meth:`.SearchManager.find_text`\n    * ``TkinterWeb.send_onload()`` - use :meth:`.EventManager.send_onload`\n\n    Version 4.12:\n\n    * ``Htmlframe.register_JS_object()`` - use :meth:`.JSEngine.register`\n\n    Version 4.14:\n\n    * The configuration option ``default_style`` - use ``tkinterweb.utilities.DEFAULT_STYLE`` or the ``defaultstyle`` configuration option.\n    * The configuration option ``dark_style`` - use ``tkinterweb.utilities.DARK_STYLE`` or the ``defaultstyle`` configuration option.\n    * The configuration option ``about_page_background`` - use ``ttk.Style().configure(\"TFrame\", background=)``.\n    * The configuration option ``about_page_foreground`` - use ``ttk.Style().configure(\"TFrame\", foreground=)``.\n\n    Version 4.16:\n\n    * :meth:`.HtmlFrame.get_caret_page_position` - use  :meth:`HtmlFrame.get_caret_position(return_element=False) <.HtmlFrame.get_caret_position>`\n    * :meth:`.HtmlFrame.set_caret_page_position` - use :meth:`HtmlFrame.set_caret_position(index=) <.HtmlFrame.set_caret_position>`\n    * :meth:`.HtmlFrame.get_selection_page_position` - use :meth:`HtmlFrame.get_selection_position(return_elements=False) <.HtmlFrame.get_selection_position>`\n    * :meth:`.HtmlFrame.set_selection_page_position` - use :meth:`HtmlFrame.set_selection_position(start_index=, end_index=) <.HtmlFrame.set_selection_position>`\n\n    Version 4.22:\n\n    * :meth:`.HtmlFrame.insert_html` - use :meth:`.HtmlFrame.add_html`\n\n\n.. dropdown:: Renamed\n\n    Version 4.0:\n\n    * ``HtmlFrame.get_currently_selected_text()`` -> :meth:`.HtmlFrame.get_selection`\n\n    * ``TkwDocumentObjectModel`` -> :class:`.HTMLDocument`\n    * ``HtmlElement`` -> :class:`.HTMLElement`\n\n    * ``HtmlElement.style()`` -> :attr:`.HTMLElement.style`\n    * ``HtmlElement.innerHTML()`` -> :attr:`.HTMLElement.innerHTML`\n    * ``HtmlElement.textContent()`` -> :attr:`.HTMLElement.textContent`\n    * ``HtmlElement.attributes()`` -> :attr:`.HTMLElement.attributes`\n    * ``HtmlElement.tagName()`` -> :attr:`.HTMLElement.tagName`\n    * ``HtmlElement.parentElement()`` -> :attr:`.HTMLElement.parentElement`\n    * ``HtmlElement.children()`` -> :attr:`.HTMLElement.children`\n\n    * The ``scroll-x`` attribute was changed to the ``tkinterweb-scroll-x`` attribute. Like the ``overflow`` CSS property, valid options are now \"auto\", \"visible\", \"clip\", \"scroll\", and \"hidden\".\n\n.. dropdown:: Added\n\n    Version 4.0:\n\n    * :meth:`.HtmlFrame.clear_selection`\n    * :meth:`.HtmlFrame.get_currently_hovered_element`\n    * :meth:`.HtmlFrame.save_page`\n    * :meth:`.HtmlFrame.snapshot_page`\n    * :meth:`.HtmlFrame.show_error_page`\n    * :meth:`.HtmlFrame.print_page`\n    * :meth:`.HtmlFrame.screenshot_page`\n\n    * :attr:`.HtmlFrame.base_url`\n    * :attr:`.HtmlFrame.icon`\n    * :attr:`.HtmlFrame.title`\n\n    * :meth:`.HTMLElement.getElementById`\n    * :meth:`.HTMLElement.getElementsByClassName`\n    * :meth:`.HTMLElement.getElementsByName`\n    * :meth:`.HTMLElement.getElementsByTagName`\n    * :meth:`.HTMLElement.querySelector`\n    * :meth:`.HTMLElement.querySelectorAll`\n    * :meth:`.HTMLElement.scrollIntoView`\n\n    * :class:`.CSSStyleDeclaration`\n    * :attr:`.CSSStyleDeclaration.*` (any camel-case CSS property)\n    * :attr:`.CSSStyleDeclaration.cssText`\n    * :attr:`.CSSStyleDeclaration.length`\n    * :attr:`.CSSStyleDeclaration.cssProperties`\n    * :attr:`.CSSStyleDeclaration.cssInlineProperties`\n\n    * :meth:`.TkinterWeb.enable_imagecache`\n    * :meth:`.TkinterWeb.destroy_node`\n    * :meth:`.TkinterWeb.get_node_properties`\n    * :meth:`.TkinterWeb.override_node_properties`\n    * :meth:`.TkinterWeb.update_tags`\n\n    * ``utilities.DOWNLOADING_RESOURCE_EVENT`` (equivalent to ``<<DownloadingResource>>``)\n    * ``utilities.DONE_LOADING_EVENT`` (equivalent to ``<<DoneLoading>>``)\n    * ``utilities.URL_CHANGED_EVENT`` (equivalent to ``<<UrlChanged>>``)\n    * ``utilities.ICON_CHANGED_EVENT`` (equivalent to ``<<IconChanged>>``)\n    * ``utilities.TITLE_CHANGED_EVENT`` (equivalent to ``<<TitleChanged>>``)\n\n    * Many new configuration options were added. See the :doc:`api/htmlframe` for a complete list.\n\n    * The ``tkinterweb-full-page`` attribute can now be added to elements to make them the same height as the viewport. Use this to align content vertically. This has no effect when shrink is enabled.\n\n    Version 4.1:\n\n    * :meth:`.HtmlFrame.register_JS_object``\n\n    * :attr:`.HTMLElement.widget` (updated again in version 4.2)\n    * :attr:`.HTMLElement.value`\n    * :attr:`.HTMLElement.checked`\n    * :attr:`.HTMLElement.onchange`\n    * :attr:`.HTMLElement.onload`\n    * :attr:`.HTMLElement.onclick`\n    * :attr:`.HTMLElement.oncontextmenu`\n    * :attr:`.HTMLElement.ondblclick`\n    * :attr:`.HTMLElement.onmousedown`\n    * :attr:`.HTMLElement.onmouseenter`\n    * :attr:`.HTMLElement.onmouseleave`\n    * :attr:`.HTMLElement.onmousemove`\n    * :attr:`.HTMLElement.onmouseout`\n    * :attr:`.HTMLElement.onmouseover`\n    * :attr:`.HTMLElement.onmouseup`\n\n    * :attr:`.CSSStyleDeclaration.setProperty`\n    * :attr:`.CSSStyleDeclaration.getPropertyValue`\n    * :attr:`.CSSStyleDeclaration.removeProperty`\n\n    * :meth:`.TkinterWeb.send_onload`\n\n    * Added support for many JavaScript events.\n\n    * The new configuration option ``on_element_script`` can be used to add a callback to run when a JavaScript event attribute on an element is encountered.\n    * The new configuration option ``javascript_enabled`` can be used to enable JavaScript support.\n\n    Version 4.2:\n\n    * :meth:`.HtmlFrame.widget_to_element`\n\n    * :meth:`.TkinterWeb.replace_node_contents`\n    * :meth:`.TkinterWeb.map_node`\n    * :meth:`.TkinterWeb.replace_node_with_widget`\n    * :meth:`.TkinterWeb.get_node_stacking`\n\n    Version 4.4:\n\n    * :class:`.HtmlParse`\n    * :class:`.TkHtmlParsedURI`\n    * :class:`.HTMLCollection`\n\n    * :meth:`.HtmlFrame.insert_html`\n\n    * :attr:`.HTMLElement.id`\n    * :attr:`.HTMLElement.className`\n\n    * :meth:`.TkinterWeb.override_node_CSS`\n    * :meth:`.TkinterWeb.write`\n    * :meth:`.TkinterWeb.get_child_text`\n    * :meth:`.TkinterWeb.safe_tk_eval`\n    * :meth:`.TkinterWeb.serialize_node`\n    * :meth:`.TkinterWeb.serialize_node_style`\n\n    * Added support for the HTML number input.\n\n    * The new configuration option ``tkhtml_version`` can be used to choose a specific Tkhtml version to load.\n\n    Version 4.5:\n\n    * The new configuration option ``ssl_cafile`` can be used to provide a path to a CA Certificate file. See `bug #28 <https://github.com/Andereoo/TkinterWeb/issues/28>`_.\n\n    Version 4.6:\n\n    * The new configuration option ``request_timeout`` can be used to specify the number of seconds to wait before a request times out.\n\n    Version 4.7:\n\n    * The new ``<<DOMContentLoaded>>`` event will be generated once the page DOM content has loaded. The page may not be done loading, but at this point it is possible to interact with the DOM.\n\n    Version 4.8:\n\n    * :meth:`.HtmlFrame.get_page_text`\n    * :meth:`.HtmlFrame.get_caret_position`\n    * :meth:`.HtmlFrame.get_caret_page_position` (deprecated in version 4.16)\n    * :meth:`.HtmlFrame.set_caret_position`\n    * :meth:`.HtmlFrame.set_caret_page_position` (deprecated in version 4.16)\n    * :meth:`.HtmlFrame.shift_caret_left`\n    * :meth:`.HtmlFrame.shift_caret_right`\n    * :meth:`.HtmlFrame.get_selection_position`\n    * :meth:`.HtmlFrame.get_selection_page_position` (deprecated in version 4.16)\n    * :meth:`.HtmlFrame.set_selection_position`\n    * :meth:`.HtmlFrame.set_selection_page_position` (deprecated in version 4.16)\n\n    * :attr:`.HTMLElement.previousSibling`\n    * :attr:`.HTMLElement.nextSibling`\n\n    * :attr:`.TkinterWeb.caret_manager`\n\n    * :meth:`.TkinterWeb.update_selection`\n    * :meth:`.TkinterWeb.tkhtml_offset_to_text_index`\n\n    * :class:`.CaretManager`\n\n    * The new configuration option ``caret_browsing_enabled`` can be used to enable or disable caret browsing mode.\n\n    Version 4.9:\n\n    * :meth:`.TkinterWeb.post_to_queue`\n    * :meth:`.TkinterWeb.allocate_image_name`\n    * :meth:`.TkinterWeb.check_images`\n\n    Version 4.10:\n\n    * :meth:`.HTMLElement.bind`\n    * :meth:`.HTMLElement.unbind`\n\n    * :attr:`.TkinterWeb.event_manager`\n\n    * :class:`.EventManager`\n\n    * You can now set ``allowstyling=\"deep\"`` on elements with embedded widgets to also style their subwidgets.\n\n    Version 4.11:\n\n    * :meth:`.HtmlFrame.unbind`\n\n    * :class:`.HtmlText`\n\n    * :attr:`.TkinterWeb.selection_manager`\n    * :attr:`.TkinterWeb.widget_manager`\n    * :attr:`.TkinterWeb.search_manager`\n    * :attr:`.TkinterWeb.script_manager`\n    * :attr:`.TkinterWeb.style_manager`\n    * :attr:`.TkinterWeb.image_manager`\n    * :attr:`.TkinterWeb.object_manager`\n    * :attr:`.TkinterWeb.form_manager`\n    * :attr:`.TkinterWeb.node_manager`\n\n    * :class:`.SelectionManager`\n    * :class:`.WidgetManager`\n\n    Version 4.12:\n\n    * :class:`.JSEngine`\n\n    Version 4.13:\n\n    * :meth:`.TkinterWeb.get_node_replacement`\n\n    Version 4.14:\n\n    * :attr:`.HTMLElement.innerText`\n\n    * The new configuration option ``request_func`` can be used to set a custom script to use to download resources.\n    * The new configuration option ``defaultstyle`` can be used to set the default stylesheet to use when parsing HTML.\n\n    Version 4.15:\n\n    * :meth:`.HtmlFrame.add_css` now accepts the additional parameter ``priority``. \n    * :meth:`.CaretManager.shift_left`, :meth:`.CaretManager.shift_right`, :meth:`.CaretManager.shift_up`, :meth:`.CaretManager.shift_down`, and :meth:`.CaretManager.shift_update` now accept the additional parameter ``update``.\n\n    * The :class:`.HtmlText` widget now supports the ``background``, ``foreground``, ``bg``, and ``fg`` keywords.\n\n    Version 4.16:\n\n    * :meth:`.HtmlFrame.get_caret_position` now accepts the additional parameter ``return_element``. \n    * :meth:`.HtmlFrame.get_selection_position` now accepts the additional parameter ``return_elements``. \n\n    * :attr:`.HtmlText.insert`\n    * :attr:`.HtmlText.delete`\n\n    * The :class:`.HtmlText` widget now supports the ``state`` keyword.\n\n    * Added introductory support for :class:`.HtmlLabel` and :class:`.HtmlFrame(shrink=True)` widget resizing. This feature is experimental and may change at any time. Set ``HtmlFrame.unshrink = True`` to enable it and let me know how it works!\n\n    Version 4.17:\n\n    * The new configuration option ``textwrap`` can be used to enable or disable text wrapping. In general, text wrapping should be disabled when shrink is enabled, and should be enabled when shrink is disabled. This is the default behaviour. This is only partially supported in Tkhtml 3.0; make sure you have the `TkinterWeb-Tkhtml-Extras <https://pypi.org/project/tkinterweb-tkhtml-extras/>`_ package installed and up-to-date.\n\n    Version 4.18:\n\n    * Added basic support for most HTML5 elements in the corresponding `TkinterWeb-Tkhtml-Extras <https://pypi.org/project/tkinterweb-tkhtml-extras/>`_ release (version 1.3.0)\n    * Added support for the HTML ``<details>``, ``<summary>``, and ``<q>`` tags. \n\n    Version 4.19:\n\n    * :meth:`.HtmlFrame.generate_style_report`\n    * :attr:`.TkinterWeb.tkhtml_default_style`\n    * :attr:`.TkinterWeb.images`\n    * :attr:`.TkinterWeb.style_report`\n    * :meth:`.TkinterWeb.decode_uri`\n    * :meth:`.TkinterWeb.encode_uri`\n    * :meth:`.TkinterWeb.escape_uri`\n\n    * Added support for the ``media`` attribute of ``<link>`` elements. Ensure our experimental Tkhtml release is installed.\n    * Added support for the HTML ``<progress>`` tag. Ensure `TkinterWeb-Tkhtml-Extras <https://pypi.org/project/tkinterweb-tkhtml-extras/>`_ is installed.\n    * The new configuration option ``javascript_backend`` can be used to evaluate ``<script>`` elements and JavaScript events as Python code.\n\n    Version 4.20:\n\n    * :meth:`.HTMLDocument.write`\n    * :meth:`.HTMLElement.removeAttribute`\n\n    Version 4.21:\n\n    * :meth:`.HtmlFrame.reload`\n\n    Version 4.22:\n    \n    * :meth:`.HtmlFrame.add_html` now accepts the new parameter ``index``.\n\n    Version 4.23:\n\n    * :meth:`.HtmlFrame.snapshot_page` now accepts the new parameter ``include_head``.\n\n    Version 4.24:\n    \n    * :meth:`.HtmlFrame.load_form_data` now accepts the new parameter ``force``.\n\n.. dropdown:: Changed/Fixed\n\n    Version 4.0:\n\n    * :meth:`.HtmlFrame.configure`, :meth:`.HtmlFrame.config`, :meth:`.HtmlFrame.cget`, and :meth:`.HtmlFrame.__init__` now support more configuration options.\n    * :meth:`.HtmlFrame.load_website`, :meth:`.HtmlFrame.load_file`, and :meth:`.HtmlFrame.load_url` no longer accept the ``insecure`` parameter. use ``HTMLElement.configure(insecure=)``.\n\n    * Enabling/disabling caches now enables/disables the Tkhtml image cache.\n    * Threading now cannot be enabled if the Tcl/Tk build does not support it.\n\n    * :meth:`.HTMLElement.remove` now raises a :py:class:`tkinter.TclError` when invoked on ``<html>`` or ``<body>`` elements, which previously caused segmentation faults.\n    * :attr:`.HTMLElement.innerHTML` and :attr:`.HTMLElement.textContent` now raise a :py:class:`tkinter.TclError` when invoked on ``<html>`` elements, which previously caused segmentation faults.\n\n    * Shorthand CSS properties can now be set and returned after the document is loaded.\n    \n    * The ability to style color selector inputs was improved.\n    * The ability to access form elements has improved.\n    * Text elements now emit the ``<<Modified>>`` event *after* the content updates.\n    * The TkinterWeb demo and some of the built-in pages have been updated. Many internal methods and variables have been renamed, removed, or modified.\n\n    Version 4.1:\n\n    * :meth:`.HtmlFrame.screenshot_page` is now partially supported on Windows and now accepts the additional parameter ``show``. \n    * The default selection and find text colors are less abrupt.\n\n    Version 4.2:\n\n    * Widgets embedded in the document can now be removed without removing the containing element. \n\n    Version 4.3:\n\n    * Prebuilt Tkhtml binaries have been split off into a new package, `TkinterWeb-Tkhtml <https://pypi.org/project/tkinterweb-tkhtml/>`_. This has been done to work towards `bug #52 <https://github.com/Andereoo/TkinterWeb/issues/52>`_ and reduce the download size of the TkinterWeb package when updating.\n\n    Version 4.4:\n\n    * :meth:`.HtmlFrame.add_html` is now accepts the additional parameter ``return_element``. \n    * It is now only possible to enable experimental mode if an experimental Tkhtml release is detected.\n    * Some experimental HTML features were enabled in Windows and Linux. ``border-radius`` is now supported!\n\n    Version 4.5:\n\n    * Periods are now supported in url fragments. See `bug #143 <https://github.com/Andereoo/TkinterWeb/issues/143>`_ .\n    * Tkhtml file loading was updated in version 4.5. Some error messages have also been updated. Please submit a bug report if you notice any issues.\n\n    Version 4.6:\n\n    * Url fragments are now tracked as the document loads. This ensures that the fragment is still visible even after loading CSS files or images that change the layout of the document.\n    * ``gzip`` and ``deflate`` content encodings are now supported. Brotli compression is also supported if the :py:mod:`brotli` module is installed. This increases page load speeds and decreases bandwidth usage in some websites.\n    * Pressing Ctrl-A in an HTML number input, text input, or textarea will cause the widget's text to be selected. Pasting will now overwrite any selected text.\n    * Loading local files with a query string in the url will no longer raise an error.\n    * Fixed :meth:`.HTMLDocument.querySelector`.\n\n    Version 4.7:\n\n    * Fixed flickering when moving the mouse over scrollbars in ``<iframe>`` elements.\n    * ``bind()`` calls to the :meth:`.HtmlFrame.bind` respect requests to bind ``<Enter>`` and ``<Leave>``. All other events are still bound to the associated :class:`~tkinterweb.TkinterWeb` instance. Keep in mind that overriding the default bindings to ``<Enter>`` and ``<Leave>`` may cause unwanted side effects. \n\n    Version 4.8:\n\n    * All HTML widgets now bind to ``<Up>``, ``<Down>``, ``<Left>``, ``<Right>``, ``<Prior>``, ``<Next>``, ``<Home>``, and ``<End>`` by default.\n    * Fixed :meth:`.HTMLElement.parentElement`.\n\n    Version 4.9:\n\n    * TkinterWeb is now thread-safe when loading resources. All callbacks now will run on the main thread.\n    * Fixed loading of data urls.\n    * Local files will now load regardless of the number of slashes before the path.\n    * Fixed some dark mode and image inversion bugs.\n\n    Version 4.10:\n\n    * Binding button presses and motion events to the widget no longer removes internal bindings.\n    * Setting ``html.maximum_thread_count = 0`` no longer disables threading. Use ``html.threading_enabled = False``.\n    * :py:mod:`PIL` is now an optional dependency. I also recommend installing the new `TkinterWeb-Tkhtml-Extras <https://pypi.org/project/tkinterweb-tkhtml-extras/>`_ package.\n    * The :attr:`.HTMLElement.widget` property now returns a Tk widget when used on ``<input>``, ``<textarea>``, ``<select>``, ``<iframe>``, and some ``<object>`` elements.\n    * Fixed scrollbar flashes when the widget opens.\n    * DOM objects now provide more useful information when printed.\n    * By default, scrolling on embedded widgets now scrolls the page if the embedded widget or subwidgets do not bind to the mousewheel.\n    * If dark theme is enabled, HTML code passed to the configuration option ``dark_style`` will now be automatically appended onto the code set by ``default_style``.\n    * Plain text is no longer rendered as a blank page.\n    * The event queue now only runs when threading is enabled.\n    * Modifying the selection when selection is disabled now raises an error.\n    * Modifying the caret position when caret browsing is disabled now raises an error.\n    * Local file loading now happens on the main thread.\n    * Fixed a fatal scrollbar error when loading TkinterWeb on Tk 8.5 on MacOS.\n    * Fixed a fatal binding error when loading TkinterWeb on MacOS.\n    * Many internal changes were made in this release. If you notice any bugs, please report them.\n\n    Version 4.11:\n\n    * Fixed some minor bugs.\n    * JavaScript events no longer fire when events are disabled.\n    * The :class:`.TkinterWeb` widget was restructured in this release. If you notice any bugs, please report them.\n\n    Version 4.12:\n\n    * Fixed more minor bugs.\n    * Side-scrolling is now supported.\n\n    Version 4.13:\n\n    * Fixed more minor bugs, including a segfault when inserting a widget into the page's root element.\n    * ``grid_propagate(0)`` and ``pack_propagate(0)`` no longer have any effect on the widget. Requested width and height will now always be respected.\n\n    Version 4.14:\n\n    * Fixed more minor bugs.\n    * The :class:`.HtmlLabel` widget now automatically matches the ttk style.\n    * Alternate text for broken images is now displayed natively through Tkhtml.\n\n    Version 4.15:\n\n    * Fixed more minor bugs.\n    * Improved some error messages.\n    * Improved code autocompletion.\n    * All HTML widgets now bind to ``<Ctrl-A>`` by default.\n    * Equality checking between :class:`.HTMLElement` objects is now fully supported.\n    * The :class:`.HtmlText` widget is now editable out-of-the-box!\n    * The :class:`.HtmlLabel` widget now uses the ``TLabel`` style by default instead of ``TFrame``.\n\n    Version 4.16:\n\n    * Fixed more minor bugs.\n    * :meth:`.HtmlFrame.set_caret_position` now sets the caret relative to the document text when no element is provided. \n    * :meth:`.HtmlFrame.set_selection_position` now sets the selection relative to the document text when no elements are provided. \n    * A ``NotImplementedError`` will now raise when changing some settings via ``HtmlFrame.configure()``. This occurs on settings that have no effect after the widget loads and on the shrink value, which has been causing segfaults when changed after the widget loads. If you absolutely need to change the shrink value on the fly use ``HtmlFrame.html.configure()``\n    * Text wrapping has been disabled by default in the :class:`.HtmlLabel` and :class:`.HtmlFrame(shrink=True)` widgets. \n    \n    Version 4.17:\n    \n    * Fixed some :class:`.HtmlFrame` shrink regressions. See `bug #147 <https://github.com/Andereoo/TkinterWeb/issues/147>`_.\n    * Fixed some image loading and ``<iframe>`` scrolling regressions.\n    * Fixed a bug where stopping a page load prevented the page from loading again when caches were enabled.\n    * ``<style>`` tags and local files are now always evaluated in the main thread.\n    * The configuration options ``horizontal_scrollbar`` and ``vertical_scrollbar`` now accept another option, ``\"dynamic\"``. This behaves like ``\"auto\"``, with the difference that scrollbars are always hidden in :class:`.HtmlLabel` and :class:`.HtmlFrame(shrink=True)` widgets. This is the new default for vertical scrollbars.\n\n    Version 4.18:\n\n    * Triple-clicking on text now highlights all text in the line, even if multiple inline elements are present.\n    * Fixed some bugs that arose when stopping a page load and introduced some minor optimizations.\n\n    Version 4.19:\n    \n    * Fixed some bugs.\n    * Callbacks now accept ``None`` as a valid value.\n    * Permitted values for configuration option are now more tightly restricted.\n    * :attr:`.CSSStyleDeclaration.cssText` can now be set.\n\n    Version 4.20:\n\n    * Fixed some regressions and bugs.\n    * The :class:`.HtmlText` widget now emits the ``<<Modified>>`` event when the user types in it.\n\n    Version 4.21:\n\n    * Fixed a bug where ``<style>`` and ``<script>`` elements were left out of the output from :meth:`.HtmlFrame.save_page`. The output of this method is now the document's original HTML and is unaffected by JavaScript or DOM changes.\n    * Fixed a bug where adding :class:`.HtmlLabel` widgets causes the app to open in the wrong part of the screen.\n    * :attr:`.HtmlFrame.current_url` no longer returns the working directory when loading plain HTML code.\n\n    Version 4.23:\n\n    * Whitespace is now automatically stripped from the page's title.\n    * Improved :meth:`.HtmlFrame.snapshot_page` output formatting and accuracy.\n    * In an effort to reduce the widget's memory footprint, all HTML widgets no longer remember the original HTML code they are displaying:\n\n        * The output of :meth:`.HtmlFrame.save_page` is now the document's original HTML only when the page has been cached. This is the case when caching is enabled and a url is loaded. Otherwise, :meth:`.HtmlFrame.snapshot_page` is used, with the contents of the ``<head>`` tag included if the widget is still loading.\n        * :meth:`.HtmlFrame.reload` now only reloads pages loaded from a url.\n        \n        The original intent of both both methods is to be used when a url is loaded, and in an ideal world caching should always be enabled.\n\n    * :meth:`.HtmlFrame.reload` and :meth:`.HtmlFrame.save_page` now fully support pages generated through form submissions.\n    * The ``<<UrlChanged>>``/:py:attr:`utilities.URL_CHANGED_EVENT` event now also fires when a url is navigated to.\n    * The page cache backend was overhauled in this release. Please file a bug report if you notice any issues. \n    * Loading cached stylesheets, scripts, and images no longer spawns new threads. This fixes some bugs when loading cached documents and improves load times on some pages.\n    * Disabling the cache now also clears it.\n\n    Version 4.24:\n\n    * Stopped windows from teleporting across the galaxy or disappearing altogether when changing :attr:`.HTMLElement.innerHTML` and :attr:`.HTMLElement.textContent` before the app opens.\n    * Fixed a minor threading bug in :meth:`.HtmlFrame.load_form_data`.\n    * Fixed `bug #150 <https://github.com/Andereoo/TkinterWeb/issues/150>`_ .\n\n    Version 4.25:\n\n    * In an effort to closer match stock Tk widgets, debug message behaviour has been adjusted:\n       \n        * Debug messages are now disabled by default. Set the configuration option ``messages_enabled=True`` to enable them.\n        * In an effort to keep backwards compatibility, if the configuration option ``message_func`` is set, the value of ``messages_enabled`` is ignored.\n\n    * Auto-scrolling behaviour when searching the page for text been improved.\n    * Fixed a regression impacting :attr:`.HTMLElement.textContent`.\n\n-------------------\n\nPlease report bugs or request new features on the `issues page <https://github.com/Andereoo/TkinterWeb/issues>`_.\n"
  },
  {
    "path": "docs/source/usage.rst",
    "content": "Getting Started\n===============\n\n.. note::\n    The API changed significantly in version 4. See :doc:`the changelog <upgrading>` for details.\n\nInstallation\n------------\n\nTo use TkinterWeb, first install it using pip:\n\n.. code-block:: console\n\n   $ pip install tkinterweb[recommended]\n\n.. tip::\n    \n    You can also choose from the following extras:\n\n    .. code-block:: console\n\n        $ pip install tkinterweb[html,images,svg,javascript,requests]\n    \n    Run ``pip install tkinterweb[full]`` to install all optional dependencies or ``pip install tkinterweb`` to install the bare minimum.\n\nRun the TkinterWeb demo to see if it worked!\n\n>>> from tkinterweb import Demo\n>>> Demo()\n\n.. image:: ../../images/tkinterweb-demo.png\n\nTkinterWeb requires Tkinter, `TkinterWeb-Tkhtml <https://pypi.org/project/tkinterweb-tkhtml/>`_, `PIL <https://pillow.readthedocs.io/>`_, and `PIL.ImageTk <https://pillow.readthedocs.io/en/stable/reference/ImageTk.html>`_. All dependencies should be installed when installing TkinterWeb the recomended way, but on some systems `PIL.ImageTk <https://pillow.readthedocs.io/en/stable/reference/ImageTk.html>`_ may need to be installed seperately in order to load most image types.\n\nGetting started\n----------------\n\nTkinterWeb is very easy to use! Here is an example:\n\n.. code-block:: python\n\n    import tkinter as tk\n    from tkinterweb import HtmlFrame # import the HtmlFrame widget\n    \n    root = tk.Tk() # create the Tkinter window\n    \n    yourhtmlframe = HtmlFrame(root, messages_enabled=True) # create the HtmlFrame widget\n    yourhtmlframe.load_html(\"<h1>Hello, World!</h1>\") # load some HTML code\n    yourhtmlframe.pack(fill=\"both\", expand=True) # attach the HtmlFrame widget to the window\n    \n    root.mainloop()\n\nYou can also use :meth:`~tkinterweb.HtmlFrame.load_website`, :meth:`~tkinterweb.HtmlFrame.load_file`, or :meth:`~tkinterweb.HtmlFrame.load_url` to load webpages.\n\nThe :class:`~tkinterweb.HtmlFrame` widget behaves like any other Tkinter widget and supports bindings. It also supports link clicks, form submittions, website title changes, and much, much more! See below for more tips and tricks.\n\n.. tip::\n    Use the :class:`~tkinterweb.HtmlLabel` widget for an HTML-based label widget and the :class:`~tkinterweb.HtmlText` widget for an HTML-based text widget.\n    \nTips and tricks\n---------------\n\nCreating bindings\n~~~~~~~~~~~~~~~~~\n\nLike any other Tkinter widget, mouse and keyboard events can be bound to the :class:`~tkinterweb.HtmlFrame` widget.\n\nThe following is an example of the usage of bingings to show a menu:\n\n.. code-block:: python\n\n    def on_right_click(event):\n        # Get the element under the mouse and its url\n        element = yourhtmlframe.get_currently_hovered_element()\n        url = element.getAttribute(\"href\")\n\n        if url:\n            # Resolve the url to ensure it is a full url\n            url = yourhtmlframe.resolve_url(url)\n\n            # Create the menu and add a button with the url\n            menu = tk.Menu(root, tearoff=0)\n            menu.add_command(label=\"Open %s\" % url, \n                command=lambda url=url: yourhtmlframe.load_url(url))\n\n            # Show the menu\n            menu.tk_popup(event.x_root, event.y_root, 0)\n\n    yourhtmlframe.bind(\"<Button-3>\", on_right_click)\n\nThis will make a popup open when the user right-clicks on a link. Clicking the link shown in the popup would load the website.\n\nNote that some keypress events are automatically bound to the widget. If you notice a feature unintentionally stops working after adding a binding, consider using ``bind(event, callback, add=\"+\")`` to add your binding instead of replacing the default one.\n\n.. tip::\n    Since version 4.10, you can also bind to a specific HTML element! See :ref:`binding-to-an-element` for more details.\n\nChanging the title\n~~~~~~~~~~~~~~~~~~\n\nTo change the title of the window every time the title of a website changes, use the following:\n\n.. code-block:: python\n\n    def change_title(event):\n        root.title(yourhtmlframe.title) # change the title\n        \n    yourhtmlframe.bind(\"<<TitleChanged>>\", change_title)\n\nSimilarily, the ``<<IconChanged>>`` event fires when the website's icon changes.\n\nHandling url changes\n~~~~~~~~~~~~~~~~~~~~\n\nNormally, a website's url may change when it is loaded. For example, \"https://github.com\" will redirect to \"https://www.github.com\". This can be handled with a binding to ``<<UrlChanged>>``:\n\n.. code-block:: python\n\n    def url_changed(event):\n        updated_url = yourhtmlframe.current_url\n        ### Do stuff, such as change the content of an address bar\n        \n    yourhtmlframe.bind(\"<<UrlChanged>>\", url_changed)\n\nThis is highly recomended if your app includes an address bar. This event will fire on page redirects and url changes when a page stops loading.\n\n\nSearching the page\n~~~~~~~~~~~~~~~~~~\n\nUse :meth:`~tkinterweb.HtmlFrame.find_text` to search the page for specific text. To search the document for the word 'python', for example, the following can be used:\n\n.. code-block:: python\n\n    number_of_matches = yourhtmlframe.find_text(\"python\")\n\nOr, to select the second match found:\n\n.. code-block:: python\n\n    number_of_matches = yourhtmlframe.find_text(\"python\", 2)\n\nRefer to the API reference for more information.\n\n.. tip::\n    \n    Check out `bug 18 <https://github.com/Andereoo/TkinterWeb/issues/18#issuecomment-881649007>`_ or the `sample web browser <https://github.com/Andereoo/TkinterWeb/blob/main/examples/TkinterWebBrowser.py>`_ for a sample find bar!\n\nDone loading?\n~~~~~~~~~~~~~\n\nWebsite loading is performed asynchronously. When loading a website, you can bind to the ``<<DoneLoading>>`` event, which fires when the document has finished loading.\n\nIf you bind to ``<<DoneLoading>>`` to update GUI state (for example, switching a 'Stop' button to 'Refresh'), it is generally recommended to also bind to the ``<<DownloadingResource>>`` event to handle the opposite case. Without this, the document may report that it has finished loading while additional resources (such as images, scripts, or stylesheets) are still being downloaded.\n\nWhen loading raw HTML or local files, the page loads synchronously and can be manipulated immediately.\n\nStop loading\n~~~~~~~~~~~~\n\nThe method :meth:`~tkinterweb.HtmlFrame.stop` can be used to stop loading a webpage. If :meth:`~tkinterweb.HtmlFrame.load_url`, :meth:`~tkinterweb.HtmlFrame.load_website`, or :meth:`~tkinterweb.HtmlFrame.load_file` was used to load the document, passing ``yourhtmlframe.current_url`` with ``force=True``  will force a page refresh.\n\nHandling link clicks\n~~~~~~~~~~~~~~~~~~~~\n\nLink clicks can also be easily handled. By default, when a link is clicked, it will be automatically loaded.\nTo, for example, run some code before loading the new website, use the following: \n\n.. code-block:: python\n\n    yourhtmlframe = HtmlFrame(master, on_link_click=load_new_page)\n    \n    def load_new_page(url):\n        ### Do stuff\n        yourhtmlframe.load_url(url) # load the new website    \n\nSimilarily, :attr:`on_form_submit` can be used to override the default form submission handlers.\n\nZooming\n~~~~~~~\n\nSetting the zoom of the :class:`~tkinterweb.HtmlFrame` widget is very easy. This can be used to improve accessibility in your application. To set the zoom to 2x magnification the following can be used: \n\n.. code-block:: python\n\n    yourhtmlframe = HtmlFrame(master, zoom=2)\n    ### Or yourhtmlframe.configure(zoom=2)\n    ### Or yourhtmlframe[\"zoom\"] = 2\n\nTo scale only the text, use ``fontscale=2`` instead.\n\nEmbedding a widget\n~~~~~~~~~~~~~~~~~~\n\nThere are many ways to embed widgets in an :class:`~tkinterweb.HtmlFrame` widget. One way is to use ``<object>`` elements:\n\n.. code-block:: python\n\n    yourcanvas = tkinter.Canvas(yourhtmlframe)\n    yourhtmlframe.load_html(f\"<p>This is a canvas!</p><object data=\"{yourcanvas}\"></object>\")\n\nRefer to :doc:`geometry` for more information.\n\nManipulating the DOM\n~~~~~~~~~~~~~~~~~~~~\n\nRefer to :doc:`dom` (new in version 3.25).\n\nUsing JavaScript\n~~~~~~~~~~~~~~~~\n\nRefer to :doc:`javascript` (new in version 4.1).\n\nMaking the page editable\n~~~~~~~~~~~~~~~~~~~~~~~~\n\nRefer to :doc:`caret` (new in version 4.8).\n\nShrinking a widget to match its contents\n~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\nRefer to :doc:`shrink`.\n\nUsing dark mode\n~~~~~~~~~~~~~~~\n\nYou can set ``dark_theme_enabled=True`` when creating your :class:`~tkinterweb.HtmlFrame` or calling :meth:`~tkinterweb.HtmlFrame.configure` to turn on dark mode and automatically modify page colours.\n\nIf you set ``image_inversion_enabled=True``, an algorithm will attempt to detect and invert images with a predominantly light-coloured background. This helps make light-coloured images or pictures with a white background darker.\n\nRefresh the page for these features to take full effect. This features may cause hangs or crashes on more complex websites.\n\n-------------------\n\nSee the :doc:`api/htmlframe` for a complete list of available commands.\n"
  },
  {
    "path": "examples/TkinterWebBrowser.py",
    "content": "\"\"\"\nA proof-of-concept web browser using TkinterWeb\n\nNote that TkinterWeb is not necessarily intended to be a full-blown modern web browser\nThese already exist and are generally resource-hungry and not highly integratable with Tkinter\nBeing based on Tkhtml, TkinterWeb is intended to be fast, lightweight, and highly integrated with Tkinter while providing far more control over layouts and styling than is feasible than Tkinter\nTkinterWeb displays older or simpler websites well but may be found lacking on more modern websites\n\nThis code was created for testing TkinterWeb and is a bit of a mess, but nonetheless is a great example of some of the things that can be done with the software, including:\n - loading pages\n - searching pages\n - embedding Tkinter widgets\n - managing input elements\n - embedding Python code\n - manipulating the DOM\n - making round buttons\n - and more\n \nCopyright (c) 2026 Andrew Clarke\n\"\"\"\n\nimport os\nimport sys\n\nsys.path.insert(0, os.path.dirname(os.path.dirname(os.path.realpath(__file__))))\n\n\nimport tkinter as tk\nfrom tkinter import filedialog\nfrom tkinter import ttk\n\nfrom tkinterweb import HtmlFrame, Notebook, __version__\nfrom tkinterweb.utilities import BUILTIN_PAGES, DONE_LOADING_EVENT, URL_CHANGED_EVENT, TITLE_CHANGED_EVENT, DOWNLOADING_RESOURCE_EVENT, DOM_CONTENT_LOADED_EVENT\nfrom tkinterweb.subwidgets import ScrolledTextBox, FormEntry\n\nimport os\n\n\nif os.name == \"nt\":\n\tfrom ctypes import windll\n\twindll.shcore.SetProcessDpiAwareness(1)\n\nversion = []\nfor letter in __version__.split(\".\"):\n    version.append(int(letter))\nif tuple(version) < (4, 19, 0):\n    raise RuntimeError(\"This demo needs TkinterWeb version 4.19.0 or higher.\")\n\nif len(sys.argv) > 1:\n    NEW_TAB = sys.argv[1]\nelse:\n    NEW_TAB = \"about:tkinterweb\"\n\n\ndef check_url(entry):\n    url = entry.get()\n    if not any((url.startswith(\"file:\"), url.startswith(\"http:\"), url.startswith(\"about:\"), url.startswith(\"view-source:\"), url.startswith(\"https:\"), url.startswith(\"data:\"))):\n        if os.path.exists(url):\n            url = f\"file://{url}\"\n        else:\n            url = \"http://{}\".format(url)\n    return url\n\n\nclass HTMLPlayground(ttk.PanedWindow):\n    def __init__(self, master):\n        ttk.PanedWindow.__init__(self, master, orient=tk.HORIZONTAL)\n\n        self.master = master\n\n        text_frame = ttk.Frame(self)\n        html_frame = ttk.Frame(self)\n        text_frame._scroll = lambda *a: None\n        text_frame._xscroll = lambda *a: None\n        text_frame._scroll_x11 = lambda *a: None\n        text_frame._xscroll_x11 = lambda *a: None\n        self.textarea = textarea = ScrolledTextBox(text_frame, content=\"Type HTML code here\", padx=8, pady=8, wrap=tk.NONE)\n        self.iframe = iframe = HtmlFrame(html_frame,\n            messages_enabled=False,\n            message_func = master.html.message_func,\n            images_enabled = master.html.images_enabled,\n            forms_enabled = master.html.forms_enabled,\n            objects_enabled = master.html.objects_enabled,\n            ignore_invalid_images = master.html.ignore_invalid_images,\n            crash_prevention_enabled = master.html.crash_prevention_enabled,\n            dark_theme_enabled = master.html.dark_theme_enabled,\n            image_inversion_enabled = master.html.image_inversion_enabled,\n            caches_enabled = master.html.caches_enabled,\n            threading_enabled = master.html.threading_enabled,\n            image_alternate_text_enabled = master.html.image_alternate_text_enabled,\n            selection_enabled = master.html.selection_enabled,\n            find_match_highlight_color = master.html.find_match_highlight_color,\n            find_match_text_color = master.html.find_match_text_color,\n            find_current_highlight_color = master.html.find_current_highlight_color,\n            find_current_text_color = master.html.find_current_text_color,\n            selected_text_highlight_color = master.html.selected_text_highlight_color,\n            selected_text_color = master.html.selected_text_color,\n            caret_browsing_enabled = master.html.caret_browsing_enabled)\n        iframe.html.text_mode = False\n        iframe.load_html(\"HTML output shows here\")\n        iframe.config(messages_enabled=True)\n\n        self.urlbar = urlbar = FormEntry(text_frame, placeholder=\"https://\")\n        urlbar.bind(\"<Return>\", self._load_url)\n        go_button = ttk.Button(text_frame, text=\"Go\", cursor=\"hand2\", command=self._load_url)\n\n        self.base_title = \"HTML Playground\"\n        master.add_html(f\"<title>{self.base_title}</title>\")\n\n        # Make a round button\n        # Quite unnecessary, but very fun\n        s = ttk.Style()\n        run_button = HtmlFrame(textarea.tbox, messages_enabled=False, shrink=True, javascript_enabled=True, javascript_backend=\"python\", selection_enabled=False)\n        run_button.load_html(f\"\"\"<body>\n                                <style>\n                                    body{{margin:0;background-color:white}}\n                                    button{{color:{s.lookup(\"TButton\", \"foreground\")};background-color:{s.lookup(\"TButton\", \"background\")};border-radius:5px;padding:6px 11px;border-width:0}}\n                                    button:hover{{background-color:{s.lookup(\"TButton\", \"background\", state=(\"active\",))}}}\n                                    button:active{{background-color:{s.lookup(\"TButton\", \"background\")}}}\n                                </style>\n                                <button onclick='update_iframe()'>&gt;</button>\n                                </body>\"\"\")\n        run_button.javascript.register(\"update_iframe\", self._update_iframe)\n        run_button.place(relx=1.0, rely=1.0, anchor=\"se\")\n\n        self.iframe.bind(DOM_CONTENT_LOADED_EVENT, self._on_page_loaded)\n        self.iframe.bind(TITLE_CHANGED_EVENT, self._on_title_change)\n\n        text_frame.grid_rowconfigure(1, weight=1)\n        text_frame.grid_columnconfigure(0, weight=1)\n        urlbar.grid(row=0, column=0, sticky=\"nsew\", padx=(5,0))\n        go_button.grid(row=0, column=1, padx=(5,0))\n        textarea.grid(row=1, column=0, columnspan=2, sticky=\"nsew\", pady=(5,0), padx=(5,0))\n        iframe.pack(expand=True, fill=\"both\", padx=(0,5))\n\n        self.add(text_frame)\n        self.add(html_frame)\n\n    def _load_url(self, event=None):\n        url = check_url(self.urlbar)\n        self.iframe.load_url(url)\n\n    def _on_title_change(self, event, addendum=None):\n        if addendum is None:\n            addendum = f\" - {self.iframe.title}\"\n        self.master.add_html(f\"<title>{self.base_title}{addendum}</title>\")\n\n    def _on_page_loaded(self, event):\n        html = self.iframe.save_page()\n        self.textarea.delete(\"0.0\", \"end\")\n        self.textarea.insert(\"1.0\", html)\n\n    def _update_iframe(self, event=None):\n        self.iframe.unbind(\"<<DOMContentLoaded>>\")\n        self.master.add_html(f\"<title>{self.base_title}</title>\")\n        self.iframe.load_html(self.textarea.get(), self.iframe.base_url)\n        self.iframe.bind(\"<<DOMContentLoaded>>\", self._on_page_loaded)\n\n\nHTML_TEST_PAGE = \"\"\"\n<head>\n    <style>\n        html, body {{margin:0;overflow: hidden}}\n        object {{width:100%}}\n    </style>\n</head>\n<body><object tkinterweb-full-page data={}></object></body>\n</html>\"\"\"\n\n\nclass Page(ttk.Frame):\n    def __init__(self, master, *args, **kwargs):\n        ttk.Frame.__init__(self, master, *args, **kwargs)\n\n        self.master = master\n        self.back_history = []\n        self.forward_history = []\n\n        self.style = ttk.Style(self)\n        self.style.theme_use(\"default\")\n\n        topbar = ttk.Frame(self)\n        self.bottombar = bottombar = ttk.Frame(self)\n        self.findbar = findbar = ttk.Frame(self)\n\n        self.html_playground = None\n        \n        self.linklabel = linklabel = ttk.Label(bottombar, text=\"Welcome to TkinterWeb!\", cursor=\"hand2\")\n\n        self.backbutton = backbutton = ttk.Button(topbar, text=\"Back\", command=self.back, state=\"disabled\")\n        self.forwardbutton = forwardbutton = ttk.Button(topbar, text=\"Forward\", command=self.forward, state=\"disabled\")\n        self.reloadbutton = reloadbutton = ttk.Button(topbar, text=\"Reload\", command=self.reload, cursor=\"hand2\")\n        self.urlbar = urlbar = ttk.Entry(topbar, width=100)\n        newbutton = ttk.Button(topbar, text=\"New tab\", command=self.open_new_tab, cursor=\"hand2\")\n        closebutton = ttk.Button(topbar, text=\"Close\", command=self.close_current_tab, cursor=\"hand2\")\n        self.findbutton = findbutton = ttk.Button(topbar, text=\"Find\",  command=self.open_findbar, cursor=\"hand2\")\n        self.settingsbutton = settingsbutton = ttk.Button(topbar, text=\"Settings\", command=self.open_sidebar, cursor=\"hand2\")\n\n        self.message_box = tk.Text(self, height=8)\n\n        self.find_select_num = 1\n        self.find_match_num = 0\n        \n        self.findbox_var = findbox_var = tk.StringVar()\n        self.find_box = find_box = ttk.Entry(findbar, textvariable=findbox_var)\n        self.find_previous = find_previous = ttk.Button(findbar, text=\"Prevous\", command=self.previous_and_find, state=\"disabled\")\n        self.find_next = find_next = ttk.Button(findbar, text=\"Next\", command=self.next_and_find, state=\"disabled\")\n        self.ignore_case_var = ignore_case_var = tk.IntVar(value=1)\n        ignore_case = ttk.Checkbutton(findbar, text=\"Ignore Cases\", variable=ignore_case_var, command=self.search_in_page, cursor=\"hand2\")\n        self.highlight_all_var = highlight_all_var = tk.IntVar(value=1)\n        highlight_all = ttk.Checkbutton(findbar, text=\"Highlight All\", variable=highlight_all_var, command=lambda change=False: self.search_in_page(change=change), cursor=\"hand2\")\n        self.find_bar_caption = find_bar_caption = ttk.Label(findbar, text=\"\")\n        find_close = ttk.Button(findbar, text=\"Close\", command=self.open_findbar, cursor=\"hand2\")\n\n        self.frame = frame = HtmlFrame(self, message_func=self.add_message, on_link_click=self.link_click, on_form_submit=self.form_submit)\n        \n        self.sidebar = sidebar = HtmlFrame(frame, width=250, fontscale=0.8, selection_enabled=False, javascript_enabled=True, javascript_backend=\"python\")\n\n        self.images_var = images_var = tk.IntVar(value=self.frame[\"images_enabled\"])\n        images_enabled = ttk.Checkbutton(sidebar, text=\"Enable images\", variable=images_var, command=self.toggle_images)\n        self.styles_var = styles_var = tk.IntVar(value=self.frame[\"stylesheets_enabled\"])\n        styles_enabled = ttk.Checkbutton(sidebar, text=\"Enable stylesheets\", variable=styles_var, command=self.toggle_styles)\n        self.forms_var = forms_var = tk.IntVar(value=self.frame[\"forms_enabled\"])\n        forms_enabled = ttk.Checkbutton(sidebar, text=\"Enable forms\", variable=forms_var, command=self.toggle_forms)\n        self.objects_var = objects_var = tk.IntVar(value=self.frame[\"objects_enabled\"])\n        objects_enabled = ttk.Checkbutton(sidebar, text=\"Enable objects\", variable=objects_var, command=self.toggle_objects)\n        self.caches_var = caches_var = tk.IntVar(value=self.frame[\"caches_enabled\"])\n        caches_enabled = ttk.Checkbutton(sidebar, text=\"Enable caches\", variable=caches_var, command=self.toggle_caches)\n        self.crashes_var = crashes_var = tk.IntVar(value=self.frame[\"crash_prevention_enabled\"])\n        emojis_enabled = ttk.Checkbutton(sidebar, text=\"Enable crash prevention\", variable=crashes_var, command=self.toggle_emojis)\n        self.threads_var = threads_var = tk.IntVar(value=self.frame[\"threading_enabled\"])\n        threads_enabled = ttk.Checkbutton(sidebar, text=\"Enable threading\", variable=threads_var, command=self.toggle_threads)\n        self.invert_page_var = invert_page_var = tk.IntVar(value=self.frame[\"dark_theme_enabled\"])\n        self.js_var = js_var = tk.IntVar(value=self.frame[\"javascript_enabled\"])\n        js_enabled = ttk.Checkbutton(sidebar, text=\"Enable javascript\", variable=js_var, command=self.toggle_js)\n        invert_page_enabled = ttk.Checkbutton(sidebar, text=\"Dark theme\", variable=invert_page_var, command=self.toggle_theme)\n        self.invert_images_var = invert_images_var = tk.IntVar(value=self.frame[\"image_inversion_enabled\"])\n        invert_images_enabled = ttk.Checkbutton(sidebar, text=\"Image inverter\", variable=invert_images_var, command=self.toggle_inverter)\n        self.selection_var = selection_var = tk.IntVar(value=self.frame[\"selection_enabled\"])\n        selection_enabled = ttk.Checkbutton(sidebar, text=\"Text selection enabled\", variable=selection_var, command=self.toggle_selection)\n        self.caret_browsing_var = caret_browsing_var = tk.IntVar(value=self.frame[\"caret_browsing_enabled\"])\n        caret_browsing_enabled = ttk.Checkbutton(sidebar, text=\"Caret browsing enabled\", variable=caret_browsing_var, command=self.toggle_caret_browsing)\n        \n        self.view_source_button = view_source_button = ttk.Button(sidebar, text=\"View page source\", command=self.view_source)\n        about_button = ttk.Button(sidebar, text=\"About TkinterWeb\", command=lambda: self.open_new_tab(\"about:tkinterweb\"))\n        html_button = ttk.Button(sidebar, text=\"HTML playground\", command=lambda: self.open_new_tab(\"about:html\"))\n        style_button = ttk.Button(sidebar, text=\"Get style report\", command=self.style_report)\n        \n        frame.bind(TITLE_CHANGED_EVENT, self.change_title)\n        frame.bind(URL_CHANGED_EVENT, self.url_change)\n        frame.bind(DONE_LOADING_EVENT, self.done_loading)\n        frame.bind(DOWNLOADING_RESOURCE_EVENT, self.on_downloading)\n\n        for i in {\"<Up>\", \"<Down>\", \"<Left>\", \"<Right>\", \"<Prior>\", \"<Next>\", \"<Home>\", \"<End>\"}:\n            frame.unbind(i)\n\n        linklabel.bind(\"<Button-1>\", self.hide_messsage_box)\n        urlbar.bind(\"<Return>\", self.load_site)\n        #frame.bind(\"<Motion>\", self.on_motion)\n        frame.bind(\"<Leave>\", lambda event: linklabel.config(text=\"Done\"))\n\n        self.columnconfigure(0, weight=1)\n        self.rowconfigure(1, weight=1)\n        topbar.grid(column=0, row=0, sticky=\"ew\")\n        frame.grid(column=0, row=1, sticky=\"nsew\")\n        bottombar.grid(column=0, row=4, sticky=\"ew\")\n\n        self.sidebar.javascript.register(\"frame\", frame)\n\n        self.sidebar.load_html(f\"\"\"<html>\n  <body>\n    <style>\n      body p, span {{ margin-top: 5px; margin-bottom: 5px; cursor: default; }}\n      object {{ width: 100%; cursor: pointer; }}\n      input[type=\"color\"] {{ height: 15px; width: 30px; border: 1px solid black; padding: 0; margin: 5px; background-color: transparent; }}\n      label {{ margin-left: 5px; }}\n    </style>\n\n    <object allowscrolling data={images_enabled}></object><br>\n    <object allowscrolling data={styles_enabled}></object><br>\n    <object allowscrolling data={forms_enabled}></object><br>\n    <object allowscrolling data={objects_enabled}></object><br>\n    <object allowscrolling data={caches_enabled}></object><br>\n    <object allowscrolling data={emojis_enabled}></object><br>\n    <object allowscrolling data={threads_enabled}></object><br>\n    <object allowscrolling data={js_enabled}></object><hr>\n    <object allowscrolling data={selection_enabled}></object>\n    <object allowscrolling data={caret_browsing_enabled}></object><hr>\n    \n    <object allowscrolling data={invert_page_enabled}></object><br>\n    <object allowscrolling data={invert_images_enabled}></object><hr>\n\n    <div>\n      <p style=\"float:left\">Zoom:</p>\n      <span style=\"float:right\" id=\"zoom\">{frame['zoom']}</span>\n      <input onchange=\"document.getElementById('zoom').textContent = this.value; frame.config(zoom=this.value)\" style=\"width: 100%\" type=\"range\" min=\"0.1\" max=\"10\" step=\"0.1\" value=\"{self.frame['zoom']}\">\n    </div>\n    \n    <div>\n      <p style=\"float:left\">Font scale:</p>\n      <span style=\"float:right\" id=\"fontscale\">{frame['fontscale']}</span>\n      <input onchange=\"document.getElementById('fontscale').textContent = this.value; frame.config(fontscale=this.value)\" style=\"width: 100%\" type=\"range\" min=\"0.1\" max=\"10\" step=\"0.1\" value=\"{self.frame['fontscale']}\">\n    </div>\n    \n    <hr style=\"margin-bottom:10px;margin-top:10px\">\n    \n    <p>User agent:</p>\n    <input onchange=\"frame['headers']['User-Agent'] = this.value\" style=\"padding: 5px 0px 3px 0px; width: 100%; color:black\" type=\"text\" value=\"{frame['headers']['User-Agent']}\">\n    <hr style=\"margin-bottom:10px;margin-top:0\">\n    \n    <p>Parse mode:</p>\n    <select onchange=\"frame.config(parsemode=this.value)\" style=\"padding: 3px 0px 1px 0px; width:100%; color:black\">\n      <option value=\"xml\">xml</option>\n      <option value=\"xhtml\">xhtml</option>\n      <option value=\"html\">html</option>\n    </select>\n    <hr style=\"margin-bottom:10px;margin-top:0\">\n\n    <input type=\"color\" onchange=\"frame.config(find_match_highlight_color=this.value)\" value=\"{frame['find_match_highlight_color']}\">\n    <input type=\"color\" onchange=\"frame.config(find_match_text_color=this.value)\" value=\"{frame['find_match_text_color']}\"><label>Find matches</label><br>\n    <input type=\"color\" onchange=\"frame.config(find_current_highlight_color=this.value)\" value=\"{frame['find_current_highlight_color']}\">\n    <input type=\"color\" onchange=\"frame.config(find_current_text_color=this.value)\" value=\"{frame['find_current_text_color']}\"><label>Current match</label><br>\n    <input type=\"color\" onchange=\"frame.config(selected_text_highlight_color=this.value)\" value=\"{frame['selected_text_highlight_color']}\">\n    <input type=\"color\" onchange=\"frame.config(selected_text_color=this.value)\" value=\"{frame['selected_text_color']}\"><label>Selected text</label><br>\n\n    <hr>\n    \n    <div style=\"margin-top: 20px\">\n      <object allowscrolling data={view_source_button}></object>\n      <object allowscrolling data={style_button}></object>\n      <object allowscrolling data={html_button}></object>\n      <object allowscrolling data={about_button}></object>\n    </div>\n  </body>\n</html>\"\"\")\n        \n        frame.config = self.html_config\n\n        linklabel.pack(expand=True, fill=\"both\")\n        topbar.columnconfigure(4, weight=1)\n        backbutton.grid(row=0, column=1, pady=5, padx=5)\n        forwardbutton.grid(row=0, column=2, pady=5)\n        reloadbutton.grid(row=0, column=3, pady=5, padx=5)\n        urlbar.grid(row=0, column=4, pady=5, padx=20, sticky=\"NS\")\n        newbutton.grid(row=0, column=5, pady=5, padx=(5,0))\n        closebutton.grid(row=0, column=6, pady=5, padx=5)\n        findbutton.grid(row=0, column=7, pady=5)\n        settingsbutton.grid(row=0, column=8, pady=5, padx=5)\n\n        findbar.columnconfigure(6, weight=1)\n        find_box.grid(row=0, column=0, padx=5)\n        find_previous.grid(row=0, column=1)\n        find_next.grid(row=0, column=2)\n        ttk.Separator(findbar, orient=\"vertical\").grid(row=0, column=3, sticky=\"ns\", pady=4, padx=8)\n        ignore_case.grid(row=0, column=4, sticky=\"ns\")\n        highlight_all.grid(row=0, column=5, sticky=\"ns\", padx=5)\n        find_bar_caption.grid(row=0, column=7)\n        find_close.grid(row=0, column=8, sticky=\"ns\", padx=5)\n        ttk.Separator(findbar).grid(row=1, column=0, columnspan=9, sticky=\"ew\", pady=4, padx=8)\n\n        findbox_var.trace(\"w\", self.search_in_page)\n\n        frame.bind(\"<Button-3>\", self.on_right_click)\n        for widget in [urlbar, find_box]:\n            widget.bind(\"<Control-a>\", lambda e: self.after(50, self.select_all_in_entry, e.widget))\n\n        for child in findbar.winfo_children():\n            child.bind(\"<Escape>\", lambda x: self.open_findbar())\n        for child in sidebar.winfo_children():\n            child.bind(\"<Escape>\", lambda x: self.close_sidebar())\n        settingsbutton.bind(\"<Escape>\", lambda x: self.close_sidebar())\n\n        self.toggle_theme(False)\n\n    def html_config(self, **kwargs):\n        self.frame.configure(**kwargs)\n        if self.html_playground is not None: \n            self.html_playground.iframe.configure(**kwargs)\n\n    def style_report(self):\n        if self.frame.base_url == \"about:html\":\n            self.html_playground.iframe.generate_style_report().grab_set()\n        else:\n            self.frame.generate_style_report().grab_set()\n\n    def apply_dark_theme(self):\n        self.style.configure(\".\", background=\"#2b2b2b\", foreground=\"#FFFFFF\")\n        self.style.configure(\"TButton\",\n            background=\"#444444\",\n            foreground=\"#FFFFFF\")\n        self.style.map(\"TButton\",\n            background=[(\"active\", \"#666666\"),\n                (\"!active\", \"#444444\")])\n        self.style.configure(\"TLabel\",\n            background=\"#2b2b2b\",\n            foreground=\"#FFFFFF\")\n        self.style.configure(\"TEntry\",\n            fieldbackground=\"#555555\",\n            foreground=\"#FFFFFF\")    \n        self.style.map('TScale',\n          background=[('active', '#2b2b2b'),\n                      ('!active', '#2b2b2b')])  \n        self.style.configure(\"TFrame\",\n            background=\"#2b2b2b\")\n        self.style.configure(\"TScrollbar\",\n            background=\"#444444\",\n            troughcolor=\"#2b2b2b\",\n            arrowcolor=\"#FFFFFF\")\n        self.style.map(\"TScrollbar\",\n            background=[(\"active\", \"#666666\"),\n                (\"!active\", \"#444444\")])\n        self.style.configure(\"TCheckbutton\",\n            foreground=\"#FFFFFF\")\n        self.style.map(\"TCheckbutton\",\n            background=[(\"active\", \"#666666\"),\n                (\"!active\", \"#2b2b2b\")])\n        self.style.configure(\"TNotebook\",\n            background=\"#2b2b2b\")\n        self.style.configure(\"TNotebook.Tab\",\n            background=\"#444444\",\n            foreground=\"#FFFFFF\")\n        self.style.map(\"TNotebook.Tab\", \n            background=[(\"selected\", \"#444444\"), \n                (\"!selected\", \"#2b2b2b\")],\n            foreground=[(\"selected\", \"#FFFFFF\"),\n                (\"!selected\", \"#FFFFFF\")])\n        self.sidebar.document.body.style.backgroundColor = \"#2b2b2b\"\n        self.sidebar.document.body.style.color = \"#FFFFFF\"\n        \n    def apply_light_theme(self):\n        self.style.configure(\".\", background=\"#F0F0F0\", foreground=\"#000000\",)\n        self.style.configure(\"TButton\",\n            background=\"#DDDDDD\",\n            foreground=\"#000000\",\n            font=(\"Arial\", 10),\n            relief=\"flat\")\n        self.style.map(\"TButton\",\n            background=[(\"active\", \"#CCCCCC\"),\n                (\"!active\", \"#DDDDDD\")])\n        self.style.configure(\"TLabel\",\n            background=\"#F0F0F0\",\n            foreground=\"#000000\",\n            font=(\"Arial\", 10))\n        self.style.configure(\"TEntry\",\n            fieldbackground=\"#FFFFFF\",\n            foreground=\"#000000\",\n            insertcolor=\"black\",\n            borderwidth=0,\n            font=(\"Arial\", 10))\n        self.style.configure(\"TScale\",\n            troughcolor=\"white\",)\n        self.style.map('TScale',\n          background=[('active', '#F0F0F0'),\n                      ('!active', '#F0F0F0')])\n        self.style.configure(\"TFrame\",\n            background=\"#F0F0F0\",)\n        self.style.configure(\"TScrollbar\",\n            background=\"#DDDDDD\",\n            troughcolor=\"#F0F0F0\",\n            arrowcolor=\"#000000\")\n        self.style.map(\"TScrollbar\",\n            background=[(\"active\", \"#CCCCCC\"),\n                (\"!active\", \"#DDDDDD\")])\n        self.style.configure(\"TCheckbutton\",\n            background=\"#F0F0F0\",\n            foreground=\"#000000\",\n            font=(\"Arial\", 10))\n        self.style.map(\"TCheckbutton\",\n            background=[(\"active\", \"#DDDDDD\"),\n                (\"!active\", \"#F0F0F0\")])\n        self.style.configure(\"TNotebook\",\n            background=\"#F0F0F0\",\n            relief=\"flat\",\n            borderwidth=0,\n            tabmargins=(5, 5, 5, 0),\n            padding=0)\n        self.style.configure(\"TNotebook.Tab\",\n            background=\"#DDDDDD\",\n            foreground=\"#000000\",\n            padding=(10, 5),\n            relief=\"flat\",\n            borderwidth=0,\n            font=(\"Arial\", 10))\n        self.style.map(\"TNotebook.Tab\", \n            background=[(\"selected\", \"#DDDDDD\"),\n                (\"!selected\", \"#F0F0F0\")],\n            foreground=[(\"selected\", \"#000000\"),\n                (\"!selected\", \"#000000\")])\n        # this only works on the non-experimental version of tkhtml\n        self.sidebar.document.body.style.backgroundColor = \"#F0F0F0\"\n        self.sidebar.document.body.style.color = \"#000000\"\n\n    def select_all_in_entry(self, widget):\n        widget.select_range(0, 'end')\n        widget.icursor('end')\n    \n    def select_all_in_text(self, widget):\n        widget.tag_add(tk.SEL, \"1.0\", tk.END)\n        widget.mark_set(tk.INSERT, \"1.0\")\n        widget.see(tk.INSERT)\n\n    def on_right_click(self, event):\n        url = self.frame.get_currently_hovered_element().getAttribute(\"href\")\n        if url:\n            url = self.frame.resolve_url(url)\n        selection = self.frame.get_selection()\n        menu = tk.Menu(self, tearoff=0)\n        if len(self.back_history) > 1:\n            menu.add_command(label=\"Back\", accelerator=\"Alt-Back\", command=self.back)\n        if len(self.forward_history) == 1: \n            menu.add_command(label=\"Forward\", accelerator=\"Alt-Forward\", command=self.forward)\n        menu.add_command(label=\"Reload\", accelerator=\"Ctrl-R\", command=self.reload)\n        menu.add_separator()\n        if url:\n            menu.add_command(label=\"Open link\", command=lambda url=url: self.link_click(url))\n            menu.add_command(label=\"Open link in new tab\", command=lambda url=url: self.open_new_tab(url))\n            menu.add_separator()\n        menu.add_command(label=\"Select all\", accelerator=\"Ctrl-A\", command=self.frame.select_all)\n        if selection:\n            menu.add_command(label=\"Copy\", accelerator=\"Ctrl-C\", command=self.frame.html.copy_selection)\n        menu.add_separator()\n        if self.frame[\"experimental\"] or not os.name == \"nt\":\n            menu.add_command(label=\"Take screenshot\", command=self.screenshot)\n        else:\n            menu.add_command(label=\"Take screenshot\", state=\"disabled\", command=self.screenshot)\n        menu.add_command(label=\"Snapshot page\", command=self.snapshot)\n        if self.frame[\"experimental\"]:\n            menu.add_command(label=\"Print page\", accelerator=\"Ctrl-P\", command=self.print)\n        else:\n            menu.add_command(label=\"Print page\", accelerator=\"Ctrl-P\", state=\"disabled\", command=self.print)\n        menu.add_command(label=\"Save page\", accelerator=\"Ctrl-S\", command=self.save)\n        menu.add_separator()\n        menu.add_command(label=\"Find in page\", accelerator=\"Ctrl-F\", command=lambda: self.open_findbar(True))\n        if str(self.view_source_button.cget(\"state\")) == \"normal\":\n            menu.add_command(label=\"View page source\", accelerator=\"Ctrl-U\", command=self.view_source)\n        menu.tk_popup(event.x_root, event.y_root, 0)\n\n    def screenshot(self):\n        file_path = filedialog.asksaveasfilename(\n            filetypes=[(\"JPG files\", \"*.jpg\"), (\"PNG files\", \"*.png\"), (\"All files\", \"*.*\")],\n            title=\"Save Screenshot As\"\n        )\n        if file_path:\n            self.frame.screenshot_page(file_path)\n\n    def snapshot(self):\n        file_path = filedialog.asksaveasfilename(\n            filetypes=[(\"HTML files\", \"*.html\"), (\"XHTML files\", \"*.xhtml\"), (\"XML files\", \"*.xml\"), (\"All files\", \"*.*\")],\n            title=\"Snapshot Page As\"\n        )\n        if file_path:\n            self.frame.snapshot_page(file_path)\n\n    def print(self):\n        if self.frame[\"experimental\"]:\n            file_path = filedialog.asksaveasfilename(\n                filetypes=[(\"Postscript files\", \"*.ps\"), (\"All files\", \"*.*\")],\n                title=\"Print Page As\"\n            )\n            if file_path:\n                self.frame.print_page()\n\n    def save(self):\n        file_path = filedialog.asksaveasfilename(\n            filetypes=[(\"HTML files\", \"*.html\"), (\"XHTML files\", \"*.xhtml\"), (\"XML files\", \"*.xml\"), (\"All files\", \"*.*\")],\n            title=\"Save Page As\"\n        )\n        if file_path:\n            self.frame.save_page(file_path)\n\n    def urlbar_focus(self):\n        self.urlbar.focus()\n        self.urlbar.select_range(0, 'end')\n        self.urlbar.icursor('end')\n\n    def previous_and_find(self):\n        self.find_select_num -= 1\n        if self.find_select_num == 0:\n            self.find_select_num = self.find_match_num\n        self.search_in_page(change=False)\n\n    def next_and_find(self):\n        self.find_select_num += 1\n        if self.find_select_num == self.find_match_num+1:\n            self.find_select_num = 1\n        self.search_in_page(change=False)\n\n    def search_in_page(self, x=None, y=None, change=True):\n        if self.frame.base_url == \"about:html\":\n            frame = self.html_playground.iframe\n        else:\n            frame = self.frame\n        if change:\n            self.find_select_num = 1\n        self.find_match_num = frame.find_text(self.findbox_var.get(), self.find_select_num, self.ignore_case_var.get(), self.highlight_all_var.get())\n        if self.find_match_num > 0:\n            self.find_bar_caption.configure(text=\"Selected {} of {} matches.\".format(self.find_select_num, self.find_match_num))\n        else:\n            self.find_bar_caption.configure(text=\"No matches\")\n\n        if self.find_match_num > 1:\n            self.find_previous.config(state=\"normal\", cursor=\"hand2\")\n            self.find_next.config(state=\"normal\", cursor=\"hand2\")\n        else:\n            self.find_previous.config(state=\"disabled\", cursor=\"arrow\")\n            self.find_next.config(state=\"disabled\", cursor=\"arrow\")\n\n    def hide_messsage_box(self, event):\n        if self.message_box.winfo_ismapped():\n            self.message_box.grid_forget()\n        else:\n            self.message_box.grid(column=0, row=5, sticky=\"ew\", padx=4, pady=(0, 4,))\n\n    def add_message(self, message):\n        self.message_box.insert(\"end\", message+\"\\n\\n\")\n        self.message_box.yview(\"end\")\n        if f\"Error loading {self.urlbar.get()}\" in message:\n            self.handle_view_source_button(\"about:error\")\n        self.linklabel.config(text=self.cut_text(message, 80))\n\n    def toggle_images(self):\n        self.frame.config(images_enabled= self.images_var.get())\n        self.reload()\n\n    def toggle_styles(self):\n        self.frame.config(stylesheets_enabled = self.styles_var.get())\n        self.reload()\n\n    def toggle_forms(self):\n        self.frame.config(forms_enabled = self.forms_var.get())\n        self.reload()\n\n    def toggle_js(self):\n        try:\n            val = self.js_var.get()\n            self.frame.config(javascript_enabled = val)\n            self.reload()\n        except ModuleNotFoundError:\n            self.js_var.set(0)\n            tk.messagebox.showerror(\"Error\", \"PythonMonkey must be installed to enable JavaScript.\")\n\n    def toggle_objects(self):\n        self.frame.config(objects_enabled = self.objects_var.get())\n        self.reload()\n\n    def toggle_caches(self):\n        self.frame.config(caches_enabled = self.caches_var.get())\n        self.reload()\n\n    def toggle_emojis(self):\n        self.frame.config(crash_prevention_enabled = self.crashes_var.get())\n        self.reload()\n\n    def toggle_threads(self):\n        self.frame.config(threading_enabled = self.threads_var.get())\n        self.reload()\n\n    def toggle_theme(self, update_page=True):\n        value = self.invert_page_var.get()\n        if value:\n            self.apply_dark_theme()\n        else:\n            self.apply_light_theme()\n\n        self.frame.config(dark_theme_enabled = value)\n        if update_page:\n            self.reload()\n\n    def toggle_inverter(self):\n        self.frame.config(image_inversion_enabled = self.invert_images_var.get())\n        self.reload()\n\n    def toggle_selection(self):\n        self.frame.config(selection_enabled = self.selection_var.get())\n\n    def toggle_caret_browsing(self):\n        self.frame.config(caret_browsing_enabled = self.caret_browsing_var.get())\n\n    def open_sidebar(self, keep_open=False):\n        if self.sidebar.winfo_ismapped() and not keep_open:\n            self.close_sidebar()\n        else:\n            self.sidebar.grid(row=0, column=2, sticky=\"nsew\")\n            self.sidebar.update()\n            self.settingsbutton.state(['pressed'])\n            \n    def close_sidebar(self):\n        if self.sidebar.winfo_ismapped():\n            self.sidebar.grid_forget()\n            self.settingsbutton.state(['!pressed'])\n\n    def open_findbar(self, keep_open=False):\n        if self.findbar.winfo_ismapped() and not keep_open:\n            self.findbar.grid_forget()\n            self.frame.find_text(\"\")\n            self.findbutton.state(['!pressed'])\n        else:\n            self.findbar.grid(column=0, row=2, sticky=\"ew\", pady=(4,0,))\n            self.find_box.focus()\n            self.findbutton.state(['pressed'])\n\n    def on_motion(self, event):\n        text = self.frame.get_currently_hovered_node_text()\n        link = self.frame.get_currently_hovered_node_attribute(\"href\")\n        if link:\n            self.linklabel.config(text=self.cut_text(\"Hyper-link: \"+link, 80))\n        elif text:\n            self.linklabel.config(text=self.cut_text(\"Text: \"+text, 80))\n        else:\n            elm = self.frame.get_currently_hovered_node_tag()\n            self.linklabel.config(text=self.cut_text(\"Element: \"+elm, 80))\n\n    def back(self):\n        if len(self.back_history) == 1:\n            return\n        self.forwardbutton.config(state=\"normal\", cursor=\"hand2\")\n        self.forward_history.append(self.back_history[-1])\n        url = self.back_history[-2]\n        self.back_history = self.back_history[:-1]\n        self.load_url(url)\n        if len(self.back_history) <= 1:\n            self.backbutton.config(state=\"disabled\", cursor=\"arrow\")\n\n    def on_downloading(self, event):\n        self.reloadbutton.config(text=\"Stop\", command=self.frame.stop)\n\n    def load_url(self, url, decode=None, force=False):\n        if url == \"about:html\":\n            if self.html_playground is None: self.html_playground = HTMLPlayground(self.frame)\n            self.frame.load_html(HTML_TEST_PAGE.format(self.html_playground), url)\n        else:\n            self.frame.load_url(url, decode, force)\n\n    def forward(self):\n        if len(self.forward_history) == 0:\n            return\n        url = self.forward_history[-1]\n        self.forward_history = self.forward_history[:-1]\n        if self.forward_history == []:\n            self.forwardbutton.config(state=\"disabled\", cursor=\"arrow\")\n        self.backbutton.config(state=\"normal\", cursor=\"hand2\")\n        self.back_history.append(url)\n        self.load_url(url)\n\n    def cut_text(self, text, limit):\n        if (len(text) > limit):\n            text = text[:limit] + \"...\"\n        return text\n\n    def focus_on_url(self):\n        self.urlbar.focus()\n        self.urlbar.select_range(0, 'end')\n\n    def open_new_tab(self, url=NEW_TAB):\n        page = Page(self.master)\n        self.master.add(page, text='')\n        self.master.select(page)\n        page.invert_page_var.set(self.invert_page_var.get())\n        page.toggle_theme(False)\n        page.link_click(url, history=False)\n\n    def close_current_tab(self):\n        self.master.forget(self)\n\n    def done_loading(self, event):\n        self.linklabel.config(text=\"Done\")\n        self.reloadbutton.config(text=\"Reload\", command=self.reload)\n\n        self.search_in_page()\n\n    def handle_view_source_button(self, url):\n        if url in BUILTIN_PAGES or url.startswith(\"view-source:\") or url == \"about:html\":\n            self.view_source_button.config(state=\"disabled\", cursor=\"arrow\")\n        else:\n            self.view_source_button.config(state=\"normal\", cursor=\"hand2\")\n\n    def url_change(self, url=None):\n        if not isinstance(url, str): url = self.frame.current_url;\n        self.master.tab(self, text=self.cut_text(url, 40))\n        self.urlbar.delete(0, \"end\")\n        self.urlbar.insert(0, url)\n        self.handle_view_source_button(url)\n\n    def addtohist(self, url):\n        self.back_history.append(url)\n        self.forward_history = []\n        self.forwardbutton.config(state=\"disabled\", cursor=\"arrow\")\n        self.backbutton.config(state=\"normal\", cursor=\"hand2\")\n\n    def form_submit(self, url, data, method):\n        if method == \"GET\":\n            self.addtohist(url+data)\n        else:\n            self.addtohist(url)\n        self.frame.load_form_data(url, data, method)\n\n    def load_site(self, event):\n        url = check_url(self.urlbar)\n        self.addtohist(url)\n        self.load_url(url, force=True)\n        self.handle_view_source_button(url)\n\n    def link_click(self, url, history=True):\n        self.addtohist(url)\n        self.master.tab(self, text=self.cut_text(url, 40))\n        if not history:\n            self.backbutton.config(state=\"disabled\", cursor=\"arrow\")\n        self.urlbar.delete(0, \"end\")\n        self.urlbar.insert(0, url)\n        self.load_url(url)\n        self.handle_view_source_button(url)\n\n    def reload(self):\n        if self.frame.base_url == \"about:html\": \n            self.html_playground.iframe.reload()\n        else:\n            self.frame.reload()\n\n    def change_title(self, event):\n        self.master.tab(self, text=self.cut_text(self.frame.title, 40))  \n    \n    def select_all(self):\n        if self.focus_get() not in (self.urlbar, self.find_box):\n            if self.frame.base_url == \"about:html\":\n                self.html_playground.iframe.select_all()\n            else:\n                self.frame.select_all()\n\n    def view_source(self):\n        if str(self.view_source_button.cget(\"state\")) == \"normal\":\n            self.open_new_tab(\"view-source:\"+self.urlbar.get())\n        \nclass Browser(tk.Tk):\n    \"TkinterWeb Browser\"\n\n    def __init__(self):\n\n        tk.Tk.__init__(self)\n        self.title(\"TkinterWebBrowser\")\n        self.minsize(800, 500)\n        self.main_frame = main_frame = tk.Frame(self, highlightthickness=0, bd=0)\n\n        self.frame = frame = Notebook(main_frame)\n        frame.enable_traversal()\n        frame.bind(\"<<NotebookTabChanged>>\", self.on_tab_change)\n\n        page = Page(frame)\n \n        self.bind_all(\"<Up>\", lambda e: frame.select().frame.html._on_up(e))\n        self.bind_all(\"<Down>\", lambda e: frame.select().frame.html._on_down(e))\n        self.bind_all(\"<Left>\", lambda e: frame.select().frame.html._on_left(e))\n        self.bind_all(\"<Right>\", lambda e: frame.select().frame.html._on_right(e))\n        self.bind_all(\"<Prior>\", lambda e: frame.select().frame.html._on_prior(e))\n        self.bind_all(\"<Next>\", lambda e: frame.select().frame.html._on_next(e))\n        self.bind_all(\"<Home>\", lambda e: frame.select().frame.html._on_home(e))\n        self.bind_all(\"<End>\", lambda e: frame.select().frame.html._on_end(e))\n        self.bind_all(\"<Control-w>\", lambda e: frame.select().close_current_tab())\n        self.bind_all(\"<Control-t>\", lambda e: frame.select().open_new_tab())\n        self.bind_all(\"<Control-f>\", lambda e: frame.select().open_findbar(True))\n        self.bind_all(\"<Control-b>\",  lambda e: frame.select().open_sidebar(True))\n        self.bind_all(\"<Control-l>\", lambda e: frame.select().urlbar_focus())\n        self.bind_all(\"<Control-i>\", lambda e: frame.select().hide_messsage_box(e))\n        self.bind_all(\"<Control-r>\", lambda e: frame.select().reloadbutton.invoke())\n        self.bind_all(\"<Control-n>\", lambda e: Browser())\n        self.bind_all(\"<Control-q>\", lambda e: self.destroy())\n        self.bind_all(\"<Control-a>\", lambda e: frame.select().select_all())\n        self.bind_all(\"<Control-u>\", lambda e: frame.select().view_source())\n        self.bind_all(\"<Control-p>\", lambda e: frame.select().print())\n        self.bind_all(\"<Control-s>\", lambda e: frame.select().save())\n        self.bind_all(\"<Alt-Left>\", lambda e: frame.select().back())\n        self.bind_all(\"<Alt-Right>\", lambda e: frame.select().forward())\n\n        frame.pack(expand=True, fill=\"both\")\n        main_frame.pack(expand=True, fill=\"both\")\n        frame.add(page, text='')\n\n        page.link_click(NEW_TAB, history=False)\n\n        self.mainloop()\n    \n    def on_tab_change(self, event):\n        if self.frame.pages:\n            self.frame.select().toggle_theme(False)\n        else:\n            self.destroy()\n\nif __name__ == \"__main__\":   \n    Browser()"
  },
  {
    "path": "setup.py",
    "content": "import pathlib\nfrom setuptools import setup, find_namespace_packages\n\nHERE = pathlib.Path(__file__).parent\nREADME = (HERE / \"README.md\").read_text()\n\nsetup(\n    name=\"tkinterweb\",\n    version=\"4.25.2\",\n    python_requires=\">=3.2\",\n    description=\"HTML/CSS widgets for Tkinter\",\n    long_description=README,\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/Andereoo/TkinterWeb\",\n    license=\"MIT\",\n    classifiers=[\n        \"Intended Audience :: Developers\",\n        \"License :: Freeware\",\n        \"License :: OSI Approved :: MIT License\",\n        \"Natural Language :: English\",\n        \"Programming Language :: Python\",\n        \"Programming Language :: Python :: 3\",\n        \"Topic :: Software Development\",\n      ],\n    keywords=\"tkinter, Tkinter, tkhtml, Tkhtml, Tk, HTML, CSS, webbrowser\",\n    packages=find_namespace_packages(include=[\"tkinterweb\", \"tkinterweb.*\"]),\n    include_package_data=True,\n    install_requires=[\"tkinterweb-tkhtml>=2.1.1\"],\n    extras_require = {\n          \"html\": [\"tkinterweb-tkhtml-extras>=1.3.1\"],\n          \"images\": [\"pillow\"],\n          \"svg\": [\"tkinterweb-tkhtml-extras>=1.3.1\", \"pillow\", \"cairosvg\"],\n          \"javascript\": [\"pythonmonkey\"],\n          \"requests\": [\"brotli\"],\n\n          \"recommended\": [\"tkinterweb-tkhtml-extras>=1.3.1\", \"pillow\"],\n          \"full\": [\"tkinterweb-tkhtml-extras>=1.3.1\", \"pillow\", \"cairosvg\", \"pythonmonkey\", \"brotli\"],\n    },\n)\n"
  },
  {
    "path": "tkinterweb/__init__.py",
    "content": "\"\"\"\nTkinterWeb v4\nThis is a wrapper for the Tkhtml3 widget from http://tkhtml.tcl.tk/tkhtml.html, \nwhich displays styled HTML documents in Tkinter.\n\nCopyright (c) 2021-2025 Andrew Clarke\n\"\"\"\n\n\ntry:\n    from .htmlwidgets import HtmlFrame, HtmlLabel, HtmlText, HtmlParse\n    from .subwidgets import Notebook\n    from .bindings import TkHtmlParsedURI, TkinterWeb\n    from .utilities import __title__, __author__, __copyright__, __license__, __version__\nexcept (ImportError, ModuleNotFoundError) as error:\n    import sys\n    import tkinter as tk\n    from tkinter import messagebox\n    # Give useful troubleshooting information as a popup, as most bundled applications don't have a visible console\n    # Also print the message in case something is also wrong with the Tkinter installation\n    error_message = f\"Error: {error} \\n\\n\\\nThis may occur when bundling TkinterWeb into an app without forcing the application maker to include all nessessary files or when some of TkinterWeb's dependencies are not installed or bundled.\\n\\n\\\nSee https://tkinterweb.readthedocs.io/en/latest/faq.html for more information.\"\n    sys.stdout.write(error_message)\n    root = tk.Tk()\n    root.withdraw()\n    # For older versions of pyinstaller, windowed app may crash without any message of any kind\n    message = messagebox.showerror(\"Fatal Error Encountered\", error_message)\n    sys.exit()\n\n\n__all__ = ['Demo', 'HtmlFrame', 'HtmlLabel', 'HtmlText', 'HtmlParse', 'Notebook', 'TkHtmlParsedURI', 'TkinterWeb']\n\n\nclass Demo():\n    \"A simple example of TkinterWeb in action displaying the Tkinter Wiki.\"\n\n    def __init__(self):\n        import tkinter as tk\n\n        self.root = root = tk.Tk()\n        self.frame = frame = HtmlFrame(root, messages_enabled=True, on_navigate_fail=self.on_error, on_link_click=self.navigate, selected_text_highlight_color=\"#e6eee6\")\n        self.button = tk.Button(root, cursor=\"hand2\")\n        \n        frame.load_url(\"https://tkinterweb.readthedocs.io/en/latest/\")\n        frame.bind(\"<<TitleChanged>>\", lambda event: self.root.title(frame.title))\n        frame.bind(\"<<DOMContentLoaded>>\", self.done_loading)\n        frame.pack(expand=True, fill=\"both\")\n\n        root.mainloop()\n\n    def HTML_to_text(self, text, start, end):\n        \"Make HTML code bwtween two strings display as plain text\"\n        import re\n        pattern = re.compile(re.escape(start) + r'(.*?)' + re.escape(end), re.DOTALL)\n        def replacer(match):\n            inner = match.group(1)\n            escaped = inner.replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\").replace(\"&gt;\", \">\", 1)\n            return start + escaped + end\n        return pattern.sub(replacer, text)\n    \n    def navigate(self, url):\n        \"Only display files from the docs page or from tkhtml.tcl.tk\"\n        from urllib.parse import urlparse\n        if urlparse(self.frame.current_url).netloc == urlparse(url).netloc or \"tkhtml.tcl.tk\" in url:\n            self.frame.load_url(url)\n        else:\n            import webbrowser\n            webbrowser.open(url)\n\n    def done_loading(self, event):\n        \"Display code blocks in iframes to allow horizontal scrolling when the page loads\"\n        from tkinter import TclError\n        try:\n            #self.frame.document.querySelector(\"div[role=\\\"search\\\"]\").remove()\n            head = self.frame.document.getElementsByTagName(\"head\")[0].innerHTML\n            for code_block in self.frame.document.getElementsByClassName(\"highlight\"):\n                iframe = HtmlFrame(self.frame, messages_enabled=False, horizontal_scrollbar=\"auto\", shrink=True, overflow_scroll_frame=self.frame.html)\n                text = self.HTML_to_text(code_block.innerHTML, \"<span\", \"</span>\")\n                iframe.load_html(f\"{head}<div class='highlight'>{text}</div>\", base_url=self.frame.base_url)\n                code_block.widget = iframe\n        except TclError:\n            pass\n\n    def on_error(self, url, error, code):\n        \"Show an error page if the page fails to load\"\n        self.button.configure(text=\"Try Again\", command=lambda url=self.frame.current_url: self.frame.load_url(url))\n        html = f\"\"\"<html><head><title>TkinterWeb Demo - Error {code}</title><style>td {{text-align:center;vertical-align:middle}} h3{{margin:0 0 10px 0;padding:0;font-weight:normal}} html,body,table,tr{{background-color:{self.frame[\"about_page_background\"]};color:{self.frame[\"about_page_foreground\"]};width:100%;height:100%;margin:0}}</style></head>\n        <body><table><tr><td tkinterweb-full-page>\n        <h3>Error {code}</h3><h3>An internet connection is required to display the TkinterWeb demo :(</h3><object id=\"button\" data={self.button}></object>\n        </td></tr></table></body></html>\"\"\"\n        self.frame.load_html(html)"
  },
  {
    "path": "tkinterweb/bindings.py",
    "content": "\"\"\"\nThe core Python bindings to Tkhtml3\n\nCopyright (c) 2021-2026 Andrew Clarke\n\"\"\"\n\nfrom re import IGNORECASE, split, sub\n\nfrom urllib.parse import urljoin\n\nfrom queue import Queue, Empty\n\nimport tkinter as tk\nfrom . import extensions, utilities, handlers\n\nimport tkinterweb_tkhtml\n\n\nclass TkinterWeb(tk.Widget):\n    \"\"\"This object provides the low-level widget that bridges the gap between the underlying Tkhtml3 widget and Tkinter. \n\n    **Do not use this widget on its own unless absolutely nessessary.** Instead use the :class:`~tkinterweb.HtmlFrame` widget.\n\n    This widget can be accessed through the :attr:`~tkinterweb.HtmlFrame.html` property of the :class:`~tkinterweb.HtmlFrame` and :class:`~tkinterweb.HtmlLabel` widgets to access underlying settings and commands that are not a part of the :class:`~tkinterweb.HtmlFrame` API.\n    \n    This widget stores many useful instance variables and configuration flags. Some are exposed through the main API, others are not. Please see the source code for more details.\"\"\"\n\n    def __init__(self, master, tkinterweb_options=None, **kwargs):\n        self.master = master\n        tkinterweb_options = tkinterweb_options.copy()\n\n        # Setup most variables\n        self._setup_status_variables()\n\n        # Setup the settings variables\n        _delayed_options = {\"dark_theme_enabled\", \"caches_enabled\", \"threading_enabled\"}\n        tkinterweb_options = self._setup_settings(tkinterweb_options, _delayed_options)\n\n        # Load Tkhtml3\n        self._load_tkhtml()\n\n        # Register image loading infrastructure\n        if \"imagecmd\" not in kwargs:\n            kwargs[\"imagecmd\"] = master.register(self._on_image_cmd)\n\n        # Get Tkhtml folder and register crash handling\n        # Not supported by standard Tkhtml releases\n        if \"drawcleanupcrashcmd\" not in kwargs and self.use_prebuilt_tkhtml:\n            kwargs[\"drawcleanupcrashcmd\"] = master.register(self._on_draw_cleanup_crash_cmd)\n\n        # Log everything\n        # if \"logcmd\" not in kwargs:\n        #    kwargs[\"logcmd\"] = tkhtml_notifier\n\n        shrink = bool(kwargs.get(\"shrink\"))\n        textwrap = kwargs.get(\"textwrap\")\n\n        # Set the textwrap value if needed\n        if self.using_tkhtml30:\n            if self.default_style:\n                # For Tkhtml 3.0, we do our best by applying CSS to block word wrapping\n                if textwrap == \"auto\" and shrink:\n                    self.default_style += utilities.TEXTWRAP_STYLE\n                elif not textwrap:\n                    self.default_style += utilities.TEXTWRAP_STYLE\n            # Version 3.0 doesn't support textwrap\n            kwargs.pop(\"textwrap\", None)\n        elif textwrap == \"auto\":\n            kwargs[\"textwrap\"] = not(shrink)\n\n        # Set the default style if needed\n        if not kwargs.get(\"defaultstyle\", \"\") and self.default_style:\n            kwargs[\"defaultstyle\"] = self.default_style\n\n        # Unset width and height if null\n        if kwargs.get(\"width\") == 0: \n            del kwargs[\"width\"]\n        if kwargs.get(\"height\") == 0: \n            del kwargs[\"height\"]\n\n        # Provide OS information for troubleshooting\n        self.post_message(f\"Starting TkinterWeb for {utilities.PLATFORM.processor} {utilities.PLATFORM.system} with Python {'.'.join(utilities.PYTHON_VERSION)}\")\n\n        # Check tkinterweb_tkhtml_extras\n        if not self.using_tkhtml30 and tkinterweb_tkhtml.TKHTML_EXTRAS_VERSION is not None:\n            version = []\n            for letter in tkinterweb_tkhtml.TKHTML_EXTRAS_VERSION.split(\".\"): version.append(int(letter))\n            if tuple(version) < (1, 3, 0):\n                raise RuntimeError(\n                    f\"tkinterweb-tkhtml-extras >= 1.3.0 is required but version {tkinterweb_tkhtml.TKHTML_EXTRAS_VERSION} is installed. \" \\\n                    \"Upgrade with pip install --upgrade tkinterweb[recommended].\"\n                )\n\n        # Initialize the Tkhtml3 widget\n        tk.Widget.__init__(self, master, \"html\", kwargs)\n\n        # Setup threading settings\n        try:\n            self.allow_threading = bool(self.tk.call(\"set\", \"tcl_platform(threaded)\"))\n        except tk.TclError:\n            self.allow_threading = True\n\n        # Set remaining settings\n        for key in _delayed_options:\n            setattr(self, key, tkinterweb_options[key])\n\n        # Create a tiny, blank frame for cursor updating\n        self.motion_frame_bg = \"white\"\n        self.motion_frame = tk.Frame(self, bg=self.motion_frame_bg, width=1, height=1)\n        self.motion_frame.place(x=0, y=0)\n\n\n        # Setup bindings        \n        self._setup_bindings()\n        self._setup_handlers()\n        \n        self.post_message(f\"\"\"Welcome to TkinterWeb!\n                                \nThe API changed in version 4. See https://tkinterweb.readthedocs.io/ for details.\n\nDebugging messages are enabled. Use the parameter `messages_enabled = False` when calling HtmlFrame() or HtmlLabel() to disable these messages.\n                                \nLoad about:tkinterweb for debugging information.\n                                \nIf you benefited from using this package, please consider supporting its development by donating at https://buymeacoffee.com/andereoo - any amount helps!\"\"\")\n        \n        # Check tkinterweb_tkhtml_extras\n        if not tkinterweb_tkhtml.TKHTML_EXTRAS_ROOT_DIR:\n            self.post_message(\"The tkinterweb-tkhtml-extras package is either not installed or does not support your system. Some functionality may be missing.\")\n\n    # --- Widget setup --------------------------------------------------------\n\n    def _setup_settings(self, options, delayed_options):\n        \"\"\"Widget settings. \n        Some settings have extra logic that needs to run when changing them, so they're defined elsewhere as properties.\n        They are set when needed. If the settings are set through the options attribute, they will be added here.\"\"\"\n        settings = {\n            \"messages_enabled\": True,\n            \"stylesheets_enabled\": True,\n            \"events_enabled\": True,\n            \"images_enabled\": True,\n            \"forms_enabled\": True,\n            \"objects_enabled\": True,\n            \"ignore_invalid_images\": True,\n            \"image_alternate_text_enabled\": True,\n            \"overflow_scroll_frame\": None,\n            \"default_style\": \"\",\n            \"dark_style\": \"\",\n            \"text_mode\": False,\n\n            \"use_prebuilt_tkhtml\": True,\n            \"tkhtml_version\": \"\",\n            \"experimental\": False,\n\n            \"find_match_highlight_color\": \"#ef0fff\",\n            \"find_match_text_color\": \"#fff\",\n            \"find_current_highlight_color\": \"#38d878\",\n            \"find_current_text_color\": \"#fff\",\n            \"selected_text_highlight_color\": \"#3584e4\",\n            \"selected_text_color\": \"#fff\",\n            \"visited_links\": [],\n\n            \"maximum_thread_count\": 20,\n\n            \"queue\": None,\n            \"queue_delay\": 50,\n            \"queue_after\": None,\n\n            \"embed_obj\": None,\n            \"manage_vsb_func\": None,\n            \"manage_hsb_func\": None,\n            \"on_link_click\": None,\n            \"on_form_submit\": None,\n            \"message_func\": None,\n            \"on_script\": None,\n            \"on_element_script\": None,\n            \"on_resource_setup\": None,\n\n            \"request_func\": None,\n            \"insecure_https\": False,\n            \"ssl_cafile\": None,\n            \"request_timeout\": 15,\n            \"headers\": {},\n            \n            \"dark_theme_limit\": 280,\n            \"style_dark_theme_regex\": r\"([^:;\\s{]+)\\s?:\\s?([^;{!]+)(?=!|;|})\",\n            \"general_dark_theme_regexes\": [\n                r'(<[^>]+bgcolor=\")([^\"]*)',\n                r'(<[^>]+text=\")([^\"]*)',\n                r'(<[^>]+link=\")([^\"]*)'\n            ],\n            \"inline_dark_theme_regexes\": [\n                r'(<[^>]+style=\")([^\"]*)',\n                r'([a-zA-Z-]+:)([^;]*)'\n            ],\n\n            \"node_tag\": f\"tkinterweb.{id(self)}.nodes\",\n            \"tkinterweb_tag\": f\"tkinterweb.{id(self)}.tkinterweb\",\n            \"scrollable_node_tag\": f\"tkinterweb.{id(self)}.scrollablenodes\",\n        }\n        settings.update(options)\n        for key, value in settings.items():\n            if key not in delayed_options:\n                setattr(self, key, value)\n\n        return settings\n\n    def _setup_status_variables(self):\n        \"Widget status variables.\"\n        self.base_url = \"\"\n        self.title = \"\"\n        self.icon = \"\"\n\n        self.fragment = \"\"\n        self.parsing = False\n        self.active_threads = []\n        self.pending_threads = []\n        self.current_active_node = None\n        self.clicked_node = None\n        self.current_hovered_node = None\n        self.hovered_nodes = []\n\n        self._style_count = 0\n        self._current_cursor = \"\"\n\n        # This set is used when resetting the widget and contains a reference to all loaded managers\n        # Managers automatically add themselves to this set as they are created\n        self._managers = set()\n\n    def _setup_bindings(self):\n        \"Widget bindtags and bindings.\"\n        self._add_bindtags(self, False, True)\n\n        self.bind_class(self.node_tag, \"<Motion>\", self._on_mouse_motion, True)\n        self.bind_class(self.node_tag, \"<FocusIn>\", self._on_focusout, True)\n\n        self.bind_class(self.tkinterweb_tag, \"<<Copy>>\", self._copy_selection, True)\n        self.bind_class(self.tkinterweb_tag, \"<Control-a>\", self._select_all, True)\n        self.bind_class(self.tkinterweb_tag, \"<B1-Motion>\", self._extend_selection, True)\n        self.bind_class(self.tkinterweb_tag, \"<Button-1>\", self._on_click, True)\n        self.bind_class(self.tkinterweb_tag, \"<Button-2>\", self._on_middle_click, True)\n        self.bind_class(self.tkinterweb_tag, \"<Button-3>\", self._on_right_click, True)\n        self.bind_class(self.tkinterweb_tag, \"<Double-Button-1>\", self._on_double_click, True)\n        self.bind_class(self.tkinterweb_tag, \"<ButtonRelease-1>\", self._on_click_release, True)\n        self.bind_class(self.tkinterweb_tag, \"<Destroy>\", self._on_destroy)\n\n        for i in {\"<Left>\", \"Control-Left>\", \"Control-Shift-Left>\", \"<KP_Left>\", \"<Control-KP_Left>\", \"<Control-Shift-KP_Left>\", \n                \"<Right>\", \"Control-Right>\", \"Control-Shift-Right>\", \"<KP_Right>\", \"<Control-KP_Right>\", \"<Control-Shift-KP_Right>\",\n                \"<Up>\", \"Control-Up>\", \"Control-Shift-Up>\", \"<KP_Up>\", \"<Control-KP_Up>\", \"<Control-Shift-KP_Up>\", \n                \"<Down>\", \"Control-Down>\", \"Control-Shift-Down>\", \"<KP_Down>\", \"<Control-KP_Down>\", \"<Control-Shift-KP_Down>\",\n                \"<Prior>\", \"<KP_Prior>\", \"<Next>\", \"<KP_Next>\", \"<Home>\", \"<KP_Home>\", \"<End>\", \"<KP_End>\", \"<FocusOut>\", \"<FocusIn>\"}:\n            method = \"_on_\" + i.strip(\"<>\").split(\"-\")[-1].split(\"_\")[-1].lower()\n            # We use bind and not bind_class here because users may want to override these bindings\n            try:\n                self.bind(i, getattr(self, method))\n            except tk.TclError:\n                # KP_ bindings don't work on MacOS\n                pass\n\n        # Fix for Bug #151\n        # Externally map .!tkinterweb.document to .!tkinterweb on Windows\n        if utilities.PLATFORM.system == \"Windows\":\n            widget = tk.Widget(self, None)\n            widget._w = self._w # or is f\"{self}.document\" better?\n            self.children[\"document\"] = widget\n\n    def _add_bindtags(self, widgetid, allowscrolling=True, master=False):\n        \"Add bindtags to allow scrolling and on_embedded_mouse function calls.\"\n        if allowscrolling:\n            tags = (\n                self.node_tag,\n                self.scrollable_node_tag,\n            )\n        else:\n            tags = (self.node_tag,)\n\n        if master:\n            tags = (self.node_tag, self.tkinterweb_tag)\n\n        widgetid.bindtags(widgetid.bindtags() + tags)\n\n    def _on_destroy(self, event):\n        self._end_queue()\n        self.stop()\n\n    def _setup_handlers(self):\n        \"Setup node handlers\"\n        # Node handlers don't work on body and html elements. \n        # Body and html elements also cannot be removed without causing a segfault in vanilla Tkhtml. \n        # Weird.\n        self.register_lazy_handler(\"parse\", \"body\", \"node_manager\")\n        self.register_lazy_handler(\"parse\", \"html\", \"node_manager\")\n\n        self.register_lazy_handler(\"node\", \"meta\", \"node_manager\")\n        self.register_lazy_handler(\"node\", \"title\", \"node_manager\")\n        self.register_lazy_handler(\"node\", \"a\", \"node_manager\")\n        self.register_lazy_handler(\"node\", \"base\", \"node_manager\")\n        self.register_lazy_handler(\"attribute\", \"a\", \"node_manager\")\n\n        if not self.using_tkhtml30:\n            #self.register_lazy_handler(\"node\", \"details\", \"node_manager\")\n            self.register_lazy_handler(\"node\", \"progress\", \"node_manager\")\n            self.register_lazy_handler(\"attribute\", \"progress\", \"node_manager\")\n            self.register_lazy_handler(\"attribute\", \"details\", \"node_manager\")\n        \n        self.register_lazy_handler(\"node\", \"form\", \"form_manager\")\n        self.register_lazy_handler(\"node\", \"table\", \"form_manager\")\n        self.register_lazy_handler(\"node\", \"select\", \"form_manager\")\n        self.register_lazy_handler(\"attribute\", \"select\", \"form_manager\")\n        self.register_lazy_handler(\"node\", \"textarea\", \"form_manager\")\n        self.register_lazy_handler(\"node\", \"input\", \"form_manager\")\n        self.register_lazy_handler(\"attribute\", \"input\", \"form_manager\")\n\n        self.register_lazy_handler(\"script\", \"script\", \"script_manager\")\n\n        self.register_lazy_handler(\"script\", \"style\", \"style_manager\")\n        self.register_lazy_handler(\"node\", \"link\", \"style_manager\")\n\n        self.register_lazy_handler(\"node\", \"img\", \"image_manager\")\n        self.register_lazy_handler(\"attribute\", \"img\", \"image_manager\")\n\n        self.register_lazy_handler(\"node\", \"iframe\", \"object_manager\")\n        self.register_lazy_handler(\"attribute\", \"iframe\", \"object_manager\")\n        self.register_lazy_handler(\"node\", \"object\", \"object_manager\")\n        self.register_lazy_handler(\"attribute\", \"object\", \"object_manager\")\n\n    def _load_tkhtml(self):\n        \"Load Tkhtml\"\n        if self.tkhtml_version == \"auto\":\n            self.tkhtml_version = None\n\n        try:\n            loaded_version = tkinterweb_tkhtml.get_loaded_tkhtml_version(self.master)\n            self.post_message(f\"Using Tkhtml {loaded_version} because it is already loaded\")\n        except tk.TclError:\n            if self.use_prebuilt_tkhtml:\n                try:\n                    file, loaded_version, self.experimental = tkinterweb_tkhtml.get_tkhtml_file(self.tkhtml_version, experimental=self.experimental)\n                    tkinterweb_tkhtml.load_tkhtml_file(self.master, file)\n                    self.post_message(f\"Tkhtml {loaded_version} successfully loaded from {tkinterweb_tkhtml.TKHTML_ROOT_DIR}\")\n                except tk.TclError as error: # If something goes wrong, try again with version 3.0 in case it is a Cairo issue\n                    self.post_message(f\"WARNING: An error occured while loading Tkhtml {loaded_version}: {error}\\n\\n\\\nIt is likely that not all dependencies are installed. Make sure Cairo is installed on your system. Some features may be missing.\")\n                    file, loaded_version, self.experimental = tkinterweb_tkhtml.get_tkhtml_file(index=0, experimental=self.experimental)\n                    try:\n                        tkinterweb_tkhtml.load_tkhtml_file(self.master, file)\n                        self.post_message(f\"Tkhtml {loaded_version} successfully loaded from {tkinterweb_tkhtml.TKHTML_ROOT_DIR}\")\n                    except tk.TclError as error: # If it still won't load it never will. It is most likely that the system is not supported. The user needs to compile and install Tkhtml.\n                        raise tk.TclError(f\"{error} It is likely that your system is not supported out of the box. {tkinterweb_tkhtml.HELP_MESSAGE}\") from error\n            else:\n                tkinterweb_tkhtml.load_tkhtml(self.master)\n                loaded_version = tkinterweb_tkhtml.get_loaded_tkhtml_version(self.master)\n                self.post_message(f\"Tkhtml {loaded_version} successfully loaded\")\n\n        self.tkhtml_version = float(loaded_version)\n        self.using_tkhtml30 = float(loaded_version) == 3\n\n    # --- Extensions ----------------------------------------------------------\n\n    # The following 'managers' each offer extra functionality. \n    # The ones in handlers.py are primarily node handlers.\n    # These objects are created when needed, if enabled\n    # Most can be disabled, except search_manager and widget_manager, which run at the user's request or via other managers that can be disabled\n    # Any calls to a disabled manager will be ignored and return 'None'\n    \n    @utilities.lazy_manager(\"selection_enabled\")\n    def selection_manager(self):\n        \"\"\"The widget's selection manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.SelectionManager`\n        \n        New in version 4.11.\"\"\"\n        return extensions.SelectionManager(self)\n        \n    @utilities.lazy_manager(\"caret_browsing_enabled\")\n    def caret_manager(self):\n        \"\"\"The widget's caret manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.CaretManager`\n        \n        New in version 4.8.\"\"\"\n        return extensions.CaretManager(self)\n    \n    @utilities.lazy_manager(\"events_enabled\")\n    def event_manager(self):\n        \"\"\"The widget's event manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.EventManager`\n        \n        New in version 4.10.\"\"\"\n        return extensions.EventManager(self)\n\n    @utilities.lazy_manager(None)\n    def widget_manager(self):\n        \"\"\"The widget's widget manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.WidgetManager`\n        \n        New in version 4.11.\"\"\"\n        return extensions.WidgetManager(self)\n    \n    @utilities.lazy_manager(None)\n    def search_manager(self):\n        \"\"\"The widget's document search manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.SearchManager`\n        \n        New in version 4.11.\"\"\"\n        return extensions.SearchManager(self)\n\n    @utilities.lazy_manager(\"javascript_enabled\")\n    def script_manager(self):\n        \"\"\"The widget's script manager.\n        \n        :rtype: :class:`~tkinterweb.handlers.ScriptManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.ScriptManager(self)\n\n    @utilities.lazy_manager(\"stylesheets_enabled\")\n    def style_manager(self):\n        \"\"\"The widget's style manager.\n        \n        :rtype: :class:`~tkinterweb.handlers.StyleManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.StyleManager(self)\n\n    @utilities.lazy_manager(\"images_enabled\")\n    def image_manager(self):\n        \"\"\"The widget's image manager.\n        \n        :rtype: :class:`~tkinterweb.handlers.ImageManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.ImageManager(self)\n\n    @utilities.lazy_manager(\"objects_enabled\")\n    def object_manager(self):\n        \"\"\"The widget's object manager.\n        \n        :rtype: :class:`~tkinterweb.handlers.ObjectManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.ObjectManager(self)\n\n    @utilities.lazy_manager(\"forms_enabled\")\n    def form_manager(self):\n        \"\"\"The widget's form manager.\n        \n        :rtype: :class:`~tkinterweb.handlers.FormManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.FormManager(self)\n\n    @utilities.lazy_manager(None)\n    def node_manager(self):\n        \"\"\"The widget's node handler manager.\n        \n        :rtype: :class:`~tkinterweb.extensions.NodeManager`\n        \n        New in version 4.11.\"\"\"\n        return handlers.NodeManager(self)\n\n    # --- Properties ----------------------------------------------------------\n\n    @utilities.special_setting(True)\n    def caches_enabled(self, prev_enabled, enabled):\n        \"Disable the Tkhtml image cache when disabling caches.\"\n        if prev_enabled != enabled: \n            self.imagecache = enabled\n            if not enabled: utilities.lru_cache.clear()\n\n    @property\n    def imagecache(self):\n        return bool(self.tk.call(self._w, \"cget\", \"-imagecache\"))\n\n    @imagecache.setter\n    def imagecache(self, toggle):\n        self.tk.call(self._w, \"configure\", \"-imagecache\", toggle)\n\n    @utilities.special_setting(False)\n    def javascript_enabled(self, prev_enabled, enabled):\n        \"Warn the user when enabling JavaScript.\"\n        if prev_enabled != enabled:\n            if enabled:\n                self.post_message(\"WARNING: JavaScript support is enabled. This feature is a work in progress. Only enable JavaScript support on documents you know and trust.\")\n\n    @utilities.special_setting(True)\n    def crash_prevention_enabled(self, prev_enabled, enabled):\n        \"Warn the user when disabling crash prevention.\"\n        if prev_enabled != enabled:\n            if not enabled:\n                self.post_message(\"WARNING: crash prevention is disabled. You may encounter segmentation faults on some pages.\")\n    \n    @utilities.special_setting(False)\n    def dark_theme_enabled(self, prev_enabled, enabled):\n        \"Warn the user when enabling dark mode.\"\n        if prev_enabled != enabled:\n            if enabled:\n                self.post_message(\"WARNING: dark theme is enabled. This feature may cause hangs or crashes on some pages.\")\n            if enabled and self.dark_style:\n                self.config(defaultstyle=self.default_style + self.dark_style)\n            elif self.default_style:\n                self.config(defaultstyle=self.default_style)\n\n    @utilities.special_setting(False)\n    def image_inversion_enabled(self, prev_enabled, enabled):\n        \"Warn the user when enabling image inversion.\"\n        if prev_enabled != enabled:\n            prev_enabled = enabled\n            if enabled:\n                self.post_message(\"WARNING: image inversion is enabled. This feature may cause hangs or crashes on some pages.\")\n\n    @utilities.special_setting(True)\n    def threading_enabled(self, prev_enabled, enabled):\n        \"Warn the user when disabling threading and ensure that threading is disabled if Tcl/Tk is not built with thread support.\"\n        if self.allow_threading:\n            if enabled:\n                # Initialize the queue\n                # The queue evaluates Tcl/Tk commands running in a thread\n                # The queue will start or stop when self.maximum_thread_count is set\n                if not self.queue:\n                    self.queue = Queue()\n                self._check_queue()\n            else:\n                self.post_message(\"WARNING: threading is disabled. Your app may hang while loading webpages.\")\n                self._end_queue()\n        else:\n            self._threading_enabled = False\n            self.post_message(\"WARNING: threading is disabled because your Tcl/Tk library does not support threading. Your app may hang while loading webpages.\")\n            self._end_queue()\n\n    @utilities.special_setting(False)\n    def caret_browsing_enabled(self, prev_enabled, enabled):\n        \"Enable or disable caret browsing.\"\n        if getattr(self, \"_caret_manager\", False) and not enabled:\n            self._caret_manager.reset()\n\n    @utilities.special_setting(True)\n    def selection_enabled(self, prev_enabled, enabled):\n        \"Enable or disable text selection.\"\n        if getattr(self, \"_selection_manager\", False) and not enabled:\n            self._selection_manager.clear_selection()\n\n    @property # will rename to default_style once I finally remove the default_style setting\n    def tkhtml_default_style(self):\n        \"\"\"Return the current document's default stylesheet. Use for debugging.\n        \n        New in version 4.19.\"\"\"\n        return self.tk.call(\"::tkhtml::htmlstyle\")\n\n    @property\n    def images(self):\n        \"\"\"Return a dictionary containing the document's images. Use for debugging.\n        \n        New in version 4.19.\"\"\"\n        NAMES = (\"name\", \"pixmap\", \"w\", \"h\", \"alpha\", \"ref\",)\n        return {i[0]:dict(zip(NAMES, i[1:])) for i in self.tk.call(self._w, \"_images\")}\n\n    @property\n    def style_report(self):\n        \"\"\"Return the document's style report. Use for debugging.\n        \n        New in version 4.19.\"\"\"\n        return self.tk.call(self._w, \"_stylereport\")\n\n    # --- Queuing, messaging, and events --------------------------------------\n\n    def _check_queue(self):\n        try:\n            while True:\n                msg = self.queue.get_nowait()\n                msg()\n        except Empty:\n            pass\n        self.queue_after = self.after(self.queue_delay, self._check_queue)\n\n    def _end_queue(self):\n        if self.queue_after:\n            self.after_cancel(self.queue_after)\n            self.queue_after = None\n        self.queue = None\n\n    def post_to_queue(self, callback, thread_safe=True):\n        \"\"\"Use this method to send a callback to TkinterWeb's thread-safety queue. The callback will be evaluated on the main thread.\n        Use this when running Tkinter commands from within a thread. \n        If the queue is not running (i.e. threading is disabled), the callback will be evaluated immediately.\n        \n        New in version 4.9.\"\"\"\n        if thread_safe and self.queue:\n            self.queue.put(callback)\n        else:\n            callback()\n\n    def post_event(self, event, thread_safe=False):\n        \"Generate a virtual event.\"\n        # NOTE: when thread_safe=True, this method is thread-safe\n        # Would you believe that?\n        if not self.events_enabled:\n            return\n\n        if thread_safe and self.queue:\n            self.post_to_queue(lambda event=event: self._post_event(event))\n        else:\n            self._post_event(event)\n\n    def _post_event(self, event):\n        \"Generate a virtual event.\"\n        try:\n            self.event_generate(event)\n        except tk.TclError:\n            # The widget doesn't exist anymore\n            pass\n\n    def post_message(self, message, thread_safe=False):\n        \"Post a message.\"\n        # NOTE: when thread_safe=True, this method is thread-safe\n        # Amazing stuff, eh?\n        if self.message_func is None and not self.messages_enabled:\n            return\n        \n        if thread_safe and self.queue:\n            self.post_to_queue(lambda message=message: self._post_message(message))\n        else:\n            self._post_message(message)\n\n    def _post_message(self, message):\n        \"Post a message.\"\n        if self.overflow_scroll_frame:\n            message = \"[EMBEDDED DOCUMENT] \" + message\n        if self.message_func is not None:\n            self.message_func(message)\n        elif self.messages_enabled:\n            utilities.notifier(message)\n\n    # --- HTML/CSS parsing ----------------------------------------------------\n\n    def parse(self, html, thread_safe=False):\n        \"Parse HTML code. Call :meth:`TkinterWeb.reset` before calling this method for the first time.\"\n        # NOTE: when thread_safe=True, this method is thread-safe\n\n        html = self._crash_prevention(html)\n        html = self._dark_mode(html)\n\n        # By default Tkhtml won't display plain text\n        if \"<\" not in html and \">\" not in html:\n            html = f\"<html><body><div>{html}</div></body></html>\"\n        elif \"<html>\" not in html and \"</html>\" not in html:\n            # Otherwise, document.write can be buggy\n            html = f\"<html>{html}</html>\"\n\n        # Send the HTML code to the queue if needed\n        # Otherwise, evaluate directly so that the document can be manipulated as soon as parse() returns\n        if thread_safe:\n            self.post_to_queue(lambda html=html: self._parse(html))\n        else:\n            self._parse(html)\n    \n    def _parse(self, html):\n        \"Parse HTML code.\"\n        # NOTE: this must run in the main thread\n        self.parsing = True\n        self.tk.call(self._w, \"parse\", html)\n        self.parsing = False\n\n        self.post_event(utilities.DOM_CONTENT_LOADED_EVENT)\n\n        # If any threads are active, they'll send the done loading signal when they finish\n        if not self.active_threads:\n            self._handle_load_finish()\n        else:\n            # Scroll to the fragment if given but do not issue a done loading event\n            self._handle_load_finish(False)\n\n        self.script_manager._submit_deferred_scripts()\n        self.event_manager.send_onload()\n\n        #if self.using_tkhtml30: # Handle unsupported tags\n        self.node_manager._handle_load_finish()\n\n    def _handle_load_finish(self, post_event=True):\n        if self.fragment:\n            try:\n                if isinstance(self.fragment, tuple):\n                    self.yview(self.fragment)\n                else:\n                    node = self.search(f\"[id='{self.fragment}']\")\n                    if not node: \n                        node = self.search(f\"[name={self.fragment}]\")\n                    if node:\n                        self.fragment = node\n                        self.yview(node)\n            except tk.TclError:\n                pass\n        \n        if post_event:\n            self.post_event(utilities.DONE_LOADING_EVENT)\n\n    def parse_css(self, sheetid=None, data=\"\", url=None, fallback_priority=\"author\"):\n        \"Parse CSS code.\"\n        if not url: url = self.base_url\n        data = self._crash_prevention(data)\n        data = self._css_dark_mode(data)\n        \n        try:\n            importcmd = self.register(\n                lambda new_url, media=None, parent_url=url: \n                    self.style_manager._on_atimport(parent_url, new_url, media)\n            )\n            urlcmd = self.register(\n                lambda new_url, url=url: self.resolve_url(\n                    new_url, url\n                )\n            )\n            if not sheetid:\n                self._style_count += 1\n                sheetid = f\"{fallback_priority}{self._style_count:04d}\"\n                \n            self.tk.call(\n                self._w, \"style\",\n                \"-id\", sheetid,\n                \"-importcmd\", importcmd,\n                \"-urlcmd\", urlcmd, data\n            )\n        except tk.TclError:\n            # The widget doesn't exist anymore\n            pass\n\n    def reset(self, thread_safe=False):\n        \"Reset the widget.\"\n        # NOTE: when thread_safe=True, this method is thread-safe. Imagine that!\n\n        self.stop()\n\n        self.title = \"\"\n        self.icon = \"\"\n        self.fragment = \"\"\n\n        if thread_safe:\n            self.post_to_queue(self._reset)\n        else:\n            self._reset()\n\n    def _reset(self):\n        # NOTE: this must run in the main thread\n        \n        # Reset the scrollbars to the default setting\n        if self.manage_vsb_func is not None:\n            self.manage_vsb_func()\n        if self.manage_hsb_func is not None:\n            self.manage_hsb_func()\n\n        # Note to self: these need to be here\n        # Or very strange errors will magically appear,\n        # Usually when switching between pages quickly\n        self.hovered_nodes.clear()\n        self.current_hovered_node = None\n\n        self._set_cursor(\"default\")\n        self.tk.call(self._w, \"reset\")\n\n        for manager in self._managers:\n            manager.reset()\n\n    def stop(self):\n        \"Stop loading resources.\"\n        for thread in self.active_threads:\n            thread.stop()\n        self.pending_threads.clear()\n    \n    def resolve_url(self, url, base=None):\n        \"Generate a full url from the specified url.\"\n        if not base: base = self.base_url\n        return urljoin(base, url)\n    \n    # --- Resource loading ----------------------------------------------------\n\n    def download_url(self, url, *args):\n        if self.request_func:\n            return self.request_func(url, *args)\n        \n        if url.startswith(\"file://\") or (not self.caches_enabled):\n            return utilities.download(url, *args, insecure=self.insecure_https, cafile=self.ssl_cafile, headers=tuple(self.headers.items()), timeout=self.request_timeout)\n        else:\n            return utilities.cache_download(url, *args, insecure=self.insecure_https, cafile=self.ssl_cafile, headers=tuple(self.headers.items()), timeout=self.request_timeout)\n    \n    def _check_url_cache_state(self, url, *args):\n        return utilities.check_download(url, *args, insecure=self.insecure_https, cafile=self.ssl_cafile, headers=tuple(self.headers.items()), timeout=self.request_timeout)\n    \n    def _thread_check(self, callback, url, *args, **kwargs):\n        if not self.threading_enabled or url.startswith(\"file://\") or self._check_url_cache_state(url):\n            callback(url, *args, **kwargs)\n        else:\n            thread = utilities.StoppableThread(target=callback, args=(url, *args,), kwargs=kwargs)\n\n            if len(self.active_threads) >= 100:\n                self.pending_threads.append(thread)\n            else:\n                thread.start()\n\n    def _begin_download(self):\n        # NOTE: this may run in a thread\n\n        thread = utilities.get_current_thread()\n        self.active_threads.append(thread)\n        self.post_event(utilities.DOWNLOADING_RESOURCE_EVENT, thread.is_subthread)\n        return thread\n\n    def _finish_download(self, thread):\n        # NOTE: this may run in a thread\n\n        self.active_threads.remove(thread)\n\n        if thread.isrunning():\n            if thread.is_subthread and self.pending_threads:\n                self.pending_threads.pop(0).start()\n\n            elif not self.parsing:\n                if len(self.active_threads) == 0:\n                    self.post_to_queue(self._handle_load_finish, thread.is_subthread)\n                else:\n                    self.post_to_queue(lambda: self._handle_load_finish(False), thread.is_subthread)\n\n    def _finish_resource_load(self, message, url, resource, success):\n        # NOTE: this must run in the main thread\n\n        self.post_message(message)\n\n        if self.on_resource_setup is not None:\n            self.on_resource_setup(url, resource, success)\n\n    # --- Bindings ------------------------------------------------------------\n\n    def node(self, *args):\n        \"Retrieve one or more document node handles from the current document.\"\n        nodes = self.tk.call(self._w, \"node\", *args)\n        if nodes:\n            return nodes\n        else:\n            return None, None\n\n    def text(self, subcommand, *args):\n        \"Interact with the text of the HTML document. Valid subcommands are bbox, index, offset, and text.\"\n        return self.tk.call(self._w, \"text\", subcommand, *args)\n\n    def tag(self, subcommand, tag_name, *args):\n        \"Highlight regions of text displayed by the widget. Valid subcommands are add, remove, configure, and delete.\"\n        return self.tk.call(self._w, \"tag\", subcommand, tag_name, *args)\n\n    def search(self, selector, *a, cnf={}, **kw):\n        \"\"\"Search the document for the specified CSS selector; return a Tkhtml node if found.\"\"\"\n        return self.tk.call((self._w, \"search\", selector)+utilities.TclOpt(a)+self._options(cnf, kw))\n\n    def xview(self, *args, auto_scroll=False):\n        \"Control horizontal scrolling.\"\n        #if args:\n        #    return self.tk.call(self._w, \"xview\", *args)\n        #coords = map(float, self.tk.call(self._w, \"xview\").split()) #raises an error\n        #return tuple(coords)\n        xview = self.tk.call(self._w, \"xview\", *args)\n        if args:\n            self.caret_manager.update(auto_scroll=auto_scroll, xview=xview)\n        return xview\n\n    def xview_scroll(self, number, what, auto_scroll=False):\n        \"\"\"Shifts the view in the window left or right, according to number and what.\n        \"number\" is an integer, and \"what\" is either \"units\" or \"pages\".\"\"\"\n        return self.xview(\"scroll\", number, what, auto_scroll=auto_scroll)\n\n    def xview_moveto(self, number, auto_scroll=False):\n        \"Shifts the view horizontally to the specified position\"\n        return self.xview(\"moveto\", number, auto_scroll=auto_scroll)\n\n    def yview(self, *args, auto_scroll=False):\n        \"\"\"Control vertical scrolling.\"\"\"\n        yview = self.tk.call(self._w, \"yview\", *args)\n        if args:\n            self.caret_manager.update(auto_scroll=auto_scroll, yview=yview)\n        return yview\n\n    def yview_scroll(self, number, what, auto_scroll=False):\n        \"\"\"Shifts the view in the window left or right, according to number and what.\n        \"number\" is an integer, and \"what\" is either \"units\" or \"pages\".\"\"\"\n        return self.yview(\"scroll\", number, what, auto_scroll=auto_scroll)\n\n    def yview_moveto(self, number, auto_scroll=False):\n        \"Moves the view vertically to the specified position.\"\n        return self.yview(\"moveto\", number, auto_scroll=auto_scroll)\n\n    def bbox(self, node=None):\n        \"Get the bounding box of the viewport or a specified node.\"\n        return self.tk.call(self._w, \"bbox\", node)\n    \n    def parse_fragment_simple(self, html):\n        return self.tk.call(self._w, \"fragment\", html)\n\n    def parse_fragment(self, html):\n        \"\"\"Parse a document fragment.\n        A document fragment isn't part of the active document but is comprised of nodes like the active document.\n        Changes made to the fragment don't affect the document.\n        Returns a root node.\"\"\"\n        html = self._crash_prevention(html)\n        html = self._dark_mode(html)\n        fragment = self.tk.call(self._w, \"fragment\", html)\n        # If any threads are active, they'll send the done loading signal when they finish\n        if not self.active_threads:\n            self.post_event(utilities.DONE_LOADING_EVENT)\n        self.script_manager._submit_deferred_scripts()\n        return fragment\n\n    def get_node_text(self, node_handle, *args):\n        \"Get the text content of the given node.\"\n        return self.tk.call(node_handle, \"text\", *utilities.TclOpt(args))\n\n    def set_node_text(self, node_handle, new):\n        \"Set the text content of the given node.\"\n        self.tk.call(node_handle, \"text\", \"set\", new)\n        self.relayout() # needed for pathName text text to return the updated string\n\n    def relayout(self):\n        self.tk.call(self._w, \"_relayout\")\n\n    def get_child_text(self, node):\n        \"\"\"Get text of node and all its descendants recursively.\n        \n        New in version 4.4.\"\"\"\n        text = self.get_node_text(node, \"-pre\")\n        for child in self.get_node_children(node):\n            text += self.get_child_text(child)\n        return text\n    \n    def get_node_tag(self, node_handle):\n        \"Get the HTML tag of the given node.\"\n        return self.tk.call(node_handle, \"tag\")\n\n    def get_node_parent(self, node_handle):\n        \"Get the parent of the given node.\"\n        return self.tk.call(node_handle, \"parent\")\n\n    def get_node_children(self, node_handle):\n        \"Get the children of the given node.\"\n        return self.tk.call(node_handle, \"children\")\n\n    def get_node_attribute(self, node_handle, attribute, default=\"\", value=None):\n        \"Get the specified attribute of the given node.\"\n        if value:  # Backwards compatability\n            return self.tk.call(node_handle, \"attribute\", attribute, value)\n        else:\n            return self.tk.call(node_handle, \"attribute\", \"-default\", default, attribute)\n\n    def set_node_attribute(self, node_handle, attribute, value):\n        \"Set the specified attribute of the given node.\"\n        return self.tk.call(node_handle, \"attribute\", attribute, value)\n\n    def get_node_attributes(self, node_handle):\n        \"Get the attributes of the given node.\"\n        attr = self.tk.call(node_handle, \"attribute\")\n        return dict(zip(attr[0::2], attr[1::2]))\n\n    def get_node_property(self, node_handle, node_property, *args):\n        \"Get the specified CSS property of the given node.\"\n        return self.tk.call(node_handle, \"property\", *utilities.TclOpt(args), node_property)\n\n    def get_node_properties(self, node_handle, *args):\n        \"Get the CSS properties of the given node.\"\n        prop = self.tk.call(node_handle, \"property\", *utilities.TclOpt(args))\n        return dict(zip(prop[0::2], prop[1::2]))\n\n    def set_node_property(self, node_handle, node_property, new_value, *args):\n        \"Set the specified CSS property of the given node.\"\n        current = self.get_node_properties(node_handle, \"-inline\")\n        current[node_property] = new_value\n        style = \" \".join(f\"{p}: {v};\" for p, v in current.items())\n        self.set_node_attribute(node_handle, \"style\", style)\n\n    def override_node_properties(self, node_handle, *props):\n        \"Get/set the CSS property override list.\"\n        if props: return self.tk.call(node_handle, \"override\", \" \".join(props))\n        return self.tk.call(node_handle, \"override\")\n\n    def insert_node(self, node_handle, child_nodes):\n        \"Experimental, insert the specified nodes into the parent node.\"\n        return self.tk.call(node_handle, \"insert\", child_nodes)\n\n    def insert_node_before(self, node_handle, child_nodes, before):\n        \"Experimental, place the specified nodes is before another node.\"\n        return self.tk.call(node_handle, \"insert\", \"-before\", before, child_nodes)\n    \n    def replace_node_contents(self, node_handle, contents, *args, check=True):\n        \"\"\"Fill a node with either a Tk widget or with Tkhtml nodes.\n        \n        New in version 4.2.\"\"\"\n        if check and (node_handle != contents) and not self.get_node_parent(node_handle):\n            raise RuntimeError(f\"root elements cannot be replaced\")\n\n        if not contents:\n            # Calling replace with empty text causes Tkhtml to segfault\n            contents = self.tk.call(self._w, \"fragment\", \" \")\n        return self.tk.call(node_handle, \"replace\", contents, *args)\n    \n    def get_node_replacement(self, node_handle):\n        \"\"\"Return the Tk widget contained by the given node.\n        \n        New in version 4.13.\"\"\"\n        return self.tk.call(node_handle, \"replace\")\n\n    def delete_node(self, node_handle):\n        \"Delete the given node.\"\n        if self.experimental:\n            node_parent = self.get_node_parent(node_handle)\n            if node_parent:\n                self.tk.call(node_parent, \"remove\", node_handle)\n            else:\n                raise RuntimeError(f\"root elements cannot be removed\")\n        else:\n            node_parent = self.get_node_parent(node_handle)\n            node_tag = self.get_node_tag(node_handle)\n            # Removing the body element causes a segfault\n            if node_parent:\n                if node_tag != \"body\":\n                    self.tk.call(node_parent, \"remove\", node_handle)\n                else:\n                    raise RuntimeError(f\"{node_tag} elements cannot be removed\")\n            elif node_tag:\n                raise RuntimeError(f\"{node_tag} elements cannot be removed\")\n            else:\n                raise RuntimeError(f\"element is invalid or has already been removed\")\n\n    def destroy_node(self, node_handle):\n        \"Destroy a node. May cause crashes so avoid it whenever possible.\"\n        self.tk.call(node_handle, \"destroy\")\n\n    def set_node_flags(self, node, name):\n        \"Set dynamic flags on the given node.\"\n        self.tk.call(node, \"dynamic\", \"set\", name)\n\n    def remove_node_flags(self, node, name):\n        \"remove dynamic flags on the given node.\"\n        self.tk.call(node, \"dynamic\", \"clear\", name)\n\n    def get_node_tkhtml(self, node_handle):\n        \"Get the path name of the node's corresponding Tkhtml instance.\"\n        return self.tk.call(node_handle, \"html\")\n\n    def get_node_stacking(self, node_handle):\n        \"\"\"Return the node-handle that forms the stacking context this node is located in.\n        Return \"\" for the root-element or any element that is part of an orphan subtree.\n        \n        New in version 4.2.\"\"\"\n        return self.tk.call(node_handle, \"stacking\")\n\n    def get_current_hovered_node(self, event):\n        \"Get the current node.\"\n        if self.widget_manager.hovered_embedded_node:\n            return self.widget_manager.hovered_embedded_node\n\n        return self.tk.eval(\n            f\"\"\"set node [lindex [lindex [{self} node {event.x} {event.y}] end] end]\"\"\"\n        )\n\n    def get_current_hovered_node_parent(self, node):\n        \"Get the parent of the node returned by :meth:`TkinterWeb.get_current_hovered_node`.\"\n        return self.tk.eval(f\"\"\"set node [lindex [lindex [{node} parent] end] end]\"\"\")\n\n    def register_handler(self, handler_type, node_tag, callback):\n        \"Register a node handler.\"\n        self.tk.call(self._w, \"handler\", handler_type, node_tag, self.register(callback))\n\n    def _lazy_handler(self, manager, method):\n        def callback(*args, **kwargs):\n            manager_obj = getattr(self, manager)\n            return getattr(manager_obj, method)(*args, **kwargs)\n        return callback\n    \n    def register_lazy_handler(self, handler_type, node_tag, manager_name):\n        \"Register a node handler to run lazily in the given manager.\"\n        if handler_type == \"attribute\":\n            callback_name = f\"_on_{node_tag}_value_change\"\n        else:\n            callback_name = f\"_on_{node_tag}\"\n\n        self.tk.call(self._w, \"handler\", handler_type, node_tag, \n                     self.register(\n                         self._lazy_handler(manager_name, callback_name)\n                         )\n                     )\n        \n    def image(self, full=False):\n        \"\"\"Return the name of a new Tk image containing the rendered document.\n        The returned image should be deleted when the script has finished with it.\n        Note that this command is mainly intended for automated testing.\n        Be wary of running this command on large documents.\n        Does not work on Windows unless experimental Tkhtml is used.\"\"\"\n        full = \"-full\" if full else \"\"\n        name = self.tk.call(self._w, \"image\", full)\n        data = self.tk.call(name, \"data\")\n        self.tk.call(\"image\", \"delete\", name)\n        return data\n\n    def postscript(self, cnf={}, **kwargs):\n        \"\"\"Print the contents of the canvas to a postscript file.\n        Valid options: colormap, colormode, file, fontmap, height, \n        pageanchor, pageheight, pagesize, pagewidth, pagex, pagey, \n        nobg, noimages, rotate, width, x, and y.\n        Does not work unless experimental Tkhtml is used.\"\"\"\n        return self.tk.call((self._w, \"postscript\")+self._options(cnf, kwargs))\n\n    def preload_image(self, url):\n        \"\"\"Preload an image for use later. \n        Only useful if caches are enabled and reset() is not called after preloading.\"\"\"\n        return self.tk.call(self._w, \"preload\", url)\n    \n    def get_computed_styles(self):\n        \"Get a tuple containing the computed CSS rules for each CSS selector.\"\n        return self.tk.call(self._w, \"_styleconfig\")\n\n    def override_node_CSS(self, node, *props):\n        \"\"\"Overrides the node's properties; if it is a text node, it overrides the parent's properties.\n        \n        New in version 4.4.\"\"\"\n        if not self.get_node_tag(node): node = self.get_node_parent(node)\n        return self.override_node_properties(node, *props)\n\n    def write(self, *arg, cnf={}, **kw):\n        \"\"\"Write directly to an open HTML document stream, may be used when parsing.\n        \n        New in version 4.4.\"\"\"\n        return self.tk.call(self._w, \"write\", *arg+self._options(cnf, kw))\n \n    # --- Cmds & crash prevention ---------------------------------------------\n\n    def _on_image_cmd(self, url):\n        \"Handle images.\"\n        return self.image_manager._on_image_cmd(url)\n       \n    def _on_draw_cleanup_crash_cmd(self):\n        if self.crash_prevention_enabled:\n            self.post_message(\"WARNING: HtmlDrawCleanup has encountered a critical error. This is being ignored because crash prevention is enabled.\")\n        else:\n            self.post_message(\"WARNING: HtmlDrawCleanup has encountered a critical error.\")\n            self.destroy()\n\n    def _crash_prevention(self, data):\n        if self.crash_prevention_enabled:\n            ### TODO: enable emojis & noto colo emoji font in Tcl/Tk 9\n\n            # From Bug #11\n            data = \"\".join(c for c in data if c <= \"\\uFFFF\")\n\n            # I moved these workarounds to Tkhtml in version 3.1\n            if self.using_tkhtml30:\n                data = sub(\n                    \"font-family:[^;']*(;)?\",\n                    self._remove_noto_emoji,\n                    data, flags=IGNORECASE,\n                )\n                data = sub(\n                    r\"rgb\\([^0-9](.*?)\\)\", \n                    \"inherit\", \n                    data, flags=IGNORECASE)\n\n                # From Bug #150\n                # Not really crash prevention\n                data = sub(\n                    r'style=([\"\\'])\\s+', \n                    r'style=\\1', \n                    data, flags=IGNORECASE)\n\n        return data\n\n    def _remove_noto_emoji(self, match):\n        \"Remove noto color emoji font, which causes Tkinter to crash.\"\n        match = match.group().lower()\n        match = match.replace(\"noto color emoji\", \"arial\")\n        return match\n\n    # --- Dark mode -----------------------------------------------------------\n\n    def _generate_altered_colour(self, match, matchtype=1):\n        \"Invert document colours. Highly experimental.\"\n        colors = match.group(2).replace(\"\\n\", \"\")\n        colors = split(r\"\\s(?![^()]*\\))\", colors)\n        changed = False\n\n        for count, color in enumerate(colors):\n            try:\n                if color.startswith(\"#\"):\n                    color = color.lstrip(\"#\")\n                    lv = len(color)\n                    if lv == 3:\n                        color = color + color\n                        lv = len(color)\n                    colors[count] = utilities.invert_color(\n                        list(\n                            int(color[i : i + lv // 3], 16)\n                            for i in range(0, lv, lv // 3)\n                        ),\n                        match.group(1), self.dark_theme_limit\n                    )\n                    changed = True\n                elif color.startswith(\"rgb(\") or color.startswith(\"rgba(\"):\n                    colors_list = (list(\n                            map(\n                                int,\n                                color.lstrip(\"rgba(\")\n                                .lstrip(\"rgb(\")\n                                .rstrip(\")\")\n                                .strip(\" \")\n                                .split(\",\"),\n                            )\n                        ),)\n                    if len(colors_list) == 3:\n                        colors[count] = utilities.invert_color(\n                            colors_list,\n                            match.group(1), self.dark_theme_limit\n                        )\n                        changed = True\n                else:\n                    try:\n                        color = list(self.winfo_rgb(color))\n                        colors[count] = utilities.invert_color(color, match.group(1), self.dark_theme_limit)\n                        changed = True\n                    except tk.TclError:\n                        pass\n            except ValueError as error:\n                pass\n\n        if changed:\n            if matchtype:\n                return match.group(1) + \" \".join(colors)\n            else:\n                return match.group(1) + \": \" + \" \".join(colors)\n        else:\n            return match.group()\n            \n    def _dark_mode(self, html):\n        if self.dark_theme_enabled:\n            html = sub(self.inline_dark_theme_regexes[0], lambda match: match.group(1) + sub(self.inline_dark_theme_regexes[1], self._generate_altered_colour, match.group(2)), html)\n            for regex in self.general_dark_theme_regexes:\n                html = sub(regex, self._generate_altered_colour, html, flags=IGNORECASE)\n        return html\n    \n    def _css_dark_mode(self, data):\n        if self.dark_theme_enabled:\n            return sub(self.style_dark_theme_regex, lambda match, matchtype=0: self._generate_altered_colour(match, matchtype), data)\n        return data\n    \n    # --- Miscellaneous -------------------------------------------------------\n\n    def safe_tk_eval(self, expr):\n        \"\"\"Always evaluate the given expression on the main thread.\n\n        Since version 4.9 all callbacks are evaluated on the main thread. Except for niche cases this command should not need to be used.\n\n        This command may be removed at any time.\n        \n        New in version 4.4.\"\"\"\n        return utilities.safe_tk_eval(self, expr)\n\n    def serialize_node(self, ib=3):\n        \"\"\"Pretty-print a node's contents. Similar to innerHTML, but formatted.\n\n        New in version 4.4.\"\"\"\n        return utilities.safe_tk_eval(self, r\"\"\"\n            proc indent {d} {return [string repeat { } $d]}\n            proc prettify {node} {\n                set depth [expr {([info level] - 1) * %d}]\n                set tag [$node tag]\n                if {$tag eq \"\"} {\n                if {[string trim [$node text]] eq \"\"} return\n                set z [string map {< &lt; > &gt;} [$node text -pre]]\n                if {[[$node parent] tag] ne \"pre\"} {\n                        return [indent $depth][regsub -all {\\s+} $z \" \"]\\n\n                } else {\n                        return [indent $depth]$z\\n\n                }\n                }\n                set ret [indent $depth]<$tag\n                foreach {zKey zVal} [$node attribute] {\n                    append ret \" $zKey=\\\"[string map [list \\x22 \\x5C\\x22] $zVal]\\\"\"\n                }\n                append ret >\\n\n                set void {area base br col embed hr img input keygen link meta param source track wbr}\n                if {[lsearch -exact $void $tag] != -1} {\n                    return $ret\n                }\n                foreach child [$node children] {\n                append ret [prettify $child]\n                }\n                return $ret[indent $depth]</$tag>\\n\n            }\n                prettify [%s node] \"\"\" % (ib, self)\n        )\n\n    def serialize_node_style(self, ib=3, return_as_dict=False):\n        \"\"\"Pretty-print a node's style.\n\n        New in version 4.4.\"\"\"\n        style = {\n            i[0]: dict(j.split(\":\", 1) for j in i[1].split(\"; \") if j.strip())\n            for i in self.get_computed_styles()\n            if \"agent\" != i[2]\n        }\n\n        if return_as_dict:\n            return style\n        else:\n            text = \"\"\n            for i in style:\n                text += i + \" {\\n\"\n                for j in style[i]:\n                    text += \" \"*ib + style[i][j] + \";\\n\"\n                text += \"}\\n\"\n            return text\n        \n    def tkhtml_offset_to_text_index(self, node, offset, invert=False):\n        \"\"\"Translate a Tkhtml node offset to a node text index or back.\n\n        New in version 4.8.\"\"\"\n\n        text = self.get_node_text(node, \"-pre\")\n\n        ws = len(text) - len(text.lstrip())\n\n        if invert:\n            #index = self.text(\"offset\", node, 0) + offset\n            #node, offset = self.text(\"index\", index)\n            return text, max(offset - ws, 0)\n        else:\n            try:\n                offset = self.text(\"offset\", node, offset + ws) - self.text(\"offset\", node, 0)\n            except TypeError:\n                pass\n            return text, offset\n\n    def _set_cursor(self, cursor):\n        \"Set the document cursor.\"\n        if self._current_cursor != cursor:\n            cursor = utilities.CURSOR_MAP[cursor]\n            try:\n                self.master.config(cursor=cursor, _override=True)\n            except tk.TclError:\n                self.master.config(cursor=cursor)\n            self._current_cursor = cursor\n            # I've noticed that the cursor won't always update when the binding is tied to a different widget than the one we are changing the cursor of\n            # However, the html widget doesn't support the cursor property so there's not much we can do about this\n            # update_idletasks() or update() have no effect, but updating the color or text of another widget does\n            # print() also works. Don't ask me why.\n            # Therefore we update the background color of a tiny frame that is barely visible whenever we need to change the cursor\n            # I might as well match the background color of the page but it doesn't really matter\n            # It's weird but hey, it works\n            self.motion_frame.config(bg=self.motion_frame_bg)\n\n    # --- Widget-user interaction ---------------------------------------------\n\n    def _finish_scrolling(self, event, widget, x11, vsb):\n        if not widget: widget = event.widget\n\n        # If the user scrolls on the page while its resources are loading, stop scrolling to the fragment\n        if isinstance(widget.fragment, tuple):\n            widget.fragment = None\n\n        if x11:\n            scroll_up = (event.num == 4)\n        else:\n            scroll_up = event.delta > 0\n\n        if vsb:\n            yview = widget.yview()\n            at_edge = yview[0] == 0 if scroll_up else yview[1] == 1\n            vsb_locked = widget.manage_vsb_func is not None and widget.manage_vsb_func(check=True) == 0\n\n            for node_handle in widget.hovered_nodes:\n                if x11: stype = \"onscrollup\" if scroll_up else \"onscrolldown\"\n                else: stype = \"onscroll\"\n                widget.event_manager.post_element_event(node_handle, stype, event)\n\n        else:\n            xview = widget.xview()\n            at_edge = xview[0] == 0 if scroll_up else xview[1] == 1\n            vsb_locked = widget.manage_hsb_func is not None and widget.manage_hsb_func(check=True) == 0\n\n        if widget.overflow_scroll_frame and (at_edge or vsb_locked):\n            if vsb:\n                widget.overflow_scroll_frame._finish_scrolling(event, widget.overflow_scroll_frame, x11, vsb)\n            else:\n                widget.overflow_scroll_frame._finish_scrolling(event, widget.overflow_scroll_frame, x11, vsb)\n        elif not vsb_locked:\n            if x11:\n                units = -4 if scroll_up else 4\n            else:\n                if utilities.PLATFORM.system == \"Darwin\":\n                    units = int(-1*event.delta)\n                else:\n                    units = int(-1*event.delta/30)\n            if vsb:\n                widget.yview_scroll(units, \"units\")\n            else:\n                widget.xview_scroll(units, \"units\")\n\n    def _scroll_x11(self, event, widget=None): self._finish_scrolling(event, widget, True, True)\n    def _xscroll_x11(self, event, widget=None): self._finish_scrolling(event, widget, True, False)\n    def _scroll(self, event): self._finish_scrolling(event, self, False, True)\n    def _xscroll(self, event): self._finish_scrolling(event, self, False, False)\n\n    def _on_right_click(self, event):\n        for node_handle in self.hovered_nodes:\n            self.event_manager.post_element_event(node_handle, \"onmousedown\")\n            self.event_manager.post_element_event(node_handle, \"oncontextmenu\", event)\n\n    def _on_middle_click(self, event):\n        for node_handle in self.hovered_nodes:\n            self.event_manager.post_element_event(node_handle, \"onmiddlemouse\", event)\n\n    def _on_focusout(self, event):\n        if self.caret_browsing_enabled:\n            try:\n                if (self.winfo_toplevel().focus_displayof() not in {None, self}):\n                    self.caret_manager.hide()\n            except KeyError:\n                # Clicked on the combobox. Not too sure why.\n                self.caret_manager.hide()\n\n    def _on_focusin(self, event):\n        self.caret_manager.update()\n\n    def _on_up(self, event):\n        if self.caret_manager.is_placed():\n            self.caret_manager.shift_up(event)\n        else:\n            self.yview_scroll(-5, \"units\")\n\n    def _on_down(self, event):\n        if self.caret_manager.is_placed():\n            self.caret_manager.shift_down(event)\n        else:\n            self.yview_scroll(5, \"units\")\n\n    def _on_left(self, event): \n        self.caret_manager.shift_left(event)\n\n    def _on_right(self, event): \n        self.caret_manager.shift_right(event)\n    \n    def _on_prior(self, event): self.yview_scroll(-1, \"pages\")\n\n    def _on_next(self, event): self.yview_scroll(1, \"pages\")\n\n    def _on_home(self, event): self.yview_moveto(0)\n\n    def _on_end(self, event): self.yview_moveto(1)\n\n    def _on_click(self, event, redirected=False):\n        \"Set active element flags.\"\n        if not self.current_hovered_node:\n            # Register current node if mouse has never moved\n            self._on_mouse_motion(event)\n\n        if not redirected:\n            self.selection_manager.reset_selection_type()\n\n        self.focus_set()\n        self.selection_manager.clear_selection()\n\n        if self.javascript_enabled or self.events_enabled:\n            for node_handle in self.hovered_nodes:\n                self.event_manager.post_element_event(node_handle, \"onmousedown\", event)\n\n        if self.hovered_nodes:\n            node, offset = self.node(\n                True, event.x, event.y\n            )\n            self.clicked_node = self.hovered_nodes[0]\n\n            self.selection_manager.begin_selection(node, offset)\n\n            self.caret_manager.set(node, offset)\n\n            if self.stylesheets_enabled and (not self.text_mode or (event.state & 0x4) != 0):\n                self.set_node_flags(self.hovered_nodes[0], \"active\")\n                self.current_active_node = self.hovered_nodes[0]\n\n    def _on_leave(self, event=None):\n        \"Reset cursor and node state when leaving this widget\"\n        self._set_cursor(\"default\")\n        if self.stylesheets_enabled:\n            for node in self.hovered_nodes:\n                try:\n                    self.remove_node_flags(node, \"hover\")\n                    self.remove_node_flags(node, \"active\")\n                    self.event_manager.post_element_event(node, \"onmouseout\", event)\n                except tk.TclError:\n                    pass\n        self.hovered_nodes = []\n        self.current_hovered_node = None\n\n    def _handle_recursive_hovering(self, event, node_handle, prev_hovered_nodes):\n        \"Set hover flags on the parents of the hovered element.\"\n        if node_handle not in self.hovered_nodes:\n            self.hovered_nodes.append(node_handle)\n\n        if node_handle not in prev_hovered_nodes:\n            self.set_node_flags(node_handle, \"hover\")\n            self.event_manager.post_element_event(node_handle, \"onmouseover\", event, \"Enter\")\n\n        self.event_manager.post_element_event(node_handle, \"onmousemove\", event)\n        if event.state & 0x0100:\n            self.event_manager.post_element_event(node_handle, \"onmouseb1move\", event)\n\n        parent = self.get_current_hovered_node_parent(node_handle)\n        if parent:\n            self._handle_recursive_hovering(event, parent, prev_hovered_nodes)            \n\n    def _on_mouse_motion(self, event):\n        \"Set hover flags, motion events, and handle the CSS 'cursor' property.\"\n\n        node_handle = self.get_current_hovered_node(event)\n\n        if not node_handle:\n            if self.text_mode:\n                try:\n                    node_handle, index = self.text(\"index\", \"-1\")    \n                except ValueError:\n                    return\n            else:\n                self._on_leave(None)\n                return\n\n        try:\n            # If we are in the same node, sumbit motion events\n            # If event.state & 0x0100, the mouse is being pressed\n            # If event.type == 5, it's coming from self._on_click_release()\n            if node_handle == self.current_hovered_node and event.type != \"5\":\n                for node_handle in self.hovered_nodes:\n                    self.event_manager.post_element_event(node_handle, \"onmousemove\", event)\n                    if event.state & 0x0100:\n                        self.event_manager.post_element_event(node_handle, \"onmouseb1move\", event)\n                return\n            \n            # If not we have some work to do\n            if self.hovered_nodes:\n                self.event_manager.post_element_event(self.hovered_nodes[0], \"onmouseleave\")\n                \n\n            prev_hovered_nodes = set(self.hovered_nodes)\n            \n            if not self.get_node_tag(node_handle):\n                useful_node_handle = self.get_current_hovered_node_parent(node_handle)\n            else:\n                useful_node_handle = node_handle\n            self.hovered_nodes = []\n            self._handle_recursive_hovering(event, useful_node_handle, prev_hovered_nodes)\n\n            cursor = self.get_node_property(useful_node_handle, \"cursor\")\n            if self.text_mode and not (event.state & 0x4):\n                self._set_cursor(\"text\")\n            elif (not (event.state & 0x0100) or event.type == \"5\" or \n                (not self.selection_enabled and \n                 (self.clicked_node == useful_node_handle or not self.clicked_node)\n                 )) and cursor in utilities.CURSOR_MAP: # if cursor is set\n                self._set_cursor(cursor)\n            elif ((useful_node_handle != node_handle) and self.selection_enabled) : # if on a text node\n                self._set_cursor(\"text\")\n            else:\n                self._set_cursor(\"default\")\n\n            # self.current_hovered_node can be a text node\n            # self.hovered nodes will never hold text nodes\n            self.current_hovered_node = node_handle\n\n            self.event_manager.post_element_event(useful_node_handle, \"onmouseenter\")\n\n            for node in prev_hovered_nodes - set(self.hovered_nodes):\n                self.remove_node_flags(node, \"hover\")\n                self.event_manager.post_element_event(node, \"onmouseout\", event, \"Leave\")\n\n        except tk.TclError:\n            # Sometimes errors are thrown if the mouse is moving while the page is loading\n            pass\n\n    def _on_click_release(self, event):\n        \"Handle click releases on hyperlinks and form elements.\"\n        if self.selection_manager.get_selection():\n            return self._on_mouse_motion(event)\n    \n        if not self.hovered_nodes:\n            return\n        \n        for node_handle in self.hovered_nodes:\n            self.event_manager.post_element_event(node_handle, \"onmouseup\", event)\n            self.event_manager.post_element_event(node_handle, \"onclick\")\n\n        node_handle = self.hovered_nodes[0]\n\n        if self.clicked_node != node_handle:\n            return\n\n        try:\n            node_tag = self.get_node_tag(node_handle).lower()\n\n            if not node_tag:\n                node_handle = self.get_node_parent(node_handle)\n                node_tag = self.get_node_tag(node_handle).lower()\n\n            node_type = self.get_node_attribute(node_handle, \"type\").lower()\n\n            if self.current_active_node and self.stylesheets_enabled:\n                self.remove_node_flags(self.current_active_node, \"active\")\n\n            if self.text_mode and not (event.state & 0x4):\n                return\n            \n            if node_tag == \"input\" and node_type == \"reset\":\n                self.form_manager._handle_form_reset(node_handle)\n            elif node_tag == \"input\" and node_type in {\"submit\", \"image\"}:\n                self.form_manager._handle_form_submission(node_handle)\n            else:\n                for node in self.hovered_nodes:\n                    if node != node_handle:\n                        node_tag = self.get_node_tag(node).lower()\n                    if node_tag == \"a\":\n                        self.node_manager._handle_link_click(node)\n                        break\n                    elif node_tag == \"button\":\n                        if node != node_handle:\n                            node_type = self.get_node_attribute(node, \"type\").lower()\n                        if node_type == \"submit\":\n                            self.form_manager._handle_form_submission(node)\n                            break\n                    elif node_tag == \"summary\":\n                        self.node_manager._handle_summary_click(node)\n                        break\n                        \n        except tk.TclError:\n            pass\n\n        self.current_active_node = None\n\n    def _on_double_click(self, event):\n        \"Cycle between normal selection, text selection, and element selection on multi-clicks.\"\n        self._on_click(event, True)\n\n        for node_handle in self.hovered_nodes:\n            self.event_manager.post_element_event(node_handle, \"ondblclick\", event)\n\n        try:\n            self.selection_manager.double_click_selection()\n        except tk.TclError:\n            self._set_cursor(\"default\")\n\n    def _extend_selection(self, event):\n        \"Alter selection and HTML element states based on mouse movement.\"\n        if self.selection_manager.selection_start_node is None:\n            return\n\n        try:\n            new_node, new_offset = self.node(True, event.x, event.y)\n\n            if new_node is None:\n                return\n\n            self.selection_manager.extend_selection(new_node, new_offset)\n\n            if self.current_active_node:\n                if self.stylesheets_enabled:\n                    self.remove_node_flags(self.current_active_node, \"active\")\n                self.current_active_node = None\n\n                if self.selection_enabled and not self.text_mode:\n                    self._set_cursor(\"default\")\n\n            self.caret_manager.set(new_node, new_offset)\n        except tk.TclError:\n            if not self.text_mode:\n                self._set_cursor(\"default\")\n\n    def _copy_selection(self, event):\n        self.selection_manager.copy_selection()\n\n    def _select_all(self, event):\n        self.selection_manager.select_all()\n\n    # --- Backwards-compatibility ---------------------------------------------\n\n    def update_tags(self):\n        utilities.deprecate(\"update_tags\", \"selection_manager\")\n        return self.selection_manager.update_tags()\n\n    def select_all(self):\n        utilities.deprecate(\"select_all\", \"selection_manager\")\n        return self.selection_manager.select_all()\n\n    def clear_selection(self):\n        utilities.deprecate(\"clear_selection\", \"selection_manager\")\n        return self.selection_manager.clear_selection()\n    \n    def update_selection(self):\n        utilities.deprecate(\"update_selection\", \"selection_manager\")\n        return self.selection_manager.update_selection()\n    \n    def get_selection(self):\n        utilities.deprecate(\"get_selection\", \"selection_manager\")\n        return self.selection_manager.get_selection()\n\n    def copy_selection(self, event=None):\n        utilities.deprecate(\"copy_selection\", \"selection_manager\")\n        return self.selection_manager.copy_selection()\n\n    def allocate_image_name(self):\n        utilities.deprecate(\"allocate_image_name\", \"image_manager\")\n        return self.image_manager.allocate_image_name()\n\n    def handle_node_replacement(self, node, widgetid, deletecmd, stylecmd=None, allowscrolling=True, handledelete=True):\n        utilities.deprecate(\"handle_node_replacement\", \"widget_manager\")\n        return self.widget_manager.handle_node_replacement(node, widgetid, deletecmd, stylecmd, allowscrolling, handledelete)\n\n    def map_node(self, node, force=False):\n        utilities.deprecate(\"map_node\", \"widget_manager\")\n        return self.widget_manager.map_node(node, force)\n\n    def replace_node_with_widget(self, node, widgetid):\n        utilities.deprecate(\"replace_node_with_widget\", \"widget_manager\")\n        return self.widget_manager.set_node_widget(node, widgetid)\n    \n    def find_text(self, searchtext, select, ignore_case, highlight_all):\n        utilities.deprecate(\"find_text\", \"search_manager\")\n        return self.search_manager.find_text(searchtext, select, ignore_case, highlight_all)\n    \n    def send_onload(self, root=None, children=None):\n        utilities.deprecate(\"send_onload\", \"event_manager\")\n        return self.event_manager.send_onload(root, children)\n    \n    # --- Tkhtml URIs ---------------------------------------------------------\n\n    def decode_uri(self, uri, base64=False):\n        \"\"\"This command is designed to help scripts process data: URIs. It is completely separate from the html widget.\n        \n        New in version 4.19.\"\"\"\n        c = (\"::tkhtml::decode\", \"-base64\", uri) if base64 else (\"::tkhtml::decode\", uri)\n        return self.tk.call(*c).strip(b\"}\")\n\n    def encode_uri(self, uri):\n        \"\"\"Encodes the uri.\n        \n        New in version 4.19.\"\"\"\n        return self.tk.call(\"::tkhtml::encode\", uri)\n\n    def escape_uri(self, uri, query=False):\n        \"\"\"Returns the decoded data.\n        \n        New in version 4.19.\"\"\"\n        a = \"-query\" if query else \"\"\n        return self.tk.call(\"::tkhtml::escape_uri\", a, uri)\n\nclass TkHtmlParsedURI:\n    \"\"\"Bindings for the Tkhtml URI parsing system. \n    \n    The underlying commands are largely unmaintained. Consider using the methods provided by the :class:`.HtmlFrame` widget and by Python's :py:mod:`urllib` library.\n    \n    New in version 4.4.\"\"\"\n\n    def __init__(self, uri, html):\n        self._html = html\n        self.parsed = self.uri(uri)\n\n    def __repr__(self):\n        return f\"{self._html._w}::{self.__class__.__name__.lower()}\"\n\n    def __str__(self):\n        return self.get(self.parsed)\n\n    def __del__(self):\n        self.destroy(self.parsed)\n\n    def uri(self, uri):\n        \"Returns name of parsed uri to be used in methods below.\"\n        return self._html.tk.call(\"::tkhtml::uri\", uri)\n\n    def tkhtml_uri_decode(self, uri, base64=False):\n        \"This command is designed to help scripts process data: URIs. It is completely separate from the html widget\"\n        return self._html.tkhtml_uri_decode(uri, base64)\n\n    def tkhtml_uri_encode(self, uri):\n        \"Encodes the uri.\"\n        return self._html.tkhtml_uri_encode(uri)\n\n    def tkhtml_uri_escape(self, uri, query=False):\n        \"Returns the decoded data.\"\n        return self._html.tkhtml_uri_escape(uri, query)\n\n    def uri_resolve(self, uri):\n        \"Resolve a uri.\"\n        return self._html.tk.call(self.parsed, \"resolve\", uri)\n\n    @property\n    def load(self, uri):\n        \"Load a uri.\"\n        return self._html.tk.call(self.parsed, \"load\", uri)\n\n    @property\n    def get(self):\n        \"Get the uri.\"\n        return self._html.tk.call(self.parsed, \"get\")\n\n    @property\n    def defrag(self):\n        \"Defrag the uri.\"\n        return self._html.tk.call(self.parsed, \"get_no_fragment\")\n\n    @property\n    def scheme(self):\n        \"Return the uri scheme.\"\n        return self._html.tk.call(self.parsed, \"scheme\")\n\n    @property\n    def authority(self):\n        \"Return the uri authority.\"\n        return self._html.tk.call(self.parsed, \"authority\")\n\n    @property\n    def path(self):\n        \"Return the uri path.\"\n        return self._html.tk.call(self.parsed, \"path\")\n\n    @property\n    def query(self):\n        \"Return the uri query.\"\n        return self._html.tk.call(self.parsed, \"query\")\n\n    @property\n    def fragment(self):\n        \"Return the uri fragment.\"\n        return self._html.tk.call(self.parsed, \"fragment\")\n\n    @property\n    def splitfrag(self):\n        \"Return namedtuple with uri and fragment\"\n        return self.defrag, self.fragment\n\n    def destroy(self):\n        \"Destroy this uri.\"\n        self._html.tk.call(self.parsed, \"destroy\")\n"
  },
  {
    "path": "tkinterweb/dom.py",
    "content": "\"\"\"\nA thin wrapper on top of bindings.py that offers some JavaScript-like functions \nand converts Tkhtml nodes into Python objects\n\nCopyright (c) 2021-2025 Andrew Clarke\n\nSome of the Tcl code in this file is modified from the Tkhtml/Hv3 project. Tkhtml is copyright (c) 2005 Dan Kennedy.\n\"\"\"\n\nfrom tkinter import TclError\n\n\nCOMPOSITE_PROPERTIES = {\n    \"margin\": (\"margin-top\", \"margin-right\", \"margin-bottom\", \"margin-left\"),\n    \"padding\": (\"padding-top\", \"padding-right\", \"padding-bottom\", \"padding-left\"),\n    \"border-width\": (\"border-top-width\", \"border-right-width\", \"border-bottom-width\", \"border-left-width\"),\n    \"border-style\": (\"border-top-style\", \"border-right-style\", \"border-bottom-style\", \"border-left-style\"),\n    \"border-color\": (\"border-top-color\", \"border-right-color\", \"border-bottom-color\", \"border-left-color\"),\n    \"border-radius\": (\"border-top-left-radius\", \"border-top-right-radius\", \"border-bottom-right-radius\", \"border-bottom-left-radius\"),\n    \"border\": (\"border-width\", \"border-style\", \"border-color\"),\n    \"outline\": (\"outline-color\", \"outline-style\", \"outline-width\"),\n    \"background\": (\"background-color\", \"background-image\", \"background-repeat\", \"background-attachment\", \"background-position\"),\n    \"list-style\": (\"list-style-type\", \"list-style-position\", \"list-style-image\"),\n    \"cue\": (\"cue-before\", \"cue-after\"),\n    \"font\": (\"font-style\", \"font-variant\", \"font-weight\", \"font-size\", \"line-height\", \"font-family\"),\n}\n\n\ndef escape_Tcl(string):\n    string = str(string)\n    escaped = \"\"\n    special_chars = '\"\\\\$[]'\n    for char in string:\n        if char in special_chars:\n            escaped += \"\\\\\"\n        escaped += char\n    return escaped\n\n\ndef extract_nested(nested):\n    if isinstance(nested, tuple) and len(nested):\n        return extract_nested(nested[0])\n    return nested\n\n\ndef camel_case_to_property(string):\n    new_string = \"\"\n    for i in string:\n        if i.isupper():\n            new_string += \"-\" + i.lower()\n        else:\n            new_string += i\n    return new_string\n\n\ndef DOM_element_events(cls):  # class\n    for event in {\n        \"onchange\", \"onclick\", \"oncontextmenu\", \"ondblclick\", \"onload\",\n        \"onmousedown\", \"onmouseenter\", \"onmouseleave\", \"onmousemove\", \"onmouseout\",\n        \"onmouseover\", \"onmouseup\"\n    }:\n        # Create the getter function\n        def getter(cls, event=event):  # Default argument to capture current event\n            return cls.getAttribute(event)\n\n        # Create the setter function\n        def setter(cls, value, event=event):  # Default argument to capture current event\n            cls.setAttribute(event, value)\n\n        # Use property to create a new property with the getter and setter\n        prop = property(lambda cls: getter(cls), lambda cls, value: setter(cls, value))\n        setattr(cls.__class__, event, prop)  # Set the property on the class\n\n\nclass HTMLDocument:\n    \"\"\"Access this class via the :attr:`~tkinterweb.HtmlFrame.document` property of the :class:`~tkinterweb.HtmlFrame` and :class:`~tkinterweb.HtmlLabel` widgets.\n    \n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\"\"\"\n    def __init__(self, html):\n        self.html = html\n        self.html.tk.createcommand(\"parse_fragment\", self.html.parse_fragment)\n        self.html.tk.createcommand(\"node_to_html\", self._node_to_html)\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n\n    @property\n    def body(self):  # Taken from hv3_dom_html.tcl line 161\n        \"\"\"The document body element.\n\n        :rtype: :class:`HTMLElement`\"\"\"\n        return HTMLElement(\n            self, self.html.safe_tk_eval(f\"\"\"set body [lindex [[{self.html} node] children] 1]\"\"\"),\n        )\n\n    @property\n    def documentElement(self):\n        \"\"\"The document root element.\n\n        :rtype: :class:`HTMLElement`\"\"\"\n        return HTMLElement(\n            self, self.html.safe_tk_eval(f\"\"\"set root [lindex [{self.html} node] 0]\"\"\"),\n        )\n    \n    def write(self, *text):\n        \"\"\"Write into the current document or output stream. If the document is loaded, this first deletes all existing HTML.\n\n        :param text: The text or HTML code to insert.\n        :type text: str\n\n        New in version 4.20.\"\"\"\n        if self.html.parsing:\n            self.html.write(\"text\", text)\n        else:\n            self.html.reset()\n            self.html.parse(\" \".join(text))\n\n    def createElement(self, tagname):  # Taken from hv3_dom_core.tcl line 214\n        \"\"\"Create and return a new HTML element with the given tag name.\n\n        :param tagname: The new element's HTML tag.\n        :type tagname: str\n        :rtype: :class:`HTMLElement`\"\"\"\n        return HTMLElement(\n            self, self.html.safe_tk_eval(\"\"\"\n            set node [%s fragment \"<%s>\"]\n            if {$node eq \"\"} {error \"DOMException NOT_SUPPORTED_ERR\"}\n            return $node\n            \"\"\" % (self.html, tagname)),\n        )\n\n    def createTextNode(self, text):\n        \"\"\"Create and return a new text node with the given text content.\n        \n        :param text: The text content of the new node.\n        :type text: str\n        :rtype: :class:`HTMLElement`\"\"\"\n        return HTMLElement(\n            self, self.html.safe_tk_eval(\"\"\"\n            set tkw %s\n            set text \"%s\"\n            if {$text eq \"\"} {\n                # Special case - The [fragment] API usually parses an empty string\n                # to an empty fragment. So create a text node with text \"X\", then \n                # set the text to an empty string.\n                set node [$tkw fragment X]\n                $node text set \"\"\n            } else {\n                set escaped [string map {< &lt; > &gt;} $text]\n                set node [parse_fragment $escaped]\n            }\n            return $node\n            \"\"\" % (self.html, escape_Tcl(text)))\n        )\n\n    def getElementById(self, query, _root=None):\n        \"\"\"Return an element that matches a given id.\n        \n        :param query: The element id to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLElement(self, self.html.search(f\"[id='{query}']\", index=0, root=_root))\n\n    def getElementsByClassName(self, query, _root=None):\n        \"\"\"Return all elements that match a given class name.\n        \n        :param query: The class to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLCollection(self, \" \".join(f\".{i}\" for i in query.split()), root=_root)\n\n    def getElementsByName(self, query, _root=None):\n        \"\"\"Return all elements that match a given given name attribute.\n        \n        :param query: The name to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLCollection(self, f\"[name='{query}']\", root=_root)\n\n    def getElementsByTagName(self, query, _root=None):\n        \"\"\"Return all elements that match a given tag name.\n        \n        :param query: The tag to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLCollection(self, query, root=_root)\n\n    def querySelector(self, query, _root=None):\n        \"\"\"Return the first element that matches a given CSS selector.\n        \n        :param query: The CSS selector to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLElement(self, self.html.search(query, index=0, root=_root))\n\n    def querySelectorAll(self, query, _root=None):\n        \"\"\"Return all elements that match a given CSS selector.\n        \n        :param query: The CSS selector to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLCollection(self, query, root=_root) \n    \n    def _node_to_html(self, node, deep=True):  # From hv3_dom_core.tcl line 311 and line 329\n        return self.html.safe_tk_eval(r\"\"\"\n            proc WidgetNode_ToHtml {node} {\n                set tag [$node tag]\n                if {$tag eq \"\"} {\n                    append ret [$node text -pre]\n                } else {\n                    append ret <$tag\n                    foreach {zKey zVal} [$node attribute] {\n                        set zEscaped [string map [list \"\\x22\" \"\\x5C\\x22\"] $zVal]\n                        append ret \" $zKey=\\\"$zEscaped\\\"\"\n                    }\n                    append ret >\n                    set void {\n                        area base br col embed hr img input keygen link meta param source track wbr\n                    }  ;# Don't add closing tags if is self-closing (void-elements)\n                    if {[lsearch -exact $void $tag] != -1} {\n                        return $ret\n                    } elseif {%d} {\n                        append ret [WidgetNode_ChildrenToHtml $node]\n                    }\n                    append ret </$tag>\n                }\n            }\n            proc WidgetNode_ChildrenToHtml {node} {\n                set ret \"\"\n                foreach child [$node children] {\n                    append ret [WidgetNode_ToHtml $child]\n                }\n                return $ret\n            }\n            return [WidgetNode_ToHtml %s]\n            \"\"\" % (int(deep), extract_nested(node))\n        ) # May split this into 2 methods in future\n\n\nclass HTMLElement:\n    \"\"\":param document_manager: The :class:`~tkinterweb.dom.HTMLDocument` instance this class is tied to.\n    :type document_manager: :class:`~tkinterweb.dom.HTMLDocument`\n    :param node: The Tkhtml3 node this class represents.\n    :type node: Tkhtml3 node\n\n    :ivar document: The element's corresponding :class:`~tkinterweb.dom.HTMLDocument` instance.\n    :ivar html: The element's corresponding :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The element's corresponding Tkhtml node.\"\"\"\n\n    ### TODO: consider caching or using a weakref for HTMLElements\n\n    def __init__(self, document_manager, node):\n        self.document = document_manager\n        self.html = document_manager.html\n        self.node = extract_nested(node)\n        self._style_cache = None  # Initialize style as None\n        DOM_element_events(self)\n        try:\n            self.html.get_node_tkhtml(node)  # check if the node is valid, rises invalid command error if not.\n        except TclError as e:\n            if \"invalid command name\" in str(e):\n                raise TclError(\"Element does not exist or is invalid\")\n\n        # We need this here or crashes happen if multiple Tkhtml instances exist (depending on the Tkhtml version)\n        # No idea why, but hey, it works\n        self.html.tk.createcommand(\"parse_fragment\", self.html.parse_fragment)\n\n    def __repr__(self):\n        return f\"{self.html._w}{self.node}\"\n    \n    def __eq__(self, other):\n        if not isinstance(other, HTMLElement):\n            return NotImplemented\n        return self.node == other.node\n    \n    def __hash__(self):\n        return hash(self.node)\n        \n    # def __str__(self):\n    #     tag = self.tagName\n        \n    #     if tag:\n    #         attributes = \"\"\n    #         for attribute in self.attributes:\n    #             attributes += f\"{attribute}=\\\"{self.attributes[attribute]}\\\"\"\n    #         return f\"<{tag} {attributes}>{self.innerHTML}</{tag}>\"\n    #     else:\n    #         return self.textContent\n\n\n    # --- JavaScript-style commands -------------------------------------------\n\n    @property\n    def style(self):\n        \"\"\"Manage the element's styling. For instance, to make the element have a blue background, use ``yourhtmlelement.style.backgroundColor = \"blue\"``.\n\n        :rtype: :class:`~tkinterweb.dom.CSSStyleDeclaration`\"\"\"\n        if self._style_cache is None:  # Lazy loading of style\n            self._style_cache = CSSStyleDeclaration(self)\n        return self._style_cache\n    \n    @property\n    def outerHTML(self):\n        return self.document._node_to_html(self.node)\n\n    @property\n    def innerHTML(self):  # Taken from hv3_dom2.tcl line 61\n        \"\"\"Get and set the inner HTML of the element. Cannot be set on ``<html>`` elements.\n        \n        :rtype: str\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.html.safe_tk_eval(\"\"\"\n            set node %s\n            if {[$node tag] eq \"\"} {error \"$node is not an HTMLElement\"}\n\n            set ret \"\"\n            foreach child [$node children] {\n                append ret [node_to_html $child 1]\n            }\n            return $ret\n            \"\"\" % extract_nested(self.node)\n        )\n\n    @innerHTML.setter\n    def innerHTML(self, contents):  # Taken from hv3_dom2.tcl line 88\n        if self.tagName:\n            # Tkhtml crashes if a node containing a widget is destroyed\n            self.widget = None\n            for node in self.html.search(f\"[{self.html.widget_manager.widget_container_attr}]\", root=self.node):\n                self.html.widget_manager.set_node_widget(node, None)\n\n        self.html.safe_tk_eval(\"\"\"\n            set html %s\n            set node %s\n            set tag [$node tag]\n            if {$tag eq \"\"} {error \"$node is a text element\"}\n            if {$tag eq \"html\"} {error \"innerHTML cannot be set on <$tag> elements\"}\n\n            # Destroy the existing children (and their descendants) of $node.\n            set children [$node children]\n\n            $node remove $children\n            foreach child $children {\n                $child destroy\n            }\n\n            # Insert the new descendants, created by parseing $newHtml.\n            set newHtml \"%s\"\n            set children [parse_fragment $newHtml]\n            $node insert $children\n\n            if {[winfo ismapped $html]} {update} ; # This must be done to see changes on-screen\n            \"\"\" % (self.html, extract_nested(self.node), escape_Tcl(contents))\n        )\n        self.html.event_manager.send_onload(root=self.node)\n\n    @property\n    def innerText(self):  # Original for this project\n        \"\"\"Get and set the text content of an element, as displayed. Cannot be set on ``<html>`` elements.\n        \n        :rtype: str\n        :raises: :py:class:`tkinter.TclError`\n        \n        New in version 4.14.\"\"\"\n        return self.html.safe_tk_eval(\"\"\"\n            proc get_child_text {node} {\n                set txt [$node text]\n                foreach child [$node children] {\n                    append txt [get_child_text $child]\n                }\n                return $txt\n            }\n            return [get_child_text %s]\n            \"\"\" % extract_nested(self.node)\n        )\n\n    @innerText.setter\n    def innerText(self, contents):  # Ditto\n        self.textContent = contents\n\n    @property\n    def textContent(self):  # Original for this project\n        \"\"\"Get and set the text content of an element. Cannot be used on ``<html>`` elements.\n        \n        :rtype: str\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.html.safe_tk_eval(\"\"\"\n            proc get_child_text {node} {\n                set txt [$node text -pre]\n                foreach child [$node children] {\n                    append txt [get_child_text $child]\n                }\n                return $txt\n            }\n            return [get_child_text %s]\n            \"\"\" % extract_nested(self.node)\n        )\n\n    @textContent.setter\n    def textContent(self, contents):  # Ditto\n        if self.tagName:\n            # Tkhtml crashes if a node containing a widget is destroyed\n            self.widget = None\n            for node in self.html.search(f\"[{self.html.widget_manager.widget_container_attr}]\", root=self.node):\n                self.html.widget_manager.set_node_widget(node, None)\n\n            self.html.safe_tk_eval(\"\"\"\n                set html %s\n                set node %s\n                set textnode %s\n                if {$textnode eq \"\"} {error \"$node is empty\"}\n                if {[$node tag] eq \"html\"} {error \"textContent cannot be set on <$tag> elements\"}\n                $node remove [$node children]\n                foreach child [$node children] {\n                    $child destroy\n                }\n                $node insert $textnode\n                \n                if {[winfo ismapped $html]} {update} ; # This must be done to see changes on-screen\n                \"\"\" % (self.html, extract_nested(self.node), self.document.createTextNode(contents).node)\n            )\n        else:\n            self.html.set_node_text(self.node, contents)\n\n    @property\n    def id(self):\n        \"\"\"Get and set the element's id attribute.\n\n        :rtype: str\n        \n        New in version 4.4.\"\"\"\n        return self.getAttribute(\"id\")\n\n    @id.setter\n    def id(self, new):\n        return self.setAttribute(\"id\", new)\n\n    @property\n    def className(self):\n        \"\"\"Get and set the element's class attribute.\n\n        :rtype: str\n        \n        New in version 4.4.\"\"\"\n        return self.getAttribute(\"class\")\n\n    @className.setter\n    def className(self, new):\n        return self.setAttribute(\"class\", new)\n\n    @property\n    def attributes(self):\n        \"\"\"Return the element's attributes.\n        \n        :rtype: dict\"\"\"\n        attributes = self.html.get_node_attributes(self.node)\n        if self.html.widget_manager.widget_container_attr in attributes:\n            del attributes[self.html.widget_manager.widget_container_attr]\n        return attributes\n\n    @property\n    def tagName(self):\n        \"\"\"Return the element's tag name.\n\n        :rtype: str\"\"\"\n        return self.html.get_node_tag(self.node)\n\n    @property\n    def parentElement(self):\n        \"\"\"Get the element's parent element.\n        \n        :rtype: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return HTMLElement(self.document, self.html.get_node_parent(self.node))\n\n    @property\n    def children(self):\n        \"\"\"Get the element's children elements.\n        \n        :rtype: list\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return [HTMLElement(self.document, i) for i in self.html.get_node_children(self.node)]\n    \n    @property\n    def previousSibling(self):\n        \"\"\"Get the element's preceding sibling.\n        \n        :rtype: :class:`HTMLElement`\n        \n        New in version 4.8.\"\"\"\n        return self._find_siblings(True)\n    \n    @property\n    def nextSibling(self):\n        \"\"\"Get the element's following sibling.\n        \n        :rtype: :class:`HTMLElement`\n        \n        New in version 4.8.\"\"\"\n        return self._find_siblings()\n    \n    @property\n    def widget(self): # Not a real JS property, but still useful\n        \"\"\"Get and set the element's widget. \n        \n        Prior to version 4.2 this only applies to ``<object>`` elements and a widget must be specified.\n\n        Since version 4.10 this can also be used to get the widget representing ``<input>``, ``<textarea>``, ``<select>`` , ``<iframe>``, and some ``<object>`` elements.\n        \n        If the widget already exists in the document, it will first be removed from its previous element.\n\n        Ensure your HtmlFrame widget was created before the widget you are embedding, or else the widget might not be visible.\n\n        :rtype: :py:class:`tkinter.Widget` or None\n        \n        New in version 4.1.\"\"\"\n        return self.html.widget_manager.get_node_widget(self.node)\n        \n    @widget.setter\n    def widget(self, widget): # Not a real JS property, but still useful\n        if self.tagName == \"object\":\n            # Really we should do better than set the data attribute\n            # Right now this also can be used to set the object's url\n            # But in practice it shouldn't really matter\n            if not widget:\n                # Tkhtml doesn't know what do do with 'None' and will refuse to fire the attribute handler\n                self.setAttribute(\"data\", \"\")\n            else:\n                self.setAttribute(\"data\", widget)\n        else:\n            self.html.widget_manager.set_node_widget(self.node, widget)\n    \n    @property\n    def value(self):\n        \"\"\"Get and set the input's value. Only works on ``<input>``, ``<textarea>``, ``<select>``, and ``progress`` elements.\n        \n        :rtype: str\n        \n        New in version 4.1.\"\"\"\n        replacement = self.html.widget_manager.get_node_widget(self.node)\n        if replacement and self.tagName in {\"select\", \"input\", \"textarea\", \"progress\"}:\n            try:\n                return replacement.get()\n            except AttributeError:\n                return self.html.get_node_attribute(self.node, \"value\")\n        return None\n        \n    @value.setter\n    def value(self, value):\n        replacement = self.html.widget_manager.get_node_widget(self.node)\n        if replacement and self.tagName in {\"select\", \"input\", \"textarea\", \"progress\"}:\n            try:\n                replacement.set(value)\n            except AttributeError:\n                self.html.set_node_attribute(self.node, \"value\", value)\n\n    @property\n    def checked(self):\n        \"\"\"Convenience property for the ``checked`` HTML attribute. Check/uncheck a radiobutton or checkbox or see if the element is checked.\n        \n        :rtype: bool\n        \n        New in version 4.1.\"\"\"\n        if str(self.node) in self.html.form_manager.form_widgets:\n            if self.html.get_node_attribute(self.node, \"checked\", \"false\") != \"false\":\n                return True\n            else:\n                return False\n        return None\n        \n    @checked.setter\n    def checked(self, value):\n        if str(self.node) in self.html.form_manager.form_widgets:\n            self.html.set_node_attribute(self.node, \"checked\", value)\n\n    def getAttribute(self, attribute):\n        \"\"\"Return the value of the given attribute.\n        \n        :param attribute: The attribute to return.\n        :type attribute: str\n        :rtype: str\"\"\"\n        try:\n            return self.html.get_node_attribute(self.node, attribute)\n        except TclError:\n            raise TclError(f\"the assoiciated element has been destroyed\")\n\n    def setAttribute(self, attribute, value):\n        \"\"\"Set the value of the given attribute.\n        \n        :param attribute: The attribute to set.\n        :type attribute: str\n        :param value: The new value of the given attribute.\n        :type value: str\"\"\"\n        try:\n            self.html.set_node_attribute(self.node, attribute, value)\n        except TclError:\n            raise TclError(f\"the assoiciated element has been destroyed\")\n        \n    def removeAttribute(self, attribute):\n        \"\"\"Remove the given attribute. At the moment this sets the value of the attribute to \"\".\n        \n        :param attribute: The attribute to remove.\n        :type attribute: str\n        \n        New in version 4.20.\"\"\"\n        self.setAttribute(attribute, \"\")\n        \n    def remove(self):\n        \"\"\"Delete the element. Cannot be used on ``<html>`` or ``<body>`` elements.\n        The element can be reinserted into the document later if needed.\n\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        try:\n            self.html.delete_node(self.node)\n        except TclError:\n            raise TclError(f\"the assoiciated element has been destroyed\")\n\n    def appendChild(self, children):\n        \"\"\"Insert the specified children into the element.\n        \n        :param children: The element(s) to be added into the element.\n        :type children: list, tuple, or :class:`HTMLElement`\"\"\"\n        self._insert_children(children)\n\n    def insertBefore(self, children, before):\n        \"\"\"Insert the specified children before a given child element.\n        \n        :param children: The element(s) to be added into the element.\n        :type children: list, tuple, or :class:`HTMLElement`\n        :param before: The child element that the added elements should be placed before.\n        :type before: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        self._insert_children(children, before)\n\n    def getElementById(self, query):\n        \"\"\"Return an element that is a child of the current element and matches the given id.\n        \n        :param query: The element id to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.getElementById(query, self.node)\n\n    def getElementsByClassName(self, query):\n        \"\"\"Return all elements that are children of the current element and match the given class name.\n        \n        :param query: The class to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.getElementsByClassName(query, self.node)\n\n    def getElementsByName(self, query):\n        \"\"\"Return all elements that are children of the current element and match the given name attribute.\n        \n        :param query: The name to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.getElementsByName(query, self.node)\n\n    def getElementsByTagName(self, query):\n        \"\"\"Return all elements that are children of the current element and match the given tag name.\n        \n        :param query: The tag to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.getElementsByTagName(query, self.node)\n\n    def querySelector(self, query):\n        \"\"\"Return the first element that is a child of the current element and matches the given CSS selector.\n        \n        :param query: The CSS selector to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLElement`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.querySelector(query, self.node)\n\n    def querySelectorAll(self, query):\n        \"\"\"Return all elements that are children of the current element and match the given CSS selector.\n        \n        :param query: The CSS selector to be searched for.\n        :type query: str\n        :rtype: :class:`HTMLCollection`\n        :raises: :py:class:`tkinter.TclError`\"\"\"\n        return self.document.querySelectorAll(query, self.node)\n    \n    def scrollIntoView(self):\n        \"Scroll the viewport so that this element is visible.\"\n        self.html.yview(self.node)\n\n    def getBoundingClientRect(self):\n        \"\"\"Get the element's position and size.\n\n        :rtype: :class:`~tkinterweb.dom.DOMRect`\"\"\"\n        return DOMRect(self)\n    \n    def _insert_children(self, children, before=None):\n        \"Helper method to insert children at a specified position\"\n        # Ensure children is a list\n        children = {children} if isinstance(children, HTMLElement) else children\n        # Extract nodes\n        tkhtml_child_nodes = tuple(i.node for i in children)\n        # Insert the nodes based on the position\n        if before:\n            self.html.insert_node_before(self.node, tkhtml_child_nodes, before.node)\n        else:\n            self.html.insert_node(self.node, tkhtml_child_nodes)\n\n        self.html.event_manager.send_onload(children=[child.node for child in children])\n    \n    def _find_siblings(self, reverse=False):\n        \"Helper method to find node children\"\n        parent = self.html.get_node_parent(self.node)\n        if parent:\n            siblings = list(self.html.get_node_children(parent))\n            if reverse:\n                list(reversed(siblings))\n            for e, i in enumerate(siblings):\n                if extract_nested(i) == self.node and len(siblings) > e + 1:\n                    return HTMLElement(self.document, siblings[e+1])\n        return None\n    \n    # --- Tkinter-style commands ----------------------------------------------\n\n    def bind(self, sequence, func, add=None):\n        \"\"\"Tkinter-style method to add a binding to this element.\n\n        The following virtual events are supported:\n\n        * ``<<ElementLoaded>>``/:py:attr:`utilities.ELEMENT_LOADED_EVENT`: Generated whenever an element loads after the page finishes.\n        * ``<<Modified>>```/:py:attr:`utilities.FIELD_CHANGED_EVENT`: Generated whenever the content of any ``<input>`` element changes.\n\n        The following Tkinter events are supported:\n        \n        * ``<Enter>``\n        * ``<Leave>``\n        * ``<MouseWheel>``\n        * All Tkinter button and motion events\n\n        :param sequence: The Tkinter event to bind to.\n        :type sequence: str\n        :param func: The callback to evaluate when the binding fires.\n        :type func: callable\n        :param add: If set to \"+\", add this binding onto existing ones. Otherwise, existing bindings will be overwritten.\n        :type add: str or None\n\n        :raise RuntimeError: If events are disabled.\n        \n        New in version 4.10.\"\"\"\n\n        if not self.html.events_enabled: \n            # should this be here, or in CaretManager?\n            # does it really matter?\n            raise RuntimeError(\"cannot add a binding when events are disabled\")\n        \n        self.html.event_manager.bind(self.node, sequence, func, add)\n\n    def unbind(self, sequence, funcid=None):\n        \"\"\"Tkinter-style method to remove a binding to this element.\n\n        :param sequence: The Tkinter event to unbind.\n        :type sequence: str\n        :param funcid: If specified, only the specified function will be removed from the list of bindings.\n        :type funcid: callable or None\n        \n        New in version 4.10.\"\"\"\n        self.html.event_manager.unbind(self.node, sequence, funcid)\n\n\nclass HTMLCollection:\n    \"\"\"This class stores results from various :class:`HTMLElement` methods. It behaves like a Python list, with some extras.\n    \n    :param document_manager: The :class:`~tkinterweb.dom.HTMLDocument` instance this class is tied to.\n    :type document_manager: :class:`~tkinterweb.dom.HTMLDocument`\n    :param search_string: The CSS query string to search using.\n    :type search_string: str\n    :param root: The Tkhtml node to search.\n    :type root: Tkhtml3 node\n    :ivar html: The element's corresponding :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The element's corresponding Tkhtml node.\n    \n    New in version 4.4.\"\"\"\n    def __init__(self, document_manager, search_string, root=None):\n        self.document = document_manager\n        self.html = document_manager.html\n        self.search_string = search_string\n        self.node = root\n\n    def __repr__(self):\n        if self.node: node = self.node\n        else: node = \"::document\"\n        return f\"{self.html._w}{node}::{self.__class__.__name__.lower()} {self.search_string}\"\n\n    def __iter__(self):\n        nodes = self.html.search(self.search_string, root=self.node)\n        return iter(HTMLElement(self.document, node) for node in nodes)\n\n    def __getitem__(self, index):\n        return self.item(index)\n\n    def __len__(self):\n        return self.length\n\n    @property\n    def length(self):\n        \"\"\"Returns the number of items in the collection.\"\"\"\n        return self.html.search(self.search_string, \"length\", root=self.node)\n    \n    def item(self, index):\n        \"\"\"Returns the element at the given index into the list.\n\n        :param index: The index of the element to get.\n        :type index: int\n        :rtype: :class:`HTMLElement`\n        :raise: KeyError\"\"\"\n        result = self.html.search(self.search_string, index=index, root=self.node)\n        if not result:\n            raise IndexError(\"index out of range\")\n        return HTMLElement(self.document, result)\n    \n    def namedItem(self, key):\n        \"\"\"Returns the element whose id or name matches key.\n\n        :param key: The id or name to search for.\n        :type key: str\n        :rtype: :class:`HTMLElement` or None\"\"\"\n        for i in self.html.search(self.search_string, root=self.node):\n            if key in (self.html.get_node_attribute(i, j) for j in (\"id\", \"name\")):\n                return HTMLElement(self.document, i)\n        return None  # If nothing is found\n\nclass DOMRect:\n    \"\"\"This class generates and stores information about the element's position and size at this point in time.\n    \n    :param element_manager: The :class:`~tkinterweb.dom.HTMLElement` instance this class is tied to.\n    :type element_manager: :class:`~tkinterweb.dom.HTMLElement`\n    :ivar x: The element's horizontal offset from the left-hand side of the page.\n    :ivar y: The element's vertical offset from the top of the page.\n    :ivar width: The element's width.\n    :ivar height: The element's height.\n    :ivar html: The element's corresponding :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The element's corresponding Tkhtml node.\"\"\"\n    def __init__(self, element_manager):\n        self.html = element_manager.html\n        self.node = element_manager.node\n\n        self.x, self.y, x2, y2 = self.html.bbox(self.node)\n\n        self.width = x2 - self.x\n        self.height = y2 - self.y\n\n    def __repr__(self):\n        return f\"{self.html._w}{self.node}::{self.__class__.__name__.lower()}\"\n\n\nclass CSSStyleDeclaration:\n    \"\"\"Access this class via the :attr:`~tkinterweb.dom.HTMLElement.style` property of the :attr:`~tkinterweb.dom.HTMLElement` class.\n    \n    :param element_manager: The :class:`~tkinterweb.dom.HTMLElement` instance this class is tied to.\n    :type element_manager: :class:`~tkinterweb.dom.HTMLElement`\n    :ivar html: The element's corresponding :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The element's corresponding Tkhtml node.\"\"\"\n    def __init__(self, element_manager):\n        self.html = element_manager.html\n        self.node = element_manager.node\n\n    def __repr__(self):\n        return f\"{self.html._w}{self.node}::{self.__class__.__name__.lower()}\"\n\n    def __getitem__(self, property):\n        # Get value from Tkhtml if it is a real and existing property\n        try:\n            value = self.html.get_node_property(self.node, property, \"-inline\")\n        except TclError:\n            # Ignore invalid properties\n            value = \"\"\n\n        if not value:\n            # Get value from sub-properties if it is a composite property\n            if property in COMPOSITE_PROPERTIES:\n                values = []\n                for key in COMPOSITE_PROPERTIES[property]:\n                    computed = self.__getitem__(key)\n                    if len(computed.split()) > 1:\n                        # If the sub-properties have multiple values (eg. have their own sub-properties),\n                        # Then this property does not have a valid value\n                        return \"\"\n                    if computed: values.append(computed)\n            \n                if len(values) == len(COMPOSITE_PROPERTIES[property]):\n                    if all(x == values[0] for x in values): \n                        # Simplify the return value if the values of the sub-properties are all the same\n                        value = values[0]\n                    else: \n                        value = \" \".join(values)\n\n            if not value:\n                # Otherwise attempt to get value from 'style' attribute\n                style = self.cssInlineStyles\n                if property in style: \n                    value = style[property]\n                    \n        return value\n\n    def __setitem__(self, property, value):\n        self.html.set_node_property(self.node, property, value)\n\n    def __delitem__(self, property):\n        value = self.__getitem__(property)\n\n        # Delete the property from the Tkhtml properties list if it exists \n        current = self.html.get_node_properties(self.node, \"-inline\")\n        if property in current: \n            del current[property]\n        else:\n            # Delete the property from the 'style' attribute if it exists \n            current = self.cssInlineStyles\n            if property in current: \n                del current[property]\n\n        # Delete the property's sub-properties properties if applicable\n        # Do this regardless of what happens above in case the property exists as a composite while its sub-properties were also set seperately\n        if property in COMPOSITE_PROPERTIES:\n            def clean(property):\n                for key in COMPOSITE_PROPERTIES[property]:\n                    if key in COMPOSITE_PROPERTIES:\n                        clean(key)\n                    elif key in current:\n                        del current[key]\n            clean(property)\n\n        style = \" \".join(f\"{p}: {v};\" for p, v in current.items())\n        self.html.set_node_attribute(self.node, \"style\", style)\n\n        return value\n\n    def __setattr__(self, property, value):\n        if property in (\"node\", \"html\"):\n            super().__setattr__(property, value)\n        else:\n            self.__setitem__(camel_case_to_property(property), value)\n\n    def __getattr__(self, property):\n        return self.__getitem__(camel_case_to_property(property))\n\n    @property\n    def cssText(self):\n        \"\"\"Get and set the element's inline style declaration.\n        \n        :rtype: str\n        \n        Updated in version 4.19.\"\"\"\n        return self.html.get_node_attribute(self.node, \"style\")\n\n    @cssText.setter\n    def cssText(self, contents):\n        return self.html.set_node_attribute(self.node, \"style\", contents)\n    \n    @property\n    def length(self):\n        \"\"\"Return the number of style declarations in the element's inline style declaration.\n        \n        :rtype: int\"\"\"\n        return len(self.html.get_node_properties(self.node, \"-inline\"))\n    \n    @property\n    def cssProperties(self): # Not a JS function, but still useful\n        \"\"\"Return all computed properties for the element.\n        \n        :rtype: dict\"\"\"\n        return self.html.get_node_properties(self.node)\n    \n    @property # Not a JS function, but still useful\n    def cssInlineProperties(self):\n        \"\"\"Return all inline properties for the element. Similar to the :attr:`cssText` property, but formatted as a dictionary.\n        \n        :rtype: dict\"\"\"\n        return self.html.get_node_properties(self.node, \"-inline\")\n    \n    @property \n    def cssInlineStyles(self):\n        \"\"\"Return the content of the element's ``style`` attribute, formatted as a dictionary.\n        \n        :rtype: dict\"\"\"\n        style = {k.strip(): o.strip() for i in self.cssText.split(\";\") if i for k, o in [i.split(\":\", 1)]}\n        return style\n    \n    def getPropertyPriority(self, property):\n        \"\"\"Return the priority of the given inline CSS property.\n        \n        :param property: The CSS property to search for.\n        :type property: str\n        :return: \"important\" or \"\".\n        :rtype: str\"\"\"\n        style = self.cssInlineStyles\n        if property in style:\n            value = style[property]\n            if value.endswith(\"!important\"): return \"important\"\n        #if self.__getitem__(property).endswith(\"!important\"): return \"important\"\n        #return \"\"\n\n    def getPropertyValue(self, property):\n        \"\"\"Return the value of the given inline CSS property.\n        \n        You can also use JavaScript-style camel-cased properties to get a CSS property value. For instance, to get the element's background color, use ``yourhtmlelement.style.backgroundColor``\n\n        :param property: The CSS property to get.\n        :type property: str\n        :rtype: str\n        \n        New in version 4.1.\"\"\"\n        return self.__getitem__(property)\n\n    def removeProperty(self, property):\n        \"\"\"Remove the given inline CSS property.\n        \n        :param property: The CSS property to remove.\n        :type property: str\n        :returns: The old value of the given property, or \"\" if the property did not exist.\n        :rtype: str\n\n        New in version 4.1.\"\"\"\n        return self.__delitem__(property)\n\n    def setProperty(self, property, value):\n        \"\"\"Set the value of the given inline CSS property.\n\n        You can also use JavaScript-style camel-cased properties to set a CSS property value. For instance, to make the element have a blue background, use ``yourhtmlelement.style.backgroundColor = \"blue\"``\n        \n        :param property: The CSS property to set.\n        :type property: str\n        :returns: The old value of the given property, or \"\" if the property did not exist.\n        :rtype: str\n        \n        New in version 4.1.\"\"\"\n        self.__setitem__(property, value)\n"
  },
  {
    "path": "tkinterweb/extensions.py",
    "content": "\"\"\"\nExtensions to Tkhtml3\n\nCopyright (c) 2021-2025 Andrew Clarke\n\"\"\"\n\nfrom re import IGNORECASE, MULTILINE, finditer\n\nfrom tkinter import Frame, Event, TclError\nfrom . import utilities\n\nclass BlinkyFrame(Frame):\n    # A blinking caret-style frame\n    def __init__(self, master, *args, blink_delays=[600, 300], **kwargs):\n        Frame.__init__(self, master, *args, **kwargs)\n        self.blink_delays = blink_delays\n\n        self._is_placed = False\n        self._x = 0\n        self._y = 0\n        self.pending = None\n\n    def place(self, x, y, *args, _internal=False, **kwargs):\n        if _internal:\n            super().place(*args, x=x, y=y, **kwargs)\n        else:\n            if self.pending:\n                self.after_cancel(self.pending)\n            self._is_placed = True\n            self._x = x\n            self._y = y\n            super().place(*args, x=x, y=y, **kwargs)\n            self.pending = self.after(self.blink_delays[0], self._blink)\n\n    def place_forget(self, _internal=False):\n        if not _internal:\n            if self.pending:\n                self.after_cancel(self.pending)\n            self.pending = None\n            self._is_placed = False\n        super().place_forget()\n\n    def _blink(self):\n        if self._is_placed:\n            self.place_forget(True)\n            delay = self.blink_delays[1]\n        else:\n            self.place(self._x, self._y, _internal=True)\n            delay = self.blink_delays[0]\n        \n        self.update()\n        self._is_placed = not(self._is_placed)\n        self.pending = self.after(delay, self._blink)\n\n\nclass SelectionManager(utilities.BaseManager):\n    \"\"\"An extension to manage the selection's state. Largely internal. \n    \n    Only interact with this object if the convenience methods provided elsewhere are insufficient.\n\n    This object can be accessed through the :attr:`~tkinterweb.TkinterWeb.selection_manager` property of the :class:`~tkinterweb.TkinterWeb` widget.\n\n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The node the caret is in.\n    :ivar selection_type: The state of the selection (0, 1, or 2), used when double-clicking.\n    :ivar selection_start_node: The node containing the start of the selection.\n    :ivar selection_start_offset: The selection's offset within the node.\n    :ivar selection_end_node: The node containing the end of the selection.\n    :ivar selection_end_offset: The selection's offset within the node.\n\n    New in version 4.11.\"\"\"\n    \n    def __init__(self, html):\n        super().__init__(html)\n\n        self.selection_type = 0\n        self.selection_start_node = None\n        self.selection_start_offset = None\n        self.selection_end_node = None\n        self.selection_end_offset = None\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def begin_selection(self, node, offset):\n        \"Begin selecting.\"\n        self.selection_start_node = node\n        self.selection_start_offset = offset\n\n    def reset(self):\n        self.selection_end_node = None\n        self.selection_end_offset = None\n\n    def reset_selection_type(self):\n        \"Reset the selection type.\"\n        self.selection_type = 0\n\n    def clear_selection(self):\n        \"Clear the current selection.\"\n        self.html.tag(\"delete\", \"selection\")\n        self.selection_start_node = None\n        self.selection_end_node = None\n\n    def update_tags(self):\n        \"Update selection and find tag colours.\"\n        self.html.tag(\"configure\", \"findtext\", \"-bg\", self.html.find_match_highlight_color, \"-fg\", self.html.find_match_text_color)\n        self.html.tag(\"configure\", \"findtextselected\", \"-bg\", self.html.find_current_highlight_color, \"-fg\", self.html.find_current_text_color)\n        self.html.tag(\"configure\", \"selection\", \"-bg\", self.html.selected_text_highlight_color, \"-fg\", self.html.selected_text_color)\n\n    def select_all(self):\n        \"Select all text in the document.\"\n        if not self.html.selection_enabled:\n            return\n        \n        self.clear_selection()\n        beginning = self.html.text(\"index\", 0)\n        end = self.html.text(\"index\", len(self.html.text(\"text\")))\n        self.selection_start_node = beginning[0]\n        self.selection_start_offset = beginning[1]\n        self.selection_end_node = end[0]\n        self.selection_end_offset = end[1]\n        self.update_selection()\n\n    def _word_in_node(self, node, offset):\n        text = self.html.get_node_text(node)\n        letters = list(text)\n\n        beg = 0\n        end = 0\n        for index, letter in enumerate(reversed(letters[:offset])):\n            beg = index + 1\n            if letter == \" \":\n                beg = index\n                break\n        for index, letter in enumerate(letters[offset:]):\n            end = index + 1\n            if letter == \" \":\n                end = index\n                break\n\n        pre = len(letters[:offset])\n        return pre - beg, pre + end\n\n    def double_click_selection(self):\n        \"Stimulate a double-click on the selection.\"\n        if not self.selection_start_node:\n            return\n        \n        if self.selection_type == 1:\n            # Tkhtml seems to wrap the output of text(\"text\") with \\n, so this works\n            text = self.html.text(\"text\")\n            index = self.html.text(\"offset\", self.selection_start_node, self.selection_start_offset)\n            self.selection_start_node, self.selection_start_offset = self.html.text(\"index\", text[:index].rfind(\"\\n\") + 1)\n            self.selection_end_node, self.selection_end_offset = self.html.text(\"index\", text.find(\"\\n\", index))\n            self.update_selection()\n            self.selection_type = 2\n\n        elif self.selection_type == 2:\n            self.clear_selection()\n            self.selection_type = 0\n\n        else:\n            start_offset, end_offset = self._word_in_node(self.selection_start_node, self.selection_start_offset)\n            self.selection_end_node = self.selection_start_node\n            self.selection_start_offset = start_offset\n            self.selection_end_offset = end_offset\n            self.update_selection()\n            self.selection_type = 1\n\n    def extend_selection(self, node, offset):\n        \"Extend the selection.\"\n        self.selection_end_node = node\n        self.selection_end_offset = offset\n        if self.selection_type == 1:\n            start_offset, end_offset = self._word_in_node(self.selection_start_node, self.selection_start_offset)\n            start_offset2, end_offset2 = self._word_in_node(self.selection_end_node, self.selection_end_offset)\n            start_index = self.html.text(\"offset\", self.selection_start_node, self.selection_start_offset)\n            end_index = self.html.text(\"offset\", self.selection_end_node, self.selection_end_offset)\n            if start_index > end_index:\n                self.selection_start_offset = end_offset\n                self.selection_end_offset = start_offset2\n            else:\n                self.selection_start_offset = start_offset\n                self.selection_end_offset = end_offset2\n\n        elif self.selection_type == 2:\n            text = self.html.text(\"text\")\n            start_index = self.html.text(\"offset\", self.selection_start_node, self.selection_start_offset)\n            end_index = self.html.text(\"offset\", self.selection_end_node, self.selection_end_offset)\n            \n            if start_index > end_index:\n                self.selection_start_node, self.selection_start_offset = self.html.text(\"index\", text.find(\"\\n\", start_index))\n                self.selection_end_node, self.selection_end_offset = self.html.text(\"index\", text[:end_index].rfind(\"\\n\") + 1)\n            else:\n                self.selection_start_node, self.selection_start_offset = self.html.text(\"index\", text[:start_index].rfind(\"\\n\") + 1)\n                self.selection_end_node, self.selection_end_offset = self.html.text(\"index\", text.find(\"\\n\", end_index))\n            \n        self.update_selection()\n\n    def update_selection(self):\n        \"Update the current selection.\"\n        self.html.tag(\"delete\", \"selection\")\n        self.html.tag(\n            \"add\",\n            \"selection\",\n            self.selection_start_node,\n            self.selection_start_offset,\n            self.selection_end_node,\n            self.selection_end_offset,\n        )\n        self.html.tag(\n            \"configure\",\n            \"selection\",\n            \"-bg\",\n            self.html.selected_text_highlight_color,\n            \"-fg\",\n            self.html.selected_text_color,\n        )\n    \n    def get_selection(self):\n        \"Return any selected text.\"\n        if self.selection_start_node is None or self.selection_end_node is None:\n            return\n        if self.selection_type == 1:\n            start_offset, end_offset = self._word_in_node(self.selection_start_node, self.selection_start_offset)\n            start_offset2, end_offset2 = self._word_in_node(self.selection_end_node, self.selection_end_offset)\n            start_index = self.html.text(\n                \"offset\", self.selection_start_node, start_offset\n            )\n            end_index = self.html.text(\n                \"offset\", self.selection_end_node, end_offset2\n            )\n            if start_index > end_index:\n                start_index = self.html.text(\n                    \"offset\", self.selection_end_node, start_offset2\n                )\n                end_index = self.html.text(\n                    \"offset\", self.selection_start_node, end_offset\n                )\n\n        elif self.selection_type == 2:\n            text = self.html.get_node_text(self.selection_start_node)\n            text2 = self.html.get_node_text(self.selection_end_node)\n            start_index = self.html.text(\n                \"offset\", self.selection_start_node, 0\n            )\n            end_index = self.html.text(\n                \"offset\", self.selection_end_node, len(text2)\n            )\n            if start_index > end_index:\n                start_index = self.html.text(\n                    \"offset\", self.selection_end_node, 0\n                )\n                end_index = self.html.text(\n                    \"offset\", self.selection_start_node, len(text)\n                )\n        else:\n            try:\n                start_index = self.html.text(\n                    \"offset\", self.selection_start_node, self.selection_start_offset\n                )\n                end_index = self.html.text(\n                    \"offset\", self.selection_end_node, self.selection_end_offset\n                )\n                if start_index > end_index:\n                    start_index, end_index = end_index, start_index\n            except TclError:\n                # When this happens something weird happened to this node\n                # Not too sure why\n                self.reset()\n                return\n                \n        whole_text = self.html.text(\"text\")\n        return whole_text[start_index:end_index]\n\n    def copy_selection(self):\n        \"Copy the selected text to the clipboard.\"\n        selected_text = self.get_selection()\n        self.html.clipboard_clear()\n        self.html.clipboard_append(selected_text)\n        self.html.post_message(f\"The text '{selected_text}' has been copied to the clipboard\")\n\n\nclass CaretManager(utilities.BaseManager):\n    \"\"\"An extension to manage the caret's state. Largely internal. \n    \n    Only interact with this object if the convenience methods provided elsewhere are insufficient.\n\n    This object can be accessed through the :attr:`~tkinterweb.TkinterWeb.caret_manager` property of the :class:`~tkinterweb.TkinterWeb` widget.\n\n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar node: The node the caret is in.\n    :ivar offset: The caret's offset within the node.\n    :ivar index: The document text index of the start of the node; fallback if the node is deleted.\n    :ivar caret_frame: The blinky widget.\n    :ivar target_offset: The text offset used for traversing up/down.\n    :ivar blink_delay: The caret's blink delay, in milliseconds. Updated in version 4.11.\n    :ivar caret_width: The caret's width, in pixels. New in version 4.11.\n    :ivar caret_color: The caret's colour. If None, the text colour under it will be matched.\n    :ivar scrolling_threshold: If the distance between the visible part of the page and the caret is nonzero but is less than this number, a scrolling animation will play.\n    :ivar scrolling_teleport: If the distance between the visible part of the page and the caret is nonzero but is greater than :attr:`scrolling_threshold`, the page is scrolled to this number before the scrolling animation plays.\n    \n    New in version 4.8.\"\"\"\n    \n    def __init__(self, html):\n        super().__init__(html)\n\n        self.node = None\n        self.offset = None\n        self.index = None\n        self.caret_frame = None\n        #self.target_node = None\n        self.target_offset = None\n\n        self.blink_delays = [600, 300]\n        self.caret_width = 1\n        self.caret_color = None\n        self.scrolling_threshold = 300 \n        self.scrolling_teleport = 75\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n\n    def set(self, node, offset, recalculate=False):\n        \"Set the caret's position.\"\n        if not node and not recalculate: return\n\n        if not node:\n            self.index = offset\n            self.node, self.offset = self.html.text(\"index\", offset)\n            self.target_offset = self.offset\n            fallback = self.shift_left\n        elif recalculate:\n            # If the caret's position is being set by the user, determine the node and offset using the document text\n            # This allows for shifting before and past the node\n            self.index = self.html.text(\"offset\", node, 0)\n            self.node, self.offset = self.html.text(\"index\", self.index + offset)\n            self.target_offset = self.offset\n            if offset > 0:\n                fallback = self.shift_left\n            else:\n                fallback = self.shift_right\n        else:\n            self.node = node #self.target_node = node\n            self.offset = self.target_offset = offset\n            self.index = self.html.text(\"offset\", node, 0)\n            fallback = self.shift_left\n        self.update(fallback=fallback)\n\n    def is_placed(self):\n        \"Check if the caret has been placed onto the document.\"\n        return True if self.node else False\n\n    def register_nodes_from_index(self, event, index, update_caret_start=False):\n        \"Update the caret's internal state.\"\n        node, offset = self.html.text(\"index\", index)\n        self.index = self.html.text(\"offset\", node, 0)\n\n        if event:\n            if (event.state & 0x1) != 0:\n                if not self.html.selection_manager.selection_start_node:\n                    self.html.selection_manager.selection_start_node = self.node\n                    self.html.selection_manager.selection_start_offset = self.offset\n                self.node, self.offset = node, offset\n                self.html.selection_manager.selection_end_node = self.node\n                self.html.selection_manager.selection_end_offset = self.offset\n            else:\n                self.node, self.offset = node, offset\n        else:\n            self.node, self.offset = node, offset\n        \n        if update_caret_start:\n            #self.target_node = self.node\n            self.target_offset = self.offset\n\n    def shift_up(self, event=None, update=True):     \n        \"Shift the caret up.\"   \n        if not self.node:\n            return\n        \n        index = self.html.text(\"offset\", self.node, self.offset)\n        text = self.html.text(\"text\")\n\n        if type(index) == str: index = self.index\n\n        # Get the previous newline\n        index = text.rfind(\"\\n\", 0, index)\n\n        if index == -1:\n            index = 0\n        else:\n            # Ensure that the index we land on is not blank or a newline\n            while index > 0 and text[index] in {\" \", \"\\n\"}:\n                index -= 1\n\n            # Get the beginning of the line\n            beginning_index = text.rfind(\"\\n\", 0, index)\n            if beginning_index != -1:\n                index += 1\n                # Attempt to go to the offset in that line corresponding to self.target_offset\n                # If the line is too short, go to the end of the line\n                ideal_index = beginning_index + self.target_offset + 1\n\n                if ideal_index < index:\n                    index = ideal_index\n\n        try:\n            # in case `node, offset = self.html.text(\"index\", index)` fails\n            self.register_nodes_from_index(event, index)\n            self.update(event, update=update)\n        except ValueError:\n            self.reset()\n\n    def shift_down(self, event=None, update=True):\n        \"Shift the caret down.\"\n        if not self.node:\n            return\n    \n        index = self.html.text(\"offset\", self.node, self.offset)\n        text = self.html.text(\"text\").rstrip(\"\\n\") + \"\\n\"\n        text_length = len(text) - 1\n\n        if type(index) == str: index = self.index\n\n        # Get the next newline\n        index = text.find(\"\\n\", index)\n        if index == -1:\n            index = text_length\n        else:\n            # Ensure that the index we land on is not blank or a newline\n            while index < text_length and text[index] in {\" \", \"\\n\"}:\n                index += 1\n\n            # Attempt to go to the offset in that line corresponding to self.target_offset\n            # If the line is too short, go to the end of the line\n            ideal_index = index + self.target_offset\n\n            if ideal_index < text_length:\n                newline_pos = text.find(\"\\n\", index, ideal_index)\n                if newline_pos != -1:\n                    index = newline_pos\n                else:\n                    index = ideal_index\n\n        try:\n            self.register_nodes_from_index(event, index)\n            self.update(event, update=update)\n        except ValueError:\n            self.reset()\n\n    def shift_left(self, event=None, update_caret_start=True, update=True):\n        \"Shift the caret left.\"\n        if not self.node:\n            return\n        \n        index = self.html.text(\"offset\", self.node, self.offset)\n        text = self.html.text(\"text\")\n        if type(index) == str: index = self.index\n        if index > len(text): index = len(text)\n        \n        # Shift left one letter\n        index -= 1\n\n        # If Ctrl is pressed, shift to the end of the previous space or newline\n        if event and ((event.state & 0x4) != 0):\n            index = max(text.rfind(\" \", 0, index), text.rfind(\"\\n\", 0, index))\n            if index == -1: \n                index = 0\n            else:\n                index += 1\n        else:\n            # Ensure that the index we land on is not a newline\n            changed = False\n            while index > 0 and text[index] == \"\\n\":\n                index -= 1\n                changed = True\n            if changed:\n                index += 1\n        \n        try:\n            self.register_nodes_from_index(event, index, update_caret_start)\n            self.update(event, update=update)\n        except ValueError:\n            self.reset()\n\n    def shift_right(self, event=None, update_caret_start=True, update=True):\n        \"Shift the caret right.\"\n        if not self.node:\n            return\n        \n        index = self.html.text(\"offset\", self.node, self.offset)\n        text = self.html.text(\"text\").rstrip(\"\\n\") + \"\\n\"\n        text_length = len(text) - 1\n\n        if type(index) == str: index = self.index\n\n        old_index = index\n\n        if event and ((event.state & 0x4) != 0):\n            # If Ctrl is pressed, shift to the start of the next space or newline\n            next_positions = [i for i in (text.find(\" \", index + 1), text.find(\"\\n\", index + 1)) if i != -1]\n            index = min(next_positions) if next_positions else text_length\n        else:\n            # Ensure that the index we land on is not a newline\n            changed = False\n            while index < text_length and text[index] == \"\\n\":\n                index += 1\n                changed = True\n            # Otherwise, shift right one letter\n            if not changed and index < text_length:\n                index += 1\n        \n        if old_index == index:\n            # Prevents recursion errors when shifting right on pages with only newlines and non-breaking spaces\n            return\n\n        try:\n            self.register_nodes_from_index(event, index, update_caret_start)\n            self.update(event, fallback=self.shift_right, update=update)\n        except ValueError:\n            self.reset()\n\n    def update(self, event=None, auto_scroll=True, fallback=None, update=True, xview=None, yview=None):\n        \"Refresh the caret or update its position.\"\n        if not fallback:\n            fallback = self.shift_left\n\n        if not self.node:\n            return\n    \n        self.html.update() # Particularly important when this method runs after the document is scrolled\n\n        # If this method was invoked by xivew() or yview(), check to see if the viewport actually changed\n        # No action is needed if nothing moved\n        if xview and xview == self.html.xview(): return\n        if yview and yview == self.html.yview(): return\n\n        if not self.caret_frame:\n            self.caret_frame = BlinkyFrame(self.html, blink_delays=self.blink_delays, width=self.caret_width)\n            \n        try:\n            a, b, c, d = self.html.text(\"bbox\", self.node, self.offset, self.node, self.offset)\n        except ValueError:\n            # A newline doesn't belong to the node\n            # If the caret is at the end of a line of text, the node returned will be different from the node we want to actually put the caret beside\n            # For some reason, when scrolling, the y values from bbox() of content that doesn't move with the document are sometimes wrong\n            # text(\"bbox\") is more accurate\n            # However, offset is not defined for a node's end\n            # So we get the end of the previous character instead\n            try:\n                a2, b, c2, d = self.html.text(\"bbox\", self.node, self.offset-1, self.node, self.offset-1)\n                a = c2\n                c = c2 + (c2-a2)\n            except ValueError:\n                return fallback(event, update_caret_start=False)\n            \n        if not update:\n            return\n                            \n        x1, y1, x2, y2 = self.html.bbox()\n        yoffset = self._scroll_if_needed(b, d, y1, y2, auto_scroll)\n        xoffset = self._scroll_if_needed(a, c, x1, x2, auto_scroll, 1)\n\n        if (xoffset != None) and (yoffset != None): # Otherwise, yview/xview automatically re-calls this function, so we exit\n            if self.caret_color:\n                bg = self.caret_color\n            else:\n                bg = self.html.get_node_property(self.html.get_node_parent(self.node), \"color\")\n            self.caret_frame.config(height=d-b, bg=bg, width=self.caret_width)\n            self.caret_frame.place(x=a-xoffset, y=b-yoffset)\n        \n            if self.html.selection_enabled and event:\n                if ((event.state & 0x1) != 0):\n                    self.html.selection_manager.update_selection()\n                else:\n                    self.html.selection_manager.clear_selection()\n\n    def hide(self):\n        \"Hide the caret. Show the caret again by calling :meth:`.CaretManager.update`.\"\n        if self.node:\n            self.caret_frame.place_forget()\n\n    def reset(self):\n        \"Hide the caret and reset its position.\"\n        if self.node:\n            self.node = None\n            self.offset = None\n            self.caret_frame.place_forget()\n\n    def _scroll_if_needed(self, node_start, node_end, viewport_start, viewport_end, auto_scroll, direction=0):\n        \"\"\"Scroll the caret into view if needed.\n        We could scroll directly to the correct position,\n        But it's easier to let Tkhtml do the work of detecting lines.\n        Using yview_moveto, for instance, can be used to scroll to the top of the node,\n        But then a node on the same line that is taller would be cut off.\n        So we use moveto to get close if needed and use scroll to do the rest.\n        As a side effect, this gives us a bit of a scrolling animation, which I think is a good thing if anything.\"\"\"\n        if direction: \n            command = self.html.xview\n        else:\n            command = self.html.yview\n\n        start, end = command()\n        top_offset = start * (viewport_end - viewport_start)\n        bottom_offset = end * (viewport_end - viewport_start)\n\n        if auto_scroll:\n            if node_end >= 0 and (node_end - self.scrolling_threshold) > bottom_offset:\n                command(\"moveto\", (node_end - self.html.winfo_height() - self.scrolling_teleport) / viewport_end, auto_scroll=True)\n                return None\n            elif node_end >= 0 and node_end > bottom_offset:\n                command(\"scroll\", 1, \"units\", auto_scroll=True)\n                return None\n            elif node_start >= 0 and (node_start + self.scrolling_threshold) < top_offset:\n                command(\"moveto\", (node_start + self.scrolling_teleport) / viewport_end, auto_scroll=True)\n                return None\n            elif node_start >= 0 and node_start < top_offset:\n                command(\"scroll\", -1, \"units\", auto_scroll=True)\n                return None\n        return top_offset\n\n\nclass EventManager(utilities.BaseManager):\n    \"\"\"An extension to manage custom node bindings and JavaScript events. Largely internal. \n    \n    Only interact with this object if the convenience methods provided elsewhere are insufficient.\n\n    This object can be accessed through the :attr:`~tkinterweb.TkinterWeb.event_manager` property of the :class:`~tkinterweb.TkinterWeb` widget.\n\n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar bindings: A dictionary of bindings. You shouldn't need to touch this.\n    :ivar loaded_elements: A list storing loaded elements.\n\n    New in version 4.10.\"\"\"\n\n    ### We use the JavaScript event system to handle some element bindings\n    ### If a binding is requested but isn't handled by TkinterWeb by default, we create a new binding to deal with it\n    \n    def __init__(self, html):\n        super().__init__(html)\n        \n        self.bindings = {}\n        self.loaded_elements = []\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    # --- Tk events -----------------------------------------------------------\n\n    def reset(self):\n        \"Reset all bindings.\"\n\n        for event in self.bindings:\n            if event not in utilities.EVENT_MAP:\n                self.html.unbind_class(self.html.tkinterweb_tag, event)\n            \n        self.bindings.clear()\n        self.loaded_elements.clear()\n\n    def post_event(self, node, JS_event_name, event=None, Tk_event_name=None):\n        \"\"\"Given a CSS node and JavaScript event name, trigger any related bindings.\n\n        If  :attr:`event` is provided, the event generated will be modified from the given event.\n        If  :attr:`Tk_event_name` is provided, an event will be created using the given name.\n\n        All generated events have the additional ``node`` property, representing the corresponding Tkhtml node. \"\"\"\n        if JS_event_name not in self.bindings:\n            return\n\n        node_callbacks = self.bindings[JS_event_name].get(node)\n        if not node_callbacks:\n            return\n\n        for callback in node_callbacks:\n            if event:\n                new_event = self.create_modified_event(node, event, Tk_event_name)\n            else:\n                new_event = self.create_new_event(node, Tk_event_name)\n                \n            self.html.after(0, callback, new_event)\n    \n    def create_modified_event(self, node, event, Tk_event_name=None):\n        \"Create a new event using details from :attr:`event`.\"\n        new_event = Event()\n        new_event.widget = self,\n        new_event.node = node,\n        new_event.delta = event.delta\n        new_event.num = event.num\n        new_event.state = event.state\n\n        if Tk_event_name:\n            new_event.type = Tk_event_name\n        else:\n            new_event.type = event.type\n            \n        new_event.char = event.char\n        new_event.x = event.x\n        new_event.y = event.y\n        return new_event\n    \n    def create_new_event(self, node, Tk_event_name):\n        \"Create a new event.\"\n        new_event = Event()\n        new_event.widget = self,\n        new_event.node = node,\n        new_event.type = Tk_event_name\n        new_event.char = 0\n        new_event.state = 0\n        new_event.delta = 0\n        return new_event\n    \n    def _on_demand_binding_callback(self, event, name):\n        if not self.html.events_enabled:\n            return\n        \n        if name not in self.bindings:\n            return\n        \n        bindings = self.bindings[name]\n        \n        if not self.html.current_node:\n            self.html._on_mouse_motion(event)\n\n        for node_handle in self.html.hovered_nodes:\n            if node_handle in bindings: \n                callbacks = bindings[node_handle]\n                for callback in callbacks:\n                    event = self.create_modified_event(node_handle, event)\n                    self.html.after(0, callback, event)  \n\n    def _check_binding_name(self, event):\n        if event in utilities.EVENT_MAP:\n            return False\n        \n        for sequence in utilities.UNHANDLED_EVENT_WHITELIST:\n            if sequence in event:\n                return True\n            \n        raise KeyError(f\"the event {event} is either unsupported on elements or invalid. Consider binding to the main widget.\")\n\n    def bind(self, node, event, callback, add=None):\n        \"Add a binding.\"        \n        if self._check_binding_name(event):\n            if event not in self.bindings:\n                self.html.bind_class(self.html.tkinterweb_tag, event, lambda event, name=event: self._on_demand_binding_callback(event, name))\n        else:\n            event = utilities.EVENT_MAP[event]\n\n        event_bindings = self.bindings.setdefault(event, {})\n        if add:\n            callbacks = event_bindings.setdefault(node, [])\n            callbacks.append(callback)\n        else:\n            callbacks = [callback]\n        self.bindings[event][node] = callbacks\n\n    def unbind(self, node, event, funcid=None):\n        \"Remove a binding.\"\n        if event not in self.bindings or node not in self.bindings[event]:\n            raise KeyError(\"the requested binding does not exist\")\n        \n        if funcid:\n            callbacks = self.bindings[event][node]           \n            try:\n                callbacks.remove(funcid)\n            except ValueError:\n                raise KeyError(\"the requested binding does not exist\")\n            if not callbacks:\n                del self.bindings[event][node]\n        else:\n            del self.bindings[event][node]\n\n        if not self.bindings[event]:\n            del self.bindings[event]\n            if event not in utilities.EVENT_MAP:\n                self.html.unbind_class(self.html.tkinterweb_tag, event)\n\n    # --- JavaScript/Tk events ------------------------------------------------\n\n    def post_element_event(self, node_handle, attribute, event=None, event_name=None):\n        \"\"\"Post an element event.\n        \n        New in version 4.11.\"\"\"\n        \n        # Post the JavaScript event first if needed\n        if self.html.javascript_enabled:\n            if attribute == \"onload\":\n                if node_handle in self.loaded_elements:\n                    # Don't run the onload script twice\n                    return\n                else:\n                    self.loaded_elements.append(node_handle)\n            jsattribute = attribute\n            if attribute in utilities.JS_EVENT_MAP:\n                # If the event is a non-standard event (i.e. onscrollup), convert it\n                jsattribute = utilities.JS_EVENT_MAP[attribute]\n            if attribute:\n                mouse = self.html.get_node_attribute(node_handle, jsattribute)\n                if mouse and self.html.on_element_script is not None:\n                    self.html.on_element_script(node_handle, jsattribute, mouse)\n        \n        # Then post the Tkinter event\n        if self.html.events_enabled and (event or event_name):\n            self.post_event(node_handle, attribute, event, event_name)\n\n    def send_onload(self, root=None, children=None):\n        \"\"\"Send the onload signal for nodes that aren't handled at runtime.\n        We keep this a seperate command so that it can be run after inserting elements or changing the innerHTML.\n\n        New in version 4.11.\"\"\"\n        # Don't bother worring about element bindings...they can't be set if the element doesn't exist\n        if not self.html.javascript_enabled:\n            return\n        if children:\n            for node in children:\n                if self.html.get_node_tag(node) not in {\"img\", \"object\", \"link\"}:\n                    self.post_element_event(node, \"onload\")\n        else:\n            for node in self.html.search(\"[onload]\", root=root):\n                if self.html.get_node_tag(node) not in {\"img\", \"object\", \"link\"}:\n                    self.post_element_event(node, \"onload\")\n                \n\nclass WidgetManager(utilities.BaseManager):\n    \"\"\"An extension to manage stored widgets. Largely internal. \n    \n    Only interact with this object if the convenience methods provided elsewhere are insufficient.\n\n    This object can be accessed through the :attr:`~tkinterweb.TkinterWeb.widget_manager` property of the :class:`~tkinterweb.TkinterWeb` widget.\n\n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\n    :ivar widget_container_attr: The HTML attribute given to elements containing a widget.\n    :ivar hovered_embedded_node: True if the mouse is over a widget in the document, otherwise False.\n\n    New in version 4.11.\"\"\"\n    \n    def __init__(self, html):\n        super().__init__(html)\n\n        ### TODO: see if there's a way we can avoid setting an attribute on replaced nodes\n        self.widget_container_attr = \"-tkinterweb-widget-container\"\n        self.hovered_embedded_node = None\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def reset(self):\n        self.hovered_embedded_node = None\n\n    def get_node_widget(self, node):\n        \"Get the widget associated with the given node.\"\n        widget = self.html.get_node_replacement(node)\n        \n        if widget == node or not widget:\n            return None\n        \n        return self.html.nametowidget(widget)\n\n    def handle_node_replacement(self, node, widgetid, deletecmd, stylecmd=None, allowscrolling=True, handledelete=True, check=True):\n        \"\"\"Replace a Tkhtml3 node with a Tkinter widget. \n        \n        This method is used internally by :meth:`~tkinterweb.extensions.WidgetManager.set_node_widget` and offers more control.\n         \n        I don't recommend using it unless absolutely needed.\"\"\"\n        self.html.set_node_attribute(node, self.widget_container_attr, widgetid)\n        if stylecmd:\n            if handledelete:\n                self.html.replace_node_contents(\n                    node, widgetid,\n                    \"-deletecmd\", self.html.register(deletecmd),\n                    \"-stylecmd\", self.html.register(stylecmd),\n                    check=check\n                )\n            else:\n                self.html.replace_node_contents(\n                    node, widgetid, \"-stylecmd\", self.html.register(stylecmd), check=check\n                )\n        else:\n            if handledelete:\n                self.html.replace_node_contents(\n                    node, widgetid, \"-deletecmd\", self.html.register(deletecmd), check=check\n                )\n            else:\n                self.html.replace_node_contents(node, widgetid)\n\n        self.html._add_bindtags(widgetid, allowscrolling)\n        for child in widgetid.winfo_children():\n            self.html._add_bindtags(child, allowscrolling)\n            \n        widgetid.bind(\n            \"<Enter>\",\n            lambda event, node_handle=node: self._on_embedded_mouse_enter(\n                event, node_handle=node_handle\n            ),\n        )\n        widgetid.bind(\n            \"<Leave>\",\n            lambda event, node_handle=None: self._on_embedded_mouse_leave(\n                event, node_handle=node_handle\n            ),\n        )\n\n    def _handle_node_removal(self, widgetid):\n        widgetid.destroy()\n\n    def _handle_node_style(self, node, widgetid, widgettype=\"button\"):\n        if widgettype == \"button\":\n            bg = \"transparent\"\n            while bg == \"transparent\" and node != \"\":\n                bg = self.html.get_node_property(node, \"background-color\")\n                node = self.html.get_node_parent(node)\n            if bg == \"transparent\":\n                bg = \"white\"\n            widgetid.configure(\n                background=bg,\n                highlightbackground=bg,\n                highlightcolor=bg,\n                activebackground=bg,\n            )\n        elif widgettype == \"range\":\n            bg = \"transparent\"\n            while bg == \"transparent\" and node != \"\":\n                bg = self.html.get_node_property(node, \"background-color\")\n                node = self.html.get_node_parent(node)\n            if bg == \"transparent\":\n                bg = \"white\"\n            widgetid.configure(background=bg)\n        elif widgettype == \"text\":\n            bg = self.html.get_node_property(node, \"background-color\")\n            fg = self.html.get_node_property(node, \"color\")\n            font = self.html.get_node_property(node, \"font\")\n            if bg == \"transparent\":\n                bg = \"white\"\n            if fg == \"transparent\":\n                fg = \"white\"\n            try:\n                widgetid.configure(background=bg, foreground=fg, font=font)\n            except (TclError, ValueError, ):\n                widgetid.configure(background=bg)\n        elif widgettype == \"auto\":\n            bg = self.html.get_node_property(node, \"background-color\")\n            fg = self.html.get_node_property(node, \"color\")\n            font = self.html.get_node_property(node, \"font\")\n            if bg == \"transparent\":\n                bg = \"white\"\n            if fg == \"transparent\":\n                fg = \"white\"\n            widgets = [widgetid] + [widget for widget in widgetid.winfo_children()]\n            for widget in widgets:\n                try:\n                    widget.configure(background=bg, foreground=fg, font=font)\n                except (TclError, ValueError, ):\n                    widget.configure(background=bg)\n\n    def map_node(self, node, force=False):\n        \"Redraw a node if it currently contains a Tk widget.\"\n        if force or (self.get_node_widget(node)):\n            self.html.replace_node_contents(node, node)\n\n    def set_node_widget(self, node, widgetid=None):\n        \"Replace a node with a Tk widget.\"\n        if not widgetid:\n            # Reset the node if a widget is not supplied\n            self.map_node(node)\n            return\n            \n        manager = widgetid.winfo_manager()\n        if manager == \"Tkhtml\":  # Don't display the same widget twice\n            for old_node in self.html.search(f\"[{self.widget_container_attr}]\"):\n                if self.html.get_node_attribute(old_node, self.widget_container_attr) == str(widgetid):\n                    # If we know where the widget is, \n                    # Replace the old node with its original contents so we can redraw the widget here\n                    self.map_node(old_node)\n                    break\n            else:\n                raise TclError(f\"cannot embed widget already managed by {manager}\")\n        # Tkhtml seems to remove the widget from the previous geometry manager if it is not Tkhtml so I think we are fine\n\n        handleremoval = self.html.get_node_attribute(node, \"handleremoval\", \"false\") != \"false\"\n\n        # Handle scrolling\n        # If set to \"auto\" (default), scrolling will work on the widget as long as no bindings are already set on it\n        allowscrolling = self.html.get_node_attribute(node, \"allowscrolling\", \"auto\")\n        if allowscrolling == \"auto\":\n            widgets = [widgetid] + [widget for widget in widgetid.winfo_children()]\n            allowscrolling = True\n            events = (\"<MouseWheel>\", \"<Button-4>\", \"<Button-5>\")\n            ignore = {\".\", \"all\"}\n            def check_scrolling():\n                for widget in widgets:\n                    for tag in widget.bindtags():\n                        if tag in ignore: continue\n                        for event in events:\n                            if widget.bind_class(tag, event):\n                                return False\n                return True\n            allowscrolling = check_scrolling()\n        elif allowscrolling in {\"\", \"true\"}:\n            allowscrolling = True\n        else:\n            allowscrolling = False\n\n        # Handle styling\n        # If set to \"false\" (default), nothing will be done\n        # If set to \"deep\", the widget and any children are styled\n        # If set to \"true\" or \"auto\", only the widget will be styled\n        allowstyling = self.html.get_node_attribute(node, \"allowstyling\", \"false\")\n        if allowstyling == \"deep\":\n            allowstyling = lambda node=node, widgetid=widgetid, widgettype=\"auto\": self._handle_node_style(node, widgetid, widgettype)\n        elif allowstyling in {\"\", \"true\", \"auto\"}:\n            allowstyling = lambda node=node, widgetid=widgetid, widgettype=\"text\": self._handle_node_style(node, widgetid, widgettype)\n        else:\n            allowstyling = None\n\n        if handleremoval:\n            # Tkhtml's -deletecmd handler is quite broken\n            # I would instead give the widget an extra class and bind to <Unmap>\n            # But apparently that doesn't fire at all. Oh well.\n            handleremoval = lambda widgetid=widgetid: self._handle_node_removal(widgetid)\n        else:\n            handleremoval = None\n        \n        # We used to add the node to a dict but we need to be able to delete it when destroy is called on any of its parents\n        # By setting an attribute we can use Tkhtml's search function to check if the widget exists elsewhere without having to invert a dict\n        # I'll probably change that eventually\n        self.handle_node_replacement(\n            node,\n            widgetid,\n            handleremoval,\n            allowstyling,\n            allowscrolling,\n            False,\n        )\n        self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n\n    def _on_embedded_mouse_enter(self, event, node_handle):\n        self.hovered_embedded_node = node_handle\n        self.html._on_mouse_motion(event)\n    \n    def _on_embedded_mouse_leave(self, event, node_handle):\n        self.hovered_embedded_node = node_handle\n        # Calling self._on_mouse_motion here seems so cause some flickering\n        # event.x and event.y are relative to this node and not self\n        # We could fix this but I can't find any noticeable side effects of not including it\n        # Not too sure why it was originally here?\n\nclass SearchManager(utilities.BaseManager):\n    \"\"\"An extension to manage search the document. Largely internal. \n    \n    Only interact with this object if the convenience methods provided elsewhere are insufficient.\n\n    This object can be accessed through the :attr:`~tkinterweb.TkinterWeb.search_manager` property of the :class:`~tkinterweb.TkinterWeb` widget.\n\n    :ivar html: The associated :class:`~tkinterweb.TkinterWeb` instance.\n\n    New in version 4.11.\"\"\"\n    \n    def __init__(self, html):\n        super().__init__(html)\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def clear_tags(self):\n        self.html.selection_manager.clear_selection()\n\n        self.html.tag(\"delete\", \"findtext\")\n        self.html.tag(\"delete\", \"findtextselected\")\n    \n    def update_tags(self, selected, matches):\n        # Highlight other matches\n        for match in matches:\n            node1, index1, node2, index2 = match\n            self.html.tag(\"add\", \"findtext\", node1, index1, node2, index2)\n            self.html.tag(\n                \"configure\",\n                \"findtext\",\n                \"-bg\",\n                self.html.find_match_highlight_color,\n                \"-fg\",\n                self.html.find_match_text_color,\n            )\n\n        # Highlight selected match\n        node1, index1, node2, index2 = selected\n        self.html.tag(\"add\", \"findtextselected\", node1, index1, node2, index2)\n        self.html.tag(\n            \"configure\",\n            \"findtextselected\",\n            \"-bg\",\n            self.html.find_current_highlight_color,\n            \"-fg\",\n            self.html.find_current_text_color,\n        )\n\n        # Scroll vertically if selected match is not visible\n        nodebox = self.html.text(\"bbox\", node1, index1, node2, index2)\n        docheight = float(self.html.bbox()[3])\n\n        view_top = docheight * self.html.yview()[0]\n        view_bottom = view_top + self.html.winfo_height()\n        node_top = float(nodebox[1])\n        node_bottom = float(nodebox[3])\n\n        if node_top < view_top:\n            self.html.yview(\"moveto\", node_top / docheight)\n        elif node_bottom > view_bottom:\n            self.html.yview(\"moveto\", (node_bottom - self.html.winfo_height()) / docheight)\n\n        # Scroll horizontally if selected match is not visible\n        docwidth = float(self.html.bbox()[2])\n\n        view_left = docwidth * self.html.xview()[0]\n        view_right = view_left + self.html.winfo_width()\n        node_left = float(nodebox[0])\n        node_right = float(nodebox[2])\n\n        if (node_left < view_left):\n            self.html.xview(\"moveto\", node_left / docwidth)\n        elif (node_right > view_right):\n            self.html.xview(\"moveto\", (node_right - self.html.winfo_width()) / docwidth)\n\n    def find_text(self, searchtext, select, ignore_case, highlight_all, test=False):\n        \"Search for and highlight specific text in the document.\"\n        if not test: self.clear_tags()\n\n        nmatches = 0\n        matches = []\n        selected = []\n        match_indexes = []\n\n        if len(searchtext) == 0 or select <= 0:\n            return nmatches, selected, matches\n\n        doctext = self.html.text(\"text\")\n\n        try:\n            # Find matches\n            if ignore_case:\n                rmatches = finditer(\n                    searchtext, doctext, flags=IGNORECASE | MULTILINE\n                )\n            else:\n                rmatches = finditer(searchtext, doctext, flags=MULTILINE)\n\n            for match in rmatches:\n                match_indexes.append(\n                    (\n                        match.start(0),\n                        match.end(0),\n                    )\n                )\n                nmatches += 1\n\n            if len(match_indexes) > 0:\n                self.html.post_message(f\"{nmatches} results for the search key '{searchtext}' have been found\")\n                if highlight_all:\n                    for num, match in enumerate(match_indexes):\n                        match = self.html.text(\"index\", match_indexes[num][0])\n                        match += self.html.text(\"index\", match_indexes[num][1])\n                        matches.append(match)\n\n                selected = self.html.text(\"index\", match_indexes[select - 1][0])\n                selected += self.html.text(\"index\", match_indexes[select - 1][1])\n\n                # Highlight matches\n                if not test: self.update_tags(selected, matches)\n            else:\n                self.html.post_message(f\"No results for the search key '{searchtext}' could be found\")\n            return nmatches, selected, matches\n        except Exception as error:\n            self.html.post_message(f\"ERROR: an error was encountered while searching for {searchtext}: {error}\")\n            return nmatches, selected, matches"
  },
  {
    "path": "tkinterweb/handlers.py",
    "content": "\"\"\"\nNode handlers and associated extensions to Tkhtml3\n\nCopyright (c) 2021-2025 Andrew Clarke\n\"\"\"\n\nimport tkinter as tk\n\nfrom urllib.parse import urlencode, urlparse\n\nfrom . import subwidgets, utilities, imageutils, dom\n\nclass NodeManager(utilities.BaseManager):\n    \"Handle body, html, title, meta, base, details, progress, and hyperlink elements.\"\n    def __init__(self, html):\n        super().__init__(html)\n\n        self._node_texts = {}\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def reset(self):\n        self._node_texts.clear()\n\n    # --- Handle title, base, and meta elements -------------------------------\n\n    def _on_title(self, node):\n        \"Handle <title> elements. We could use a script handler but then the node is no longer visible to the DOM.\"\n        children = self.html.get_node_children(node)\n        if children: # Fix for Bug #136, where an empty title tag raises an exception\n            self.html.title = self.html.get_node_text(self.html.get_node_children(node), \"-pre\").strip()\n            self.html.post_event(utilities.TITLE_CHANGED_EVENT)\n\n    def _on_base(self, node):\n        \"Handle <base> elements.\"\n        href = self.html.get_node_attribute(node, \"href\", \"\")\n        if href:\n            self.html.base_url = self.html.resolve_url(href)\n    \n    def _on_meta(self, node):\n        \"Partly handle <meta> elements.\"\n        if self.html.get_node_attribute(node, \"http-equiv\") == \"refresh\":\n            content = self.html.get_node_attribute(node, \"content\").split(\";\")\n            if len(content) == 2:\n                if content[1].startswith(\"url=\"):\n                    url = self.html.resolve_url(content[1].lstrip(\"url=\"))\n                    self.html.post_message(f\"Redirecting to '{utilities.shorten(url)}'\")\n                    if self.html.on_link_click is not None:\n                        if url not in self.html.visited_links:\n                            self.html.visited_links.append(url)\n                        self.html.on_link_click(url)\n\n    # --- Handle hyperlinks ---------------------------------------------------\n\n    def _on_a(self, node):\n        \"Handle <a> elements.\"\n        self.html.set_node_flags(node, \"link\")\n        try:\n            href = self.html.get_node_attribute(node, \"href\")\n            url = self.html.resolve_url(href)\n            if url in self.html.visited_links:\n                self.html.set_node_flags(node, \"visited\")\n        except tk.TclError:\n            pass\n\n    def _on_a_value_change(self, node, attribute, value):\n        if attribute == \"href\":\n            url = self.html.resolve_url(value)\n            if url in self.html.visited_links:\n                self.html.set_node_flags(node, \"visited\")\n            else:\n                self.html.remove_node_flags(node, \"visited\")\n    \n    def _handle_link_click(self, node_handle):\n        \"Handle link clicks.\"\n        href = self.html.get_node_attribute(node_handle, \"href\")\n        url = self.html.resolve_url(href)\n        self.html.post_message(f\"A link to '{utilities.shorten(url)}' was clicked\")\n        if self.html.on_link_click is not None:\n            self.html.set_node_flags(node_handle, \"visited\")\n            if url not in self.html.visited_links:\n                self.html.visited_links.append(url)\n            self.html.on_link_click(url)\n\n    # --- Handle body elements ------------------------------------------------\n\n    def _on_body(self, node, index):\n        \"Wait for style changes on the root node.\"\n        self.html.replace_node_contents(node,\n                    node,\n                    \"-stylecmd\",\n                    self.html.register(lambda node=node: self._set_overflow(node)))\n        \n    def _on_html(self, node, index):\n        self._on_body(node, index)\n\n    def _handle_overflow_property(self, overflow, overflow_function):\n        if overflow != \"visible\": # Visible is the Tkhtml default, so it's largely meaningless\n            overflow_map = {\"hidden\": 0,\n                            \"auto\": 2,\n                            \"scroll\": 1,\n                            \"clip\": 0}\n            if overflow in overflow_map:\n                overflow = overflow_map[overflow]\n                return overflow_function(overflow)\n        return None\n\n    def _set_overflow(self, node):\n        \"Look for and handle the overflow property.\"\n        # Eventually we'll make overflow a composite property of overflow-x and overflow-y\n        # But for now it's its own thing and the only one of the three that is actually respected by Tkhtml in rendering\n        if self.html.experimental: \n            overflow_options = (\"overflow\", \"overflow-y\")\n        else:\n            overflow_options = (\"overflow\",)\n\n        if self.html.manage_vsb_func is not None:\n            for overflow_type in overflow_options:\n                overflow = self.html.get_node_property(node, overflow_type) \n                overflow = self._handle_overflow_property(overflow, self.html.manage_vsb_func)\n        \n        if self.html.manage_hsb_func is not None:\n            if self.html.experimental:\n                self._handle_overflow_property(self.html.get_node_property(node, \"overflow-x\") , self.html.manage_hsb_func)\n            overflow = self.html.get_node_attribute(node, utilities.BUILTIN_ATTRIBUTES[\"overflow-x\"]) # Tkhtml doesn't support overflow-x\n            overflow = self._handle_overflow_property(overflow, self.html.manage_hsb_func)\n\n        background = self.html.get_node_property(node, \"background-color\")\n        if background != \"transparent\" and self.html.motion_frame_bg != background: # Transparent is the Tkhtml default, so it's largely meaningless\n            self.html.motion_frame_bg = background\n            self.html.motion_frame.config(bg=background)\n\n    \n    # --- Handle <details> elements -------------------------------------------\n\n    # Technically <details> elements should be visible whenever the open attribute is present\n    # But Tkhtml can't remove attributes (or at lease I can't figure out how to do it)\n    # So for now we hide the content if open=\"false\"\n    # I could cut out most of this code if we could remove attributes though\n\n    def _is_open(self, node):\n        return self.html.get_node_attribute(node, \"open\", \"false\") != \"false\"\n\n    def _update_details(self, node, display):\n        for child in self.html.get_node_children(node):\n            if self.html.get_node_tag(child) == \"summary\":\n                continue\n\n            try:\n                self.html.override_node_properties(child, \"display\", \"\" if display else \"none\")\n            except tk.TclError:\n                # We need a better solution here\n                if display and child in self._node_texts:\n                    self.html.set_node_text(child, self._node_texts[child])\n                else:\n                    self._node_texts[child] = self.html.get_node_text(child)\n                    self.html.set_node_text(child, \"\")\n\n    def _set_open(self, node, display):\n        self.html.set_node_attribute(node, \"open\", \"\" if display else \"false\")\n        if self.html.using_tkhtml30:\n            # In Tkhtml 3.1+ we add an attribute handler, which does this for us\n            self._update_details(node, display)\n\n    def _close_other_details(self, node):\n        node = dom.extract_nested(node)\n        name = self.html.get_node_attribute(node, \"name\")\n        if not name:\n            return\n        \n        for details in self.html.search(f\"DETAILS[name={name}]\"):\n            if dom.extract_nested(details) != node:\n                self._set_open(details, False)\n\n    def _on_details(self, node):\n        \"Handle <details> elements.\"\n        if self._is_open(node):\n            self._close_other_details(node)\n        else:\n            self._update_details(node, False)\n\n    def _on_details_value_change(self, node, attribute, value):\n        if attribute != \"open\":\n            return\n        \n        open = value != \"false\"\n        self._update_details(node, open)\n        if open:\n            self._close_other_details(node)\n\n    def _handle_load_finish(self):\n        \"Collapse <details> elements. Only needed for Tkhtml 3.0, which doesn't support HTML5 elements.\"\n        \"It turns out if groups are used this preserves the first box while handling _on_details preserves the last.\"\n        \"I think there's value in preserving behaviour between Tkhtml versions, so for now I guess we'll keep this.\"\n        for details in self.html.search(\"DETAILS\"):\n            self._on_details(details)\n    \n    def _handle_summary_click(self, node):\n        \"Handle clicks on <summary> elements\"\n        details = self.html.get_node_parent(node)\n        if self.html.get_node_tag(details).lower() != \"details\":\n            return\n        \n        open = not self._is_open(details)\n        self._set_open(details, open)\n        if open and self.html.using_tkhtml30:\n            self._close_other_details(details)\n\n    def _on_progress(self, node):\n        widgetid = tk.ttk.Progressbar(self.html, maximum=self.html.get_node_attribute(node, \"max\", 100))\n        widgetid[\"value\"] = self.html.get_node_attribute(node, \"value\", 0)\n        self.html.replace_node_contents(node, widgetid)\n\n    def _on_progress_value_change(self, node, attribute, value):\n        if attribute == \"value\":\n            widgetid = self.html.nametowidget(self.html.get_node_replacement(node))\n            widgetid[\"value\"] = value\n        elif attribute == \"max\":\n            widgetid = self.html.nametowidget(self.html.get_node_replacement(node))\n            widgetid[\"maximum\"] = value\n\n\nclass FormManager(utilities.BaseManager):\n    \"Handle forms and form elements.\"\n    def __init__(self, html):\n        super().__init__(html)\n        self.radiobutton_token = \"TKWtsvLKac1\"\n\n        self.waiting_forms = 0\n        self.form_nodes = {}\n        self.form_widgets = {}\n        self.loaded_forms = {}\n        self.radio_buttons = {}\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def reset(self):\n        self.waiting_forms = 0\n        self.form_nodes.clear()\n        self.form_widgets.clear()\n        self.loaded_forms.clear()\n        self.radio_buttons.clear()\n\n    def _handle_form_reset(self, node):\n        \"Reset HTML forms.\"\n        if node not in self.form_nodes:\n            return\n\n        form = self.form_nodes[node]\n\n        for formelement in self.loaded_forms[form]:\n            if formelement in self.form_widgets:\n                nodetype = self.html.get_node_attribute(formelement, \"type\")\n                nodetag = self.html.get_node_tag(formelement)\n                widget = self.form_widgets[formelement]\n                if nodetag == \"textarea\":\n                    nodevalue = self.html.get_node_text(self.html.get_node_children(formelement), \"-pre\")\n                    widget.set(nodevalue)\n                elif nodetype == \"checkbox\":\n                    if self.html.get_node_attribute(formelement, \"checked\", \"false\") != \"false\": widget.variable.set(1)\n                    else: widget.variable.set(0)\n                elif nodetype == \"radio\":\n                    nodevalue = self.html.get_node_attribute(formelement, \"value\")\n                    if self.html.get_node_attribute(formelement, \"checked\", \"false\") != \"false\": \n                        widget.variable.set(nodevalue)\n                else:\n                    nodevalue = self.html.get_node_attribute(formelement, \"value\")\n                    widget.set(nodevalue)\n\n    def _handle_form_submission(self, node, event=None):\n        \"Submit HTML forms.\"\n        if node not in self.form_nodes:\n            return\n\n        data = []\n        form = self.form_nodes[node]\n        action = self.html.get_node_attribute(form, \"action\")\n        method = self.html.get_node_attribute(form, \"method\", \"GET\").upper()\n\n        for formelement in self.loaded_forms[form]:\n            nodeattrname = self.html.get_node_attribute(formelement, \"name\")\n\n            if nodeattrname:\n                nodetype = self.html.get_node_attribute(formelement, \"type\")\n\n                if formelement in self.form_widgets:\n                    nodevalue = self.form_widgets[formelement].get()\n                    if nodetype == \"number\":\n                        if not self.form_widgets[formelement].check():\n                            return\n                elif self.html.get_node_tag(formelement) == \"hidden\":\n                    nodevalue = self.html.get_node_attribute(formelement, \"value\")\n                    \n                if nodetype == \"submit\" or nodetype == \"reset\":\n                    continue\n                elif nodetype == \"file\":\n                    for value in nodevalue:\n                        data.append(\n                            (nodeattrname, value),\n                        )\n                else:\n                    data.append(\n                        (nodeattrname, nodevalue),\n                    )\n        if not event:\n            nodeattrname = self.html.get_node_attribute(node, \"name\")\n            nodevalue = self.html.get_node_attribute(node, \"value\")\n            if nodeattrname and nodevalue:\n                data.append(\n                    (nodeattrname, nodevalue),\n                )\n\n        data = urlencode(data)\n\n        if action == \"\":\n            url = urlparse(self.html.base_url)\n            url = f\"{url.scheme}://{url.netloc}{url.path}\"\n        else:\n            url = self.html.resolve_url(action)\n\n        if method == \"GET\":\n            data = \"?\" + data\n        else:\n            data = data.encode()\n\n        self.html.post_message(f\"A form was submitted to {utilities.shorten(url)}\")\n        if self.html.on_form_submit is not None:\n            self.html.on_form_submit(url, data, method)\n\n    # --- Handle forms --------------------------------------------------------\n\n    def _on_form(self, node):\n        \"Handle <form> elements.\"\n        inputs = self.html.search(\"input, select, textarea, button\", root=node)\n        for i in inputs:\n            self.form_nodes[i] = node\n\n        if len(inputs) == 0:\n            self.waiting_forms += 1\n        else:\n            self.loaded_forms[node] = inputs\n            self.html.post_message(\"Successfully setup form\")\n            #self.html.post_message(f\"Successfully setup form element {node}\")\n\n    def _on_table(self, node):\n        \"\"\"Handle <form> elements in tables; workaround for bug #48.\"\n        In tables, Tkhtml doesn't seem to notice that forms have children.\n        We get all children of the table and associate inputs with the previous form.\n        Not perfect, but it usually works.\n        If a <td> tag is not present, this fails, as Tkhtml seems to not even notice inputs at all\"\"\"        \n        if self.waiting_forms > 0:\n            form = None\n            inputs = {}\n\n            for node in (self.html.search(\"*\")):\n                tag = self.html.get_node_tag(node)\n                if tag == \"form\":\n                    form = node\n                    inputs[form] = []\n                elif tag.lower() in {\"input\", \"select\", \"textarea\", \"button\"} and form:\n                    self.form_nodes[node] = form\n                    inputs[form].append(node)\n           \n            for form in inputs:\n                self.loaded_forms[form] = inputs[form]\n                self.waiting_forms -= 1\n                self.html.post_message(\"Successfully setup table form\")\n                #self.html.post_message(f\"Successfully setup table form element {node}\")\n\n    # --- Handle dropdowns ----------------------------------------------------\n\n    def _on_select(self, node):\n        \"Handle <select> elements.\"\n        text = []\n        values = []\n        selected = None\n        for child in self.html.get_node_children(node):\n            if self.html.get_node_tag(child) == \"option\":\n                try:\n                    child2 = self.html.get_node_children(child)[0]\n                    nodevalue = self.html.get_node_attribute(child, \"value\")\n                    nodeselected = self.html.get_node_attribute(child, \"selected\")\n                    values.append(nodevalue)\n                    text.append(self.html.get_node_text(child2))\n                    if nodeselected:\n                        selected = nodevalue\n                except IndexError:\n                    continue\n        if not selected and values:\n            selected = values[0]\n        widgetid = subwidgets.Combobox(self.html)\n        widgetid.insert(text, values, selected)\n        widgetid.configure(onchangecommand=lambda *_, widgetid=widgetid: self._on_input_change(node, widgetid))\n        self.form_widgets[node] = widgetid\n        state = self.html.get_node_attribute(node, \"disabled\", False) != \"0\"\n        if state:\n            widgetid.configure(state=\"disabled\")\n        self.html.widget_manager.handle_node_replacement(\n            node,\n            widgetid,\n            lambda widgetid=widgetid: self.html.widget_manager._handle_node_removal(widgetid),\n            lambda node=node, widgetid=widgetid, widgettype=\"text\": self.html.widget_manager._handle_node_style(\n                node, widgetid, widgettype\n            ),\n            check=False\n        )\n        #self.html.post_message(f\"Successfully setup select element {node}\")\n\n    def _on_select_value_change(self, node, attribute, value):\n        return self._on_input_value_change(node, attribute, value)\n\n    # --- Handle textareas ----------------------------------------------------\n\n    def _on_textarea(self, node):\n        \"Handle <textarea> elements.\"\n        widgetid = subwidgets.ScrolledTextBox(self.html, self.html.get_node_text(self.html.get_node_children(node), \"-pre\"), lambda widgetid, node=node: self._on_input_change(node, widgetid))\n\n        self.form_widgets[node] = widgetid\n        state = self.html.get_node_attribute(node, \"disabled\", False) != \"0\"\n        if state:\n            widgetid.configure(state=\"disabled\")\n        self.html.widget_manager.handle_node_replacement(\n            node,\n            widgetid,\n            deletecmd=lambda widgetid=widgetid: self.html.widget_manager._handle_node_removal(widgetid),\n            stylecmd=lambda node=node, widgetid=widgetid, widgettype=\"text\": self.html.widget_manager._handle_node_style(\n                node, widgetid, widgettype\n            ),\n            allowscrolling=False,\n            check=False\n        )\n        #self.html.post_message(f\"Successfully setup select element {node}\")\n\n    # --- Handle inputs -------------------------------------------------------\n\n    def _on_input(self, node):\n        \"Handle <input> elements.\"\n        self.html.tk.eval('set type \"\"')\n        nodetype = self.html.tk.eval(\n            \"set nodetype [string tolower [%s attr -default {} type]]\" % node\n        )\n        nodevalue = self.html.get_node_attribute(node, \"value\")\n        state = self.html.get_node_attribute(node, \"disabled\", \"false\")\n\n        if nodetype in {\"image\", \"submit\", \"reset\", \"button\"}:\n            return\n        elif nodetype == \"file\":\n            accept = self.html.get_node_attribute(node, \"accept\")\n            multiple = (\n                self.html.get_node_attribute(node, \"multiple\", self.radiobutton_token)\n                != self.radiobutton_token\n            )\n            widgetid = subwidgets.FileSelector(self.html, accept, multiple, lambda widgetid, node=node: self._on_input_change(node, widgetid))\n            stylecmd = lambda node=node, widgetid=widgetid: self.html.widget_manager._handle_node_style(\n                node, widgetid\n            )\n        elif nodetype == \"color\":\n            widgetid = subwidgets.ColourSelector(self.html, nodevalue, lambda widgetid, node=node: self._on_input_change(node, widgetid))\n            stylecmd = lambda *a, **k: None\n        elif nodetype == \"checkbox\":\n            if self.html.get_node_attribute(node, \"checked\", \"false\") != \"false\": \n                checked = 1\n            else:\n                checked = 0\n\n            widgetid = subwidgets.FormCheckbox(self.html, checked, lambda widgetid, node=node: self._on_input_change(node, widgetid))\n            widgetid.set = lambda nodevalue, node=node: self.html.set_node_attribute(node, \"value\", nodevalue)\n            widgetid.get = lambda node=node: self.html.get_node_attribute(node, \"value\")\n            stylecmd = lambda node=node, widgetid=widgetid: self.html.widget_manager._handle_node_style(\n                node, widgetid\n            )\n        elif nodetype == \"range\":\n            widgetid = subwidgets.FormRange(self.html, \n                nodevalue,\n                self.html.get_node_attribute(node, \"min\", 0),\n                self.html.get_node_attribute(node, \"max\", 100),\n                self.html.get_node_attribute(node, \"step\", 1),\n                lambda widgetid, node=node: self._on_input_change(node, widgetid)\n            )\n            stylecmd = lambda node=node, widgetid=widgetid, widgettype=\"range\": self.html.widget_manager._handle_node_style(\n                node, widgetid, widgettype\n            )\n        elif nodetype == \"number\":\n            widgetid = subwidgets.FormNumber(self.html, \n                nodevalue,\n                self.html.get_node_attribute(node, \"min\", 0),\n                self.html.get_node_attribute(node, \"max\", 100),\n                self.html.get_node_attribute(node, \"step\", 1),\n                lambda widgetid, node=node: self._on_input_change(node, widgetid)\n            )\n            stylecmd = lambda node=node, widgetid=widgetid: self.html.widget_manager._handle_node_style(\n                node, widgetid\n            )\n        elif nodetype == \"radio\":\n            name = self.html.get_node_attribute(node, \"name\", \"\")\n            if self.html.get_node_attribute(node, \"checked\", \"false\") != \"false\": \n                checked = True\n            else:\n                checked = False\n            \n            if name in self.radio_buttons:\n                variable = self.radio_buttons[name]\n            else:\n                variable = None\n\n            widgetid = subwidgets.FormRadioButton(\n                self.html,\n                self.radiobutton_token,\n                nodevalue,\n                checked,\n                variable,\n                lambda widgetid, node=node: self._on_input_change(node, widgetid)\n            )\n            widgetid.set = lambda nodevalue, node=node: self.html.set_node_attribute(node, \"value\", nodevalue)\n            self.radio_buttons[name] = widgetid.variable\n            stylecmd = lambda node=node, widgetid=widgetid: self.html.widget_manager._handle_node_style(\n                node, widgetid\n            )\n        else:\n            placeholder = self.html.get_node_attribute(node, \"placeholder\", \"\")\n            widgetid = subwidgets.FormEntry(self.html, nodevalue, placeholder, nodetype, lambda widgetid, node=node: self._on_input_change(node, widgetid))\n            widgetid.bind(\n                \"<Return>\",\n                lambda event, node=node: self._handle_form_submission(\n                    node=node, event=event\n                ),\n            )\n            stylecmd = lambda node=node, widgetid=widgetid, widgettype=\"text\": self.html.widget_manager._handle_node_style(\n                node, widgetid, widgettype\n            )\n\n        self.form_widgets[node] = widgetid\n        self.html.widget_manager.handle_node_replacement(\n            node,\n            widgetid,\n            lambda widgetid=widgetid: self.html.widget_manager._handle_node_removal(widgetid),\n            stylecmd,\n            check=False\n        )\n\n        if state != \"false\": \n            widgetid.configure(state=\"disabled\")\n        #self.html.post_message(f\"Successfully setup {nodetype if nodetype else \"text\"} input element {node}\")\n\n    def _on_input_value_change(self, node, attribute, value):\n        if node not in self.form_widgets:\n            return\n\n        nodetype = self.html.get_node_attribute(node, \"type\")\n        widget = self.form_widgets[node]\n        if attribute == \"value\" and nodetype not in {\"checkbox\", \"radio\"}:\n            widget.set(value)\n        elif attribute in {\"min\", \"max\", \"step\"} and nodetype in {\"range\", \"number\"}:\n            CONFIG_MAP = {\"min\": \"from_\", \"max\": \"to\", \"step\": \"step\"}\n            widget.configure(**{CONFIG_MAP[attribute]: value})\n        elif attribute == \"checked\":\n            if nodetype == \"checkbox\":\n                widget.variable.set(1 if value != \"false\" else 0)\n            elif nodetype == \"radio\":\n                nodevalue = self.html.get_node_attribute(node, \"value\")\n                if value != \"false\":\n                    widget.variable.set(nodevalue)\n        elif attribute == \"placeholder\":\n            widget.placeholder = value\n\n    def _on_input_change(self, node, widgetid):\n        widgetid.event_generate(utilities.FIELD_CHANGED_EVENT)\n        self.html.event_manager.post_element_event(node, \"onchange\", None, utilities.FIELD_CHANGED_EVENT)\n        return True\n\nclass ScriptManager(utilities.BaseManager):\n    \"Handle scripts.\"\n    def __init__(self, html):\n        super().__init__(html)\n        self.pending_scripts = []\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def _on_script(self, attributes, tag_contents):\n        \"\"\"A JavaScript engine could be used here to parse the script.\n        Returning any HTMl code here (should) cause it to be parsed in place of the script tag.\"\"\"\n        attributes = attributes.split()\n        attributes = dict(zip(attributes[::2], attributes[1::2])) # Make attributes a dict\n\n        if \"src\" in attributes:\n            src = attributes[\"src\"]\n            src = src.strip(\"{\").strip(\"}\")\n            self.html._thread_check(self.fetch_scripts, self.html.resolve_url(src), attributes)\n        elif \"defer\" in attributes:\n            self.pending_scripts.append((attributes, tag_contents))\n        elif self.html.on_script is not None:\n            self.html.on_script(attributes, tag_contents)\n\n        #self.html.write(\"text\", f\"<tkw_script style='display:none'>{tag_contents.replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\")}</tkw_script>\")\n    \n    def fetch_scripts(self, url=None, attributes=\"\", data=None):\n        \"Fetch and run scripts\"\n        # NOTE: this may run in a thread\n\n        thread = self.html._begin_download()\n\n        if url and thread.isrunning():\n            self.html.post_message(f\"Fetching script from {utilities.shorten(url)}\", thread.is_subthread)\n            try:\n                data = self.html.download_url(url)[1]\n            except Exception as error:\n                self.html.post_to_queue(lambda message=f\"ERROR: could not load script {url}: {error}\",\n                            url=url: self.html._finish_resource_load(message, url, \"script\", False), thread.is_subthread)\n\n        if data and thread.isrunning():\n            if \"defer\" in attributes:\n                self.pending_scripts.append((attributes, data))\n            elif self.html.on_script is not None:\n                self.html.post_to_queue(lambda attributes=attributes, data=data: self.html.on_script(attributes, data), thread.is_subthread)\n                \n            if url:\n                self.html.post_to_queue(lambda message=f\"Successfully loaded {utilities.shorten(url)}\", \n                            url=url: self.html._finish_resource_load(message, url, \"script\", True), thread.is_subthread)\n\n        self.html._finish_download(thread)\n\n    def _submit_deferred_scripts(self):\n        if self.pending_scripts:\n            for index, script in enumerate(self.pending_scripts):\n                self.on_script(*script)\n            self.pending_scripts = []\n\n\nclass StyleManager(utilities.BaseManager):\n    \"Handle stylesheets.\"\n    def __init__(self, html):\n        super().__init__(html)\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def _on_style(self, attributes, tag_contents):\n        \"Handle <style> elements.\"\n        self._finish_fetching_styles(data=tag_contents)\n\n        #self.html.write(\"text\", f\"<tkw_style style='display:none'>{tag_contents.replace(\"<\", \"&lt;\").replace(\">\", \"&gt;\")}</tkw_style>\")\n\n    def _on_link(self, node):\n        \"Handle <link> elements.\"\n        try:\n            rel = self.html.get_node_attribute(node, \"rel\").lower()\n            media = self.html.get_node_attribute(node, \"media\", default=\"all\").lower()\n            href = self.html.get_node_attribute(node, \"href\")\n            url = self.html.resolve_url(href)\n        except tk.TclError:\n            return\n\n        if ((\"stylesheet\" in rel)\n            and (media in {\"screen\", \"print\", \"all\"})):\n            self.html._thread_check(self.fetch_styles, url, node, media)\n            # Onload is fired if and when the stylesheet is parsed\n        elif \"icon\" in rel:\n            self.html.icon = url\n            self.html.post_event(utilities.ICON_CHANGED_EVENT)\n            self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n        else:\n            self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n\n    def _on_atimport(self, parent_url, new_url, media=None):\n        \"Load @import scripts.\"\n        try:\n            new_url = self.html.resolve_url(new_url, parent_url)\n            self.html.post_message(f\"Loading stylesheet from {utilities.shorten(new_url)}\")\n            self.html._thread_check(self.fetch_styles, new_url, media=media)\n\n        except Exception as error:\n            self.html.post_message(f\"ERROR: could not load stylesheet {new_url}: {error}\")\n           \n    def _fix_css_urls(self, match, url):\n        \"Make relative uris in CSS files absolute.\"\n        newurl = match.group()\n        newurl = utilities.strip_css_url(newurl)\n        newurl = self.html.resolve_url(newurl, url)\n        newurl = f\"url('{newurl}')\"\n        return newurl\n    \n    def fetch_styles(self, url=None, node=None, media=None):\n        \"Fetch stylesheets and parse the CSS code they contain\"\n        # NOTE: this may run in a thread\n\n        thread = self.html._begin_download()\n        if url and thread.isrunning():\n            self.html.post_message(f\"Fetching stylesheet from {utilities.shorten(url)}\", thread.is_subthread)\n            try:\n                data = self.html.download_url(url)[1]\n                if media is not None and media != \"all\": data = f\"@media {media} {{{data}}}\"\n\n                if data and thread.isrunning():\n                    self.html.post_to_queue(lambda node=node, url=url, data=data: self._finish_fetching_styles(node, url, data), thread.is_subthread)\n\n            except Exception as error:\n                self.html.post_to_queue(lambda message=f\"ERROR: could not load stylesheet {url}: {error}\",\n                    url=url: self.html._finish_resource_load(message, url, \"stylesheet\", False), thread.is_subthread)\n                        \n        self.html._finish_download(thread)\n\n    def _finish_fetching_styles(self, node=None, url=None, data=None):\n        # NOTE: this must run in the main thread\n\n        self.html._style_count += 1\n        sheetid = \"user.\" + str(self.html._style_count).zfill(4)\n\n        self.html.parse_css(f\"{sheetid}.9999\", data, url)\n        if node:\n            self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n        if url:\n            self.html.post_message(f\"Successfully loaded {utilities.shorten(url)}\")\n            if self.html.on_resource_setup is not None:\n                self.html.on_resource_setup(url, \"stylesheet\", True)\n\n\nclass ImageManager(utilities.BaseManager):\n    \"Handle images.\"\n    def __init__(self, html):\n        super().__init__(html)\n\n        self.loaded_images = {}\n        self.image_directory = {}\n        self.bad_paths = set()\n        self.loaded_image_counter = 0\n        self.image_name_prefix = f\"_tkinterweb_img_{id(self.html)}_\"\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def reset(self):\n        self.image_directory.clear()\n        self.bad_paths.clear()\n\n    def _on_img(self, node):\n        # Remember the node and it's url, so that when -imagecmd sends the url for loading we know where it came from\n        url = self.html.resolve_url(self.html.get_node_attribute(node, \"src\"))\n        self.image_directory.setdefault(url, set()).add(node)\n    \n    def _on_img_value_change(self, node, attribute, value):\n        if attribute == \"src\":\n            url = self.html.resolve_url(value)\n\n            # Update the image directory\n            for saved_url, node_set in list(self.image_directory.items()):\n                if node in node_set:\n                    node_set.remove(node)\n                    if not node_set:\n                        del self.image_directory[saved_url]\n                    break\n\n            self.image_directory.setdefault(url, set()).add(node)\n\n            # Tkhtml won't call -imagecmd twice on the same url after the document loads\n            # This can prevent alt text from showing\n            # Disabling the image cache doesn't seem to change that\n            # So we clear the image here if needed\n            if url in self.bad_paths and self.html.ignore_invalid_images and self.html.image_alternate_text_enabled:\n                self.html.set_node_property(node, \"-tkhtml-replacement-image\", None)\n            else:\n                # Force the replacement image in case the image had alt text\n                self.html.set_node_property(node, \"-tkhtml-replacement-image\", value)\n\n    def load_alt_text(self, url, name):\n        # NOTE: this must run in the main thread\n        self.bad_paths.add(url)\n        \n        if not self.html.ignore_invalid_images:\n            image, data_is_image = self.check_images(utilities.BROKEN_IMAGE, name, url, \"image/png\", False)\n            image = imageutils.data_to_image(image, name, \"image/png\", data_is_image)\n\n            self.loaded_images.setdefault(name, set()).add(image)\n\n        elif self.html.image_alternate_text_enabled and (url in self.image_directory):\n            for node in self.image_directory[url]:\n                try:  # Ensure thread safety when closing\n                    alt = self.html.get_node_attribute(node, \"alt\")\n                    if alt:\n                        self.html.set_node_property(node, \"-tkhtml-replacement-image\", None)\n                        # Update the alt text property to force Tkhtml to update/display the node\n                        # For some reason without the after() the text won't always show when being changed from a binding\n                        self.html.after(0, lambda node=node, alt=alt: self.html.set_node_attribute(node, \"alt\", alt))\n                except (RuntimeError, tk.TclError): \n                    return  # Widget no longer exists\n\n    def _on_image_cmd(self, url):\n        \"Handle images.\"\n        name = self.allocate_image_name()\n\n        if url.startswith(self.image_name_prefix):\n            name = url\n        else:\n            image = imageutils.blank_image(name)\n            self.loaded_images[name] = {image}\n\n            if any({\n                    url.startswith(\"linear-gradient(\"),\n                    url.startswith(\"radial-gradient(\"),\n                    url.startswith(\"repeating-linear-gradient(\"),\n                    url.startswith(\"repeating-radial-gradient(\"),\n                }):\n                self.html.post_message(f\"Fetching image: {utilities.shorten(url)}\")\n                self.load_alt_text(url, name)\n                for image in url.split(\",\"):\n                    self.html.post_message(f\"ERROR: could not display the image {utilities.shorten(url)} because it is not supported yet\")\n                if self.html.on_resource_setup is not None:\n                    self.html.on_resource_setup(url, \"image\", False)\n            else:\n                url = url.split(\"), url(\", 1)[0].replace(\"'\", \"\").replace('\"', \"\")\n                url = self.html.resolve_url(url)\n                self.html._thread_check(self.fetch_images, url, name)\n\n        return list((name, self.html.register(self._on_image_delete)))\n\n    def fetch_images(self, url, name):\n        \"Fetch images and display them in the document.\"\n        # NOTE: this may run in a thread\n\n        thread = self.html._begin_download()\n        if thread.isrunning():\n            self.html.post_message(f\"Fetching image from {utilities.shorten(url)}\", thread.is_subthread)\n\n            if url == self.html.base_url:\n                self.html.post_to_queue(lambda url=url, name=name, error=\"ERROR: image url not specified\": \n                                        self._on_image_error(url, name, error), thread.is_subthread)\n            else:\n                try:\n                    url, data, filetype, code = self.html.download_url(url)\n                    data, data_is_image = self.check_images(data, name, url, filetype, thread.is_subthread)                \n                        \n                    if thread.isrunning():\n                        self.html.post_to_queue(lambda data=data, name=name, url=url, filetype=filetype, data_is_image=data_is_image: \n                                                self.finish_fetching_images(data, name, url, filetype, data_is_image), thread.is_subthread)\n                except Exception as error:\n                    self.html.post_to_queue(lambda url=url, name=name, error=f\"ERROR: could not load image {url}: {error}\": \n                                            self._on_image_error(url, name, error), thread.is_subthread)\n\n        self.html._finish_download(thread)\n\n    def check_images(self, data, name, url, filetype, thread_safe):\n        \"Invert images if needed and convert SVG images to PNGs.\"\n        # NOTE: this may run in a thread\n\n        data_is_image = False\n        if \"svg\" in filetype:\n            try:\n                data = imageutils.svg_to_png(data)\n            except (ValueError, ImportError, ModuleNotFoundError,):\n                raise RuntimeError(f\"could not display the image {url}: either PyGObject, CairoSVG, or both PyCairo and Rsvg must be installed to parse .svg files.\")\n            \n        if self.html.image_inversion_enabled:\n            try:\n                data = imageutils.invert_image(data, self.html.dark_theme_limit)\n                data_is_image = True\n            except (ImportError, ModuleNotFoundError,):\n                error = f\"ERROR: could not invert the image {url}: PIL and PIL.ImageTk must be installed.\"\n                self.html.post_to_queue(lambda url=url, name=name, error=error: self._on_image_error(url, name, error), thread_safe)\n            \n        return data, data_is_image\n\n    def finish_fetching_images(self, data, name, url, filetype, data_is_image=False):\n        # NOTE: this must run in the main thread\n\n        try:\n            image = imageutils.data_to_image(data, name, filetype, data_is_image)\n            \n            self.html.post_message(f\"Successfully loaded {utilities.shorten(url)}\")\n            if self.html.on_resource_setup is not None:\n                self.html.on_resource_setup(url, \"image\", True)\n            if url in self.image_directory:\n                for node in self.image_directory[url]:\n                    self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n\n            self.loaded_images.setdefault(name, set()).add(image)\n\n            return image\n        except (ImportError, ModuleNotFoundError,):\n            error = f\"ERROR: could not display image {url}: PIL and PIL.ImageTk must be installed\"\n            self._on_image_error(url, name, error)\n        except Exception as error:\n            self._on_image_error(url, name, f\"ERROR: could not display image {url}: {error}\")\n\n    def _on_image_error(self, url, name, error):\n        # NOTE: this must run in the main thread\n        self.html.post_message(error)\n        self.load_alt_text(url, name)\n        if self.html.on_resource_setup is not None:\n            self.html.on_resource_setup(url, \"image\", False)\n\n    def _on_image_delete(self, name):\n        # Remove the reference to the image in the main thread\n        self.html.post_to_queue(lambda name=name: self._finish_image_delete(name))\n\n    def _finish_image_delete(self, name):\n        # NOTE: this must run in the main thread\n        if name in self.loaded_images:\n            del self.loaded_images[name]\n\n    def allocate_image_name(self):\n        \"Get a unique image name.\"\n        name = self.image_name_prefix + str(self.loaded_image_counter)\n        self.loaded_image_counter += 1\n        return name\n    \nclass ObjectManager(utilities.BaseManager):\n    \"Handle objects.\"\n    def __init__(self, html):\n        super().__init__(html)\n\n        self.loaded_iframes = {}\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n    \n    def reset(self):\n        self.loaded_iframes.clear()\n    \n    # --- Handle iframes ------------------------------------------------------\n\n    def _on_iframe(self, node):\n        \"Handle <iframe> elements.\"\n        src = self.html.get_node_attribute(node, \"src\")\n        srcdoc = self.html.get_node_attribute(node, \"srcdoc\")\n        \n        if self.html.get_node_attribute(node, \"scrolling\") == \"no\":\n            scrolling = False\n        else:\n            scrolling = \"auto\"\n\n        if srcdoc:\n            self._create_iframe(node, None, srcdoc, scrolling)\n        elif src and (src != self.html.base_url):\n            src = self.html.resolve_url(src)\n            self.html.post_message(f\"Creating iframe from {utilities.shorten(src)}\")\n            self._create_iframe(node, src, vertical_scrollbar=scrolling)\n\n    def _on_iframe_value_change(self, node, attribute, value):\n        if attribute == \"srcdoc\":\n            if node in self.loaded_iframes:\n                self.loaded_iframes[node].load_html(value)\n            else:\n                self._create_iframe(node, None, value)\n        elif attribute == \"src\" and (value != self.html.base_url):\n            if node in self.loaded_iframes:\n                self.loaded_iframes[node].load_url(self.html.resolve_url(value))\n            else:\n                self._create_iframe(node, value)\n\n    def _create_iframe(self, node, url, html=None, vertical_scrollbar=\"auto\"):\n        if self.html.embed_obj:\n            widgetid = self.html.embed_obj(self.html,\n                messages_enabled=self.html.messages_enabled,\n                message_func=self.html.message_func,\n                overflow_scroll_frame=self.html,\n                stylesheets_enabled = self.html.stylesheets_enabled,\n                vertical_scrollbar = vertical_scrollbar,\n                images_enabled = self.html.images_enabled,\n                forms_enabled = self.html.forms_enabled,\n                objects_enabled = self.html.objects_enabled,\n                ignore_invalid_images = self.html.ignore_invalid_images,\n                crash_prevention_enabled = self.html.crash_prevention_enabled,\n                dark_theme_enabled = self.html.dark_theme_enabled,\n                image_inversion_enabled = self.html.image_inversion_enabled,\n                caches_enabled = self.html.caches_enabled,\n                threading_enabled = self.html.threading_enabled,\n                image_alternate_text_enabled = self.html.image_alternate_text_enabled,\n                selection_enabled = self.html.selection_enabled,\n                find_match_highlight_color = self.html.find_match_highlight_color,\n                find_match_text_color = self.html.find_match_text_color,\n                find_current_highlight_color = self.html.find_current_highlight_color,\n                find_current_text_color = self.html.find_current_text_color,\n                selected_text_highlight_color = self.html.selected_text_highlight_color,\n                selected_text_color = self.html.selected_text_color,\n                visited_links = self.html.visited_links,\n                insecure_https = self.html.insecure_https,\n                ssl_cafile = self.html.ssl_cafile,\n                request_timeout = self.html.request_timeout,\n                caret_browsing_enabled = self.html.caret_browsing_enabled\n            )\n\n            if html:\n                widgetid.load_html(html, url)\n            elif url:\n                widgetid.load_url(url)\n\n            self.loaded_iframes[node] = widgetid\n\n            self.html.widget_manager.handle_node_replacement(\n                node, widgetid, lambda widgetid=widgetid: self.html.widget_manager._handle_node_removal(widgetid), allowscrolling=False, check=False\n            )\n        else:\n            self.html.post_message(f\"WARNING: the embedded page {url} could not be shown because no embed widget was provided.\")\n\n    # --- Handle objects ------------------------------------------------------\n\n    def _on_object(self, node, data=None):\n        \"Handle <object> elements.\"\n        if data == None:\n            # This doesn't work when in an attribute handler\n            data = self.html.get_node_attribute(node, \"data\")\n\n        if data != \"\":\n            try:\n                # Load widgets presented in <object> elements\n                widgetid = self.html.nametowidget(data)\n                self.html.widget_manager.set_node_widget(node, widgetid)\n            except KeyError:\n                data = self.html.resolve_url(data)\n                if data == self.html.base_url:\n                    # Don't load the object if it is the same as the current file\n                    # Otherwise the page will load the same object indefinitely and freeze the GUI forever\n                    return\n\n                self.html.post_message(f\"Creating object from {utilities.shorten(data)}\")\n                self.html._thread_check(self.fetch_objects, data, node)\n\n    def _on_object_value_change(self, node, attribute, value):\n        if attribute == \"data\":\n            if value:\n                self._on_object(node, value)\n            else:\n                # Reset the element if data is not supplied\n                # Force reset because it might contain widgets that are added internally\n                self.html.widget_manager.map_node(node, True)\n\n    def fetch_objects(self, url, node):\n        # NOTE: this may run in a thread\n\n        thread = self.html._begin_download()\n\n        if thread.isrunning():\n            try:\n                url, data, filetype, code = self.html.download_url(url)\n\n                if data and thread.isrunning():\n                    if filetype.startswith(\"image\"):\n                        name = self.html.image_manager.allocate_image_name()\n                        data, data_is_image = self.html.image_manager.check_images(data, name, url, filetype, thread.is_subthread)\n                        self.html.post_to_queue(lambda node=node, data=data, name=name, url=url, filetype=filetype, data_is_image=data_is_image: \n                                                self._finish_fetching_image_objects(node, data, name, url, filetype, data_is_image), thread.is_subthread)\n                    elif filetype == \"text/html\":\n                        self.html.post_to_queue(lambda node=node, data=data, url=url, filetype=filetype: \n                                                self._finish_fetching_HTML_objects(node, data, url, filetype), thread.is_subthread)\n\n            except Exception as error:\n                self.html.post_message(f\"ERROR: could not load object element with data {url}: {error}\", True)\n        \n        self.html._finish_download(thread)\n\n    def _finish_fetching_image_objects(self, node, data, name, url, filetype, data_is_image):\n        # NOTE: this must run in the main thread \n\n        image = self.html.image_manager.finish_fetching_images(data, name, filetype, url, data_is_image)\n        self.html.override_node_properties(node, \"-tkhtml-replacement-image\", f\"url({image})\")\n        self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n\n    def _finish_fetching_HTML_objects(self, node, data, url, filetype):\n        # NOTE: this must run in the main thread\n\n        self._create_iframe(node, url, data)\n        self.html.event_manager.post_element_event(node, \"onload\", None, utilities.ELEMENT_LOADED_EVENT)\n"
  },
  {
    "path": "tkinterweb/htmlwidgets.py",
    "content": "\"\"\"\r\nWidgets that expand on the functionality of the basic bindings\r\nby adding scrolling, file loading, and many other convenience functions\r\n\r\nCopyright (c) 2021-2026 Andrew Clarke\r\n\"\"\"\r\n\r\nfrom . import bindings, dom, js, utilities, subwidgets, imageutils\r\n\r\nfrom urllib.parse import urldefrag, urlparse, urlunparse\r\nfrom textwrap import indent\r\n\r\nimport tkinter as tk\r\nfrom tkinter.ttk import Frame, Style\r\n\r\n\r\nclass HtmlFrame(Frame):\r\n    \"\"\"TkinterWeb's flagship HTML widget.\r\n\r\n    :param master: The parent widget.\r\n    :type master: :py:class:`tkinter.Widget`\r\n\r\n    Callbacks:\r\n\r\n    :param on_navigate_fail: The function to be called when a url cannot be loaded. The target url, error, and code will be passed as arguments. By default the TkinterWeb error page is shown.\r\n    :type on_navigate_fail: None or function\r\n    :param on_link_click: The function to be called when a hyperlink is clicked. The target url will be passed as an argument. By default the url is navigated to.\r\n    :type on_link_click: None or function\r\n    :param on_form_submit: The function to be called when a form is submitted. The target url, data, and method (\"GET\" or \"POST\") will be passed as arguments. By default the response is loaded.\r\n    :type on_form_submit: None or function\r\n    :param on_script: The function to be called when a ``<script>`` element is encountered. This can be used to connect a script handler, such as a JavaScript engine. The script element's attributes and contents will be passed as arguments.\r\n    :type on_script: None or function\r\n    :param on_element_script: The function to be called when a JavaScript event attribute on an element is encountered. This can be used to connect a script handler, such as a JavaScript engine, or even to run your own Python code. The element's corresponding Tkhtml3 node, relevant attribute, and attribute contents will be passed as arguments. New in version 4.1.\r\n    :type on_element_script: None or function\r\n    :param on_resource_setup: The function to be called when an image, stylesheet, or script load finishes. The resource's url, type (\"image\", \"stylesheet\", or \"script\"), and whether setup was successful or not (True or False) will be passed as arguments.\r\n    :type on_resource_setup: None or function\r\n\r\n    Widget appearance:\r\n\r\n    :param visited_links: The list used to determine if a hyperlink should be given the CSS ``:visited`` flag.\r\n    :type visited_links: list\r\n    :param zoom: The page zoom multiplier.\r\n    :type zoom: float\r\n    :param fontscale: The page fontscale multiplier.\r\n    :type fontscale: float\r\n    :param defaultstyle: The default stylesheet to use when parsing HTML. Use caution when changing this setting. The default is ``tkintereb.utilities.DEFAULT_STYLE``.\r\n    :type defaultstyle: str\r\n\r\n    Widget sizing and overflow:\r\n\r\n    :param vertical_scrollbar: Show the vertical scrollbar. You can also set the CSS ``overflow`` property on the ``<html>`` or ``<body>`` element instead.\r\n    :type vertical_scrollbar: bool, \"auto\", or \"dynamic\"\r\n    :param horizontal_scrollbar: Show the horizontal scrollbar. It is usually best to leave this hidden. You can also set the ``tkinterweb-overflow-x=\"scroll\" | \"auto\" | \"hidden\"`` attribute on the ``<html>`` or ``<body>`` element instead.\r\n    :type horizontal_scrollbar: bool, \"auto\", or \"dynamic\"\r\n    :param shrink: If False, the widget's width and height are set by the width and height options as per usual. If this option is set to True, the widget's width and height are determined by the current document.\r\n    :type shrink: bool\r\n    :param textwrap: Determines whether text is allowed to wrap. This is similar to the CSS ``text-wrap: normal | nowrap`` property, but more forceful. By default, wrapping will be disabled when shrink is True and will be enabled when shrink is False. Make sure the tkinterweb-tkhtml-extras package is installed; this is only partially supported in Tkhtml version 3.0. New in version 4.17.\r\n    :type textwrap: bool or \"auto\"\r\n    \r\n    Debugging:\r\n\r\n    :param messages_enabled: Enable/disable messages. Prior to version 4.25 this is enabled by default.\r\n    :type messages_enabled: bool\r\n    :param message_func: The function to be called when a debug message is issued. Prior to version 4.25 this only works if messages are enabled. The message will be passed as an argument. If unset and enabled, by default the message is printed.\r\n    :type message_func: None or function\r\n\r\n    Features:\r\n\r\n    :param selection_enabled: Enable/disable selection. This is enabled by default.\r\n    :type selection_enabled: bool\r\n    :param caret_browsing_enabled: Enable/disable caret browsing. This is disabled by default. New in version 4.8.\r\n    :type caret_browsing_enabled: bool\r\n    :param stylesheets_enabled: Enable/disable stylesheets. This is enabled by default.\r\n    :type stylesheets_enabled: bool\r\n    :param images_enabled: Enable/disable images. This is enabled by default.\r\n    :type images_enabled: bool\r\n    :param forms_enabled: Enable/disable forms and form elements. This is enabled by default.\r\n    :type forms_enabled: bool\r\n    :param objects_enabled: Enable/disable embedding of ``<object>`` and ``<iframe>`` elements. This is enabled by default.\r\n    :type objects_enabled: bool\r\n    :param caches_enabled: Enable/disable caching. Disabling this option will conserve memory, but will also result in longer page and image reload times. This is enabled by default. Largely for debugging.\r\n    :type caches_enabled: bool\r\n    :param crash_prevention_enabled: Enable/disable crash prevention. In older Tkhtml versions, disabling this option may improve page load speed, but crashes will occur on some websites. This is enabled by default. Largely for debugging.\r\n    :type crash_prevention_enabled: bool\r\n    :param events_enabled: Enable/disable generation of Tk events. This is enabled by default. Largely for debugging.\r\n    :type events_enabled: bool\r\n    :param threading_enabled: Enable/disable threading. Has no effect if the Tcl/Tk build does not support threading. This is enabled by default. Largely for debugging.\r\n    :type threading_enabled: bool\r\n    :param javascript_enabled: Enable/disable JavaScript support. This is disabled by default. Experimental. New in version 4.1.\r\n    :type javascript_enabled: bool\r\n    :param javascript_backend: The scripting backend to use. Set to ``pythonmonkey`` (the default) to evaluate scripts as JavaScript code, or set to ``python`` to evaluate as Python code. Experimental. New in version 4.19.\r\n    :type javascript_backend: \"pythonmonkey\" or \"python\"\r\n    :param image_alternate_text_enabled: Enable/disable the display of alt text for broken images. This is enabled by default.\r\n    :type image_alternate_text_enabled: bool\r\n    :param dark_theme_enabled: Enable/disable dark mode. This feature is a work-in-progress and may cause hangs or crashes on more complex websites.\r\n    :type dark_theme_enabled: bool\r\n    :param image_inversion_enabled: Enable/disable image inversion. If enabled, an algorithm will attempt to detect and invert images with a predominantly light-coloured background. Photographs and dark-coloured images should be left as is. This feature is a work-in-progress and may cause hangs or crashes on more complex websites.\r\n    :type image_inversion_enabled: bool\r\n    :param ignore_invalid_images: If enabled and alt text is disabled or the image has no alt text, a broken image icon will be displayed in place of the image.\r\n    :type ignore_invalid_images: bool\r\n\r\n    Widget colours and styling:\r\n\r\n    :param find_match_highlight_color: The background colour of matches found by :py:func:`find_text()`. \r\n    :type find_match_highlight_color: str\r\n    :param find_match_text_color: The foreground colour of matches found by :py:func:`find_text()`. \r\n    :type find_match_text_color: str\r\n    :param find_current_highlight_color: The background colour of the current match selected by :py:func:`find_text()`. \r\n    :type find_current_highlight_color: str\r\n    :param find_current_text_color: The foreground colour of the current match selected by :py:func:`find_text()`. \r\n    :type find_current_text_color: str\r\n    :param selected_text_highlight_color: The background colour of selected text. \r\n    :type selected_text_highlight_color: str\r\n    :param selected_text_color: The foreground colour of selected text. \r\n    :type selected_text_color: str\r\n\r\n    Download behaviour:\r\n\r\n    :param insecure_https: If True, website certificate errors are ignored. This can be used to work around issues where :py:mod:`ssl` is unable to get a page's certificate on some older Mac systems.\r\n    :type insecure_https: bool\r\n    :param ssl_cafile: Path to a file containing CA certificates. This can be used to work around issues where :py:mod:`ssl` is unable to get a page's certificate on some older Mac systems. New in version 4.5.\r\n    :type ssl_cafile: None or str\r\n    :param headers: The headers used by urllib's :py:class:`~urllib.request.Request` when fetching a resource.\r\n    :type headers: dict\r\n    :param request_timeout: The number of seconds to wait when fetching a resource before timing out. New in version 4.6.\r\n    :type request_timeout: int\r\n    :param request_func: The function to be called when a resource is requested. This overrides all other download settings. The callback must accept the following arguments: the resource's url, data, method (\"GET\" or \"POST\"), and encoding. The callback must return the following: url, data, file type, and HTTP code.\r\n    :type request_func: None or function\r\n\r\n    HTML rendering behaviour:\r\n\r\n    :param experimental: If True, experimental features will be enabled. If \"auto\", experimental features will be enabled if the loaded Tkhtml version supports experimental features. You will need to compile the cutting-edge Tkhtml widget from https://github.com/Andereoo/TkinterWeb-Tkhtml/tree/experimental and replace the default Tkhtml binary for your system with the experimental version. Unless you need to screenshot the full page on Windows or print your page for now it is likely best to use the default Tkhtml binary and leave this setting alone.\r\n    :type experimental: bool or \"auto\"\r\n    :param use_prebuilt_tkhtml: If True (the default), the Tkhtml binary for your system supplied by TkinterWeb will be used. If your system isn't supported and you don't want to compile the Tkhtml widget from https://github.com/Andereoo/TkinterWeb-Tkhtml yourself, you could try installing Tkhtml3 system-wide and set :attr:`use_prebuilt_tkhtml` to False. Note that some crash prevention features will no longer work.\r\n    :type use_prebuilt_tkhtml: bool\r\n    :param tkhtml_version: The Tkhtml version to use. If the requested version is not found, TkinterWeb will fallback to Tkhtml 3.0. Only one Tkhtml version can be loaded at a time. New in version 4.4.\r\n    :type tkhtml_version: float or \"auto\"\r\n    :param parsemode: The parse mode. In \"html\" mode, explicit XML-style self-closing tags are not handled specially and unknown tags are ignored. \"xhtml\" mode is similar to \"html\" mode except that explicit self-closing tags are recognized. \"xml\" mode is similar to \"xhtml\" mode except that XML CDATA sections and unknown tag names are recognized. It is usually best to leave this setting alone.\r\n    :type parsemode: \"xml\", \"xhtml\", or \"html\"\r\n    :param mode: The rendering engine mode. It is usually best to leave this setting alone.\r\n    :type mode: \"standards\", \"almost standards\", or \"quirks\"\r\n\r\n    Other ttk.Frame arguments, such as ``width``, ``height``, and ``style`` are also supported.\r\n    \r\n    :raise TypeError: If the value type is wrong and cannot be converted to the correct type.\"\"\"\r\n\r\n    def __init__(self, master, *, \r\n                    zoom = utilities.UNSET, fontscale = utilities.UNSET, messages_enabled = utilities.UNSET, \\\r\n                    vertical_scrollbar = utilities.UNSET, horizontal_scrollbar = utilities.UNSET, \\\r\n                    on_navigate_fail = utilities.UNSET, on_link_click = utilities.UNSET, on_form_submit = utilities.UNSET, \r\n                    on_script = utilities.UNSET, on_element_script = utilities.UNSET, on_resource_setup = utilities.UNSET, \\\r\n                    message_func = utilities.UNSET, request_func = utilities.UNSET, caret_browsing_enabled = utilities.UNSET, \r\n                    selection_enabled = utilities.UNSET, stylesheets_enabled = utilities.UNSET, images_enabled = utilities.UNSET, \\\r\n                    forms_enabled = utilities.UNSET, objects_enabled = utilities.UNSET, caches_enabled = utilities.UNSET, \\\r\n                    dark_theme_enabled = utilities.UNSET, image_inversion_enabled = utilities.UNSET, \\\r\n                    javascript_enabled = utilities.UNSET, javascript_backend = utilities.UNSET, events_enabled = utilities.UNSET, \\\r\n                    threading_enabled = utilities.UNSET, crash_prevention_enabled = utilities.UNSET, \\\r\n                    image_alternate_text_enabled = utilities.UNSET, ignore_invalid_images = utilities.UNSET, \\\r\n                    visited_links = utilities.UNSET, find_match_highlight_color = utilities.UNSET, find_match_text_color = utilities.UNSET, \\\r\n                    find_current_highlight_color = utilities.UNSET, find_current_text_color = utilities.UNSET, \\\r\n                    selected_text_highlight_color = utilities.UNSET, selected_text_color = utilities.UNSET, \\\r\n                    insecure_https = utilities.UNSET, ssl_cafile = utilities.UNSET, request_timeout = utilities.UNSET, \\\r\n                    headers = utilities.UNSET, experimental = utilities.UNSET, use_prebuilt_tkhtml = utilities.UNSET, \\\r\n                    tkhtml_version = utilities.UNSET, parsemode = utilities.UNSET, shrink = utilities.UNSET, textwrap = utilities.UNSET, \\\r\n                    mode = utilities.UNSET, defaultstyle = utilities.UNSET, height = utilities.UNSET, width = utilities.UNSET, **kwargs):\r\n        \r\n        init_args = locals().copy()\r\n        \r\n        # State and settings variables\r\n        self._current_url = \"\"\r\n        self._current_data = \"\"\r\n        self._previous_url = \"\"\r\n        self._accumulated_styles = []\r\n        self._waiting_for_reset = False\r\n        self._thread_in_progress = None\r\n        self._prev_height = 0\r\n        self._prev_configure = ()\r\n        self._button = None\r\n        self._style = None\r\n\r\n        ### TODO: Would be lovely to make it more Tk-ish: i.e.\r\n        # zoom\r\n        # fontscale\r\n        # scrollbars = none | auto | dynamic | (a, b)\r\n        # shrink\r\n        # wrap\r\n        # theme = normal | dark | night\r\n        # mode = cursor | normal | readonly\r\n        # defaultstyle\r\n        # width\r\n        # height\r\n\r\n        # navigationfailcommand\r\n        # linkclickcommand\r\n        # submitcommand\r\n        # scriptcommand\r\n        # scripteventcommand\r\n        # resourcecommand\r\n        # messagecommand\r\n        # requestcommand\r\n\r\n        # visitedsites\r\n\r\n        # findbackground\r\n        # findforeground\r\n        # findcurrentbackground\r\n        # findcurrentforeground\r\n        # selectbackground\r\n        # selectforeground\r\n\r\n        # tkhtmlversion\r\n        # experimental\r\n        # bundledtkhtml\r\n\r\n        # scripting_configure (or should this be javascript.configure)\r\n        #     enablescripting\r\n        #     backend\r\n\r\n        # engine_configure (or should this be in html.configure)\r\n        #     enablemessages (keep default True but remove built-in messagecmd handling)\r\n        #     enablestylesheets\r\n        #     enableimages\r\n        #     enableforms\r\n        #     enableobjects\r\n        #     enablecaches\r\n        #     enablethreading\r\n        #     enableevents\r\n        #     enablealttext\r\n        #     enablealtimage\r\n        #     mode\r\n        #     parsemode\r\n            \r\n        # session_configure (or should we make this session.configure)\r\n        #     cafile\r\n        #     timeout\r\n        #     headers\r\n        #     insecure\r\n        ### or something. It would be nice to better match tk commands as well.\r\n\r\n        self._htmlframe_options = {\r\n            \"on_navigate_fail\": {\"default\": self.show_error_page, \"type\": \"callable\"},\r\n            \"vertical_scrollbar\": {\"default\": \"dynamic\", \"type\": \"scrollbar\"},\r\n            \"horizontal_scrollbar\": {\"default\": False, \"type\": \"scrollbar\"},\r\n            \"javascript_backend\": {\"default\": \"pythonmonkey\", \"type\": str},\r\n            \"unshrink\": {\"default\": False},\r\n            \"about_page_background\": {\"default\": \"\", \"deprecated\": \"ttk.Style().configure('TFrame', background=)\"},\r\n            \"about_page_foreground\": {\"default\": \"\", \"deprecated\": \"ttk.Style().configure('TFrame', foreground=)\"},\r\n        }\r\n\r\n        self._tkinterweb_options = {\r\n            \"on_link_click\": {\"default\": self.load_url, \"type\": \"callable\"},\r\n            \"on_form_submit\": {\"default\": self.load_form_data, \"type\": \"callable\"},\r\n            \"on_script\": {\"default\": self._on_script, \"type\": \"callable\"},\r\n            \"on_element_script\": {\"default\": self._on_element_script, \"type\": \"callable\"},\r\n            \"on_resource_setup\": {\"default\": None, \"type\": \"callable\"},\r\n            \"message_func\": {\"default\": None, \"type\": \"callable\"},\r\n            \"messages_enabled\": {\"default\": False, \"type\": bool},\r\n            \"caret_browsing_enabled\": {\"default\": False, \"type\": bool},\r\n            \"selection_enabled\": {\"default\": True, \"type\": bool},\r\n            \"stylesheets_enabled\": {\"default\": True, \"type\": bool},\r\n            \"images_enabled\": {\"default\": True, \"type\": bool},\r\n            \"forms_enabled\": {\"default\": True, \"type\": bool},\r\n            \"objects_enabled\": {\"default\": True, \"type\": bool},\r\n            \"caches_enabled\": {\"default\": True, \"type\": bool},\r\n            \"dark_theme_enabled\": {\"default\": False, \"type\": bool},\r\n            \"image_inversion_enabled\": {\"default\": False, \"type\": bool},\r\n            \"crash_prevention_enabled\": {\"default\": True, \"type\": bool},\r\n            \"events_enabled\": {\"default\": True, \"type\": bool},\r\n            \"threading_enabled\": {\"default\": True, \"type\": bool},\r\n            \"javascript_enabled\": {\"default\": False, \"type\": bool},\r\n            \"image_alternate_text_enabled\": {\"default\": True, \"type\": bool},\r\n            \"ignore_invalid_images\": {\"default\": True, \"type\": bool},\r\n            \"visited_links\": {\"default\": [], \"type\": list},\r\n            \"find_match_highlight_color\": {\"default\": \"#f1a1f7\", \"type\": str},\r\n            \"find_match_text_color\": {\"default\": \"#000\", \"type\": str},\r\n            \"find_current_highlight_color\": {\"default\": \"#8bf0b3\", \"type\": str},\r\n            \"find_current_text_color\": {\"default\": \"#000\", \"type\": str},\r\n            \"selected_text_highlight_color\": {\"default\": \"#9bc6fa\", \"type\": str},\r\n            \"selected_text_color\": {\"default\": \"#000\", \"type\": str},\r\n            \"default_style\": {\"default\": utilities.DEFAULT_STYLE, \"deprecated\": \"utilities.DEFAULT_STYLE or defaultstyle\"},\r\n            \"dark_style\": {\"default\": utilities.DARK_STYLE, \"deprecated\": \"utilities.DARK_STYLE or defaultstyle\"},\r\n            \"request_func\": {\"default\": None, \"type\": \"callable\"},\r\n            \"insecure_https\": {\"default\": utilities.INSECURE_HTTPS, \"type\": bool},\r\n            \"ssl_cafile\": {\"default\": utilities.SSL_CAFILE, \"type\": \"nonestr\"},\r\n            \"request_timeout\": {\"default\": utilities.REQUEST_TIMEOUT, \"type\": int},\r\n            \"headers\": {\"default\": utilities.HEADERS, \"type\": dict},\r\n            \"experimental\": {\"default\": False, \"type\": \"autobool\", \"changeable\": False},\r\n            \"use_prebuilt_tkhtml\": {\"default\": True, \"type\": bool, \"changeable\": False},\r\n            \"tkhtml_version\": {\"default\": \"auto\", \"type\": \"autofloat\", \"changeable\": False},\r\n            # Internal\r\n            \"overflow_scroll_frame\": {\"default\": None},\r\n            \"embed_obj\": {\"default\": HtmlFrame},\r\n            \"manage_vsb_func\": {\"default\": self._manage_vsb},\r\n            \"manage_hsb_func\": {\"default\": self._manage_hsb},\r\n        }\r\n\r\n        self._tkhtml_options = {\r\n            \"zoom\": {\"default\": 1.0},\r\n            \"fontscale\": {\"default\": 1.0},\r\n            \"parsemode\": {\"default\": utilities.DEFAULT_PARSE_MODE},\r\n            # Shrink seems to cause segfaults when changed after the widget loads\r\n            \"shrink\": {\"default\": False, \"changeable\": False},\r\n            \"textwrap\": {\"default\": \"auto\", \"type\": \"autobool\"},\r\n            \"mode\": {\"default\": utilities.DEFAULT_ENGINE_MODE},\r\n            \"defaultstyle\": {\"default\": \"\"},\r\n            \"height\": {\"default\": 0},\r\n            \"width\": {\"default\": 0},\r\n        }\r\n        \r\n        \r\n        self._check_options(self._htmlframe_options, init_args, kwargs, True)\r\n        _tkinterweb_options = self._check_options(self._tkinterweb_options, init_args, kwargs)\r\n        _tkhtml_options = self._check_options(self._tkhtml_options, init_args, kwargs)\r\n\r\n\r\n        super().__init__(master, **kwargs)\r\n\r\n        # Setup sub-widgets\r\n        self._html = html = bindings.TkinterWeb(self, _tkinterweb_options, **_tkhtml_options)\r\n        self._hsb = hsb = subwidgets.AutoScrollbar(self, orient=\"horizontal\", command=html.xview)\r\n        self._vsb = vsb = subwidgets.AutoScrollbar(self, orient=\"vertical\", command=html.yview)\r\n\r\n        html.configure(xscrollcommand=hsb.set, yscrollcommand=vsb.set)\r\n\r\n        self.columnconfigure(0, weight=1)\r\n        self.rowconfigure(0, weight=1)\r\n        html.grid(row=0, column=0, sticky=\"nsew\")\r\n        hsb.grid(row=1, column=0, sticky=\"nsew\")\r\n        vsb.grid(row=0, column=1, sticky=\"nsew\")\r\n\r\n        self._manage_hsb()\r\n        self._manage_vsb()\r\n\r\n        # html.document only applies to the document it is bound to (which makes things easy)\r\n        # Html applies to all html widgets\r\n        # For some reason, binding to Html only works on Linux/Unix and binding to html.document only works on Windows\r\n        # Html fires on all documents (i.e. <iframe> elements), so it has to be handled slightly differently\r\n        if not self._html.overflow_scroll_frame:\r\n            self.bind_class(\"Html\", \"<Button-4>\", html._scroll_x11)\r\n            self.bind_class(\"Html\", \"<Button-5>\", html._scroll_x11)\r\n            self.bind_class(\"Html\", \"<Shift-Button-4>\", html._xscroll_x11)\r\n            self.bind_class(\"Html\", \"<Shift-Button-5>\", html._xscroll_x11)\r\n\r\n        for i in (f\"{html}.document\", html.scrollable_node_tag):\r\n            self.bind_class(i, \"<MouseWheel>\", html._scroll)\r\n            self.bind_class(i, \"<Shift-MouseWheel>\", html._xscroll)\r\n\r\n        self.bind_class(html.scrollable_node_tag, \"<Button-4>\", lambda event, widget=html: html._scroll_x11(event, widget))\r\n        self.bind_class(html.scrollable_node_tag, \"<Button-5>\", lambda event, widget=html: html._scroll_x11(event, widget))\r\n        self.bind_class(html.scrollable_node_tag, \"<Shift-Button-4>\", lambda event, widget=html: html._xscroll_x11(event, widget))\r\n        self.bind_class(html.scrollable_node_tag, \"<Shift-Button-5>\", lambda event, widget=html: html._xscroll_x11(event, widget))\r\n\r\n        # Overwrite the default bindings for scrollbars so that:\r\n        # A) scrolling on the page while loading stops it from tracking the fragment\r\n        # B) scrolling horizontally on a vertical scrollbar scrolls horizontally (the default is to scroll vertically)\r\n        # C) scrolling vertically on a horizontal scrollbar scrolls vertically (the default is to block scrolling)\r\n        for i in (vsb, hsb):\r\n            i.bind(\"<Button-4>\", lambda event, widget=html: html._scroll_x11(event, widget))\r\n            i.bind(\"<Button-5>\", lambda event, widget=html: html._scroll_x11(event, widget))\r\n            i.bind(\"<MouseWheel>\", html._scroll)\r\n            i.bind(\"<Shift-Button-4>\", lambda event, widget=html: html._xscroll_x11(event, widget))\r\n            i.bind(\"<Shift-Button-5>\", lambda event, widget=html: html._xscroll_x11(event, widget))\r\n            i.bind(\"<Shift-MouseWheel>\", html._xscroll)\r\n            i.bind(\"<Enter>\", html._on_leave)\r\n\r\n        self.bind(\"<Leave>\", html._on_leave)\r\n        self.bind(\"<Enter>\", html._on_mouse_motion)\r\n        self.bind_class(html.tkinterweb_tag, \"<Configure>\", self._handle_html_resize)\r\n        \r\n        if shrink: super().bind(\"<Configure>\", self._handle_frame_resize)\r\n\r\n    @property\r\n    def title(self):\r\n        \"\"\"The document's title.\r\n\r\n        :rtype: str\"\"\"\r\n        return self._html.title\r\n    \r\n    @property\r\n    def icon(self):\r\n        \"\"\"The document icon's url.\r\n        \r\n        :rtype: str\"\"\"\r\n        return self._html.icon\r\n    \r\n    @property\r\n    def current_url(self):\r\n        \"\"\"The document's url.\r\n        \r\n        :rtype: str\"\"\"\r\n        return self._current_url\r\n    \r\n    @property\r\n    def base_url(self):\r\n        \"\"\"The documents's base url. This is automatically generated from but will also change if explicitly specified by the document.\r\n        \r\n        :rtype: str\"\"\"\r\n        return self._html.base_url\r\n        \r\n    @property # could use utilities.lazy_manager(None) and save some work, but then autocomplete fails\r\n    def document(self):\r\n        \"\"\"The DOM manager. Use this to access :class:`~tkinterweb.dom.HTMLDocument` methods to manupulate the DOM.\r\n        \r\n        :rtype: :class:`~tkinterweb.dom.HTMLDocument`\"\"\"\r\n        try:\r\n            return self._document\r\n        except AttributeError:\r\n            self._document = dom.HTMLDocument(self._html)\r\n            return self._document\r\n\r\n    @property\r\n    def javascript(self):\r\n        \"\"\"The JavaScript manager. Use this to access :class:`~tkinterweb.js.JSEngine` methods.\r\n        \r\n        :rtype: :class:`~tkinterweb.js.JSEngine`\"\"\"\r\n        try:\r\n            return self._javascript\r\n        except AttributeError:\r\n            self._javascript = js.JSEngine(self._html, self.document, self.javascript_backend)\r\n            return self._javascript\r\n\r\n    @property\r\n    def html(self):\r\n        \"\"\"The underlying html widget. Use this to access internal :class:`~tkinterweb.TkinterWeb` methods.\r\n        \r\n        :rtype: :class:`~tkinterweb.TkinterWeb`\"\"\"\r\n        return self._html\r\n    \r\n    def grid_propagate(self, *args, **kwargs):\r\n        \"\"\r\n        utilities.warn(\"grid_propagate is being ignored, because since version 4.13 widget geometry is always respected by default. If this is a problem, please file a bug report.\")\r\n        pass\r\n\r\n    def pack_propagate(self, *args, **kwargs):\r\n        \"\"\r\n        utilities.warn(\"pack_propagate is being ignored, because since version 4.13 widget geometry is always respected by default. If this is a problem, please file a bug report.\")\r\n        pass\r\n\r\n    def load_html(self, html_source, base_url=None, fragment=None):\r\n        \"\"\"Clear the current page and parse the given HTML code.\r\n        \r\n        :param html_source: The HTML code to render.\r\n        :type html_source: str\r\n        :param base_url: The base url to use when parsing stylesheets and images. If this argument is not supplied, it will be set to the current working directory.\r\n        :type base_url: str, optional\r\n        :param fragment: The url fragment to scroll to after the document loads.\r\n        :type fragment: str, optional\"\"\"        \r\n\r\n        if base_url == None:\r\n            path = utilities.WORKING_DIR\r\n            if not path.startswith(\"/\"):\r\n                path = f\"/{path}\"\r\n            base_url = f\"file://{path}/\"\r\n\r\n        self._current_url = \"\"\r\n\r\n        self._load_html(html_source, base_url, fragment)    \r\n\r\n    def _load_html(self, html_source, base_url=None, fragment=None, _thread_safe=False):\r\n        if self._thread_in_progress:\r\n            self._thread_in_progress.stop()\r\n        if fragment: \r\n            fragment = \"\".join(char for char in fragment if char.isalnum() or char in (\"-\", \"_\", \".\")).replace(\".\", r\"\\.\")\r\n\r\n        self._html.reset(_thread_safe)\r\n        self._html.base_url = base_url\r\n        self._html.fragment = fragment\r\n        self._html.parse(html_source, _thread_safe)\r\n\r\n        if _thread_safe:\r\n            self._html.post_to_queue(self._finish_loading_html)\r\n        else:\r\n            self._finish_loading_html()\r\n    \r\n    def _finish_loading_html(self):\r\n        # NOTE: must be run from main thread\r\n        \r\n        self._finish_css()\r\n        self._handle_html_resize(force=True)\r\n\r\n    def load_file(self, file_url, decode=None, force=False):\r\n        \"\"\"Convenience method to load a local HTML file.\r\n\r\n        This method will always load the file in the main thread. If you want to load the file in a seperate thread, use :meth:`HtmlFrame.load_url`.\r\n        \r\n        :param file_url: The HTML file to render.\r\n        :type file_url: str\r\n        :param decode: The decoding to use when loading the file.\r\n        :type decode: str or None, optional\r\n        :param force: Force the page to reload all elements.\r\n        :type force: bool, optional\"\"\"\r\n        self._previous_url = self._current_url\r\n        if not file_url.startswith(\"file://\"):\r\n            file_url = \"file://\" + str(file_url)\r\n        self.load_url(file_url, decode, force)\r\n\r\n    def load_website(self, website_url, decode=None, force=False):\r\n        \"\"\"Convenience method to load a website.\r\n        \r\n        :param website_url: The url to load.\r\n        :type website_url: str\r\n        :param decode: The decoding to use when loading the website.\r\n        :type decode: str or None, optional\r\n        :param force: Force the page to reload all elements.\r\n        :type force: bool, optional\"\"\"\r\n        self._previous_url = self._current_url\r\n        if (not website_url.startswith(\"https://\")) and (not website_url.startswith(\"http://\")) and (not website_url.startswith(\"about:\")):\r\n            website_url = \"http://\" + str(website_url)\r\n        self.load_url(website_url, decode, force)\r\n\r\n    def load_url(self, url, decode=None, force=False):\r\n        \"\"\"Loads and renders HTML from the given url. \r\n        \r\n        A local file will be loaded if the url begins with \"file://\". \r\n        A website will be loaded if the url begins with \"https://\" or \"http://\". \r\n        If the url begins with \"view-source:\", the source code of the webpage will be displayed. \r\n        Loading \"about:tkinterweb\" will open a page with debugging information.\r\n        \r\n        :param url: The url to load.\r\n        :type url: str\r\n        :param decode: The decoding to use when loading the url.\r\n        :type decode: str or None, optional\r\n        :param force: Force the page to reload all elements.\r\n        :type force: bool, optional\"\"\"\r\n        ### TODO: Maybe consider merging load_url, load_file, and load_website into one\r\n        ### One could use the checker from the sample web browser\r\n        if not self._current_url == url:\r\n            self._previous_url = self._current_url\r\n        if url in utilities.BUILTIN_PAGES:\r\n            utilities.BUILTIN_PAGES._html = self._html\r\n            self._update_current_url(url, False)\r\n            return self._load_html(self._get_about_page(url), url)\r\n\r\n        self._waiting_for_reset = True\r\n\r\n        # Set the base url now in case it takes a while for the website to download\r\n        self._html.base_url = url\r\n\r\n        if self._thread_in_progress:\r\n            self._thread_in_progress.stop()\r\n            \r\n        if not self._html.threading_enabled or url.startswith(\"file://\"):\r\n            #or self._html._check_url_cache_state(url, \"\", \"GET\", decode):\r\n            self._continue_loading(url, decode=decode, force=force)\r\n        else:\r\n            thread = utilities.StoppableThread(target=self._continue_loading, args=(\r\n                url,), kwargs={\"decode\": decode, \"force\": force, \"thread_safe\": True})\r\n            self._thread_in_progress = thread\r\n            thread.start()            \r\n\r\n    def load_form_data(self, url, data, method=\"GET\", decode=None, force=False):\r\n        \"\"\"Submit form data to a server and load the response.\r\n        \r\n        :param url: The url to load.\r\n        :type url: str\r\n        :param data: The data to pass to the server.\r\n        :type data: str\r\n        :param method: The form submission method.\r\n        :type method: \"GET\" or \"POST\", optional\r\n        :param decode: The decoding to use when loading the file.\r\n        :type decode: str or None, optional\r\n        :param force: Force the page to reload all elements. New in version 4.24.\r\n        :type force: bool, optional\"\"\"\r\n        self._previous_url = self._current_url\r\n        if self._thread_in_progress:\r\n            self._thread_in_progress.stop()\r\n        if self._html.threading_enabled:\r\n            thread = utilities.StoppableThread(\r\n                target=self._continue_loading, args=(url, data, method, decode, force, True))\r\n            self._thread_in_progress = thread\r\n            thread.start()\r\n        else:\r\n            self._continue_loading(url, data, method, decode)\r\n\r\n    def reload(self):\r\n        \"\"\"Reload the page. This only affects pages loaded from a url.\r\n        \r\n        New in version 4.21\"\"\"\r\n\r\n        if self._current_url:\r\n            if self._current_data:\r\n                self.load_form_data(self._current_url, self._current_data, \"POST\")\r\n            else:\r\n                self.load_url(self._current_url, force=True)\r\n        # else, we could snapshot the page and load that\r\n        # But I think that's completely useless\r\n\r\n    def add_html(self, html_source, return_element=False, index=-1):\r\n        \"\"\"Parse HTML and add it to the end of the current document. Unlike :meth:`HtmlFrame.load_html`, :meth:`HtmlFrame.add_html` adds rendered HTML code without clearing the original document.\r\n        \r\n        :param html_source: The HTML code to render.\r\n        :type html_source: str\r\n        :param return_element: If True, return the root element of the added HTML.\r\n        :type return_element: bool, optional\r\n        :param index: The index of the element to insert before. Default -1. New in version 4.22.\r\n        :type index: int, optional\r\n        :return: :class:`~tkinterweb.dom.HTMLElement` or None\"\"\"\r\n\r\n        self._previous_url = \"\"\r\n        node = None\r\n        \r\n        if return_element or index != -1:\r\n            node = self._html.parse_fragment(html_source)\r\n            body = self.document.body.node\r\n            if index == -1:\r\n                self._html.insert_node(body, node)\r\n            else:\r\n                child = self._html.get_node_children(body)[index]\r\n                self._html.insert_node_before(body, node, child)\r\n            if return_element:\r\n                node = dom.HTMLElement(self.document, node)\r\n        else:\r\n            self._html.parse(html_source)\r\n\r\n        self._finish_css()\r\n        self._handle_html_resize(force=True)\r\n\r\n        return node\r\n    \r\n    def insert_html(self, html_source, index=0, return_element=False):\r\n        utilities.deprecate_param(\"insert_html\", \"add_html\")\r\n        self.add_html(html_source, return_element, index)\r\n    \r\n    def add_css(self, css_source, priority=\"author\"):\r\n        \"\"\"Send CSS stylesheets to the parser. This can be used to alter the appearance of already-loaded documents.\r\n        \r\n        :param css_source: The CSS code to parse.\r\n        :type css_source: str\r\n        :param priority: The priority of the CSS code. CSS code loaded by webpages is \"user\" priority. \"agent\" is lower priority than \"user\" and \"author\" (the default) is higher .\r\n        :type priority: \"agent\", \"user\", or \"author\"\r\n        \"\"\"\r\n        if self._waiting_for_reset:\r\n            self._accumulated_styles.append(css_source)\r\n        else:\r\n            self._html.parse_css(data=css_source, fallback_priority=priority)\r\n\r\n    def import_css(self, url):\r\n        \"\"\"Add a CSS stylesheet given a url.\r\n\r\n        :param url: The url of the CSS stylesheet.\r\n        :type url: str\r\n        \r\n        New in version 4.19.\"\"\"\r\n        self._html.style_manager._on_atimport(self._html.base_url, url)\r\n\r\n    def stop(self):\r\n        \"\"\"Stop loading this page and abandon all pending requests.\"\"\"\r\n        if self._thread_in_progress:\r\n            self._thread_in_progress.stop()\r\n            self._update_current_url(self._previous_url, False)\r\n        self._html.stop()\r\n        self._html.post_event(utilities.DONE_LOADING_EVENT)\r\n\r\n    def find_text(self, text, select=1, ignore_case=True, highlight_all=True, detailed=False):\r\n        \"\"\"Search the document for text and highlight matches. \r\n        \r\n        :param text: The Regex expression to use to find text. If this is set to a blank string (\"\"), all highlighted text will be cleared.\r\n        :type text: str\r\n        :param select: The index of the match to select and scroll to. Use this to implement find next/find previous functionality.\r\n        :type select: int, optional\r\n        :param ignore_case: If True, uppercase and lowercase letters will be treated as the same character.\r\n        :type ignore_case: bool, optional\r\n        :param highlight_all: If True, all matches will be highlighted.\r\n        :type highlight_all: bool, optional\r\n        :param detailed: If True, this method will also return information on the nodes that were found. See `bug #93 <https://github.com/Andereoo/TkinterWeb/issues/93#issuecomment-2052516492>`_ for more details.\r\n        :type detailed: bool, optional\r\n        :return: The number of matches.\r\n        :rtype: int\"\"\"\r\n        # TODO: maybe add option to not highlight but return corresponding HTMLElements/indexes?\r\n        nmatches, selected, matches = self._html.search_manager.find_text(text, select, ignore_case, highlight_all)\r\n        if detailed:\r\n            return nmatches, selected, matches\r\n        else:\r\n            return nmatches\r\n    \r\n    def widget_to_element(self, widget):\r\n        \"\"\"Get the HTML element containing the given widget.\r\n        \r\n        :param widget: The widget to search for.\r\n        :type widget: :py:class:`tkinter.Widget`\r\n        :return: The element containing the given widget.\r\n        :rtype: :class:`~tkinterweb.dom.HTMLElement`\r\n        :raise KeyError: If the given widget is not in the document.\r\n        \r\n        New in version 4.2.\"\"\"\r\n        for node in self._html.search(f\"[{self._html.widget_manager.widget_container_attr}]\"):\r\n            if self._html.get_node_attribute(node, self._html.widget_manager.widget_container_attr) == str(widget):\r\n                return dom.HTMLElement(self.document, node)\r\n        raise KeyError(\"the specified widget is not in the document\")\r\n\r\n    def screenshot_page(self, filename=None, full=False, show=False):\r\n        \"\"\"Take a screenshot. \r\n        \r\n        This command should be used with care on large documents if :attr:`full` is set to True, as it may generate very large images that take a long time to create and consume large amounts of memory.\r\n\r\n        On Windows, if experimental mode is not enabled, ensure you run ``ctypes.windll.shcore.SetProcessDpiAwareness(1)`` before creating your Tkinter window or else the screenshot may be badly offset. On Windows it's good practice to run this anyway.\r\n        \r\n        :param filename: The file path to save the screenshot to. If None, the image is not saved to the disk.\r\n        :type filename: str or None, optional\r\n        :param full: If True, the entire page is captured. On Windows, experimental mode must be enabled. If False, only the visible content is captured.\r\n        :type full: bool, optional\r\n        :param show: Display the screenshot in the default system handler.\r\n        :type show: bool, optional\r\n        :return: A PIL Image containing the rendered document.\r\n        :rtype: :py:class:`PIL.Image`\r\n        :raise NotImplementedError: If experimental mode is not enabled, :attr:`full` is set to True, and TkinterWeb is running on Windows.\"\"\"\r\n        if self._html.experimental or utilities.PLATFORM.system != \"Windows\":\r\n            self._html.post_message(f\"Taking a screenshot of {self._current_url}...\")\r\n            data = self._html.image(full=full)\r\n            height = len(data)\r\n            width = len(data[0].split())\r\n            image = imageutils.create_RGB_image(data, width, height)\r\n        elif not full:\r\n            # Vanilla Tkhtml image does not work on Windows\r\n            # We use PIL's ImageGrab instead for visible content\r\n            # We could also use this for visible content on other systems\r\n            # It's faster than Tkhtml image, but it does not work on Wayland and is less foolproof\r\n            from PIL import ImageGrab\r\n\r\n            x = self.winfo_rootx()\r\n            y = self.winfo_rooty()\r\n            width = self.winfo_width()\r\n            height = self.winfo_height()\r\n            \r\n            image = ImageGrab.grab(bbox=(x, y, x+width, y+height))\r\n        else:\r\n            self._html.post_message(\"ERROR: A screenshot could not be taken because screenshot_page(full=True) is an experimental feature on Windows\")\r\n            raise NotImplementedError(\"a screenshot could not be taken because screenshot_page(full=True) is an experimental feature on Windows\")\r\n        \r\n        if filename:\r\n            image.save(filename)\r\n            self._html.post_message(f\"Screenshot taken: {width}px by {height}px!\")\r\n        if show:\r\n            image.show()\r\n        return image\r\n\r\n    def print_page(self, filename=None, cnf={}, **kwargs):\r\n        \"\"\"Print the document to a PostScript file. \r\n        \r\n        This method is experimental and requires experimental mode to be enabled.\r\n\r\n        :param filename: The file path to print the page to. If None, the image is not saved to the disk.\r\n        :type filename: str or None, optional\r\n        :param kwargs: Other valid options are colormap, colormode, file, fontmap, height, pageanchor, pageheight, pagesize (can be A3, A4, A5, LEGAL, and LETTER), pagewidth, pagex, pagey, nobg, noimages, rotate, width, x, and y.\r\n        :return: A string containing the PostScript code.\r\n        :rtype: str\r\n        :raise NotImplementedError: If experimental mode is not enabled.\"\"\"\r\n        if self._html.experimental:\r\n            cnf |= kwargs\r\n            self._html.post_message(f\"Printing {self._current_url}...\")\r\n            if filename:\r\n                cnf[\"file\"] = filename\r\n            if \"pagesize\" in cnf:\r\n                pagesizes = {\r\n                    \"A3\": \"842x1191\", \"A4\": \"595x842\", \"A5\": \"420x595\",\r\n                    \"Legal\": \"612x792\", \"Letter\": \"612x1008\"\r\n                }\r\n                try:\r\n                    cnf[\"pagesize\"] = pagesizes[cnf[\"pagesize\"].upper()]\r\n                    self._html.post_message(f\"Setting printer page size to {cnf['pagesize']} PostScript points.\")\r\n                except KeyError:\r\n                    raise KeyError(\"Parameter 'pagesize' must be A3, A4, A5, Legal, or Letter\")\r\n\r\n            self._html.update() # Update the root window to ensure HTML is rendered\r\n            file = self._html.postscript(cnf)\r\n            \r\n            # No need to save - Tkhtml handles that for us\r\n            if filename:\r\n                self._html.post_message(\"Printed!\")\r\n            if file: return file\r\n        else:\r\n            self._html.post_message(\"ERROR: The page could not be printed because print_page is an experimental feature\")\r\n            raise NotImplementedError(\"the page could not be printed because print_page is an experimental feature\")\r\n\r\n    def save_page(self, filename=None):\r\n        \"\"\"Return the page's HTML code or save the page as an HTML file.\r\n\r\n        As of version 4.23, if caching is enabled and a url is loaded, this method returns the page's original HTML.\r\n        Otherwise, the returned page will be the output of :meth:`HtmlFrame.snapshot_page`, with the contents of the ``<head>`` tag included if the widget is still loading.\r\n\r\n        :param filename: The file path to save the page to. If None, the page is not saved to the disk.\r\n        :type filename: str or None, optional\r\n        :return: A string containing the page's HTML/CSS code.\r\n        :rtype: str\"\"\"\r\n        ### TODO: consider combining this method with snapshot_page\r\n        \r\n        ### TODO: I don't like that snapshot_page is used here, but serialize_node omits BOTH style and scripts\r\n        ### At least snapshot_page doesn't omit styles\r\n        ### But I suppose that in the majority of cases, either caching is enabled and a url is loaded, in which case we're good,\r\n        ### or load_html is used, in which case users don't really need to use this at all\r\n\r\n        if self._html.active_threads: include_head=True\r\n        else: include_head=False\r\n    \r\n        method = \"POST\" if self._current_data else \"GET\"\r\n        \r\n        if self._current_url and self.html.caches_enabled and \\\r\n            self._html._check_url_cache_state(self.current_url, self._current_data, method):\r\n            try:\r\n                _, html, _, _ = self._html.download_url(self._current_url, self._current_data, method)\r\n            except Exception:\r\n                html = self.snapshot_page(include_head=include_head)\r\n        else:\r\n            html = self.snapshot_page(include_head=include_head)\r\n\r\n        if filename:\r\n            self._html.post_message(f\"Saving {self._current_url}...\")\r\n            with open(filename, \"w+\") as handle:\r\n                handle.write(html)\r\n            self._html.post_message(\"Saved!\")\r\n        return html\r\n    \r\n    def snapshot_page(self, filename=None, allow_agent=False, include_head=False):\r\n        \"\"\"Save a snapshot of the document. \r\n        \r\n        Unlike :py:func:`save_page`, which returns the original document, :py:func:`snapshot_page` returns the page as rendered. By default ``<link>`` elements are ignored and instead one ``<style>`` element contains all of the necessary CSS information for the document. This can be useful for saving documents for offline use.\r\n                \r\n        :param filename: The file path to save the page to. If None, the page is not saved to the disk.\r\n        :type filename: str or None, optional\r\n        :param allow_agent: If True, CSS properties added by the rendering engine (eg. those affected by the widget's :attr:`default_style` option) are also included.\r\n        :type allow_agent: bool, optional\r\n        :param include_head: If True, the contents of the page's ``<head>`` element (i.e. ``<link>`` and ``<meta>`` tags) are included. Default False. New in version 4.23.\r\n        :type include_head: bool, optional\r\n        :return: A string containing the page's rendered HTML/CSS code.\r\n        :rtype: str\"\"\"\r\n        ### TODO: scripts are omitted\r\n\r\n        self._html.post_message(f\"Snapshotting {self._current_url}...\")\r\n        title = \"\"\r\n        icon = \"\"\r\n        base = \"\"\r\n        other = \"\"\r\n        style = \"\\n\"\r\n        tab = \"   \"\r\n        \r\n        for rule in self._html.get_computed_styles():\r\n            selector, prop, origin = rule\r\n            if origin == \"agent\" and not allow_agent: continue\r\n            style += f\"{tab*3}{selector} {{{prop.replace('-tkhtml-no-color', 'transparent')}}}\\n\"\r\n\r\n        if self._html.title: title = f\"\\n{tab*2}<title>{self._html.title}</title>\"\r\n        if self._html.icon: icon = f\"\\n{tab*2}<link rel=\\\"icon\\\" type=\\\"image/x-icon\\\" href=\\\"/{self._html.icon}\\\">\"\r\n        if self._html.base_url: base = f\"\\n{tab*2}<base href=\\\"{self._html.base_url}\\\"></base>\"\r\n        if style.strip(): style = f\"\\n{tab*2}<style>{style}{tab*2}</style>\"\r\n        if include_head:\r\n            try: other = self.document.querySelector(\"head\").innerHTML\r\n            except tk.TclError: pass\r\n\r\n        html = f\"\"\"<html>\\n{tab}<head>{title}{icon}{base}{other}{style}\\n{tab}</head>\\n{indent(self.document.body.outerHTML, tab*2)}\\n</html>\"\"\"\r\n        if filename:\r\n            with open(filename, \"w+\") as handle:\r\n                handle.write(html)\r\n            self._html.post_message(\"Saved!\")\r\n        return html\r\n    \r\n    def get_page_text(self):\r\n        \"\"\"Return the page's text content.\r\n        \r\n        :return: A string containing the page's text content.\r\n        :rtype: str\r\n        \r\n        New in version 4.8.\"\"\"\r\n        return self._html.text(\"text\")\r\n    \r\n    def show_error_page(self, url, error, code):\r\n        \"\"\"Show the error page.\r\n        \r\n        :param url: The url of the broken page.\r\n        :type url: str\r\n        :param error: The error message.\r\n        :type error: str\r\n        :param code: The HTTP error code.\r\n        :type code: str\r\n        \"\"\"\r\n        if self.winfo_exists():\r\n            if not self._button:\r\n                self._button = tk.Button(self, text=\"Try Again\", command=self.reload)\r\n            self._load_html(self._get_about_page(\"about:error\", code, self._button), url)\r\n\r\n    def resolve_url(self, url):\r\n        \"\"\"Generate a full url from the specified url. This can be used to generate full urls when given a relative url.\r\n\r\n        :param url: The url to modify if needed.\r\n        :type url: str\r\n        :return: The full, resolved url.\r\n        :rtype: str\"\"\"\r\n        return self._html.resolve_url(url)\r\n\r\n    def yview(self, *args):\r\n        \"\"\"Adjust the viewport. \r\n        \r\n        This method uses the standard interface copied from other built-in scrollable Tkinter widgets. Additionally, if a Tkhtml3 node is supplied as an argument, the document will scroll to the top of the given node.\"\"\"\r\n        self._html.yview(*args)\r\n\r\n    def yview_moveto(self, number):\r\n        \"\"\"Moves the view vertically to the specified position.\r\n        \r\n        :param number: The position to scroll to.\r\n        :type number: float\"\"\"\r\n        self._html.yview_moveto(number)\r\n\r\n    def yview_scroll(self, number, what):\r\n        \"\"\"Shifts the view in up or down.\r\n        \r\n        :param number: Specifies the number of 'whats' to scroll by; make positive to scroll down or negative to scroll up.\r\n        :type number: int\r\n        :param what: Either \"units\" or \"pages\"\r\n        :type what: str\"\"\"\r\n        self._html.yview_scroll(number, what)\r\n        \r\n    def get_currently_hovered_element(self, ignore_text_nodes=True):\r\n        \"\"\"Get the element under the mouse. Particularly useful for creating right-click menus or displaying hints when the mouse moves.\r\n        \r\n        :param ignore_text_nodes: If True, text nodes (i.e. the contents of a ``<p>`` element) will be ignored and their parent node returned. It is generally best to leave leave this at the default.\r\n        :type ignore_text_nodes: bool, optional\r\n        :return: The element under the mouse.\r\n        :rtype: :class:`~tkinterweb.dom.HTMLElement` or None\"\"\"\r\n        if not self._html.current_hovered_node:\r\n            return None\r\n        \r\n        if ignore_text_nodes:\r\n            node = self._html.hovered_nodes[0]\r\n        else:\r\n            node = self._html.current_hovered_node\r\n        return dom.HTMLElement(self.document, node)\r\n    \r\n    def get_caret_position(self, return_element=True):\r\n        \"\"\"Get the position of the caret. This can be used to modify the document's text when the user types. \r\n\r\n        :param return_element: If True, this method will also return information on the nodes that were found.\r\n        :type return_element: bool, optional\r\n\r\n        If ``return_element=True``:\r\n\r\n            :return: The :class:`~tkinterweb.dom.HTMLElement` under the caret, the element's :class:`~tkinterweb.dom.HTMLElement.textContent`, and an index representing the position in that string that the caret is at. If the caret is not visible, this method will return None.\r\n            :rtype: :class:`~tkinterweb.dom.HTMLElement`, str, and int, or None\r\n\r\n            The element returned will always be a text node. If you need to change the style or HTML content of a text node you will first need to get its parent.\r\n\r\n        If ``return_element=False``:\r\n\r\n            :return: The text content of the page, and an index representing the position in that string that the caret is at. If the caret is not visible, this method will return None.\r\n            :rtype: str and int, or None \r\n        \r\n        Changed in version 4.16.\"\"\"\r\n        if self._html.caret_manager.node:\r\n            if return_element:\r\n                text, index = self._html.tkhtml_offset_to_text_index(self._html.caret_manager.node, self._html.caret_manager.offset)\r\n                return dom.HTMLElement(self.document, self._html.caret_manager.node), text, index\r\n            else:\r\n                return self._html.text(\"text\"), self._html.text(\"offset\", self._html.caret_manager.node, self._html.caret_manager.offset)\r\n        else:\r\n            return None\r\n        \r\n    def get_caret_page_position(self):\r\n        utilities.deprecate_param(\"get_caret_page_position\", \"get_caret_position(return_element=False)\")\r\n        pos = self.get_caret_position(False)\r\n        if pos: pos = pos[1]\r\n        return pos\r\n        \r\n    def set_caret_position(self, element=None, index=0):\r\n        \"\"\"Set the position of the caret, given an index and, optionally, an HTML element.\r\n\r\n        If the given index extends out of the bounds of the given element, the caret will be moved into the preceeding or following elements.\r\n        \r\n        :param element: Specifies the element to place the caret in. This element must be a text node, must contain text, and must be visible.\r\n        :type element, optional: :class:`~tkinterweb.dom.HTMLElement`\r\n        :param index: The index in the element's :class:`~tkinterweb.dom.HTMLElement.textContent` to place the caret at. If ``element`` is None, the index will be used relative to the page's text content.\r\n        :type index: int\r\n\r\n        :raise RuntimeError: If caret browsing is disabled or the given element is empty or has been removed.\r\n        \r\n        Changed in version 4.16.\"\"\"\r\n        if not self._html.caret_browsing_enabled:\r\n            # This is here not because things break when caret browsing is disabled,\r\n            # But because I bet someone somewhere is trying to set the caret's position\r\n            # With caret browsing disabled and pulling their hair out over it\r\n            raise RuntimeError(\"cannot modify the caret when caret browsing is disabled\")\r\n        \r\n        if element:\r\n            text, offset = self._html.tkhtml_offset_to_text_index(element.node, index, True)\r\n            if not self._html.bbox(element.node):\r\n                raise RuntimeError(f\"the element {element} is not visible.\")\r\n            if text == \"\":\r\n                raise RuntimeError(f\"the element {element} either is empty or is not a text node. Either provide a different element or set the caret's position using set_caret_page_position.\")\r\n            self._html.caret_manager.set(element.node, offset, recalculate=True)\r\n        else:\r\n            self._html.caret_manager.set(None, index, recalculate=True)\r\n\r\n    def set_caret_page_position(self, index):\r\n        utilities.deprecate_param(\"set_caret_page_position\", f\"set_caret_position(index={index})\")\r\n        self.set_caret_position(index=index)\r\n\r\n    def shift_caret_left(self):\r\n        \"\"\"Shift the caret left. \r\n        If the caret is at the beginning of a node, this method will move the caret to the end of the previous text node.\r\n\r\n        :raise RuntimeError: If caret browsing is disabled.\r\n        \r\n        New in version 4.8.\"\"\"\r\n        if not self._html.caret_browsing_enabled:\r\n            raise RuntimeError(\"cannot shift the caret when caret browsing is disabled\")\r\n        \r\n        self._html.caret_manager.shift_left()\r\n\r\n    def shift_caret_right(self):\r\n        \"\"\"Shift the caret right. \r\n        If the caret is at the end of a node, this method will move the caret to the beginning of the next text node.\r\n\r\n        :raise RuntimeError: If caret browsing is disabled.\r\n        \r\n        New in version 4.8.\"\"\"\r\n        if not self._html.caret_browsing_enabled:\r\n            raise RuntimeError(\"cannot shift the caret when caret browsing is disabled\")\r\n        \r\n        self._html.caret_manager.shift_right()\r\n\r\n    def get_selection_position(self, return_elements=True):\r\n        \"\"\"Get the start position, end position, and, optionally, contained elements of selected text.\r\n        \r\n        :param return_elements: If True, this method will also return information on the nodes that were found.\r\n        :type return_elements: bool, optional\r\n\r\n        If ``return_elements=True``:\r\n\r\n            :return: \r\n                - A tuple containing:\r\n                    - The :class:`~tkinterweb.dom.HTMLElement` containing the start of the selection\r\n                    - The text content of that element\r\n                    - An index representing the position in that string that the selection begins at\r\n                - A second tuple containing:\r\n                    - The :class:`~tkinterweb.dom.HTMLElement` containing the end of the selection\r\n                    - The text content of that element\r\n                    - An index representing the position in that string that the selection ends at\r\n                - A list containing an :class:`~tkinterweb.dom.HTMLElement` for each other element under the selection, in sequencial order.\r\n                \r\n            :rtype: A pair of (:class:`~tkinterweb.dom.HTMLElement`, str, int) tuples and a list of :class:`~tkinterweb.dom.HTMLElement` objects, or None\r\n\r\n            The elements returned will always be text nodes. If you need to change the style or HTML content of a text node you will first need to get its parent.\r\n            \r\n        If ``return_elements=False``:\r\n            \r\n            :return: The document's text, and two indexes representing the selection's start and end positions in that string. \r\n            :rtype: str, int, and int, or None\r\n\r\n        If no selection is found, this method will return None\r\n        \r\n        Changed in version 4.16.\"\"\"\r\n\r\n        if self._html.selection_manager.selection_start_node and self._html.selection_manager.selection_end_node:\r\n            if return_elements:\r\n                if self._html.selection_manager.selection_start_node != self._html.selection_manager.selection_end_node:\r\n                    start_index = self._html.text(\"offset\", self._html.selection_manager.selection_start_node, self._html.selection_manager.selection_start_offset)\r\n                    end_index = self._html.text(\"offset\", self._html.selection_manager.selection_end_node, self._html.selection_manager.selection_end_offset)\r\n                    true_start_index, true_end_index = sorted([start_index, end_index])\r\n\r\n                    if start_index == true_start_index: # ensure that the output is independent of selection direction\r\n                        start_node, end_node = self._html.selection_manager.selection_start_node, self._html.selection_manager.selection_end_node\r\n                        start_offset, end_offset = self._html.selection_manager.selection_start_offset, self._html.selection_manager.selection_end_offset\r\n                    else:\r\n                        start_node, end_node = self._html.selection_manager.selection_end_node, self._html.selection_manager.selection_start_node\r\n                        start_offset, end_offset = self._html.selection_manager.selection_end_offset, self._html.selection_manager.selection_start_offset\r\n                    \r\n                    text, index = self._html.tkhtml_offset_to_text_index(start_node, start_offset)\r\n                    text2, index2 = self._html.tkhtml_offset_to_text_index(end_node, end_offset)\r\n\r\n                    contained_nodes = []\r\n                    excluded_nodes = {dom.extract_nested(start_node), dom.extract_nested(end_node)}\r\n                    page_index = true_start_index\r\n                    for page_index in range(true_start_index, true_end_index + 1):\r\n                        node, offset = self._html.text(\"index\", page_index)\r\n                        if node not in contained_nodes and str(dom.extract_nested(node)) not in excluded_nodes:\r\n                            contained_nodes.append(node)\r\n\r\n                    return (\r\n                        (dom.HTMLElement(self.document, start_node), text, index),\r\n                        (dom.HTMLElement(self.document, end_node), text2, index2),\r\n                        list(dom.HTMLElement(self.document, node) for node in contained_nodes),\r\n                    )\r\n                else:\r\n                    element = dom.HTMLElement(self.document, self._html.selection_manager.selection_start_node)\r\n                    start_offset, end_offset = sorted([self._html.selection_manager.selection_start_offset, self._html.selection_manager.selection_end_offset])\r\n                    text, index = self._html.tkhtml_offset_to_text_index(self._html.selection_manager.selection_start_node, start_offset)\r\n                    text2, index2 = self._html.tkhtml_offset_to_text_index(self._html.selection_manager.selection_start_node, end_offset)\r\n                    return (\r\n                        (element, text, index),\r\n                        (element, text2, index2),\r\n                        [],\r\n                    ) \r\n            else:\r\n                start_index = self._html.text(\"offset\", self._html.selection_manager.selection_start_node, self._html.selection_manager.selection_start_offset)\r\n                end_index = self._html.text(\"offset\", self._html.selection_manager.selection_end_node, self._html.selection_manager.selection_end_offset)\r\n                start_index, end_index = tuple(sorted([start_index, end_index]))\r\n                return self._html.text(\"text\"), start_index, end_index\r\n        else:\r\n            return None\r\n        \r\n    def get_selection_page_position(self):\r\n        utilities.deprecate_param(\"get_selection_page_position\", f\"get_selection_position(return_elements=False)\")\r\n        pos = self.get_selection_position(return_elements=False)\r\n        if pos: pos = pos[1:]\r\n        return pos\r\n        \r\n    def set_selection_position(self, start_element=None, start_index=0, end_element=None, end_index=0):\r\n        \"\"\"Set the current selection, given starting and ending text indexes and, optionally, HTML elements.\r\n        \r\n        :param start_element: Specifies the element to begin the selection in. This element must be text nodes, must contain text, and must be visible.\r\n        :type start_element, optional: :class:`~tkinterweb.dom.HTMLElement`\r\n        :param start_index: The index in the element's :class:`~tkinterweb.dom.HTMLElement.textContent` to begin the selection at. If ``start_element`` is None, this index instead is relative to the page's text content.\r\n        :type start_index: int\r\n        :param end_element: Specifies the element to end the selection in. This element must be text nodes, must contain text, and must be visible.\r\n        :type end_element, optional: :class:`~tkinterweb.dom.HTMLElement`\r\n        :param end_index: The index in the element's :class:`~tkinterweb.dom.HTMLElement.textContent` to end the selection at. If ``end_element`` is None, this index instead is relative to the page's text content.\r\n        :type end_index: int\r\n\r\n        :raise RuntimeError: If selection is disabled or the given elements are empty or have been removed.\r\n        \r\n        Changed in version 4.16.\"\"\"\r\n        if not self._html.selection_enabled:\r\n            raise RuntimeError(\"cannot modify the selection when selection is disabled\")\r\n\r\n        if start_element:\r\n            text, start_offset = self._html.tkhtml_offset_to_text_index(start_element.node, start_index, True)\r\n            if not self._html.bbox(start_element.node):\r\n                raise RuntimeError(f\"the element {start_element} is not visible.\")\r\n            if text == \"\":\r\n                raise RuntimeError(f\"the element {start_element} either is empty or is not a text node. Either provide a different element or set the selection using set_selection_page_position.\")\r\n            start_node = start_element.node\r\n        else:\r\n            start_node, start_offset = self._html.text(\"index\", start_index)\r\n\r\n        if end_element:\r\n            text, end_offset = self._html.tkhtml_offset_to_text_index(end_element.node, end_index, True)\r\n            if not self._html.bbox(end_element.node):\r\n                raise RuntimeError(f\"the element {end_element} is not visible.\")\r\n            if text == \"\":\r\n                raise RuntimeError(f\"the element {end_element} either is empty or is not a text node. Either provide a different element or set the selection using set_selection_page_position.\")\r\n            end_node = end_element.node\r\n        else:\r\n            end_node, end_offset = self._html.text(\"index\", end_index)\r\n\r\n        self._html.selection_manager.reset_selection_type()\r\n        self._html.selection_manager.begin_selection(start_node, start_offset)\r\n        self._html.selection_manager.extend_selection(end_node, end_offset)\r\n\r\n    def set_selection_page_position(self, start_index, end_index):\r\n        utilities.deprecate_param(\"set_selection_page_position\", f\"set_selection_position(start_index={start_index}, end_index={end_index})\")\r\n        self.set_selection_position(start_index=start_index, end_index=end_index)\r\n\r\n    def get_selection(self):\r\n        \"\"\"Return any selected text.\r\n\r\n        :return: The current selection.\r\n        :rtype: str\"\"\"\r\n        return self._html.selection_manager.get_selection()\r\n\r\n    def clear_selection(self):\r\n        \"\"\"Clear the current selection.\"\"\"\r\n        self._html.selection_manager.clear_selection()\r\n\r\n    def select_all(self):\r\n        \"\"\"Select all text in the document.\"\"\"\r\n        if not self._html.selection_enabled:\r\n            raise RuntimeError(\"cannot set the selection when selection is disabled\")\r\n\r\n        self._html.selection_manager.select_all()\r\n\r\n    # --- Internals -----------------------------------------------------------\r\n    \r\n    def _handle_html_resize(self, event=None, force=False):\r\n        \"\"\"Make all elements with the 'tkinterweb-full-page' attribute the same height as the html widget.\r\n        This can be used in conjunction with table elements to vertical align pages,\r\n        which is otherwise not possible with Tkhtml. Hopefully we won't need this forever.\"\"\"\r\n        if self._html.cget(\"shrink\"):\r\n            return\r\n\r\n        if event:\r\n            height = event.height\r\n        else:\r\n            height = self._html.winfo_height()\r\n        if self._prev_height != height or force:\r\n            resizeable_elements = self._html.search(f\"[{utilities.BUILTIN_ATTRIBUTES['vertical-align']}]\")\r\n            for node in resizeable_elements:\r\n                self._html.set_node_property(node, \"height\", f\"{height/self['zoom']}px\")\r\n        self._prev_height = height\r\n\r\n        if self._html.caret_browsing_enabled:\r\n            self._html.caret_manager.update()\r\n\r\n    def _handle_frame_resize(self, event):\r\n        # Tkhtml doesn't handle resizing outwards when shrink is enabled\r\n        # Disabling text wrapping works great except that it has no effect on multiple inline text nodes in Tkhtml\r\n\r\n        # When the widget resizes, resize it to the screen's width, and let it shrink back\r\n        # Otherwise, the widget will shrink when it can and return\r\n        # Not ideal, but still less ideal than the default behaviour\r\n\r\n        ### TODO: Needs improvement\r\n        ### TODO: Fix from within Tkhtml???\r\n\r\n        if self.unshrink:\r\n            if event.x and self._prev_configure != (event.width, event.x):\r\n                # if not self._html.using_tkhtml30:\r\n                #    self._html.configure(textwrap=False)\r\n                #    self.after(10, lambda: self._html.configure(textwrap=True))\r\n                self.after_idle(lambda: self._html.configure(\r\n                    width=self.winfo_screenwidth(), \r\n                    height=event.height)\r\n                )\r\n                self._prev_configure = (event.width, event.x)\r\n\r\n    def _adjust_allow(self, allow):\r\n        if allow == \"auto\":\r\n            return 2\r\n        elif allow == \"dynamic\":\r\n            if self._html.cget(\"shrink\"):\r\n                return 0\r\n            else:\r\n                return 2\r\n        else:\r\n            return allow\r\n\r\n    def _manage_vsb(self, allow=None, check=False):\r\n        \"Show or hide the scrollbars.\"\r\n        if check:\r\n            return self._vsb.scroll\r\n        if allow == None:\r\n            allow = self.vertical_scrollbar\r\n        allow = self._adjust_allow(allow)\r\n        self._vsb.set_type(allow, *self._html.yview())\r\n        return allow\r\n    \r\n    def _manage_hsb(self, allow=None, check=False):\r\n        \"Show or hide the scrollbars.\"\r\n        if check:\r\n            return self._hsb.scroll\r\n        if allow == None:\r\n            allow = self.horizontal_scrollbar\r\n        allow = self._adjust_allow(allow)\r\n        self._hsb.set_type(allow, *self._html.xview())\r\n        return allow\r\n\r\n    def _get_about_page(self, url, i1=\"\", i2=\"\"):\r\n        style_type = None\r\n        if not self.about_page_background: \r\n            if not self._style: self._style = Style()\r\n            style_type = self.cget(\"style\")\r\n            self.about_page_background = self._style.lookup(style_type, \"background\")\r\n        if not self.about_page_foreground: \r\n            if not self._style: self._style = Style()\r\n            if not style_type: style_type = self.cget(\"style\")\r\n            self.about_page_foreground = self._style.lookup(style_type, \"foreground\")\r\n\r\n        return utilities.BUILTIN_PAGES[url].format(bg=self.about_page_background, fg=self.about_page_foreground, i1=i1, i2=i2)\r\n\r\n    def _update_current_url(self, url, thread_safe=True):\r\n        if self._current_url != url:\r\n            self._current_url = url\r\n            # This way URL_CHANGED_EVENT fires when the user clicks on a link or submits a form after local HTML is parsed\r\n            self._html.post_event(utilities.URL_CHANGED_EVENT, thread_safe)\r\n\r\n    def _continue_loading(self, url, data=\"\", method=\"GET\", decode=None, force=False, thread_safe=False):\r\n        \"Finish loading urls and handle URI fragments.\"\r\n        # NOTE: this may run in a thread\r\n\r\n        code = 404\r\n\r\n        # This way URL_CHANGED_EVENT fires when the user clicks on a link or submits a form after local HTML is parsed\r\n        self._update_current_url(url)\r\n\r\n        self._html.post_event(utilities.DOWNLOADING_RESOURCE_EVENT, True)\r\n        \r\n        try:\r\n            method = method.upper()\r\n            parsed = urlparse(url)\r\n\r\n            if method == \"GET\":\r\n                url = str(url) + str(data)\r\n                data = \"\"\r\n            \r\n            self._current_data = data\r\n\r\n            fragment = parsed.fragment\r\n\r\n            # Workaround for Bug #40, where urllib.urljoin constructs improperly formatted urls on Linux when url starts with file:///\r\n            # As a side effect, this also makes it possible to load files even when given the wrong number of slashes\r\n            if parsed.scheme == \"file\":\r\n                path = parsed.path.lstrip(\"/\\\\\")\r\n                netloc = parsed.netloc.lstrip(\"/\\\\\")\r\n                if netloc:\r\n                    url = urlunparse((\"file\", \"/\" + netloc, path, \"\", \"\", \"\"))\r\n                else:\r\n                    url = urlunparse((\"file\", \"\", \"/\" + path, \"\", \"\", \"\"))\r\n                self._update_current_url(url)\r\n\r\n            # If url is different than the current one, load the new site\r\n            if force or (method == \"POST\") or ((urldefrag(url)[0]).replace(\"/\", \"\") != (urldefrag(self._previous_url)[0]).replace(\"/\", \"\")):\r\n                view_source = False\r\n                if url.startswith(\"view-source:\"):\r\n                    view_source = True\r\n                    url = url.replace(\"view-source:\", \"\")\r\n                    parsed = urlparse(url)\r\n\r\n                thread = utilities.get_current_thread()\r\n\r\n                location = parsed.netloc if parsed.netloc else parsed.path\r\n                self._html.post_message(f\"Connecting to {location}\", True)\r\n                if self._html.insecure_https: self._html.post_message(\"WARNING: Using insecure HTTPS session\", True)\r\n                \r\n                newurl, data, filetype, code = self._html.download_url(url, data, method, decode)\r\n                self._html.post_message(f\"Successfully connected to {location}\", True)\r\n\r\n                if view_source:\r\n                    newurl = \"view-source:\"+newurl\r\n\r\n                if thread.isrunning():\r\n                    self._update_current_url(newurl)\r\n\r\n                    if view_source:\r\n                        data = str(data).replace(\"<\",\"&lt;\").replace(\">\", \"&gt;\")\r\n                        data = data.splitlines()\r\n                        length = int(len(str(len(data))))\r\n                        if len(data) > 1:\r\n                            data = \"</code><br><code>\".join(data)\r\n                            data = data.rsplit(\"<br><code>\", 1)[0]\r\n                            data = data.split(\"</code><br>\", 1)[1]\r\n                        else:\r\n                            data = \"\".join(data)\r\n                        text = self._get_about_page(\"about:view-source\", length*9, data)\r\n                        self._load_html(text, newurl, _thread_safe=thread_safe)\r\n                    elif \"image\" in filetype:\r\n                        name = self._html.image_manager.allocate_image_name()\r\n                        if name:\r\n                            data, data_is_image = self._html.image_manager.check_images(data, name, url, filetype, thread.is_subthread)\r\n                            self._html.post_to_queue(lambda data=data, name=name, url=url, filetype=filetype, data_is_image=data_is_image: self._finish_loading_image(data, name, url, filetype, data_is_image))\r\n                        else:\r\n                            self._load_html(self._get_about_page(\"about:image\", name), newurl, _thread_safe=thread_safe)\r\n                    else:\r\n                        self._load_html(data, newurl, fragment, _thread_safe=thread_safe)\r\n            else:\r\n                # If no requests need to be made, we can signal that the page is done loading, handle fragments, etc.\r\n                self._html.fragment = fragment\r\n                self._html.post_to_queue(self._finish_loading_nothing)\r\n        except Exception as error:\r\n            self._html.post_to_queue(lambda url=url, error=error, code=code: self._finish_loading_error(url, error, code))\r\n\r\n        self._thread_in_progress = None\r\n\r\n    def _finish_loading_image(self, data, name, url, filetype, data_is_image):\r\n        # NOTE: must be run in main thread\r\n        # Inject the image into the webpage, as it has already been downloaded\r\n\r\n        text = self._get_about_page(\"about:image\", name)\r\n        self._html.image_manager.finish_fetching_images(data, name, url, filetype, data_is_image)\r\n        self._load_html(text, url)\r\n    \r\n    def _finish_loading_nothing(self):\r\n        # NOTE: must be run in main thread\r\n\r\n        if not self._html.fragment and self.html.yview() != 0:\r\n            # Reset the view if needed\r\n            self.html.yview_moveto(0)\r\n        \r\n        self._html._handle_load_finish()\r\n        self._finish_css()\r\n    \r\n    def _finish_loading_error(self, url, error, code):\r\n        # NOTE: must be run in main thread\r\n\r\n        self._html.post_message(f\"ERROR: could not load {url}: {error}\")\r\n        if \"CERTIFICATE_VERIFY_FAILED\" in str(error):\r\n            self._html.post_message(f\"Check that you are using the right url scheme. Some websites only support http.\\n\\\r\nThis might also happen if your Python distribution does not come installed with website certificates.\\n\\\r\nThis is a known Python bug on older MacOS systems. \\\r\nRunning something along the lines of \\\"/Applications/Python {'.'.join(utilities.PYTHON_VERSION[:2])}/Install Certificates.command\\\" (with the qoutes) to install the missing certificates may do the trick.\\n\\\r\nOtherwise, use 'HtmlFrame(master, insecure_https=True)' to ignore website certificates or 'HtmlFrame(master, ssl_cafile=[path_to_your_cafile])' to specify the path to your CA file if you know where it is.\")\r\n        if self.on_navigate_fail is not None:\r\n            self.on_navigate_fail(url, error, code)\r\n\r\n    def _finish_css(self):\r\n        ### TODO: consider handling add_html/insert_html this way too   \r\n        ### But then again I don't think they're quite as commonly used\r\n        ### And these days one could just bind to DOM_CONTENT_LOADED_EVENT\r\n\r\n        if self._waiting_for_reset:\r\n            self._waiting_for_reset = False\r\n            for style in self._accumulated_styles:\r\n                self.add_css(style)\r\n            self._accumulated_styles = []\r\n\r\n    def register_JS_object(self, name, obj):\r\n        utilities.deprecate(\"register_JS_object\", \"javascript\", \"register\")\r\n        return self.javascript.register(name, obj)\r\n    \r\n    def _on_script(self, attributes, tag_contents):\r\n        self.javascript._on_script(attributes, tag_contents)\r\n\r\n    def _on_element_script(self, node_handle, attribute, attr_contents):\r\n        self.javascript._on_element_script(node_handle, attribute, attr_contents)\r\n\r\n    def generate_style_report(self, return_report=False):\r\n        \"\"\"Return or load a window showing this widget's style report.\r\n\r\n        :param return_report: If False, a window opens showing the report. If True, the report is simply returned.\r\n        :type return_report: str, optional\r\n        \r\n        New in version 4.19.\"\"\"\r\n        if return_report: return self.html.style_report\r\n        \r\n        if getattr(self, \"style_report_win\", None):\r\n            self.style_report_win.destroy()\r\n\r\n        self.style_report_win = submaster = tk.Toplevel(self.html)\r\n        submaster.title(\"Style Report\")\r\n        submaster.columnconfigure(0, weight=1)\r\n        submaster.rowconfigure(0, weight=1)\r\n\r\n        # I had to remove settings copying after making changes to how settings are parsed\r\n        # TODO: maybe copy more settings\r\n        tkw = HtmlFrame(submaster, messages_enabled=False, dark_theme_enabled=self.html.dark_theme_enabled)\r\n        tkw.load_html(self.html.style_report)\r\n        tkw.pack(expand=True, fill=\"both\")\r\n        return tkw\r\n\r\n    def _check_options(self, options, init_args, kwargs, set_attr=False):\r\n        result = {}\r\n        for key, data in options.items():\r\n            value = init_args.get(key, kwargs.pop(key, utilities.UNSET))\r\n            default = data.get(\"default\")\r\n            if value is utilities.UNSET: value = default\r\n            elif value != default: \r\n                value = self._check_value(key, data, value)\r\n            if set_attr: setattr(self, key, value)\r\n            else: result[key] = value\r\n        return result\r\n        \r\n    def _check_value(self, key, settings, value):\r\n        \"\"\"Ensure new configuration option values are a valid type and post deprecation warnings.\"\"\"\r\n        if \"deprecated\" in settings:\r\n            utilities.deprecate_param(key, settings[\"deprecated\"], 5)\r\n        if \"type\" in settings:\r\n            expected_type = settings[\"type\"]\r\n            extras = \"\"\r\n            if expected_type == \"scrollbar\":\r\n                if value in {\"auto\", \"dynamic\"}: return value\r\n                extras = \"\\\"auto\\\", \\\"dynamic\\\", or \"\r\n                expected_type = bool\r\n            elif expected_type == \"autobool\":\r\n                if value == \"auto\": return value\r\n                extras = \"\\\"auto\\\" or \"\r\n                expected_type = bool\r\n            elif expected_type == \"autofloat\":\r\n                if value == \"auto\": return value\r\n                extras = \"\\\"auto\\\" or \"\r\n                expected_type = float\r\n            elif expected_type == \"nonestr\":\r\n                if value is None: return value\r\n                extras = \"None or \"\r\n                expected_type = str\r\n            elif expected_type == \"callable\":\r\n                if value is None or callable(value): \r\n                    return value\r\n                raise TypeError(f\"expected None or callable object, got <{type(value).__name__}> for {key}\")\r\n            \r\n            if not isinstance(value, expected_type):\r\n                if expected_type is bool and value in {0, 1} or expected_type is int and isinstance(value, float):\r\n                    return expected_type(value)\r\n                raise TypeError(f\"expected {extras}<{expected_type.__name__}> object, got <{type(value).__name__}> for {key}\")\r\n        \r\n        return value\r\n    \r\n    def _check_changeability(self, key, settings):\r\n        changeable = settings.get(\"changeable\", True)\r\n        if not changeable:\r\n            raise RuntimeError(f\"{key} should not be changed after the widget is loaded\")\r\n\r\n    def configure(self, **kwargs):\r\n        \"\"\"\r\n        Change the widget's configuration options. See above for options.\r\n        \"\"\"\r\n\r\n        for key in list(kwargs.keys()):\r\n            if key in self._htmlframe_options:\r\n                settings = self._htmlframe_options[key]\r\n                self._check_changeability(key, settings)\r\n                value = self._check_value(key, settings, kwargs.pop(key))\r\n                setattr(self, key, value)\r\n                if key == \"vertical_scrollbar\":\r\n                    self._manage_vsb(value)\r\n                elif key == \"horizontal_scrollbar\":\r\n                    self._manage_hsb(value)\r\n            elif key in self._tkinterweb_options:\r\n                settings = self._tkinterweb_options[key]\r\n                self._check_changeability(key, settings)\r\n                value = self._check_value(key, settings, kwargs.pop(key))\r\n                setattr(self._html, key, value)\r\n                if key in {\"find_match_highlight_color\", \"find_match_text_color\", \"find_current_highlight_color\",\r\n                           \"find_current_text_color\", \"selected_text_highlight_color\", \"selected_text_color\"}:\r\n                    self._html.selection_manager.update_tags()\r\n            elif key in self._tkhtml_options:\r\n                self._check_changeability(key, self._tkhtml_options[key])\r\n                self._html[key] = kwargs.pop(key)\r\n                if key == \"zoom\":\r\n                    self._handle_html_resize(force=True)\r\n                    self._html.caret_manager.update()\r\n                elif key == \"fontscale\":\r\n                    self._html.caret_manager.update()\r\n        super().configure(**kwargs)\r\n\r\n    def config(self, _override=False, **kwargs):\r\n        \"\"\"\r\n        Change the widget's configuration options. See above for options.\r\n        \"\"\"\r\n        if _override:\r\n            super().configure(**kwargs)\r\n        else:\r\n            self.configure(**kwargs)\r\n\r\n    def cget(self, key):\r\n        \"\"\"\r\n        Return the value of a given configuration option. See above for options.\r\n        \"\"\"\r\n        if key in self._htmlframe_options:\r\n            return getattr(self, key)\r\n        elif key in self._tkinterweb_options.keys():\r\n            return getattr(self._html, key)\r\n        elif key in self._tkhtml_options.keys():\r\n            return self._html.cget(key)\r\n        return super().cget(key)\r\n    \r\n    def bind(self, sequence, *args, **kwargs):\r\n        \"Add an event binding. For convenience, some bindings will be bound to this widget and others will be bound to its associated :class:`~tkinterweb.TkinterWeb` instance.\"\r\n        if sequence in {\"<Leave>\", \"<Enter>\"}:\r\n            super().bind(sequence, *args, **kwargs)\r\n        else:\r\n            self._html.bind(sequence, *args, **kwargs)\r\n\r\n    def unbind(self, sequence, *args, **kwargs):\r\n        \"Remove an event binding.\"\r\n        if sequence in {\"<Leave>\", \"<Enter>\"}:\r\n            super().unbind(sequence, *args, **kwargs)\r\n        else:\r\n            self._html.unbind(sequence, *args, **kwargs)\r\n    \r\n    def __getitem__(self, key):\r\n        return self.cget(key)\r\n\r\n    def __setitem__(self, key, value):\r\n        self.configure(**{key: value})\r\n\r\n\r\nclass HtmlLabel(HtmlFrame):\r\n    \"\"\"The :class:`HtmlLabel` widget is a label-like HTML widget. It inherits from the :class:`HtmlFrame` class. \r\n    \r\n    For a complete list of avaliable methods, properties, configuration options, and generated events, see the :class:`HtmlFrame` docs.\r\n    \r\n    This widget also accepts one additional parameter:\r\n\r\n    :param text: The HTML or text content of the widget\r\n    :type text: str\r\n\r\n    By default the widget will be styled to match the :py:class:`ttk.Label` style. To change this, alter the ttk style or use CSS.\r\n    \"\"\"\r\n\r\n    def __init__(self, master, *, text=\"\", **kwargs):\r\n        if \"style\" not in kwargs: \r\n            kwargs[\"style\"] = \"TLabel\"\r\n        else:\r\n            utilities.warn(\"Since version 4.14 the style keyword no longer sets the HtmlLabel's CSS code. Please use the add_css() method instead.\")\r\n        \r\n        HtmlFrame.__init__(self, master, shrink=True, **kwargs)\r\n\r\n        tags = list(self._html.bindtags())\r\n        tags.remove(\"Html\")\r\n        self._html.bindtags(tags)\r\n\r\n        self._style = Style()\r\n\r\n        if text: self.load_html(text)\r\n        # I'd like to just make this an else statement to prevent the widget from being a massive white screen when text=\"\"\r\n        elif self.unshrink or (not self._html.using_tkhtml30 and not self._html.cget(\"textwrap\")):\r\n            # A fellow in issue 145 mentioned layout issues when this was used\r\n            # I can't seem to reproduce it though...?\r\n            self.load_html(\"<body></body>\", _relayout=False)\r\n    \r\n    def load_html(self, *args, _relayout=True, **kwargs):\r\n        \"\"\r\n        # Match the ttk theme\r\n        style_type = self.cget(\"style\")\r\n        bg = self._style.lookup(style_type, 'background')\r\n        fg = self._style.lookup(style_type, 'foreground')\r\n        style = self._html.default_style + \\\r\n            (self._html.dark_style if self._html.dark_theme_enabled else \"\") +\\\r\n            f\"BODY {{ background-color: {bg}; color: {fg}; }}\"\r\n        self._html.configure(defaultstyle=style)\r\n        \r\n        # Load the HTML\r\n        super().load_html(*args, **kwargs)\r\n\r\n        # This stops infinite flickering when tables are present\r\n        # My computer was having this bug for a while but now I don't experience it\r\n        # But this doesn't seem to have any major side effects\r\n        if _relayout and self.winfo_ismapped(): \r\n            # Note to self: without the winfo_ismapped(), the window may teleport across the galaxy\r\n            self.update_idletasks()\r\n            self._html.relayout()\r\n\r\n    def configure(self, **kwargs):\r\n        \"\"\r\n        if \"text\" in kwargs:\r\n            self.load_html(kwargs.pop(\"text\"))\r\n            \r\n        if \"style\" in kwargs:\r\n            utilities.warn(\"Since version 4.14 the style keyword no longer sets the HtmlLabel's CSS code. Please use the add_css() method instead.\")\r\n\r\n        if kwargs: super().configure(**kwargs)\r\n\r\n    def cget(self, key):\r\n        \"\"\r\n        if \"text\" == key:\r\n            return \"\".join(self._html.serialize_node(0).splitlines())\r\n        elif \"style\" == key:\r\n           return \"\".join(self._html.serialize_node_style(0).splitlines())\r\n        return super().cget(key)\r\n\r\n    def config(self, **kwargs):\r\n        \"\"\r\n        self.configure(**kwargs)\r\n\r\nclass HtmlText(HtmlFrame):\r\n    \"\"\"The :class:`HtmlText` widget is a text-like HTML widget. It inherits from the :class:`HtmlFrame` class. \r\n    \r\n    For a complete list of avaliable methods, properties, configuration options, and generated events, see the :class:`HtmlFrame` docs.\r\n\r\n    The intent of this widget is to mimic the Tkinter Text widget. \r\n\r\n    This widget accepts the following :py:class:`tk.Text` parameters:\r\n\r\n    :param background: the widget's background colour\r\n    :type background: str\r\n    :param foreground: the widget's foreground (text) colour\r\n    :type foreground: str\r\n    :param selectbackground: the background colour of selected text\r\n    :type selectbackground: str\r\n    :param selectforeground: the foreground colour of selected text\r\n    :type selectforeground: str\r\n    :param insertontime: the number of milliseconds the insertion cursor is visible\r\n    :type insertontime: int\r\n    :param insertofftime: the number of milliseconds the insertion cursor is invisible\r\n    :type insertofftime: int\r\n    :param insertwidth: the width of the insertion cursor in pixels\r\n    :type insertwidth: int\r\n    :param insertbackground: the background colour of the insertion cursor\r\n    :type insertbackground: str\r\n    :param state: the widget's state (``\"normal\"`` or ``\"disabled\"``)\r\n    :type state: str\r\n\r\n    Changed in version 4.15.\r\n    \"\"\"\r\n\r\n    def __init__(self, master, *, background=\"#ffffff\", foreground=\"#000000\", selectbackground=\"#9bc6fa\", selectforeground=\"#000\", insertontime=600, insertofftime=300, insertwidth=1, insertbackground=None, state=\"enabled\", **kwargs):\r\n        self._background = kwargs.pop(\"bg\", background)\r\n        self._foreground = kwargs.pop(\"fg\", foreground)\r\n\r\n        self.preserve_flow = True # mostly for debugging\r\n\r\n        if \"horizontal_scrollbar\" not in kwargs:\r\n            kwargs[\"horizontal_scrollbar\"] = \"dynamic\"\r\n        \r\n        if state == \"enabled\":\r\n            caret_browsing_enabled=True\r\n        elif state == \"disabled\":\r\n            caret_browsing_enabled=False\r\n        else:\r\n            raise ValueError(\"state must be 'enabled' or 'disabled'\")\r\n        \r\n        self._option_types = {\"insertontime\": {\"type\": int}, \"insertofftime\": {\"type\": int}, \"insertwidth\": {\"type\": int}}\r\n\r\n        HtmlFrame.__init__(self, master, caret_browsing_enabled=caret_browsing_enabled, **kwargs)\r\n\r\n        self.configure(selectbackground=selectbackground, selectforeground=selectforeground, \r\n                       insertontime=insertontime, insertofftime=insertofftime, \r\n                       insertwidth=insertwidth, insertbackground=insertbackground)\r\n                \r\n        self._html.text_mode = True\r\n\r\n        self.load_html(\"<body><div>\\xa0</div></body>\")\r\n\r\n        if state == \"enabled\":\r\n            self._html.bind(\"<Key>\", self._on_key)\r\n\r\n    def load_html(self, *args, **kwargs):\r\n        \"\"\r\n        # Match the set background and foreground\r\n        style = self._html.default_style + \\\r\n            (self._html.dark_style if self._html.dark_theme_enabled else \"\") +\\\r\n            f\"BODY {{ background-color: {self._background}; color: {self._foreground}; }}\"\r\n        self._html.configure(defaultstyle=style)\r\n        \r\n        # Load the HTML\r\n        super().load_html(*args, **kwargs)\r\n\r\n    def _duplicate(self, element):\r\n        \"Duplicate an element.\"\r\n        parent = element.parentElement\r\n\r\n        new = self.document.createElement(parent.tagName)\r\n        new.className = parent.className\r\n        new.setAttribute(\"style\", parent.getAttribute(\"style\"))\r\n\r\n        # Wrap inline elements in a div so that they display on an new line\r\n        # Return the inner element so that setting the textContent doesn't overwrite this structure\r\n        if parent.style.display == \"inline\":\r\n            outer = self.document.createElement(\"div\")\r\n            outer.appendChild(new)\r\n            new, inner = outer, new\r\n        else:\r\n            inner = new\r\n\r\n        sibling = parent.nextSibling\r\n        if sibling:\r\n            parent.parentElement.insertBefore(new, sibling)\r\n        else:\r\n            parent.parentElement.appendChild(new)\r\n\r\n        return inner, element\r\n    \r\n    def _delete(self, element):\r\n        parent = element.parentElement\r\n        element.remove()\r\n        if len(parent.children) == 0:\r\n            self._delete(parent)\r\n    \r\n    def _check_text(self, text):\r\n        if not text.strip():\r\n            return \"\\xa0\"\r\n        return text\r\n    \r\n    def _strip_text(self, text):\r\n        if text == \"\\xa0\":\r\n            return \"\"\r\n        return text\r\n\r\n    def _next_in_document(self, node):\r\n        # Go to first child if exists\r\n        if node.children:\r\n            return node.children[0]\r\n        # Otherwise, next sibling\r\n        if node.nextSibling:\r\n            return node.nextSibling\r\n        # Otherwise, go up parents until we find a parent's nextSibling\r\n        while node.parentElement:\r\n            node = node.parentElement\r\n            if node.nextSibling:\r\n                return node.nextSibling\r\n        return None  # reached the end of the tree\r\n\r\n    def _descendants(self, node):\r\n        result = []\r\n        stack = list(node.children)\r\n\r\n        while stack:\r\n            current = stack.pop()\r\n            result.append(current)\r\n            stack.extend(current.children)\r\n\r\n        return result\r\n\r\n    def _remove_between(self, element1, element2):\r\n        current = self._next_in_document(element1)\r\n        to_remove = []\r\n        while current and current != element2:\r\n            to_remove.append(current)\r\n            current = self._next_in_document(current)\r\n          \r\n        for node in to_remove:\r\n            descendants = self._descendants(node)\r\n            if element1 in descendants or element2 in descendants:\r\n                continue\r\n\r\n            node.remove()\r\n\r\n    def delete(self, start_index, end_index=None, start_element=None, end_element=None):\r\n        \"\"\"Delete text between two points.\r\n        \r\n        :param start_index: The starting index to delete text from. Similar to the :py:class:`tk.Text` widget, if this is \"sel.first\", this will be the beginning of the selection.\r\n        :type start_index: int or \"sel.first\"\r\n        :param end_index: The ending index to delete text at. If this is \"sel.last\", this will be the end of the selection. If this is \"end\", this will be the end of the document or the end of the ending element if provided. If this is None, only the character at the start index will be deleted.\r\n        :type end_index: int, \"sel.last\", \"end\", or None, optional\r\n        :param start_element: If provided, the start index will be used relative to this element's :class:`~tkinterweb.dom.HTMLElement.textContent`.\r\n        :type start_element: None or :class:`~tkinterweb.dom.HTMLElement`, optional\r\n        :param end_element: If provided, the end index will be used relative to this element's :class:`~tkinterweb.dom.HTMLElement.textContent`.\r\n        :type end_element: None or :class:`~tkinterweb.dom.HTMLElement`, optional\r\n\r\n        :raise RuntimeError: if \"sel.first\" or \"self.last\" is requested but no text is selected.\r\n\r\n        New in version 4.16.\r\n        \"\"\"\r\n        pos = self.get_caret_position(return_element=False)\r\n\r\n        # Translate string indexes\r\n        selection = None\r\n        if start_index == \"sel.first\":\r\n            selection = self.get_selection_position(return_elements=False)\r\n            self.clear_selection()\r\n            try:\r\n                start_index = selection[1]\r\n                start_element = None\r\n            except TypeError:\r\n                raise RuntimeError(\"no text is selected\")\r\n\r\n        if end_index == \"sel.last\":\r\n            if not selection:\r\n                selection = self.get_selection_position(return_elements=False)\r\n                self.clear_selection()\r\n            try:\r\n                end_index = selection[2]\r\n                end_element = None\r\n            except TypeError:\r\n                raise RuntimeError(\"no text is selected\")\r\n\r\n        elif end_index is None:\r\n            end_index = start_index + 1\r\n        if end_index == \"end\":\r\n            if end_element:\r\n                end_index = len(end_element.textContent)\r\n            else:\r\n                end_index = len(self._html.text(\"text\"))\r\n\r\n        # Gather data\r\n        if start_element:\r\n            _, ti = self._html.tkhtml_offset_to_text_index(start_element.node, start_index, True)\r\n            abs1 = self._html.text(\"offset\", start_element.node, ti)\r\n            text1 = start_element.textContent\r\n        else:\r\n            abs1 = start_index\r\n            start_element, start_index = self._html.text(\"index\", start_index)\r\n            text1, start_index = self._html.tkhtml_offset_to_text_index(start_element, start_index)\r\n            start_element = dom.HTMLElement(self.document, start_element)\r\n\r\n        if end_element:\r\n            _, ti = self._html.tkhtml_offset_to_text_index(end_element.node, end_index, True)\r\n            abs2 = self._html.text(\"offset\", end_element.node, ti)\r\n            text2 = end_element.textContent\r\n        else:\r\n            abs2 = end_index\r\n            end_element, end_index = self._html.text(\"index\", end_index)\r\n            text2, end_index = self._html.tkhtml_offset_to_text_index(end_element, end_index)\r\n            end_element = dom.HTMLElement(self.document, end_element)\r\n\r\n        # Ensure index 1 comes before index 2\r\n        if abs1 > abs2:\r\n            end_element, start_element = start_element, end_element\r\n            end_index, start_index = start_index, end_index\r\n        \r\n        # Deletions\r\n        if start_element == end_element:\r\n            start_element.textContent = self._check_text(text1[:start_index] + text2[end_index:])\r\n        else:\r\n            start_element.textContent = self._check_text(self._strip_text(text1[:start_index]) + self._strip_text(text2[end_index:]))\r\n            self._remove_between(start_element, end_element)\r\n            self._delete(end_element)\r\n        \r\n        # Shift the caret if needed\r\n        if pos and pos[1] > abs1:\r\n            index = max(pos[1] + len(self._html.text(\"text\")) - len(pos[0]), abs1)\r\n            self.set_caret_position(index=index)\r\n    \r\n    def insert(self, index, text_or_element, element=None):\r\n        \"\"\"Insert text or an element at the given index.\r\n        \r\n        :param index: The index to insert text at. Similar to the :py:class:`tk.Text` widget, if this is \"insert\", this method will insert at the caret's position. If this is \"end\", this will insert at the end of the document or the end of the element if provided.\r\n        :type index: int, \"end\", or \"insert\"\r\n        :param text_or_element: The text or HTML element to insert.\r\n        :type text_or_element: str or :class:`~tkinterweb.dom.HTMLElement`\r\n        :param element: If provided, the given index will be used relative to this element's :class:`~tkinterweb.dom.HTMLElement.textContent`.\r\n        :type element: None or :class:`~tkinterweb.dom.HTMLElement`, optional\r\n\r\n        :raise RuntimeError: if \"insert\" is requested but the caret is not visible.\r\n\r\n        New in version 4.16.\r\n        \"\"\"\r\n        pos = self.get_caret_position(return_element=False)\r\n\r\n        # Translate string indexes\r\n        if index == \"end\":\r\n            if element:\r\n                index = len(element.textContent)\r\n            else:\r\n                index = len(self._html.text(\"text\"))\r\n        elif index == \"insert\":\r\n            try:\r\n                element, text, index = self.get_caret_position()\r\n            except TypeError:\r\n                raise RuntimeError(\"the caret is not visible\")\r\n        \r\n        # Gather data\r\n        if element:\r\n            text = element.textContent\r\n            if pos:\r\n                _, offset = self._html.tkhtml_offset_to_text_index(element.node, index, True)\r\n                abs_index = self._html.text(\"offset\", element.node, offset)\r\n        else:\r\n            abs_index = index\r\n            element, index = self._html.text(\"index\", index)\r\n            text, index = self._html.tkhtml_offset_to_text_index(element, index)\r\n            element = dom.HTMLElement(self.document, element)\r\n        \r\n        # Insertions\r\n        if isinstance(text_or_element, dom.HTMLElement):\r\n            element2, element = self._duplicate(element)\r\n            element.textContent = self._strip_text(text[:index])\r\n            element2.textContent = text[index:]\r\n            element2.parentElement.insertBefore(text_or_element, element2)\r\n\r\n            children = text_or_element.children\r\n            while children:\r\n                text_or_element = text_or_element.children[-1]\r\n                children = text_or_element.children\r\n            #if pos and pos[1] >= abs_index:\r\n            #    self.set_caret_position(text_or_element, len(text_or_element.textContent))\r\n        else:\r\n            element.textContent = self._strip_text(text[:index]) + text_or_element + text[index:]\r\n        \r\n        # Shift the caret if needed\r\n        if pos and pos[1] >= abs_index:\r\n            index = pos[1] + len(self._html.text(\"text\")) - len(pos[0])\r\n            self.set_caret_position(index=index)\r\n\r\n    def _on_key_selection(self, event, selection):\r\n        \"Handle key presses when text is selected.\"\r\n        self._html.selection_manager.clear_selection()\r\n\r\n        start, end, middle = selection\r\n        start_element, start_element_text, start_element_index = start\r\n        end_element, end_element_text, end_element_index = end\r\n\r\n        # We don't want to insert any characters when pressing Return, BackSpace, or Delete\r\n        if event.keysym in {\"Return\", \"KP_Enter\", \"BackSpace\", \"Delete\"}:\r\n            event.char = \"\"\r\n        # Map spaces to non-breaking spaces\r\n        elif event.char == \" \": \r\n            event.char = \"\\xa0\"\r\n\r\n        # If the text to delete is within one element, cut out the section\r\n        # If the resulting text is empty, make it \\xa0 to prevent the node from collapsing\r\n        if start_element == end_element:\r\n            # If pressing Return, split the node into two\r\n            if event.keysym in {\"Return\", \"KP_Enter\"}:\r\n                new, start_element = self._duplicate(start_element)\r\n                text1 = start_element_text[:start_element_index]\r\n                text2 = start_element_text[end_element_index:]\r\n                start_element.textContent = self._check_text(text1)\r\n                new.textContent = self._check_text(text2)\r\n                start_element = new.children[0]\r\n                start_element_index = 0\r\n            else:\r\n                start_element.textContent = self._check_text(self._strip_text(start_element_text[:start_element_index]) + event.char + start_element_text[end_element_index:])\r\n        else:\r\n            # Otherwise, first delete all nodes in between if self.preserve_flow\r\n            if self.preserve_flow:\r\n                self._remove_between(start_element, end_element)\r\n            else:\r\n                # Or, delete all text nodes in between\r\n                for element in middle:\r\n                    self._delete(element)\r\n\r\n            # If pressing Return, cut the end of the start node and the beginning of the end node\r\n            if event.keysym in {\"Return\", \"KP_Enter\"}:\r\n                start_element.textContent = self._check_text(start_element_text[:start_element_index] + event.char)\r\n                end_element.textContent = self._check_text(end_element_text[end_element_index:])\r\n                start_element = end_element\r\n                start_element_index = 0\r\n\r\n            # Otherwise, cut out the section and move the end node's text into the start node\r\n            # Delete the end node\r\n            else:\r\n                start_element.textContent = self._check_text(self._strip_text(start_element_text[:start_element_index]) + event.char + end_element_text[end_element_index:])\r\n                self._delete(end_element)\r\n\r\n        self.set_caret_position(start_element, start_element_index + len(event.char))\r\n        self.html.event_generate(utilities.FIELD_CHANGED_EVENT)\r\n\r\n    def _on_key(self, event):\r\n        \"Handle key presses.\"\r\n        if not event.char:\r\n            return\r\n        \r\n        if event.state & 0x4:\r\n            if event.keysym == \"v\":\r\n                # Ctrl-V\r\n                event.char = self._html.clipboard_get()\r\n            elif event.keysym == \"x\":\r\n                # Ctrl-X\r\n                self._html.selection_manager.copy_selection()\r\n                event.keysym = \"BackSpace\"\r\n            elif event.keysym not in {\"BackSpace\", \"Delete\", \"Return\", \"KP_Enter\"}:\r\n                # Ctrl-[anything else that isn't Return, BackSpace, or Delete]\r\n                return\r\n\r\n        selection = self.get_selection_position()\r\n        if selection:\r\n            # Nodes can be technically marked as selected without actually being highlighted\r\n            if self._html.selection_manager.get_selection():\r\n                return self._on_key_selection(event, selection)\r\n        \r\n        caret_position = self.get_caret_position()\r\n        if not caret_position:\r\n            return\r\n        element, text, index = caret_position\r\n\r\n        if event.keysym == \"BackSpace\":\r\n            self._html.caret_manager.shift_left(event)\r\n            caret_position = self.get_caret_position()\r\n            element2, text2, index2 = caret_position\r\n\r\n            # If the text to delete is within one element, cut out the section\r\n            # If the resulting text is empty, make it \\xa0 to prevent the node from collapsing\r\n            if element == element2:\r\n                newtext = self._check_text(text[:index2] + text[index:])\r\n                element.textContent = newtext\r\n            # Otherwise, cut out the section and move the start node's text into the end node\r\n            # Delete the start node\r\n            else:\r\n                element2.textContent = self._check_text(self._strip_text(text2[:index2]) + self._strip_text(text[index:]))\r\n                if self.preserve_flow:\r\n                    self._remove_between(element2, element)\r\n                self._delete(element)\r\n                self._html.caret_manager.update()\r\n            self.html.event_generate(utilities.FIELD_CHANGED_EVENT)\r\n            return\r\n\r\n        if event.keysym == \"Delete\":\r\n            self._html.caret_manager.shift_right(event, update=False)\r\n            caret_position = self.get_caret_position()\r\n            element2, text2, index2 = caret_position\r\n\r\n            # If the text to delete is within one element, cut out the section\r\n            # If the resulting text is empty, make it \\xa0 to prevent the node from collapsing\r\n            if element == element2:\r\n                element.textContent = self._check_text(text[:index] + text[index2:])\r\n                self.set_caret_position(element, index)\r\n            # Otherwise, cut out the section but move the end node's text to the start node\r\n            # Delete the end node\r\n            # Strip the start node's text in case it is \\xa0\r\n            else:\r\n                text = self._strip_text(text[:index])\r\n                element.textContent = self._check_text(text + text2[index2:])\r\n                if self.preserve_flow:\r\n                    self._remove_between(element, element2)\r\n                self._delete(element2)\r\n\r\n                self.set_caret_position(element, len(text))\r\n            self.html.event_generate(utilities.FIELD_CHANGED_EVENT)\r\n            return\r\n\r\n        # Create a new node with the same tag and class as the current node\r\n        # Move this node's text to the right of the caret into the new node\r\n        if event.keysym in {\"Return\", \"KP_Enter\"}:\r\n            element, new = self._duplicate(element)\r\n            element.textContent = self._check_text(text[index:])\r\n            new.textContent = self._check_text(text[:index])\r\n            self.shift_caret_right()\r\n            self.html.event_generate(utilities.FIELD_CHANGED_EVENT)\r\n            return\r\n        \r\n        # Map spaces to non-breaking spaces\r\n        if event.char == \" \": event.char = \"\\xa0\"\r\n\r\n        # Insert characters\r\n        text2 = self._strip_text(text[:index])\r\n        newtext = text2 + event.char + text[index:]\r\n        element.textContent = newtext\r\n        self.set_caret_position(element, len(text2) + len(event.char))\r\n\r\n        self.html.event_generate(utilities.FIELD_CHANGED_EVENT)\r\n\r\n    def configure(self, **kwargs):\r\n        \"\"\r\n        if \"caret_browsing_enabled\" in kwargs:\r\n            raise RuntimeError(\"caret browsing is always enabled in this widget\")\r\n\r\n        for key in list(kwargs.keys()):\r\n            if key == \"background\":\r\n                self._background = kwargs.pop(key)\r\n                self.add_css(f\"BODY {{ background-color: {self._background}; }}\", \"agent\")\r\n            if key == \"foreground\":\r\n                self._foreground = kwargs.pop(key)\r\n                self.add_css(f\"BODY {{ color: {self._foreground}; }}\", \"agent\")\r\n            if key == \"bg\":\r\n                self._background = kwargs.pop(key)\r\n                self.add_css(f\"BODY {{ background-color: {self._background}; }}\", \"agent\")\r\n            if key == \"fg\":\r\n                self._foreground = kwargs.pop(key)\r\n                self.add_css(f\"BODY {{ color: {self._foreground}; }}\", \"agent\")\r\n            if key == \"selectbackground\":\r\n                self._html.selected_text_highlight_color = kwargs.pop(key)\r\n                self._html.selection_manager.update_tags()\r\n            if key == \"selectforeground\":\r\n                self._html.selected_text_color = kwargs.pop(key)\r\n                self._html.selection_manager.update_tags()\r\n            if key == \"insertontime\":\r\n                value = self._check_value(key, self._option_types[key], kwargs.pop(key))\r\n                self._html.caret_manager.blink_delays[0] = value\r\n            if key == \"insertofftime\":\r\n                value = self._check_value(key, self._option_types[key], kwargs.pop(key))\r\n                self._html.caret_manager.blink_delays[1] = value\r\n            if key == \"insertwidth\":\r\n                value = self._check_value(key, self._option_types[key], kwargs.pop(key))\r\n                self._html.caret_manager.caret_width = value\r\n            if key == \"insertbackground\":\r\n                self._html.caret_manager.caret_color = kwargs.pop(key)\r\n            if key == \"state\":\r\n                state = kwargs.pop(key)\r\n                if state == \"enabled\":\r\n                    self._html.bind(\"<Key>\", self._on_key)\r\n                    self._html.caret_browsing_enabled = True\r\n                elif state == \"disabled\":\r\n                    self._html.unbind(\"<Key>\")\r\n                    self._html.caret_browsing_enabled = False\r\n                else:\r\n                    raise ValueError(\"state must be 'enabled' or 'disabled'\")\r\n\r\n        if kwargs: super().configure(**kwargs)\r\n\r\n    def cget(self, key):\r\n        \"\"\r\n        if \"background\" == key:\r\n            return self._background\r\n        elif \"foreground\" == key:\r\n            return self._foreground\r\n        elif \"bg\" == key:\r\n            return self._background\r\n        elif \"fg\" == key:\r\n            return self._foreground\r\n        elif \"selectbackground\" == key:\r\n            return self._html.selected_text_highlight_color\r\n        elif \"selectforeground\" == key:\r\n            return self._html.selected_text_color\r\n        elif \"insertontime\" == key:\r\n            return self._html.caret_manager.blink_delays[0]\r\n        elif \"insertofftime\" == key:\r\n            return self._html.caret_manager.blink_delays[1]\r\n        elif \"insertwidth\" == key:\r\n            return self._html.caret_manager.caret_width\r\n        elif \"insertbackground\" == key:\r\n            return self._html.caret_manager.caret_color\r\n        elif \"state\" == key:\r\n            return \"enabled\" if self._html.caret_browsing_enabled else \"disabled\"\r\n        \r\n        return super().cget(key)\r\n\r\n    def config(self, **kwargs):\r\n        \"\"\r\n        self.configure(**kwargs)\r\n\r\n\r\nclass HtmlParse(HtmlFrame):\r\n    \"\"\"The :class:`HtmlParse` class parses HTML but does not spawn a widget. It inherits from the :class:`HtmlFrame` class. \r\n    \r\n    For a complete list of avaliable methods, properties, configuration options, and generated events, see the :class:`HtmlFrame` docs.\r\n    \r\n    New in version 4.4.\"\"\"\r\n    \r\n    def __init__(self, **kwargs): #markup=\"\"\r\n        self.root = root = tk.Tk()\r\n\r\n        for flag in {\"events_enabled\", \"images_enabled\", \"forms_enabled\", \"stylesheets_enabled\"}:\r\n            if flag not in kwargs:\r\n                kwargs[flag] = False\r\n                \r\n        HtmlFrame.__init__(self, root, **kwargs)\r\n\r\n        # Should I keep this, remove this, or keep it and add it to HtmlFrame...?\r\n        # if markup:\r\n        #     if isfile(markup): markup = f\"file:///{markup}\"\r\n        #     parsed = urlparse(markup)\r\n        #     if parsed.scheme and parsed.path:\r\n        #         self.load_url(markup)\r\n        #     else:\r\n        #         self.load_html(markup)\r\n\r\n        root.withdraw()\r\n\r\n    def __str__(self):\r\n        return f\"<html>{self.document.documentElement.innerHTML}</html>\"\r\n\r\n    def destroy(self):\r\n        super().destroy()\r\n        self.root.destroy()\r\n"
  },
  {
    "path": "tkinterweb/imageutils.py",
    "content": "\"\"\"\nGenerate Tk images and alt text\n\nCopyright (c) 2021-2025 Andrew Clarke\n\"\"\"\n\nfrom tkinter import PhotoImage as TkPhotoImage\n\n# Some folks only use TkinterWeb as a fancy label widget and don't need to load images\n# Or, if they do load images, they don't need support for images not supported by Tk\n# We only import PIL if/when needed\n# On my machine this reduces initial load time by up to a third\n# The same applies to alt-text and image inversion imports\n\n# For the same reasons as above, we only attempt to load Cairo when needed\n# Additionally, CairoSVG will only detect TkinterWeb-Tkhtml's Cairo binary after Tkhtml is loaded \nrsvg_type = None\n\n\ndef load_cairo():\n    global rsvg_type\n    if rsvg_type is None:\n        try:\n            import cairo\n            globals()['cairo'] = cairo\n            import rsvg\n            globals()['rsvg'] = rsvg\n            rsvg_type = 1\n        except ImportError:\n            try:\n                import cairosvg as cairo\n                globals()['cairo'] = cairo\n                rsvg_type = 2\n            except (ImportError, FileNotFoundError, OSError,):\n                import gi\n                gi.require_version('Rsvg', '2.0')\n                from gi.repository import Rsvg as rsvg\n                globals()['rsvg'] = rsvg\n                # Don't import PyGobject's Cairo if PyCairo has already been imported\n                if not cairo:\n                    gi.require_version('cairo', '1.0')\n                    from gi.repository import cairo\n                    globals()['cairo'] = cairo\n                rsvg_type = 3\n\n\ndef photoimage_del(image):\n    \"Monkey-patch to quiet Photoimage error messages. I think it's a PIL bug.\"\n    try:\n        name = image.__photo.name\n        image.__photo.name = None\n        image.__photo.tk.call(\"image\", \"delete\", name)\n    except AttributeError:\n        pass\n\n\ndef invert_image(image, limit):\n    from PIL import Image, ImageOps\n    from collections import Counter\n    from io import BytesIO\n\n    def is_mostly_one_color(image, tolerance=30):\n        pixels = list(image.resize((100, 100), Image.Resampling.NEAREST).getdata())\n        counter = Counter(pixels)\n        dominant_color = max(counter, key=counter.get)\n        def is_similar(c1, c2):\n            return sum(abs(a - b) for a, b in zip(c1, c2)) < tolerance * 3\n        similar_count = sum(1 for p in pixels if is_similar(p, dominant_color))\n        ratio = similar_count / len(pixels)\n        return ratio, dominant_color\n    image = Image.open(BytesIO(image))\n\n    if image.mode not in {'RGBA', 'LA', 'P'} or \"transparent\" in image.info or \"transparency\" in image.info:\n        image = image.convert(\"RGBA\")\n        white_bg = Image.new(\"RGB\", image.size, (255, 255, 255))\n        white_bg.paste(image, mask=image.split()[3])\n        ratio, dominant_color = is_mostly_one_color(white_bg)\n        if ratio >= 0.5 and sum(dominant_color) > limit:\n            r, g, b, a = image.split()\n            rgb_image = Image.merge('RGB', (r, g, b))\n            ratio, dominant_color = is_mostly_one_color(rgb_image)\n            inverted_rgb = ImageOps.invert(rgb_image)\n            r2, g2, b2 = inverted_rgb.split()\n            image = Image.merge('RGBA', (r2, g2, b2, a))\n        return image\n    else:\n        image = image.convert(\"RGB\")\n        ratio, dominant_color = is_mostly_one_color(image)\n        if ratio >= 0.5 and sum(dominant_color) > limit:\n            image = ImageOps.invert(image)\n        return image\n\n\ndef svg_to_png(data):\n    load_cairo()\n    if rsvg_type == 1 or rsvg_type == 3:\n        from io import BytesIO\n        if rsvg_type == 1:\n            svg = rsvg.Handle(data=data)\n            img = cairo.ImageSurface(\n                cairo.FORMAT_ARGB32, svg.props.width, svg.props.height)\n        else:\n            handle = rsvg.Handle()\n            svg = handle.new_from_data(data.encode(\"utf-8\"))\n            dim = svg.get_dimensions()\n            img = cairo.ImageSurface(\n                cairo.FORMAT_ARGB32, dim.width, dim.height)\n        ctx = cairo.Context(img)\n        svg.render_cairo(ctx)\n        png_io = BytesIO()\n        img.write_to_png(png_io)\n        svg.close()\n        return png_io.getvalue()\n    else:\n        return cairo.svg2png(bytestring=data)\n\n\ndef data_to_image(data, name, imagetype, data_is_image):\n    if data_is_image:\n        from PIL import Image\n        from PIL.ImageTk import PhotoImage\n        \n        if PhotoImage.__del__ is not photoimage_del:\n            PhotoImage.__del__ = photoimage_del\n\n        return PhotoImage(image=data, name=name)\n    elif imagetype in (\"image/png\", \"image/gif\", \"image/ppm\", \"image/pgm\",):\n        # tkinter.PhotoImage has less overhead, so use it when possible\n        return TkPhotoImage(data=data, name=name)\n    else:\n        from PIL import Image\n        from PIL.ImageTk import PhotoImage\n\n        if PhotoImage.__del__ is not photoimage_del:\n            PhotoImage.__del__ = photoimage_del\n        \n        return PhotoImage(data=data, name=name)\n\n\ndef blank_image(name): \n    return TkPhotoImage(name=name)\n\n\ndef create_RGB_image(data, w, h):\n    from PIL import Image\n    \n    image = Image.new(\"RGB\", (w, h))\n    for y, row in enumerate(data):\n        for x, hexc in enumerate(row.split()):\n            rgb = tuple(int(hexc[1:][i:i+2], 16) for i in (0, 2, 4))\n            image.putpixel((x, y), rgb)\n    return image\n"
  },
  {
    "path": "tkinterweb/js.py",
    "content": "\"\"\"\nA simple JavaScript-Tkhtml bridge.\nCopyright (c) 2021-2025 Andrew Clarke\n\"\"\"\n\nfrom . import dom, utilities\n\nfrom textwrap import dedent\nfrom traceback import format_exc\n\n# JavaScript is experimental and not used by everyone\n# We only import PythonMonkey if/when needed\npm = None\n\nclass JSEngine:\n    \"\"\"Access this class via the :attr:`~tkinterweb.HtmlFrame.javascript` property of the :class:`~tkinterweb.HtmlFrame` and :class:`~tkinterweb.HtmlLabel` widgets.\n\n    New in version 4.12.\n    \n    :ivar html: The associated :class:`.TkinterWeb` instance.\n    :ivar document: The associated :class:`.HTMLDocument` instance.\n    :ivar backend: The scripting backend in use. May be ``pythonmonkey`` or ``python``. New in version 4.19.\n    :ivar sandbox: If True, scripts running Python code will lose access to built-in objects. Default False. New in version 4.19.\"\"\"\n\n    def __init__(self, html, document, backend):\n        self.html = html\n        self.document = document\n        self.backend = backend\n        \n        self.sandbox = False\n\n        if backend not in {\"pythonmonkey\", \"python\"}: \n            raise RuntimeError(f\"Unknown backend {backend}\")\n\n    def __repr__(self):\n        return f\"{self.html._w}::{self.__class__.__name__.lower()}\"\n\n    def register(self, name, obj):\n        \"\"\"Register new JavaScript object. This can be used to access Python variables, functions, and classes from JavaScript (eg. to add a callback for the JavaScript ``alert()`` function or add a ``window`` API). \n        \n        JavaScript must be enabled.\n        \n        :param name: The name of the new JavaScript object.\n        :type name: str\n        :param obj: The Python object to pass.\n        :type obj: anything\n        :raise RuntimeError: If JavaScript is not enabled.\"\"\"\n        # TODO: it would be nice to make name optional\n        if self.html.javascript_enabled:\n            if self.backend == \"pythonmonkey\":\n                self._initialize_javascript()\n                pm.eval(f\"(function(pyObj) {{globalThis.{name} = pyObj}})\")(obj)\n            elif self.backend == \"python\":\n                self._initialize_exec_context()\n                self._globals[name] = obj\n        else:\n            raise RuntimeError(\"JavaScript support must be enabled to register a JavaScript object\")\n        \n    def eval(self, expr, _this=None):\n        \"\"\"Evaluate JavaScript code.\n        \n        JavaScript must be enabled.\n        \n        :param expr: The JavaScript code to evaluate.\n        :type expr: str\n        :raise RuntimeError: If JavaScript is not enabled.\"\"\"\n        if self.html.javascript_enabled:\n            if self.backend == \"pythonmonkey\":\n                self._initialize_javascript()\n                if _this is not None:\n                    # Bind 'this'\n                    # Note: if anyone tries to run a function named TkinterWeb_JSEngine_run (unlikely); this will throw up.\n                    return pm.eval(f\"(element) => {{function TkinterWeb_JSEngine_run() {{ {expr} }}; TkinterWeb_JSEngine_run.bind(element)()}}\")(_this)\n                else:\n                    return pm.eval(expr)\n            elif self.backend == \"python\":\n                self._initialize_exec_context()\n                # Pass _locals to copy JavaScript 'this' behaviour.\n                _locals = {}\n                if _this is not None:\n                    _locals[\"this\"] = _this\n                    \n                exec(expr, self._globals, _locals)\n\n                # Add new variables to _global\n                for var in _locals:\n                    if _this is not None and var == \"this\":\n                        continue\n                    self._globals[var] = _locals[var]\n        else:\n            raise RuntimeError(\"JavaScript support must be enabled to run JavaScript\")\n        \n    def _initialize_javascript(self):\n        global pm\n        if pm is None:\n            try:\n                import pythonmonkey as pm\n                self.register(\"document\", self.document)\n            except ModuleNotFoundError:\n                raise ModuleNotFoundError(\"PythonMonkey is required to run JavaScript files but is not installed.\")\n            \n    def _initialize_exec_context(self):\n        if not hasattr(self, \"_globals\"):\n            self._globals = {\n                # Full built-ins may be intentionally exposed if execution is trusted.\n                \"__builtins__\": {} if self.sandbox else __builtins__,\n                \"document\": self.document,\n            }\n\n    def _on_script(self, attributes, tag_contents):\n        try:\n            tag_contents = dedent(tag_contents).strip()\n            self.eval(tag_contents)\n        except Exception as error:\n            if self.backend == \"python\": error = format_exc()\n            if \"src\" in attributes:\n                self.html.post_message(f\"ERROR: the JavaScript interpreter encountered an error while running the script from {attributes['src']}: {error}\")\n            else:\n                self.html.post_message(f\"ERROR: the JavaScript interpreter encountered an error while running the script \\n\\\"{utilities.shorten(tag_contents)}\\\":\\n{error}\")\n\n    def _on_element_script(self, node_handle, attribute, attr_contents):\n        try:\n            element = dom.HTMLElement(self.document, node_handle)\n            self.eval(attr_contents, element)\n        except Exception as error:\n            if self.backend == \"python\": error = format_exc()\n            self.html.post_message(f\"ERROR: the JavaScript interpreter encountered an error while running an {attribute} script: {error}\")\n"
  },
  {
    "path": "tkinterweb/subwidgets.py",
    "content": "\"\"\"\r\nVarious constants and utilities used by TkinterWeb\r\n\r\nCopyright (c) 2021-2026 Andrew Clarke\r\n\r\nSome of the CSS code in this file is modified from the Tkhtml/Hv3 project. Tkhtml is copyright (c) 2005 Dan Kennedy.\r\n\"\"\"\r\n\r\nimport mimetypes\r\nimport os\r\n\r\nfrom decimal import Decimal, InvalidOperation\r\n\r\nimport tkinter as tk\r\nfrom tkinter import colorchooser, filedialog, ttk\r\n\r\nfrom .utilities import ROOT_DIR, rgb_to_hex\r\n\r\ncombobox_loaded = False\r\n\r\ndef load_combobox(master, force=False):\r\n    \"Load combobox.tcl\"\r\n    global combobox_loaded\r\n    if not (combobox_loaded) or force:\r\n        master.tk.call(\"lappend\", \"auto_path\", ROOT_DIR)\r\n        master.tk.call(\"package\", \"require\", \"combobox\")\r\n        combobox_loaded = True\r\n\r\nclass Combobox(tk.Widget):\r\n    \"Bindings for Bryan Oakley's combobox widget.\"\r\n\r\n    def __init__(self, master):\r\n        try:\r\n            load_combobox(master)\r\n            tk.Widget.__init__(self, master, \"::combobox::combobox\")\r\n        except tk.TclError:\r\n            load_combobox(master, force=True)\r\n            tk.Widget.__init__(self, master, \"::combobox::combobox\")\r\n        self.configure(\r\n            highlightthickness=0,\r\n            borderwidth=0,\r\n            editable=False,\r\n            takefocus=0,\r\n            selectbackground=\"#6eb9ff\",\r\n            relief=\"flat\",\r\n            elementborderwidth=0,\r\n            buttonbackground=\"white\",\r\n        )\r\n        self.data = [\"\"]\r\n        self.values = [\"\"]\r\n        self.default = 0\r\n\r\n    def insert(self, data, values, selected):\r\n        for elem in reversed(data):\r\n            self.tk.call(self._w, \"list\", \"insert\", 0, elem)\r\n        self.data = data\r\n        self.values = values\r\n        if selected:\r\n            self.default = self.values.index(selected)\r\n        self.reset()\r\n\r\n    def set(self, value):\r\n        if value in self.values:\r\n            self.tk.call(self._w, \"select\", self.values.index(value))\r\n\r\n    def reset(self):\r\n        self.tk.call(self._w, \"select\", self.default)\r\n\r\n    def get(self):\r\n        val = self.tk.call(self._w, \"curselection\")[0]\r\n        return self.values[val]\r\n\r\n\r\nclass AutoScrollbar(ttk.Scrollbar):\r\n    \"Scrollbar that hides itself when not needed\"\r\n    def __init__(self, *args, **kwargs):\r\n        ttk.Scrollbar.__init__(self, *args, **kwargs)\r\n        self.scroll = None\r\n        self.visible = True\r\n\r\n    def set(self, low, high):\r\n        if self.visible and (self.scroll == 0):\r\n            self.tk.call(\"grid\", \"remove\", self)\r\n            self.visible = False\r\n        elif not self.visible and (self.scroll == 1):\r\n            self.grid()\r\n            self.visible = True\r\n        elif self.scroll == 2:\r\n            if float(low) <= 0.0 and float(high) >= 1.0:\r\n                self.tk.call(\"grid\", \"remove\", self)\r\n                self.visible = False\r\n            else:\r\n                self.grid()\r\n                self.visible = True\r\n        ttk.Scrollbar.set(self, low, high)\r\n    \r\n    def set_type(self, scroll, low, high):\r\n        if self.scroll != scroll:\r\n            self.scroll = scroll\r\n            self.set(low, high)\r\n\r\n    def pack(self, **kwargs):\r\n        raise tk.TclError(\"cannot use pack with this widget\")\r\n\r\n    def place(self, **kwargs):\r\n        raise tk.TclError(\"cannot use place with this widget\")\r\n\r\n\r\nclass ScrolledTextBox(tk.Frame):\r\n    \"Text widget with a scrollbar\"\r\n\r\n    def __init__(self, parent, content=\"\", onchangecommand=None, **kwargs):\r\n        self.parent = parent\r\n        self.onchangecommand = onchangecommand\r\n\r\n        tk.Frame.__init__(self, parent)\r\n\r\n        self.grid_rowconfigure(0, weight=1)\r\n        self.grid_columnconfigure(0, weight=1)\r\n        self.tbox = tbox = tk.Text(self, \r\n                                    borderwidth=0,\r\n                                    selectborderwidth=0,\r\n                                    highlightthickness=0,\r\n                                    undo=True, \r\n                                    maxundo=-1, \r\n                                    autoseparators=True,\r\n                                    **kwargs)\r\n        tbox.grid(row=0, column=0, sticky=\"nsew\")\r\n\r\n        tbox.insert(\"1.0\", content)\r\n    \r\n        self.vsb = vsb = AutoScrollbar(self, command=tbox.yview)\r\n        vsb.grid(row=0, column=1, sticky=\"nsew\")\r\n        tbox.configure(yscrollcommand=vsb.set)\r\n        vsb.set_type(2, *tbox.yview())\r\n\r\n        if kwargs.get(\"wrap\", True) == tk.NONE:\r\n            self.hsb = hsb = AutoScrollbar(self, orient=\"horizontal\", command=tbox.xview)\r\n            hsb.grid(row=1, column=0, sticky=\"nsew\")\r\n            tbox.configure(xscrollcommand=hsb.set)\r\n            hsb.set_type(2, *tbox.xview())\r\n\r\n        tbox.bind(\"<MouseWheel>\", self.scroll)\r\n        tbox.bind(\"<Button-4>\", self.scroll_x11)\r\n        tbox.bind(\"<Button-5>\", self.scroll_x11)\r\n        tbox.bind(\"<Control-Key-a>\", self.select_all)\r\n        tbox.bind('<KeyRelease>', lambda event: onchangecommand(self) if onchangecommand else None)\r\n        tbox.bind(\"<<Paste>>\", self._on_paste)\r\n\r\n    def _on_paste(self, event):\r\n        try:\r\n            self.tbox.delete(\"sel.first\", \"sel.last\")\r\n        except tk.TclError:\r\n            pass\r\n        self.tbox.insert(\"insert\", self.tbox.clipboard_get())\r\n        return \"break\"\r\n\r\n    def select_all(self, event):\r\n        self.tbox.tag_add(\"sel\", \"1.0\", \"end\")\r\n        self.tbox.mark_set(\"insert\", \"1.0\")\r\n        self.tbox.see(\"insert\")\r\n        return \"break\"\r\n\r\n    def scroll(self, event):\r\n        yview = self.tbox.yview()\r\n        if yview[0] == 0 and event.delta > 0:\r\n            self.parent._scroll(event)\r\n        elif yview[1] == 1 and event.delta < 0:\r\n            self.parent._scroll(event)\r\n\r\n    def scroll_x11(self, event):\r\n        yview = self.tbox.yview()\r\n        if event.num == 4 and yview[0] == 0:\r\n            self.parent._scroll_x11(event, self.parent)\r\n        elif event.num == 5 and yview[1] == 1:\r\n            self.parent._scroll_x11(event, self.parent)\r\n\r\n    def configure(self, *args, **kwargs):\r\n        self.tbox.configure(*args, **kwargs)\r\n\r\n    def insert(self, *args, **kwargs):\r\n        return self.tbox.insert(*args, **kwargs)\r\n\r\n    def get(self, *args, **kwargs):\r\n        if not args and not kwargs:\r\n            args = (\"1.0\", \"end-1c\")\r\n        return self.tbox.get(*args, **kwargs)\r\n\r\n    def delete(self, *args, **kwargs):\r\n        self.tbox.delete(*args, **kwargs)\r\n\r\n    def set(self, value):\r\n        self.tbox.delete(\"0.0\", \"end\")\r\n        self.tbox.insert(\"1.0\", value)\r\n        if self.onchangecommand:\r\n            self.onchangecommand(self)\r\n\r\nclass FormEntry(tk.Entry):\r\n    def __init__(self, parent, value=\"\", placeholder=\"\", entry_type=\"\", onchangecommand=None, insertwidth=1, **kwargs):\r\n        if entry_type == \"password\":\r\n            kwargs[\"show\"] = \"*\"\r\n        tk.Entry.__init__(self, parent, borderwidth=0, highlightthickness=0, insertwidth=insertwidth, **kwargs)\r\n\r\n        self._placeholder = placeholder\r\n        self.colour = self.cget(\"fg\")\r\n\r\n        if value:\r\n            self.placeholder_shown = False\r\n            self.insert(0, value)\r\n        else:\r\n            self.placeholder_shown = True\r\n            self._halfway()\r\n            self.insert(0, placeholder)\r\n\r\n        self.bind(\"<KeyRelease>\", lambda e: onchangecommand(self) if onchangecommand else None)\r\n        self.bind(\"<KeyPress>\", self._on_key_press)\r\n        self.bind(\"<Control-a>\", self._select_all)\r\n        self.bind(\"<<Paste>>\", self._on_paste)\r\n        self.bind(\"<Button-1>\", self._on_click)\r\n        self.bind(\"<Motion>\", self._on_motion)\r\n\r\n    @property\r\n    def placeholder(self):\r\n        return self._placeholder\r\n    \r\n    @placeholder.setter\r\n    def placeholder(self, value):\r\n        self._placeholder = value\r\n        if self.placeholder_shown:\r\n            self.delete(0, \"end\")\r\n            self.insert(0, self._placeholder)\r\n\r\n    def _halfway(self, bg=None):\r\n        fg = list(self.winfo_rgb(self.colour))\r\n        bg = list(self.winfo_rgb(bg if bg else self.cget(\"bg\")))\r\n        new = [int((fg[0] + bg[0]) / 2),\r\n               int((fg[1] + bg[1]) / 2),\r\n               int((fg[2] + bg[2]) / 2)]\r\n        super().config(fg=rgb_to_hex(*new))\r\n\r\n    def _on_key_press(self, event):\r\n        if self.placeholder_shown:\r\n            if event.keysym in {\"BackSpace\", \"Delete\"}:\r\n                return \"break\"\r\n            elif event.char:\r\n                self._hide_placeholder()\r\n\r\n        if self._placeholder and (event.keysym == \"BackSpace\" and len(super().get()) == 1) or \\\r\n            (event.keysym == \"Delete\" and len(super().get()) == 1 and self.index(\"insert\") == 0) or \\\r\n            (event.keysym in {\"BackSpace\", \"Delete\"} and self.select_present() and self.selection_get() == super().get()):\r\n            self.delete(0, \"end\")\r\n            self._show_placeholder()\r\n            return \"break\"\r\n\r\n    def _on_click(self, event):\r\n        if self.placeholder_shown:\r\n            self.focus_set()\r\n            self.icursor(0)\r\n            return \"break\"\r\n    \r\n    def _on_motion(self, event):\r\n        if self.placeholder_shown:\r\n            return \"break\"\r\n\r\n    def _on_paste(self, event):\r\n        self.set(self.clipboard_get())\r\n        return \"break\"\r\n\r\n    def _select_all(self, event):\r\n        if self.placeholder_shown:\r\n            return \"break\"\r\n        else:\r\n            self.selection_range(0, \"end\")\r\n            self.icursor(\"end\")\r\n            return \"break\"\r\n\r\n    def set(self, value):\r\n        self.delete(0, \"end\")\r\n\r\n        if value:\r\n            if self.placeholder_shown:\r\n                self._hide_placeholder()\r\n            self.insert(0, value)\r\n        elif self._placeholder:\r\n            self._show_placeholder()\r\n\r\n    def _show_placeholder(self):\r\n        self._halfway()\r\n        self.insert(0, self._placeholder)\r\n        self.icursor(0)\r\n        self.placeholder_shown = True\r\n\r\n    def _hide_placeholder(self):\r\n        self.delete(0, \"end\")\r\n        super().config(fg=self.colour)\r\n        self.placeholder_shown = False\r\n\r\n    def get(self):\r\n        if self.placeholder_shown:\r\n            return \"\"\r\n        else:\r\n            return super().get()\r\n\r\n    def configure(self, **kwargs):\r\n        kwargs.pop(\"borderwidth\", None)\r\n        kwargs.pop(\"highlightthickness\", None)\r\n\r\n        if \"bg\" in kwargs: bg = kwargs[\"bg\"]\r\n        elif \"background\" in kwargs: bg = kwargs[\"background\"]\r\n        else: bg = None\r\n\r\n        if \"fg\" in kwargs:\r\n            kwargs[\"insertbackground\"] = kwargs[\"fg\"]\r\n            if self.placeholder_shown:\r\n                self.colour = kwargs.pop(\"fg\")\r\n                self._halfway(bg)\r\n        elif \"foreground\" in kwargs:\r\n            kwargs[\"insertbackground\"] = kwargs[\"foreground\"]\r\n            if self.placeholder_shown:\r\n                self.colour = kwargs.pop(\"foreground\")\r\n                self._halfway(bg)\r\n        elif bg: self._halfway(bg)\r\n\r\n        super().configure(**kwargs)\r\n    \r\n    def config(self, **kwargs):\r\n        self.configure(**kwargs)\r\n\r\nclass FormCheckbox(ttk.Checkbutton):\r\n    def __init__(self, parent, value=0, onchangecommand=None, **kwargs):\r\n        self.variable = variable = tk.IntVar(parent, value=value)\r\n\r\n        tk.Checkbutton.__init__(\r\n            self,\r\n            parent,\r\n            borderwidth=0,\r\n            padx=0,\r\n            pady=0,\r\n            highlightthickness=0,\r\n            variable=variable,\r\n            **kwargs\r\n        )\r\n        variable.trace_add(\"write\", lambda *args: onchangecommand(self) if onchangecommand else None)\r\n\r\nclass FormRadioButton(ttk.Checkbutton):\r\n    def __init__(self, parent, token, value=0, checked=False, variable=None, onchangecommand=None, **kwargs):\r\n        if not variable: \r\n            variable = tk.StringVar(parent)\r\n            variable.trace_add(\"write\", lambda *args: onchangecommand(self) if onchangecommand else None)\r\n        self.variable = variable\r\n\r\n        tk.Radiobutton.__init__(\r\n            self,\r\n            parent,\r\n            value=value,\r\n            variable=variable,\r\n            tristatevalue=token,\r\n            borderwidth=0,\r\n            padx=0,\r\n            pady=0,\r\n            highlightthickness=0,\r\n            **kwargs\r\n        )\r\n        if checked:\r\n            variable.set(value)\r\n\r\n    def set(self, value):\r\n        self.variable.set(value)\r\n        \r\n    def get(self):\r\n        return self.variable.get()\r\n\r\nclass FormRange(ttk.Scale):\r\n    def __init__(self, parent, value=50, from_=0, to=100, step=1, onchangecommand=None, **kwargs):\r\n        step_str = str(step)\r\n        self.step = self._check_value(step, 1)\r\n        from_ = self._check_value(from_, 0)\r\n        to = self._check_value(to, 100)\r\n        self.onchangecommand = onchangecommand\r\n        self.decimal_places = len(step_str.split('.')[-1]) if '.' in step_str else 0\r\n        value = round(self._check_value(value, (to - from_) / 2) / self.step) * self.step\r\n        self.variable = variable = tk.DoubleVar(parent, value=round(value, self.decimal_places))\r\n\r\n        ttk.Scale.__init__(self, parent, variable=variable, from_=from_, to=to, **kwargs)\r\n\r\n        variable.trace_add(\"write\", self._update_value)\r\n\r\n    def _update_value(self, *args):\r\n        value = round(self.variable.get() / self.step) * self.step\r\n        self.set(round(value, self.decimal_places))\r\n        self.onchangecommand(self)\r\n\r\n    def _check_value(self, value, default):\r\n        try: \r\n            return float(value)\r\n        except ValueError:\r\n            return default\r\n        \r\n    def configure(self, **kwargs):\r\n        if \"bg\" in kwargs:\r\n            bg = kwargs.pop(\"bg\")\r\n            style = ttk.Style()\r\n            stylename = f\"Scale{self}.Horizontal.TScale\"\r\n            style.configure(stylename, troughcolor=bg)\r\n            self.configure(style=stylename)\r\n\r\n        if \"background\" in kwargs:\r\n            bg = kwargs.pop(\"background\")\r\n            style = ttk.Style()\r\n            stylename = f\"Scale{self}.Horizontal.TScale\"\r\n            style.configure(stylename, troughcolor=bg)\r\n            self.configure(style=stylename)\r\n\r\n        if \"step\" in kwargs:\r\n            self.step = step = self._check_value(kwargs.pop(\"step\"), self.step)\r\n            step_str = str(step)\r\n            self.decimal_places = len(step_str.split('.')[-1]) if '.' in step_str else 0\r\n\r\n        if \"from_\" in kwargs:\r\n            kwargs[\"from_\"] = self._check_value(kwargs[\"from_\"], 0)\r\n        \r\n        if \"to\" in kwargs:\r\n            kwargs[\"to\"] = self._check_value(kwargs[\"to\"], 100)\r\n            \r\n        super().configure(**kwargs)\r\n\r\n\r\nclass Tooltip:\r\n    def __init__(self, widget, text=\"\"):\r\n        self.widget = widget\r\n        self.text = text\r\n        self.tip_window = None\r\n\r\n        self.custom_tag = f\"tkinterweb.{self}.tooltiptoplevel\"\r\n        \r\n    def show(self, text=None):\r\n        if text:\r\n            self.text = text\r\n        \r\n        self.hide()\r\n\r\n        x = self.widget.winfo_rootx()\r\n        y = self.widget.winfo_rooty() + self.widget.winfo_height() + 2\r\n\r\n        self.tip_window = tw = tk.Toplevel(self.widget)\r\n        tw.wm_overrideredirect(True)\r\n        tw.wm_geometry(f\"+{x}+{y}\")\r\n\r\n        self.label = label = tk.Label(tw, text=self.text, background=\"#ffffe0\",\r\n                         relief=\"solid\", borderwidth=1, font=(\"tahoma\", 8))\r\n        label.pack(ipadx=4, ipady=2)\r\n\r\n        current_tags = self.widget.winfo_toplevel().bindtags()\r\n        self.widget.winfo_toplevel().bindtags((self.custom_tag,) + current_tags)\r\n\r\n        self.tip_window.focus_force()\r\n\r\n        tw.bind(\"<FocusOut>\", self.hide)\r\n        self.widget.winfo_toplevel().bind_class(self.custom_tag, \"<Configure>\", self.hide)\r\n\r\n    def hide(self, event=None):\r\n        if self.tip_window:\r\n            self.tip_window.destroy()\r\n            self.tip_window = None\r\n            self.widget.winfo_toplevel().unbind_class(self.custom_tag, \"<Configure>\")\r\n            current_tags = list(self.widget.winfo_toplevel().bindtags())\r\n            current_tags.remove(self.custom_tag)\r\n            self.widget.winfo_toplevel().bindtags(tuple(current_tags))\r\n            self.widget.focus_force()\r\n\r\n\r\nclass FormNumber(tk.Spinbox):\r\n    def __init__(self, parent, value=0, from_=0, to=100, step=1, onchangecommand=None, **kwargs):\r\n        self.onchangecommand = onchangecommand\r\n        self.step = self._decimalize_value(step, 1)\r\n        from_ = self._check_value(from_, 0)\r\n        to = self._check_value(to, 100)\r\n\r\n        self.variable = tk.DoubleVar(parent, value=value)\r\n\r\n        super().__init__(parent, textvariable=self.variable, from_=from_, to=to, **kwargs)\r\n\r\n        self.variable.trace_add(\"write\", self._update_value)\r\n\r\n        self.tooltip = Tooltip(self)\r\n\r\n        self.bind(\"<Control-a>\", self._select_all)\r\n        self.bind(\"<<Paste>>\", self._on_paste)\r\n\r\n    def _on_paste(self, event):\r\n        try:\r\n            self.delete(\"sel.first\", \"sel.last\")\r\n        except tk.TclError:\r\n            pass\r\n        self.insert(\"insert\", self.clipboard_get())\r\n        return \"break\"\r\n\r\n    def _select_all(self, event):\r\n        self.selection_range(0, \"end\")\r\n        self.icursor(\"end\")\r\n        return \"break\"\r\n\r\n    def _update_value(self, *args):\r\n        self.onchangecommand(self)\r\n\r\n    def check(self):\r\n        try:\r\n            current_value = self.variable.get()\r\n            current_value = Decimal(str(current_value))\r\n            from_ = Decimal(str(self.cget(\"from\")))\r\n            to = Decimal(str(self.cget(\"to\")))\r\n            \r\n            if current_value < from_:\r\n                if from_ == from_.to_integral_value():\r\n                    from_ = int(from_)\r\n                self.tooltip.show(f\"Please enter a number that is larger than {from_}\")\r\n            elif current_value > to:\r\n                if to == to.to_integral_value():\r\n                    to = int(to)\r\n                self.tooltip.show(f\"Please enter a number that is smaller than {to}\")\r\n            elif ((current_value - from_) % self.step) != 0:\r\n                lower = from_ + ((current_value - from_) // self.step) * self.step\r\n                upper = lower + self.step\r\n                if lower == lower.to_integral_value():\r\n                    lower = int(lower)\r\n                if upper == upper.to_integral_value():\r\n                    upper = int(upper)\r\n                if upper > to and lower > from_:\r\n                    self.tooltip.show(f\"Please enter a valid number. The nearest number is {lower}.\")\r\n                elif lower < from_ and upper < to:\r\n                    self.tooltip.show(f\"Please enter a valid number. The nearest number is {upper}.\")\r\n                else:\r\n                    self.tooltip.show(f\"Please enter a valid number. The nearest numbers are {lower} and {upper}.\")\r\n            else:\r\n                return True                \r\n        except tk.TclError:\r\n            self.tooltip.show(\"Please enter a number\")\r\n        return False\r\n\r\n    def _check_value(self, value, default):\r\n        try:\r\n            return float(value)\r\n        except ValueError:\r\n            return default\r\n    \r\n    def _decimalize_value(self, value, default):\r\n        try:\r\n            return Decimal(str(value))\r\n        except (InvalidOperation):\r\n            return Decimal(str(default))\r\n\r\n    def configure(self, **kwargs):\r\n        if \"step\" in kwargs:\r\n            self.step = self._decimalize_value(kwargs.pop(\"step\"), self.step)\r\n\r\n        if \"from_\" in kwargs:\r\n            kwargs[\"from_\"] = self._check_value(kwargs[\"from_\"], 0)\r\n        \r\n        if \"to\" in kwargs:\r\n            kwargs[\"to\"] = self._check_value(kwargs[\"to\"], 100)\r\n\r\n        super().configure(**kwargs)\r\n\r\n    def set(self, value):\r\n        self.variable.set(value)\r\n        \r\n    def get(self):\r\n        try:\r\n            return self.variable.get()\r\n        except tk.TclError:\r\n            return None\r\n\r\nclass FileSelector(tk.Frame):\r\n    \"File selector widget\"\r\n\r\n    def __init__(self, parent, accept, multiple, onchangecommand=None, **kwargs):\r\n        self.multiple = multiple\r\n        self.onchangecommand = onchangecommand\r\n        self.files = []\r\n\r\n        tk.Frame.__init__(self, parent)\r\n        self.selector = selector = tk.Button(\r\n            self, text=\"Browse\", command=self.select_file\r\n        )\r\n        self.label = label = tk.Label(self, bg=\"red\", text=\"No files selected.\")\r\n\r\n        selector.grid(row=0, column=1)\r\n        label.grid(row=0, column=2, padx=5)\r\n\r\n        self.generate_filetypes(accept)\r\n\r\n    def generate_filetypes(self, accept):\r\n        if accept:\r\n            accept_list = [a.strip() for a in accept.split(\",\")]\r\n            all_extensions = set()\r\n            filetypes = []\r\n\r\n            # First find all the MIME types\r\n            for mimetype in [a for a in accept_list if not a.startswith(\".\")]:\r\n                # The HTML spec specifies these three wildcard cases only:\r\n                if mimetype in (\"audio/*\", \"video/*\", \"image/*\"):\r\n                    extensions = [\r\n                        k\r\n                        for k, v in mimetypes.types_map.items()\r\n                        if v.startswith(mimetype[:-1])\r\n                    ]\r\n                else:\r\n                    extensions = mimetypes.guess_all_extensions(mimetype)\r\n                filetypes.append((mimetype, \" \".join(extensions)))\r\n                all_extensions.update(extensions)\r\n\r\n            # Now add any non-MIME types not already included as part of a MIME type.\r\n            for suffix in [a for a in accept_list if a.startswith(\".\")]:\r\n                if suffix not in all_extensions:\r\n                    mimetype = mimetypes.guess_type(f\" {suffix}\", suffix)[0]\r\n                    if mimetype:\r\n                        extensions = mimetypes.guess_all_extensions(mimetype)\r\n                        filetypes.append((mimetype, \" \".join(extensions)))\r\n                        all_extensions.update(extensions)\r\n                    else:\r\n                        filetypes.append((f\"{suffix} files\", suffix))\r\n\r\n            if len(filetypes) > 1:\r\n                filetypes.insert(\r\n                    0, (\"All Supported Types\", \" \".join(sorted(all_extensions)))\r\n                )\r\n\r\n            self.filetypes = filetypes\r\n        else:\r\n            self.filetypes = []\r\n\r\n    def select_file(self):\r\n        if self.multiple:\r\n            files = filedialog.askopenfilenames(\r\n                title=\"Select files\", filetypes=self.filetypes\r\n            )\r\n            if files:\r\n                self.files = []\r\n                for file in files:\r\n                    self.files.append(os.path.basename(file.replace('\\\\', '/')))\r\n                files = self.files\r\n        else:\r\n            files = filedialog.askopenfilename(\r\n                title=\"Select file\", filetypes=self.filetypes\r\n            )\r\n            if files:\r\n                self.files = files = (os.path.basename(files.replace('\\\\', '/')),)\r\n        number = len(files)\r\n        if number == 0:\r\n            self.label.config(text=\"No files selected.\")\r\n        elif number == 1:\r\n            files = files[0].replace(\"\\\\\", \"/\").split(\"/\")[-1]\r\n            self.label.config(text=files)\r\n        else:\r\n            self.label.config(text=f\"{number} files selected.\")\r\n        if self.onchangecommand:\r\n            self.onchangecommand(self)\r\n\r\n    def set(self, value):\r\n        self.label.config(text=\"No files selected.\")\r\n        if self.onchangecommand:\r\n            self.onchangecommand(self)\r\n\r\n    def get(self):\r\n        return self.files\r\n\r\n    def configure(self, *args, **kwargs):\r\n        self.selector.config(*args, **kwargs)\r\n        if \"activebackground\" in kwargs:\r\n            del kwargs[\"activebackground\"]\r\n        self.label.config(*args, **kwargs)\r\n        if \"state\" in kwargs:\r\n            del kwargs[\"state\"]\r\n        self.config(*args, **kwargs)\r\n\r\n\r\nclass ColourSelector(tk.Frame):\r\n    \"Colour selector widget\"\r\n\r\n    def __init__(self, parent, colour=\"#000000\", onchangecommand=None, **kwargs):\r\n        self.onchangecommand = onchangecommand\r\n        colour = colour if colour else \"#000000\"\r\n        tk.Button.__init__(self, parent,\r\n            bg=colour,\r\n            command=self.select_colour,\r\n            activebackground=colour,\r\n            highlightthickness=0,\r\n            borderwidth=0,\r\n            **kwargs\r\n        )\r\n\r\n    def select_colour(self):\r\n        colour = colorchooser.askcolor(title=\"Choose color\", initialcolor=self.cget(\"bg\"))[1]\r\n        if colour:\r\n            self.set(colour)\r\n\r\n    def set(self, colour):\r\n        colour = colour if colour else \"#000000\"\r\n        self.config(bg=colour, activebackground=colour)\r\n        if self.onchangecommand:\r\n            self.onchangecommand(self)\r\n\r\n    def get(self):\r\n        return self.cget(\"bg\")\r\n\r\n\r\nclass Notebook(ttk.Frame):\r\n    \"Drop-in replacement for the :py:class:`ttk.Notebook` widget.\"\r\n\r\n    def __init__(self, master, takefocus=True, **kwargs):\r\n        ttk.Frame.__init__(self, master, **kwargs)\r\n        self.notebook = notebook = ttk.Notebook(self, takefocus=takefocus)\r\n        self.blankframe = lambda: tk.Frame(\r\n            notebook, height=0, bd=0, highlightthickness=0\r\n        )\r\n\r\n        self.columnconfigure(0, weight=1)\r\n        self.rowconfigure(1, weight=1)\r\n\r\n        notebook.grid(row=0, column=0, sticky=\"ew\")\r\n\r\n        notebook.bind(\"<<NotebookTabChanged>>\", self.on_tab_change)\r\n\r\n        self.pages = []\r\n        self.previous_page = None\r\n\r\n    def on_tab_change(self, event):\r\n        self.event_generate(\"<<NotebookTabChanged>>\")\r\n        try:\r\n            tabId = self.notebook.index(self.notebook.select())\r\n            newpage = self.pages[tabId]\r\n            if self.previous_page:\r\n                self.previous_page.grid_forget()\r\n            newpage.grid(row=1, column=0, sticky=\"nsew\")\r\n            self.previous_page = newpage\r\n        except tk.TclError:\r\n            pass\r\n\r\n    def add(self, child, **kwargs):\r\n        \"Adds a new tab to the notebook.\"\r\n        if child in self.pages:\r\n            raise ValueError(f\"{child} is already managed by {self}.\")\r\n        frame = self.blankframe()\r\n        self.notebook.add(frame, **kwargs)\r\n        self.pages.append(child)\r\n\r\n    def insert(self, where, child, **kwargs):\r\n        \"Adds a new tab at the specified position.\"\r\n        if child in self.pages:\r\n            raise ValueError(f\"{child} is already managed by {self}.\")\r\n        frame = self.blankframe()\r\n        self.notebook.insert(where, frame, **kwargs)\r\n        self.pages.insert(where, child)\r\n\r\n    def enable_traversal(self):\r\n        \"Enable keyboard traversal for a toplevel window containing this notebook.\"\r\n        self.notebook.enable_traversal()\r\n\r\n    def select(self, tabId=None):\r\n        \"Select the given tabId.\"\r\n        if tabId in self.pages:\r\n            tabId = self.pages.index(tabId)\r\n            return self.notebook.select(tabId)\r\n        else:\r\n            self.notebook.select(tabId)\r\n            return self.transcribe(self.notebook.select())\r\n\r\n    def transcribe(self, item, reverse=False):\r\n        return self.pages[self.notebook.index(item)]\r\n\r\n    def tab(self, tabId, option=None, **kwargs):\r\n        \"Query or modify the options of the given tabId.\"\r\n        if not isinstance(tabId, int) and tabId in self.pages:\r\n            tabId = self.pages.index(tabId)\r\n        return self.notebook.tab(tabId, option, **kwargs)\r\n\r\n    def forget(self, tabId):\r\n        \"Removes the tab specified by tabId and unmaps the associated window.\"\r\n        if isinstance(tabId, int):\r\n            del self.pages[tabId]\r\n            self.notebook.forget(tabId)\r\n        else:\r\n            index = self.pages.index(tabId)\r\n            self.pages.remove(tabId)\r\n            self.notebook.forget(index)\r\n\r\n    def index(self, child):\r\n        \"Returns the numeric index of the tab specified by child, or the total number of tabs if child is the string “end”.\"\r\n        try:\r\n            return self.pages.index(child)\r\n        except (IndexError, ValueError):\r\n            return self.transcribe(self.notebook.index(child))\r\n\r\n    def tabs(self):\r\n        \"Returns a list of widgets managed by the notebook.\"\r\n        return self.pages\r\n"
  },
  {
    "path": "tkinterweb/utilities.py",
    "content": "\"\"\"\r\nVarious constants and utilities used by TkinterWeb\r\n\r\nCopyright (c) 2021-2026 Andrew Clarke\r\n\r\nSome of the CSS code in this file is modified from the Tkhtml/Hv3 project. Tkhtml is copyright (c) 2005 Dan Kennedy.\r\n\"\"\"\r\n\r\nimport os\r\nimport platform\r\nimport sys\r\nimport threading\r\n\r\nfrom functools import wraps\r\nfrom collections import OrderedDict\r\n\r\nimport ssl, gzip, zlib\r\nfrom urllib.request import Request, urlopen\r\nfrom urllib.parse import urlunparse, urlparse\r\n\r\ntry:\r\n    import brotli\r\n    brotli_installed = True\r\nexcept ImportError:\r\n    brotli_installed = False\r\n\r\n\r\n# We need this information here so the built-in pages can access it\r\n__title__ = \"TkinterWeb\"\r\n__author__ = \"Andrew Clarke\"\r\n__copyright__ = \"(c) 2021-2025 Andrew Clarke\"\r\n__license__ = \"MIT\"\r\n__version__ = \"4.25.2\"\r\n\r\n\r\nROOT_DIR = os.path.join(os.path.abspath(os.path.dirname(__file__)), \"resources\")\r\nWORKING_DIR = os.getcwd()\r\nPLATFORM = platform.uname()\r\nPYTHON_VERSION = platform.python_version_tuple()\r\n\r\n\r\nHEADERS = {\r\n    \"User-Agent\": \"Mozilla/5.1 (X11; U; Linux i686; en-US; rv:1.8.0.3) Gecko/20060425 SUSE/1.5.0.3-7 Hv3/alpha\",\r\n    \"Accept-Encoding\": (\"gzip, deflate, br\" if brotli_installed else \"gzip, deflate\"),\r\n}\r\nINSECURE_HTTPS = False\r\nSSL_CAFILE = None\r\nREQUEST_TIMEOUT = 15\r\nCACHE_MAXSIZE = 128\r\nDEFAULT_PARSE_MODE = \"xml\"\r\nDEFAULT_ENGINE_MODE = \"standards\"\r\nBROKEN_IMAGE = b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\x19\\x00\\x00\\x00\\x1e\\x08\\x03\\x00\\x00\\x00\\xee2E\\xe9\\x00\\x00\\x03\\x00PLTE\\xc5\\xd5\\xf4\\xcd\\xdb\\xf4\\xdf\\xe8\\xfc\\xd5\\xdd\\xf4\\xa5\\xa3\\xa5\\x85\\x83\\x85\\xfc\\xfe\\xfc\\xf4\\xf6\\xf9\\x95\\x93\\x95S\\xb39\\x9d\\x9f\\x9d\\xc5\\xd3\\xedo\\xbbg\\xd5\\xe3\\xf4\\xd5\\xdf\\xfc\\xd5\\xe3\\xfc\\xb5\\xcf\\xd5\\x9d\\xc7\\xb5\\xc5\\xdf\\xe5S\\xaf9\\x8d\\xc7\\x8d\\x15\\x15\\x15\\x16\\x16\\x16\\x17\\x17\\x17\\x18\\x18\\x18\\x19\\x19\\x19\\x1a\\x1a\\x1a\\x1b\\x1b\\x1b\\x1c\\x1c\\x1c\\x1d\\x1d\\x1d\\x1e\\x1e\\x1e\\x1f\\x1f\\x1f   !!!\"\"\"###$$$%%%&&&\\'\\'\\'((()))***+++,,,---...///000111222333444555666777888999:::;;;<<<===>>>???@@@AAABBBCCCDDDEEEFFFGGGHHHIIIJJJKKKLLLMMMNNNOOOPPPQQQRRRSSSTTTUUUVVVWWWXXXYYYZZZ[[[\\\\\\\\\\\\]]]^^^___```aaabbbcccdddeeefffggghhhiiijjjkkklllmmmnnnooopppqqqrrrssstttuuuvvvwwwxxxyyyzzz{{{|||}}}~~~\\x7f\\x7f\\x7f\\x80\\x80\\x80\\x81\\x81\\x81\\x82\\x82\\x82\\x83\\x83\\x83\\x84\\x84\\x84\\x85\\x85\\x85\\x86\\x86\\x86\\x87\\x87\\x87\\x88\\x88\\x88\\x89\\x89\\x89\\x8a\\x8a\\x8a\\x8b\\x8b\\x8b\\x8c\\x8c\\x8c\\x8d\\x8d\\x8d\\x8e\\x8e\\x8e\\x8f\\x8f\\x8f\\x90\\x90\\x90\\x91\\x91\\x91\\x92\\x92\\x92\\x93\\x93\\x93\\x94\\x94\\x94\\x95\\x95\\x95\\x96\\x96\\x96\\x97\\x97\\x97\\x98\\x98\\x98\\x99\\x99\\x99\\x9a\\x9a\\x9a\\x9b\\x9b\\x9b\\x9c\\x9c\\x9c\\x9d\\x9d\\x9d\\x9e\\x9e\\x9e\\x9f\\x9f\\x9f\\xa0\\xa0\\xa0\\xa1\\xa1\\xa1\\xa2\\xa2\\xa2\\xa3\\xa3\\xa3\\xa4\\xa4\\xa4\\xa5\\xa5\\xa5\\xa6\\xa6\\xa6\\xa7\\xa7\\xa7\\xa8\\xa8\\xa8\\xa9\\xa9\\xa9\\xaa\\xaa\\xaa\\xab\\xab\\xab\\xac\\xac\\xac\\xad\\xad\\xad\\xae\\xae\\xae\\xaf\\xaf\\xaf\\xb0\\xb0\\xb0\\xb1\\xb1\\xb1\\xb2\\xb2\\xb2\\xb3\\xb3\\xb3\\xb4\\xb4\\xb4\\xb5\\xb5\\xb5\\xb6\\xb6\\xb6\\xb7\\xb7\\xb7\\xb8\\xb8\\xb8\\xb9\\xb9\\xb9\\xba\\xba\\xba\\xbb\\xbb\\xbb\\xbc\\xbc\\xbc\\xbd\\xbd\\xbd\\xbe\\xbe\\xbe\\xbf\\xbf\\xbf\\xc0\\xc0\\xc0\\xc1\\xc1\\xc1\\xc2\\xc2\\xc2\\xc3\\xc3\\xc3\\xc4\\xc4\\xc4\\xc5\\xc5\\xc5\\xc6\\xc6\\xc6\\xc7\\xc7\\xc7\\xc8\\xc8\\xc8\\xc9\\xc9\\xc9\\xca\\xca\\xca\\xcb\\xcb\\xcb\\xcc\\xcc\\xcc\\xcd\\xcd\\xcd\\xce\\xce\\xce\\xcf\\xcf\\xcf\\xd0\\xd0\\xd0\\xd1\\xd1\\xd1\\xd2\\xd2\\xd2\\xd3\\xd3\\xd3\\xd4\\xd4\\xd4\\xd5\\xd5\\xd5\\xd6\\xd6\\xd6\\xd7\\xd7\\xd7\\xd8\\xd8\\xd8\\xd9\\xd9\\xd9\\xda\\xda\\xda\\xdb\\xdb\\xdb\\xdc\\xdc\\xdc\\xdd\\xdd\\xdd\\xde\\xde\\xde\\xdf\\xdf\\xdf\\xe0\\xe0\\xe0\\xe1\\xe1\\xe1\\xe2\\xe2\\xe2\\xe3\\xe3\\xe3\\xe4\\xe4\\xe4\\xe5\\xe5\\xe5\\xe6\\xe6\\xe6\\xe7\\xe7\\xe7\\xe8\\xe8\\xe8\\xe9\\xe9\\xe9\\xea\\xea\\xea\\xeb\\xeb\\xeb\\xec\\xec\\xec\\xed\\xed\\xed\\xee\\xee\\xee\\xef\\xef\\xef\\xf0\\xf0\\xf0\\xf1\\xf1\\xf1\\xf2\\xf2\\xf2\\xf3\\xf3\\xf3\\xf4\\xf4\\xf4\\xf5\\xf5\\xf5\\xf6\\xf6\\xf6\\xf7\\xf7\\xf7\\xf8\\xf8\\xf8\\xf9\\xf9\\xf9\\xfa\\xfa\\xfa\\xfb\\xfb\\xfb\\xfc\\xfc\\xfc\\xfd\\xfd\\xfd\\xfe\\xfe\\xfe\\xff\\xff\\xff\\x01\\xb3\\x9a&\\x00\\x00\\x01+IDATx\\x9c\\x9d\\x91\\xe9\\x92\\x84 \\x0c\\x84s (\\x08A\\xc6\\xf7\\x7f\\xd6M8\\x9c\\x9d\\xa9\\xda?\\xdb\\x96W\\x7f\\xb6\\xd5\\x04\\xf0\\x7f\\t\\xdcT\\x9c\\xf7}\\x0f\\xf4I\\x16U\\x12\\x16\\t\\x1f\\xdaw\\xe7\\x16!\\xcay\\x9cL\\xac\\xc4\\xfb\\x18\\x06\\xc9\\x81\\x14\\xd0\\xd4o\\xc2\\x88\\xa5X\\x1e\\x0b\"\\x1a\\xf1\\xd1\\x05\\x0f1f3\\x06\\xc9\\x85\\xb6Nb\\x08\\xe0\\xa2d\\x9cK\\xd00\\xefKF\\x16\\xf0E\\ti?\\xb2\\x8aJ2\\xf9\\'\\x83\\xa8]Fy#\\xa8\\x1d\\x00\\x91\\xa1\\x01d\\xad\\x9e1h\\x11m EM(\\xa2vA\\xe0\\xc2,T,\\xe3\\x98$\\xc1T\\xd307 \\xda6[)C\\xea\\x16\\x1aK\\x8c\\rDv#BF\\xd4\\x03\\xb4\\x0b\\xa4\\x02,:\\x83\\xe8H i\\xc2<\\xec,%\\xa2>\\x1d\\xc9)\\x8dD\\xad\\xfd\\x89a\\xce\\xad\\x10\\xdbw\\xa0\\xa0Z.\\xa54v!\\x8a@\\x85\\xeb:^\\xaf\\xe38\\xcfZ\\x19\\xfc\"E\\xbf\\xbf.\\x03F\\x1a\\xf0 Q\\xbbUM\\xbc\\xd5\\xfd\\xbeR\\xa2\\xda\\x9d\\xb3\\x1f\\xdd\\x97\\xbc\\xf5Y\\xf35\\xc9\\x93\\xd0\\x19\\xe8\\xdc\\\\k_\\x7f\\xf2g\\xb6\\x19\\xc4\\xf8\\x90s\\x91\\x17\\xe5\\xbe\\x0b\\xf7\\xf9\\x99\\xd0\\x87\\xfbV\\xb2\\xbd\\xd5\\xfd\\xe7\\xed?\\xe4\\x07\\xca\\xeb\\x13o\\x88}\\xa9\\x12\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82'\r\nCURSOR_MAP = {\r\n    \"crosshair\": \"crosshair\",\r\n    \"default\": \"\",\r\n    \"pointer\": \"hand2\",\r\n    \"move\": \"fleur\",\r\n    \"text\": \"xterm\",\r\n    \"wait\": \"watch\",\r\n    \"progress\": \"watch\",\r\n    \"help\": \"question_arrow\",\r\n    \"e-resize\": \"right_side\",\r\n    \"ne-resize\": \"top_right_corner\",\r\n    \"nw-resize\": \"top_left_corner\",\r\n    \"n-resize\": \"top_side\",\r\n    \"se-resize\": \"bottom_right_corner\",\r\n    \"sw-resize\": \"bottom_left_corner\",\r\n    \"s-resize\": \"bottom_side\",\r\n    \"w-resize\": \"left_side\",\r\n    # The following cursors only work with Tkhtml 3.1+\r\n    \"context-menu\": \"\",\r\n    \"cell\": \"cross\",\r\n    \"vertical-text\": \"xterm\",\r\n    \"alias\": \"hand2\",\r\n    \"copy\": \"cross\",\r\n    \"no-drop\": \"X_cursor\",\r\n    \"not-allowed\": \"X_cursor\", # Circle works too on some platforms\r\n    \"grab\": \"hand2\",\r\n    \"grabbing\": \"fleur\",\r\n    \"all-scroll\": \"\",\r\n    \"col-resize\": \"sb_h_double_arrow\",\r\n    \"row-resize\": \"sb_v_double_arrow\",\r\n    \"ew-resize\": \"sb_h_double_arrow\",\r\n    \"ns-resize\": \"sb_v_double_arrow\",\r\n    \"nesw-resize\": \"top_right_corner\",\r\n    \"nwse-resize\": \"bottom_right_corner\",\r\n    \"zoom-in\": \"\",\r\n    \"zoom-out\": \"\",\r\n    \"none\": \"none\",\r\n    \"gobbler\": \"gobbler\" # Why not?\r\n}\r\nDEFAULT_STYLE = r\"\"\"\r\n/* Default stylesheet to be loaded whenever HTML is parsed. */\r\n/* This is a modified version of the stylesheet that comes bundled with Tkhtml. */\r\n/* Display types for non-table items. */\r\n  ADDRESS, BLOCKQUOTE, BODY, DD, DIV, DL, DT, FIELDSET, \r\n  FRAME, H1, H2, H3, H4, H5, H6, NOFRAMES, DETAILS, SUMMARY,\r\n  OL, P, UL, APPLET, CENTER, DIR, HR, MENU, PRE, FORM\r\n                { display: block }\r\nHEAD, SCRIPT, TITLE { display: none }\r\nBODY {\r\n  margin: 8px;\r\n}\r\n/* Rules for lists */\r\nLI                   { display: list-item }\r\nOL, UL, DIR, MENU, DD  { padding-left: 40px ; margin-left: 1em }\r\nOL[type]         { list-style-type : tcl(::tkhtml::ol_liststyletype) }\r\nUL>LI { list-style-type : disc }\r\nUL>UL>LI { list-style-type : circle }\r\nUL>UL UL>LI { list-style-type : square }\r\nUL[type=\"square\"]>LI { list-style-type : square } \r\nUL[type=\"disc\"]>LI   { list-style-type : disc   } \r\nUL[type=\"circle\"]>LI { list-style-type : circle } \r\nLI[type=\"circle\"]    { list-style-type : circle }\r\nLI[type=\"square\"]    { list-style-type : square }\r\nLI[type=\"disc\"]      { list-style-type : disc   }\r\nNOBR {\r\n  white-space: nowrap;\r\n}\r\n/* Map the 'align' attribute to the 'float' property. Todo: This should\r\n * only be done for images, tables etc. \"align\" can mean different things\r\n * for different elements.\r\n */\r\nTABLE[align=\"left\"]       { float:left } \r\nTABLE[align=\"right\"]      { \r\n    float:right; \r\n    text-align: inherit;\r\n}\r\nTABLE[align=\"center\"]     { \r\n    margin-left:auto;\r\n    margin-right:auto;\r\n    text-align:inherit;\r\n}\r\nIMG[align=\"left\"]         { float:left }\r\nIMG[align=\"right\"]        { float:right }\r\n/* If the 'align' attribute was not mapped to float by the rules above, map\r\n * it to 'text-align'. The rules above take precedence because of their\r\n * higher specificity. \r\n *\r\n * Also the <center> tag means to center align things.\r\n */\r\n[align=\"right\"]              { text-align: -tkhtml-right }\r\n[align=\"left\"]               { text-align: -tkhtml-left  }\r\nCENTER, [align=\"center\"]     { text-align: -tkhtml-center }\r\n/* Rules for unordered-lists */\r\nTD, TH {\r\n  padding: 1px;\r\n  border-bottom-color: grey60;\r\n  border-right-color: grey60;\r\n  border-top-color: grey25;\r\n  border-left-color: grey25;\r\n}\r\n/* For a horizontal line, use a table with no content. We use a table\r\n * instead of a block because tables are laid out around floating boxes, \r\n * whereas regular blocks are not.\r\n */\r\n/*\r\nHR { \r\n  display: table; \r\n  border-top: 1px solid grey45;\r\n  background: grey80;\r\n  height: 1px;\r\n  width: 100%;\r\n  text-align: center;\r\n  margin: 0.5em 0;\r\n}\r\n*/\r\nHR {\r\n  display: block;\r\n  border-top:    1px solid grey45;\r\n  border-bottom: 1px solid grey80;\r\n  margin: 0.5em auto 0.5em auto;\r\n}\r\n/* Basic table tag rules. */\r\nTABLE { \r\n  display: table;\r\n  border-spacing: 0px;\r\n  border-bottom-color: grey25;\r\n  border-right-color: grey25;\r\n  border-top-color: grey60;\r\n  border-left-color: grey60;\r\n  text-align: left;\r\n}\r\nTR              { display: table-row }\r\nTHEAD           { display: table-header-group }\r\nTBODY           { display: table-row-group }\r\nTFOOT           { display: table-footer-group }\r\nCOL             { display: table-column }\r\nCOLGROUP        { display: table-column-group }\r\nTD, TH          { display: table-cell }\r\nCAPTION         { display: table-caption }\r\nTH              { font-weight: bolder; text-align: center }\r\nCAPTION         { text-align: center }\r\n/* General formatting */\r\nH1              { font-size: 2em; margin: .67em 0 }\r\nH2              { font-size: 1.5em; margin: .83em 0 }\r\nH3              { font-size: 1.17em; margin: 1em 0 }\r\nH4, P,\r\nBLOCKQUOTE, UL,\r\nFIELDSET, \r\nOL, DL, DIR,\r\nMENU            { margin-top: 1.0em; margin-bottom: 1.0em }\r\nH5              { font-size: .83em; line-height: 1.17em; margin: 1.67em 0 }\r\nH6              { font-size: .67em; margin: 2.33em 0 }\r\nH1, H2, H3, H4,\r\nH5, H6, B,\r\nSTRONG          { font-weight: bolder }\r\nBLOCKQUOTE      { margin-left: 40px; margin-right: 40px }\r\nI, CITE, EM,\r\nVAR, ADDRESS    { font-style: italic }\r\nPRE, TT, CODE,\r\nKBD, SAMP       { font-family: monospace }\r\nBIG             { font-size: 1.17em }\r\nSMALL, SUB, SUP { font-size: .83em }\r\nSUB             { vertical-align: sub }\r\nSUP             { vertical-align: super }\r\nS, STRIKE, DEL  { text-decoration: line-through }\r\nOL              { list-style-type: decimal }\r\nOL UL, UL OL,\r\nUL UL, OL OL    { margin-top: 0; margin-bottom: 0 }\r\nU, INS          { text-decoration: underline }\r\nABBR, ACRONYM   { font-variant: small-caps; letter-spacing: 0.1em }\r\n/* Formatting for <pre> etc. */\r\nPRE, PLAINTEXT, XMP { \r\n  display: block;\r\n  white-space: pre;\r\n  margin: 1em 0;\r\n  font-family: monospace;\r\n}\r\n/* Rules for recently-added elements*/\r\nMARK { background: yellow }\r\nQ:before { content: \"“\" }\r\nQ:after { content: \"”\" }\r\nDETAILS[open] SUMMARY:before {\r\n    content: \"▾\";\r\n    margin-right: 5px;\r\n}\r\nDETAILS SUMMARY:before, DETAILS[open=\"false\"] SUMMARY:before {\r\n    content: \"▸\";\r\n    margin-right: 5px;\r\n}\r\n/* Display properties for hyperlinks */\r\n:link    { color: darkblue; text-decoration: underline ; cursor: pointer }\r\n:visited { color: purple; text-decoration: underline ; cursor: pointer }\r\nA:active {\r\n    color:red;\r\n    cursor:pointer;\r\n}\r\n/* Deal with the \"nowrap\" HTML attribute on table cells. */\r\nTD[nowrap] ,     TH[nowrap]     { white-space: nowrap; }\r\nTD[nowrap=\"0\"] , TH[nowrap=\"0\"] { white-space: normal; }\r\nBR { \r\n    display: block;\r\n}\r\n/* BR:before       { content: \"\\A\" } */\r\n/*\r\n * Default decorations for form items. \r\n */\r\nINPUT[type=\"hidden\"] { display: none }\r\nINPUT, TEXTAREA, SELECT, BUTTON { \r\n  border: 1px solid #828282;\r\n  background-color: white;\r\n  line-height: normal;\r\n  vertical-align: middle;\r\n}\r\nINPUT[type=\"image\"][src] {\r\n  -tkhtml-replacement-image: attr(src);\r\n  cursor: pointer;\r\n  border-width: 0;\r\n}\r\nINPUT[type=\"checkbox\"], INPUT[type=\"radio\"], input[type=\"file\"], input[type=\"range\"], input[type=\"color\"] {\r\n  background-color: transparent;\r\n  border: none;\r\n}\r\nINPUT[type=\"submit\"],INPUT[type=\"button\"], INPUT[type=\"reset\"], BUTTON {\r\n  display: -tkhtml-inline-button;\r\n  position: relative;\r\n  white-space: nowrap;\r\n  cursor: pointer;\r\n  border: 1px solid;\r\n  border-top-color:    tcl(::tkhtml::if_disabled #828282 #e7e9eb);\r\n  border-left-color:   tcl(::tkhtml::if_disabled #828282 #e7e9eb);\r\n  border-right-color:  tcl(::tkhtml::if_disabled #e7e9eb #828282);\r\n  border-bottom-color: tcl(::tkhtml::if_disabled #e7e9eb #828282);\r\n  padding-top: 3px;\r\n  padding-left: 8px;\r\n  padding-right: 8px;\r\n  padding-bottom: 3px;\r\n  background-color: #d9d9d9;\r\n  color: #000000;\r\n  color: tcl(::tkhtml::if_disabled #666666 #000000);\r\n}\r\nINPUT[type=\"color\"] {\r\n  cursor: pointer;\r\n  padding: 5px;\r\n  background-color: #ccc;\r\n}\r\nINPUT[disabled], BUTTON[disabled] {\r\n    cursor: auto;\r\n}\r\nINPUT[type=\"submit\"]:after {\r\n  content: \"Submit\";\r\n}\r\nINPUT[type=\"reset\"]:after {\r\n  content: \"Reset\";\r\n}\r\nINPUT[type=\"submit\"][value]:after,INPUT[type=\"button\"][value]:after, INPUT[type=\"reset\"][value]:after {\r\n  content: attr(value);\r\n}\r\nINPUT[type=\"submit\"]:hover:active, INPUT[type=\"reset\"]:hover:active,INPUT[type=\"button\"]:hover:active, BUTTON:hover:active {\r\n  border-top-color:    tcl(::tkhtml::if_disabled #e7e9eb #828282);\r\n  border-left-color:   tcl(::tkhtml::if_disabled #e7e9eb #828282);\r\n  border-right-color:  tcl(::tkhtml::if_disabled #828282 #e7e9eb);\r\n  border-bottom-color: tcl(::tkhtml::if_disabled #828282 #e7e9eb);\r\n}\r\nINPUT[size] { width: tcl(::tkhtml::inputsize_to_css) }\r\n/* Handle \"cols\" and \"rows\" on a <textarea> element. By default, use\r\n * a fixed width font in <textarea> elements.\r\n */\r\nTEXTAREA[cols] { width: tcl(::tkhtml::textarea_width) }\r\nTEXTAREA[rows] { height: tcl(::tkhtml::textarea_height) }\r\nTEXTAREA {\r\n  font-family: fixed;\r\n}\r\nFRAMESET {\r\n  display: none;\r\n}\r\n/* Default size for <IFRAME> elements */\r\nIFRAME {\r\n  width: 300px;\r\n  height: 200px;\r\n  border: 1px solid #828282;\r\n}\r\n/*\r\n *************************************************************************\r\n * Below this point are stylesheet rules for mapping presentational \r\n * attributes of Html to CSS property values. Strictly speaking, this \r\n * shouldn't be specified here (in the UA stylesheet), but it doesn't matter\r\n * in practice. See CSS 2.1 spec for more details.\r\n */\r\n/* 'color' */\r\n[color]              { color: attr(color) }\r\nbody a[href]:link    { color: attr(link x body) }\r\nbody a[href]:visited { color: attr(vlink x body) }\r\n/* 'width', 'height', 'background-color' and 'font-size' */\r\n[width]            { width:            attr(width l) }\r\n[height]           { height:           attr(height l) }\r\nbasefont[size]     { font-size:        attr(size) }\r\nfont[size]         { font-size:        tcl(::tkhtml::size_to_fontsize) }\r\nBR[clear]          { clear: attr(clear) }\r\nBR[clear=\"all\"]    { clear: both; }\r\n/* Standard html <img> tags - replace the node with the image at url $src */\r\nIMG[src]              { -tkhtml-replacement-image: attr(src) }\r\nIMG                   { -tkhtml-replacement-image: \"\" }\r\nIMG[alt]:before        { content: attr(alt) }\r\n/*\r\n * Properties of table cells (th, td):\r\n *\r\n *     'border-width'\r\n *     'border-style'\r\n *     'padding'\r\n *     'border-spacing'\r\n */\r\nTABLE[border], TABLE[border] TD, TABLE[border] TH {\r\n    border-top-width:     attr(border l table);\r\n    border-right-width:   attr(border l table);\r\n    border-bottom-width:  attr(border l table);\r\n    border-left-width:    attr(border l table);\r\n    border-top-style:     attr(border x table solid);\r\n    border-right-style:   attr(border x table solid);\r\n    border-bottom-style:  attr(border x table solid);\r\n    border-left-style:    attr(border x table solid);\r\n}\r\nTABLE[border=\"\"], TABLE[border=\"\"] td, TABLE[border=\"\"] th {\r\n    border-top-width:     attr(border x table solid);\r\n    border-right-width:   attr(border x table solid);\r\n    border-bottom-width:  attr(border x table solid);\r\n    border-left-width:    attr(border x table solid);\r\n}\r\nTABLE[cellpadding] td, TABLE[cellpadding] th {\r\n    padding-top:    attr(cellpadding l table);\r\n    padding-right:  attr(cellpadding l table);\r\n    padding-bottom: attr(cellpadding l table);\r\n    padding-left:   attr(cellpadding l table);\r\n}\r\nTABLE[cellspacing], table[cellspacing] {\r\n    border-spacing: attr(cellspacing l);\r\n}\r\n/* Map the valign attribute to the 'vertical-align' property for table \r\n * cells. The default value is \"middle\", or use the actual value of \r\n * valign if it is defined.\r\n */\r\nTD,TH                        {vertical-align: middle}\r\nTR[valign]>TD, TR[valign]>TH {vertical-align: attr(valign x tr)}\r\nTR>TD[valign], TR>TH[valign] {vertical-align: attr(valign)}\r\n/* Support the \"text\" attribute on the <body> tag */\r\nbody[text]       {color: attr(text)}\r\nbody[bgcolor]    { background-color: attr(bgcolor) }\r\n/* Allow background images to be specified using the \"background\" attribute.\r\n * According to HTML 4.01 this is only allowed for <body> elements, but\r\n * many websites use it arbitrarily.\r\n */\r\n[background] { background-image: attr(background) }\r\n/* The vspace and hspace attributes map to margins for elements of type\r\n * <IMG>, <OBJECT> and <APPLET> only. Note that this attribute is\r\n * deprecated in HTML 4.01.\r\n */\r\nIMG[vspace], OBJECT[vspace], IFRAME[vspace], APPLET[vspace] {\r\n    margin-top: attr(vspace l);\r\n    margin-bottom: attr(vspace l);\r\n}\r\nIMG[hspace], OBJECT[hspace], IFRAME[hspace], APPLET[hspace] {\r\n    margin-left: attr(hspace l);\r\n    margin-right: attr(hspace l);\r\n}\r\n/* marginheight and marginwidth attributes on <BODY> */\r\nBODY[marginheight] {\r\n  margin-top: attr(marginheight l);\r\n  margin-bottom: attr(marginheight l);\r\n}\r\nBODY[marginwidth] {\r\n  margin-left: attr(marginwidth l);\r\n  margin-right: attr(marginwidth l);\r\n}\r\nSPAN[spancontent]:after {\r\n  content: attr(spancontent);\r\n}\r\nIFRAME[frameborder]{\r\n  border-width: attr(frameborder l);\r\n}\r\n\"\"\"\r\n\r\nDARK_STYLE = \"\"\"\r\n/* Additional stylesheet to be loaded whenever dark mode is enabled. */\r\n/* Display properties document body. */\r\nHTML, BODY {\r\n  background-color: #0d0b1a;\r\n  color: #ffffff;\r\n}\r\n\r\n/* Display properties for mark elements. */\r\nMARK {\r\n    background: #8c7c00;\r\n}\r\n\r\n/* Display properties for hyperlinks */\r\n:link    { color: #7768d9; }\r\n:visited { color: #5245a8; }\r\n\r\n/* Display properties for form items. */\r\nINPUT, TEXTAREA, SELECT, BUTTON { \r\n  background-color: #171524;\r\n  color: #ffffff;\r\n}\r\nINPUT[type=\"submit\"],INPUT[type=\"button\"], INPUT[type=\"reset\"], BUTTON {\r\n  background-color: #171524;\r\n  color: #ffffff;\r\n  color: tcl(::tkhtml::if_disabled #666666 #ffffff);\r\n}\r\n\"\"\"\r\n\r\nTEXTWRAP_STYLE = \"BODY { white-space: nowrap; }\"\r\n\r\nclass BuiltinPageGenerator():\r\n    \"\"\"BUILTIN_PAGES used to be a dictionary of URIs and corresponding HTML code.\r\n    Instead, we use this page generator class so that we can generate debugging information on demand.\"\"\"\r\n    def __init__(self):\r\n        self._html = None\r\n        self._pages = {\r\n    \"about:blank\": \"<html><head><style>html,body{{width:100%;height:100%;margin:0}}</style><title>about:blank</title></head><body></body></html>{bg}{fg}{i1}{i2}\",\r\n    \"about:tkinterweb\": \"\"\"<html tkinterweb-overflow-x=auto><head>\r\n        <style>html,body{{{{background-color:{bg};color:{fg}}}}}\r\n            code{{{{display:block}}}}\r\n            span{{{{cursor:gobbler;margin-right:10px;border:1px solid black;width:20px;padding-left:45px}}}}\r\n            summary{{{{font-weight:bold;cursor:pointer;font-family:monospace;display:inline}}}}\r\n            details[open]{{{{margin-bottom:25px;}}}}\r\n            details[open='false'],details[open].bottom{{{{margin-bottom:0px;}}}}\r\n            .section{{{{margin-left:10px;border-left:1px solid;padding-left:10px}}}}\r\n            .indented{{{{margin-left:20px}}}}\r\n            .colourbox{{{{margin-right:75px;display:inline}}}}</style>\r\n        <title>about:tkinterweb</title></head><body>\r\n        <code>Welcome to {title}!</code><code>Licenced under the {license} licence</code><code>Copyright {copyright}</code>\r\n        <code>✉ <a href=https://github.com/Andereoo/TkinterWeb>github.com/Andereoo/TkinterWeb</a></code>\r\n        <code>✨ <a href=https://tkinterweb.readthedocs.io>tkinterweb.readthedocs.io</a></code>\r\n        <code>☕ <a href=https://buymeacoffee.com/andereoo>buymeacoffee.com/andereoo</a></code>\r\n        <details open style='margin-top:25px'><summary>Debugging information</summary><code class='section'>\r\n            <details open><summary>Versioning</summary>\r\n            <code>Version: {__version__}</code><code>Tkhtml version: {tkhtml_version}</code><code>TkinterWeb-Tkhtml version: {tkw_tkhtml_version}</code>\r\n            <code>Python version: {python_version}</code><code>Tcl version: {tcl_version}</code><code>Tk version: {tk_version}</code></details>\r\n            <details open><summary>Resource loading</summary>\r\n            <code>Use prebuilt Tkhtml: {use_prebuilt_tkhtml}</code>\r\n            <code>Available Tkhtml binaries: {tkhtml_binaries}</code>\r\n            <code>Resource directories: {root}{tkhtml_root}{tkhtml_extras_root}</code><code>Working directory: {working_dir}</code>\r\n            <code>Tcl paths: {tcl_path}</code>\r\n            <code>System dependency paths: {path}</code></details>\r\n            <details open class='bottom'><summary>System specs</summary>\r\n            <code>Platform: {platform}</code><code>Machine: {machine}</code><code>Processor: {processor}</code>\r\n        </code></details>\r\n        <details open><summary>Preferences</summary><code class='section'>\r\n            <details open><summary>Renderer settings</summary>\r\n            <code>Parse mode: {parse_mode}</code>\r\n            <code>Rendering engine mode: {rendering_mode}</code>\r\n            <code>Zoom: {zoom}</code>\r\n            <code>Font scale: {font_scale}</code></details>\r\n            <details open><summary>HTTP settings</summary>\r\n            <code>Headers: {headers}</code>\r\n            <code>Insecure HTTPS: {insecure_https}</code>\r\n            <code>CA file path: {ssl_cafile}</code>\r\n            <code>Request timeout: {request_timeout}s</code></details>\r\n            <details open><summary>Threading settings</summary>\r\n            <code>Threading enabled: {threading_enabled}</code>\r\n            <code>Tcl allows threading: {allow_threading}</code>\r\n            <code>Maximum thread count: {maximum_thread_count}</code></details>\r\n            <details open><summary>Image settings</summary>\r\n            <code>Images enabled: {images_enabled}</code>\r\n            <code>Ignore invalid images: {ignore_invalid_images}</code>\r\n            <code>Image alt text: {image_alternate_text_enabled}</code></details>\r\n            <details open><summary>Flags</summary>\r\n            <code>Experimental mode: {experimental}</code>\r\n            <code>Caret browsing mode: {caret_mode}</code>\r\n            <code>Stylesheets enabled: {stylesheets_enabled}</code>\r\n            <code>Javascript enabled: {javascript_enabled}</code>\r\n            <code>Forms enabled: {forms_enabled}</code>\r\n            <code>Objects enabled: {objects_enabled}</code>\r\n            <code>Caches enabled: {caches_enabled}</code>\r\n            <code>Crash prevention enabled: {crash_prevention_enabled}</code>\r\n            <code>Debug messages enabled: {messages_enabled}</code>\r\n            <code>Events enabled: {events_enabled}</code>\r\n            <code>Selection enabled: {selection_enabled}</code></details>\r\n            <details open><summary>Colours</summary>\r\n            <div><span style='background-color:{find_match_highlight_color}'>&nbsp;</span><code class='colourbox'>Found text highlight colour: {find_match_highlight_color}</code><code></code>\r\n            <span style='background-color:{find_match_text_color}'>&nbsp;</span><code class='colourbox'>Found text colour: {find_match_text_color}</code><code></code>\r\n            <span style='background-color:{find_current_highlight_color}'>&nbsp;</span><code class='colourbox'>Current found match highlight colour: {find_current_highlight_color}</code><code></code>\r\n            <span style='background-color:{find_current_text_color}'>&nbsp;</span><code class='colourbox'>Current found match text colour: {find_current_text_color}</code><code></code>\r\n            <span style='background-color:{selected_text_highlight_color}'>&nbsp;</span><code class='colourbox'>Selected text highlight colour: {selected_text_highlight_color}</code><code></code>\r\n            <span style='background-color:{selected_text_color}'>&nbsp;</span><code class='colourbox'>Selected text colour: {selected_text_color}</code><code></code>\r\n            <span style='background-color:{bg}'>&nbsp;</span><code class='colourbox'>About page background colour: {bg}</code><code></code>\r\n            <span style='background-color:{fg}'>&nbsp;</span><code class='colourbox'>About page foreground colour: {fg}</code><code></code></div></details>\r\n            <details open class='bottom'><summary>Dark mode settings</summary>\r\n            <code>Dark mode: {dark_theme_enabled}</code>\r\n            <code>Image inversion: {image_inversion_enabled}</code>\r\n            <code>Dark theme general regexes: {general_dark_theme_regexes}</code>\r\n            <code>Dark theme inline regexes: {inline_dark_theme_regexes}</code>\r\n            <code>Dark theme style regex: {style_dark_theme_regex}</code>\r\n            <code>Colour threshold: {dark_theme_limit}</code></details>\r\n        </code></details>\r\n        <details open class='bottom'><summary>Site memory</summary>\r\n            <code class='section'><code>Visited hyperlinks: {visited_links}</code></code></details>\r\n        </body></html>{i1}{i2}\"\"\",\r\n    \"about:error\": \"\"\"<html><head><style>html,body,table,tr,td{{background-color:{bg};color:{fg};width:100%;height:100%;margin:0}}</style><title>Error {i1}</title></head>\r\n        <body><table><tr><td tkinterweb-full-page style='text-align:center;vertical-align:middle'>\r\n        <h2 style='margin:0;padding:0;font-weight:normal'>Oops</h2>\r\n        <h3 style='margin-top:10px;margin-bottom:25px;font-weight:normal'>The page you've requested could not be found :(</h3>\r\n        <object handleremoval allowscrolling style='cursor:pointer' data='{i2}'></object>\r\n        </td></tr></table></body></html>\"\"\",\r\n    \"about:loading\": \"\"\"<html><head><style>html,body,table,tr,td{{width:100%;height:100%;margin:0}}</style></head>\r\n        <body><table><tr><td tkinterweb-full-page style='text-align:center;vertical-align:middle'>\r\n        <p>Loading...</p>\r\n        </td></tr></table></body></html>{bg}{fg}{i1}{i2}\"\"\",\r\n    \"about:image\": \"\"\"<html><head><style>html,body,table,tr {{background-color:{bg};color:{fg};width:100%;height:100%;margin:0}}</style></head><body>\r\n        <table><tr><td tkinterweb-full-page style='text-align:center;vertical-align:middle;padding:4px 4px 0px 4px'><img style='max-width:100%;max-height:100%' src='{i1}'><h3 style='margin:0;padding:0;font-weight:normal'></td></tr></table></body></html>{i2}\"\"\",\r\n    \"about:view-source\": \"\"\"<html tkinterweb-overflow-x=auto><head><style>\r\n        html,body{{background-color:{bg};color:{fg};}}\r\n        pre::before{{counter-reset:listing}}\r\n        code{{counter-increment:listing}}\r\n        code::before{{content:counter(listing);display:inline-block;width:{i1}px;margin-left:5px;padding-right:5px;margin-right:5px;text-align:right;border-right:1px solid grey60;color:grey60}}\r\n        </style></head><body><pre style='margin:0;padding:0'>{i2}</pre></body></html>\"\"\",\r\n}\r\n    \r\n    def __getitem__(self, key):\r\n        import tkinterweb_tkhtml\r\n\r\n        if key == \"about:tkinterweb\":\r\n            return self._pages[key].format(bg=\"{bg}\", fg=\"{fg}\", i1=\"{i1}\", i2=\"{i2}\", \r\n                title=__title__, license=__license__, copyright=__copyright__, __version__=__version__, \r\n                \r\n                headers=(\"\".join(f\"<br><code class='indented'>{k}: {v}</code>\" for k, v in self._html.headers.items())), \r\n                general_dark_theme_regexes=(\"\".join(f\"<code class='indented'>{i.replace('{', '{{').replace('}', '}}')}</code>\" for i in self._html.general_dark_theme_regexes)), \r\n                inline_dark_theme_regexes=(\"\".join(f\"<br><code class='indented'>{i.replace('{', '{{').replace('}', '}}')}</code>\" for i in self._html.inline_dark_theme_regexes)),\r\n                style_dark_theme_regex=f\"<code class='indented'>{self._html.style_dark_theme_regex.replace('{', '{{').replace('}', '}}')}</code>\", \r\n                visited_links=((\"\".join(f\"<code class='indented'><a href='{i}'>{i}</a></code>\" for i in self._html.visited_links)) if self._html.visited_links else None),\r\n                tkhtml_binaries=(\"\".join(f\"<code class='indented'>{os.path.join(i, e)}</code>\" for i, e in tkinterweb_tkhtml.TKHTML_BINARIES)),\r\n                root=f\"<code class='indented'>{ROOT_DIR}</code>\", \r\n                tkhtml_root=f\"<code class='indented'>{tkinterweb_tkhtml.TKHTML_ROOT_DIR}</code>\", \r\n                tkhtml_extras_root=f\"<code class='indented'>{tkinterweb_tkhtml.TKHTML_EXTRAS_ROOT_DIR}</code>\" if tkinterweb_tkhtml.TKHTML_EXTRAS_ROOT_DIR else \"\", \r\n                working_dir=f\"<code class='indented'>{WORKING_DIR}</code>\", \r\n                tcl_path=(\"\".join(f\"<code class='indented'>{i}</code>\" for i in self._html.tk.getvar(\"auto_path\"))),\r\n                path=(\"\".join(f\"<code class='indented'>{i}</code>\" for i in os.environ[\"PATH\"].split(os.pathsep))),\r\n\r\n                parse_mode=self._html.cget(\"parsemode\"), rendering_mode=self._html.cget(\"mode\"),\r\n                zoom=self._html.cget(\"zoom\"), font_scale=self._html.cget(\"fontscale\"), \r\n                \r\n                tkw_tkhtml_version=tkinterweb_tkhtml.__version__, python_version=\".\".join(PYTHON_VERSION), tcl_version=self._html.tk.call(\"info\", \"patchlevel\"), tk_version=self._html.tk.call(\"package\", \"present\", \"Tk\"),\r\n                platform=PLATFORM.system, machine=PLATFORM.machine, processor=PLATFORM.processor,\r\n                \r\n                insecure_https=self._html.insecure_https, ssl_cafile=self._html.ssl_cafile, request_timeout=self._html.request_timeout, use_prebuilt_tkhtml=self._html.use_prebuilt_tkhtml,\r\n                allow_threading=self._html.allow_threading, threading_enabled=self._html.threading_enabled, \r\n                caches_enabled=self._html.caches_enabled, dark_theme_enabled=self._html.dark_theme_enabled,\r\n                image_inversion_enabled=self._html.image_inversion_enabled, crash_prevention_enabled=self._html.crash_prevention_enabled, \r\n                javascript_enabled=self._html.javascript_enabled, messages_enabled=self._html.messages_enabled, \r\n                events_enabled=self._html.events_enabled, selection_enabled=self._html.selection_enabled, \r\n                stylesheets_enabled=self._html.stylesheets_enabled, images_enabled=self._html.images_enabled, \r\n                forms_enabled=self._html.forms_enabled, objects_enabled=self._html.objects_enabled, \r\n                ignore_invalid_images=self._html.ignore_invalid_images, image_alternate_text_enabled=self._html.image_alternate_text_enabled, \r\n                find_match_highlight_color=self._html.find_match_highlight_color, find_match_text_color=self._html.find_match_text_color, \r\n                find_current_highlight_color=self._html.find_current_highlight_color, find_current_text_color=self._html.find_current_text_color,\r\n                selected_text_highlight_color=self._html.selected_text_highlight_color, selected_text_color=self._html.selected_text_color,\r\n                maximum_thread_count=self._html.maximum_thread_count, experimental=self._html.experimental, caret_mode=self._html.caret_browsing_enabled,\r\n                dark_theme_limit=self._html.dark_theme_limit, tkhtml_version=tkinterweb_tkhtml.get_loaded_tkhtml_version(self._html)\r\n            )\r\n        else:\r\n            return self._pages[key]\r\n\r\n    def __len__(self):\r\n        return len(self._pages)\r\n\r\n    def __iter__(self):\r\n        return iter(self._pages)\r\n\r\n    def keys(self):\r\n        return self._pages.keys()\r\n\r\n    def items(self):\r\n        return self._pages.items()\r\n\r\n    def values(self):\r\n        return self._pages.values()\r\n\r\n# We make this dictionary a class so that we can generate debugging information on the fly\r\nBUILTIN_PAGES = BuiltinPageGenerator()\r\n\r\nBUILTIN_ATTRIBUTES = {\r\n    \"overflow-x\": \"tkinterweb-overflow-x\",\r\n    \"vertical-align\": \"tkinterweb-full-page\"\r\n}\r\n\r\nDOWNLOADING_RESOURCE_EVENT = \"<<DownloadingResource>>\"\r\nDONE_LOADING_EVENT = \"<<DoneLoading>>\"\r\nDOM_CONTENT_LOADED_EVENT = \"<<DOMContentLoaded>>\"\r\nURL_CHANGED_EVENT = \"<<UrlChanged>>\"\r\nICON_CHANGED_EVENT = \"<<IconChanged>>\"\r\nTITLE_CHANGED_EVENT = \"<<TitleChanged>>\"\r\nFIELD_CHANGED_EVENT = \"<<Modified>>\"\r\nELEMENT_LOADED_EVENT = \"<<ElementLoaded>>\"\r\n\r\ntkhtml_loaded = False\r\ncombobox_loaded = False\r\n\r\n# These events are handled through the JS event system\r\nEVENT_MAP = {\r\n    \"<Button-1>\": \"onmousedown\",\r\n    \"<ButtonPress-1>\": \"onmousedown\",\r\n    \"<B1-Press>\": \"onmousedown\",\r\n    \"<ButtonRelease-1>\": \"onmouseup\",\r\n    \"<B1-Release>\": \"onmouseup\",\r\n    \"<Double-Button-1>\": \"ondblclick\",\r\n\r\n    \"<Button-2>\": \"onmiddlemouse\",\r\n    \"<ButtonPress-2>\": \"onmiddlemouse\",\r\n    \"<B2-Press>\": \"onmiddlemouse\",\r\n\r\n    \"<Button-3>\": \"oncontextmenu\",\r\n    \"<ButtonPress-3>\": \"oncontextmenu\",\r\n    \"<B3-Press>\": \"oncontextmenu\",\r\n\r\n    \"<Button-4>\": \"onscrollup\",\r\n    \"<Button-5>\": \"onscrolldown\",\r\n    \"<MouseWheel>\": \"onscroll\",\r\n\r\n    \"<Enter>\": \"onmouseover\",\r\n    \"<Leave>\": \"onmouseout\",\r\n\r\n    \"<Motion>\": \"onmousemove\",\r\n    \"<B1-Motion>\": \"onmouseb1move\",\r\n\r\n    ELEMENT_LOADED_EVENT: \"onload\",\r\n    FIELD_CHANGED_EVENT: \"onchange\"\r\n}\r\n\r\n# Events with the following words are allowed to be handled independently\r\nUNHANDLED_EVENT_WHITELIST = [\r\n    \"Button\", \"B1\", \"B2\", \"B3\", \"B4\", \"B5\"\r\n]\r\n\r\n# These JS events don't exist but are used internally. Translate them when needed.\r\nJS_EVENT_MAP = {\r\n    \"onscrollup\": \"onscroll\",\r\n    \"onscrolldown\": \"onscroll\",\r\n    \"onmiddlemouse\": \"onmousedown\",\r\n    \"onmouseb1move\": None,\r\n}\r\n\r\nclass unset:\r\n    # We set this so that Sphinx documentation pages look nicer and make more sense to viewers\r\n    def __repr__(self):\r\n        return \"DEFAULT\"\r\n\r\nUNSET = unset()\r\n\r\nclass StoppableThread(threading.Thread):\r\n    \"A thread that stores a state flag that can be set and used to check if the thread is supposed to be running\"\r\n\r\n    def __init__(self, *args, **kwargs):\r\n        threading.Thread.__init__(self, *args, **kwargs)\r\n        self.daemon = True\r\n        self.running = True\r\n\r\n        self.is_subthread = True\r\n\r\n    def stop(self):\r\n        self.running = False\r\n\r\n    def isrunning(self):\r\n        return self.running\r\n\r\n\r\nclass PlaceholderThread:\r\n    \"\"\"Fake StoppableThread. The only purpose of this is to provide fake methods that mirror the StoppableThread class.\r\n    This means that if a download is running in the MainThread, the stop flags can still be set without raising errors, though they won't do anything.\"\"\"\r\n\r\n    def __init__(self, *args, **kwargs):\r\n        self.is_subthread = False\r\n\r\n    def stop(self):\r\n        return\r\n\r\n    def isrunning(self):\r\n        return True\r\n\r\nclass Empty:\r\n    __slots__ = ()\r\n    def __init__(self, *args, **kwargs):\r\n        pass\r\n    def __call__(self, *args, **kwargs):\r\n        return None\r\n    def __getattr__(self, name):\r\n        return self\r\n    def __getitem__(self, key):\r\n        return self\r\n    def __bool__(self):\r\n        return False\r\n    def __repr__(self):\r\n        return \"None\"\r\n\r\nempty = Empty()\r\n\r\nclass PlaceholderClass:\r\n    def __getattr__(self, name):\r\n        return empty\r\n    \r\n    def __getitem__(self, key):\r\n        return empty\r\n\r\nclass BaseManager:\r\n    def __init__(self, html):\r\n        self.html = html\r\n        html._managers.add(self)\r\n\r\n    def reset(self):\r\n        pass\r\n\r\nplacebo = PlaceholderClass()\r\n\r\ndef lazy_manager(setting):\r\n    def decorator(func):\r\n        attr_name = f\"_{func.__name__}\"\r\n\r\n        @property\r\n        @wraps(func)\r\n        def wrapper(self):\r\n            if setting is not None and not getattr(self, setting, False):\r\n                return placebo\r\n\r\n            if attr_name not in self.__dict__:\r\n                manager = func(self)\r\n                self.__dict__[attr_name] = manager\r\n\r\n            return self.__dict__[attr_name]\r\n    \r\n        return wrapper\r\n    \r\n    return decorator\r\n\r\n\r\ndef special_setting(default=None):\r\n    def decorator(func):\r\n        attr_name = f\"_{func.__name__}\"\r\n\r\n        @property\r\n        def prop(self):\r\n            return getattr(self, attr_name, default)\r\n\r\n        @prop.setter\r\n        def prop(self, value):\r\n            old = getattr(self, attr_name, default)\r\n            setattr(self, attr_name, value)\r\n            func(self, old, value)\r\n\r\n        return prop\r\n    return decorator\r\n\r\n\r\ndef download(url, data=\"\", method=\"GET\", decode=None, insecure=False, cafile=None, headers=(), timeout=15):\r\n    \"Fetch files. Note that headers should be converted from dict to tuple before calling download() as dicts aren't hashable.\"\r\n    if insecure or cafile:\r\n        context = ssl.create_default_context(cafile=cafile)\r\n        if insecure:\r\n            context.check_hostname = False\r\n            context.verify_mode = ssl.CERT_NONE\r\n    else:\r\n        context = None\r\n    \r\n    # Remove the query string if it exists and the url points to a local file\r\n    if url.startswith(\"file://\") and (\"?\" in url):\r\n        parsed = urlparse(url)\r\n        url = urlunparse(parsed._replace(query=\"\"))\r\n\r\n    url = url.replace(\" \", \"%20\")\r\n    if data and (method == \"POST\"):\r\n        req = Request(url, data, headers=dict(headers))\r\n    else:\r\n        req = Request(url, headers=dict(headers))\r\n    \r\n    with urlopen(req, context=context, timeout=timeout) as res:\r\n        data = res.read()\r\n        url = res.geturl()\r\n        info = res.info()\r\n        code = res.getcode()\r\n\r\n        if not url.startswith(\"file://\") and not url.startswith(\"data:\"):\r\n            enc = res.getheader(\"Content-Encoding\", \"\").lower()\r\n            if enc == \"gzip\":\r\n                data = gzip.decompress(data)\r\n            elif enc == \"deflate\":\r\n                try:\r\n                    data = zlib.decompress(data)\r\n                except zlib.error:\r\n                    data = zlib.decompressobj(-zlib.MAX_WBITS).decompress(data)\r\n            elif enc == \"br\" and brotli_installed:\r\n                data = brotli.decompress(data)\r\n\r\n        try:\r\n            maintype = info.get_content_maintype()\r\n            filetype = info.get_content_type()\r\n        except AttributeError:\r\n            maintype = info.maintype\r\n            filetype = info.type\r\n        if (maintype != \"image\") or (\"svg\" in filetype):\r\n            if decode:\r\n                data = data.decode(decode, errors=\"ignore\")\r\n            else:\r\n                data = data.decode(errors=\"ignore\")\r\n\r\n        return url, data, filetype, code\r\n\r\n\r\nclass LRUCache:\r\n    \"\"\"Fetch files and add them to the LRU cache, or check if they're in the cache already.\r\n    If a url redirects, store the final url.\r\n    This way, downloading the redirected url (eg. by saving the page or reloading) still points to the cached entry.\"\"\"\r\n    \r\n    # TODO: consider TTL, LFU, extension to write to disk, etc.\r\n    def __init__(self):\r\n        self.cache = OrderedDict()\r\n        self.redirects = {}\r\n        self.lock = threading.RLock()\r\n\r\n    def check(self, url, *args):\r\n        with self.lock:\r\n            url = self.redirects.get(url, url)\r\n            key = (url, *args)\r\n\r\n            if key in self.cache:\r\n                return True\r\n            else:\r\n                return False\r\n\r\n    def fetch(self, url, *args):\r\n        with self.lock:\r\n            url = self.redirects.get(url, url)\r\n            key = (url, *args)\r\n\r\n            if key in self.cache:\r\n                return self.cache[key]\r\n            \r\n        newurl, data, filetype, code = download(url, *args)\r\n\r\n        with self.lock:\r\n            self.cache[key] = newurl, data, filetype, code\r\n\r\n            if len(self.cache) > CACHE_MAXSIZE:\r\n                self.cache.popitem(last=False)\r\n\r\n            if newurl != url:\r\n                self.redirects[newurl] = url\r\n                \r\n            return newurl, data, filetype, code\r\n            \r\n    def clear(self):\r\n        with self.lock:\r\n            self.cache.clear()\r\n\r\nlru_cache = LRUCache()\r\n\r\ndef cache_download(url, data=\"\", method=\"GET\", decode=None, insecure=False, cafile=None, headers=(), timeout=15):\r\n    return lru_cache.fetch(url, data, method, decode, insecure, cafile, headers, timeout)\r\n\r\ndef check_download(url, data=\"\", method=\"GET\", decode=None, insecure=False, cafile=None, headers=(), timeout=15):\r\n    return lru_cache.check(url, data, method, decode, insecure, cafile, headers, timeout)\r\n\r\n\r\ndef shorten(string):\r\n    \"Shorten text to avoid overloading the terminal\"\r\n    if len(string) > 100:\r\n        string = string[:100] + \"...\"\r\n    return string\r\n\r\n\r\ndef get_current_thread():\r\n    \"Return the currently running thread\"\r\n    thread = threading.current_thread()\r\n    # Py 3.4+: Use is threading.main_thread()\r\n    if thread.name == \"MainThread\":\r\n        thread = PlaceholderThread()\r\n    return thread\r\n\r\n\r\ndef rgb_to_hex(red, green, blue, *args):\r\n    \"Convert RGB colour code to HEX\"\r\n    return f\"#{red:02x}{green:02x}{blue:02x}\"\r\n\r\ndef invert_color(rgb, match, limit):\r\n    \"Check colour, invert if necessary, and convert\"\r\n    if (\"background\" in match and sum(rgb) < limit) or (\r\n        match == \"color\" and sum(rgb) > limit\r\n    ):\r\n        return rgb_to_hex(*rgb)\r\n    else:\r\n        rgb[0] = max(1, min(255, 240 - rgb[0]))\r\n        rgb[1] = max(1, min(255, 240 - rgb[1]))\r\n        rgb[2] = max(1, min(255, 240 - rgb[2]))\r\n        return rgb_to_hex(*rgb)\r\n\r\n\r\ndef notifier(text):\r\n    \"Notifications printer\"\r\n    try:\r\n        sys.stdout.write(str(text) + \"\\n\\n\")\r\n    except Exception:\r\n        \"\"\"sys.stdout.write doesn't work in .pyw files.\r\n        Since .pyw files have no console, we won't bother printing messages.\"\"\"\r\n\r\n\r\ndef tkhtml_notifier(name, text, *args):\r\n    \"Tkhtml -logcmd printer\"\r\n    try:\r\n        sys.stdout.write(\"DEBUG \" + str(name) + \": \" + str(text) + \"\\n\\n\")\r\n    except Exception:\r\n        \"\"\"sys.stdout.write doesn't work in .pyw files.\r\n        Since .pyw files have no console, we won't bother printing messages.\"\"\"\r\n\r\n\r\ndef deprecate(name, manager, new_name=None, stacklevel=3):\r\n    import warnings\r\n    if not new_name: new_name = name\r\n    warnings.warn(f\"{name} is deprecated. Please use {manager}.{new_name}.\", FutureWarning, stacklevel=stacklevel)\r\n\r\ndef deprecate_param(name, new_name, stacklevel=3):\r\n    import warnings\r\n    warnings.warn(f\"{name} is deprecated. Please use {new_name}.\", FutureWarning, stacklevel=stacklevel)\r\n\r\ndef warn(message, stacklevel=3):\r\n    import warnings\r\n    return warnings.warn(message, UserWarning, stacklevel=stacklevel)\r\n    \r\n\r\ndef TclOpt(options):\r\n    \"Format string into Tcl option command-line names\"\r\n    return tuple(o if o.startswith(\"-\") else \"-\"+o for o in options)\r\n\r\n\r\ndef safe_tk_eval(html, expr):\r\n    \"\"\"Always evaluate the given expression on the main thread.\"\"\"\r\n    if threading.current_thread() is threading.main_thread():\r\n        return html.tk.eval(expr)\r\n    else:\r\n        result = [None]\r\n        event = threading.Event()\r\n\r\n        def wrapper():\r\n            result[0] = html.tk.eval(expr)\r\n            event.set()\r\n\r\n        html.after(0, wrapper)\r\n        event.wait()\r\n        return result[0]\r\n"
  },
  {
    "path": "tools/preparewheels.py",
    "content": "\"\"\"\r\nWheel and sdist generator for TkinterWeb\r\n\r\nCopyright (c) 2025 Andrew Clarke\r\n\"\"\"\r\n\r\nimport os, shutil, sys\r\nimport subprocess\r\n\r\nif os.name == \"nt\":\r\n    PYTHON_CMD = \"python\"\r\nelse:\r\n    PYTHON_CMD = \"python3\"\r\n\r\nROOT_PATH = os.path.abspath(os.path.dirname(os.path.dirname(__file__)))\r\nDIST_ROOT_PATH = os.path.join(ROOT_PATH, \"dist\")\r\nBUILD_ROOT_PATH = os.path.join(ROOT_PATH, \"build\")\r\nEGG_ROOT_PATH = os.path.join(ROOT_PATH, \"tkinterweb.egg-info\")\r\nTKINTERWEB_ROOT_PATH = os.path.join(ROOT_PATH, \"tkinterweb\")\r\n\r\nexisting_folders = []\r\nif os.path.exists(DIST_ROOT_PATH):\r\n    existing_folders.append(DIST_ROOT_PATH)\r\nif os.path.exists(BUILD_ROOT_PATH):\r\n    existing_folders.append(BUILD_ROOT_PATH)\r\nif os.path.exists(EGG_ROOT_PATH):\r\n    existing_folders.append(EGG_ROOT_PATH)\r\n\r\nif existing_folders:\r\n    should_continue = input(\"The following directories already exist:\\n    {} \\nRemove and continue (Y/N)? \".format(\"\\n    \".join(existing_folders)))\r\n    if should_continue.upper() == \"Y\":\r\n        print()\r\n        for path in existing_folders:\r\n            print(f\"Removing {path}\")\r\n            shutil.rmtree(path)\r\n    else:\r\n        print(\"Cancelling\")\r\n        #exit()\r\n\r\nprint()\r\n\r\ndef run_shell(*args, cwd=ROOT_PATH, is_wheel=False):\r\n    p = subprocess.Popen(args, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)\r\n    out, err = p.communicate()\r\n    out = out.decode('utf-8')\r\n    if is_wheel and \"Successful\" in out:\r\n        print(\"success!\\n\")\r\n    else:\r\n        print(f\"\\n{out}\")\r\n    if err:\r\n        err = err.decode('utf-8').replace(\"\\n\\n\", \"\\n\") # For some reason some errors have tons of blank space\r\n        print(f\"Error: {err}\", file=sys.stderr)\r\n\r\nprint(f\"Creating wheel and sdist for {TKINTERWEB_ROOT_PATH}...\", end=\"\")\r\nrun_shell(PYTHON_CMD, \"-m\", \"build\", \"--no-isolation\", is_wheel=True)\r\n        \r\n# Upload to pip\r\nshould_continue = input(\"Upload to pip (Y/N)?\")\r\nif should_continue.upper() == \"Y\":\r\n    run_shell(\"twine\", \"upload\", \"dist/*\", \"-u\", \"__token__\")\r\n"
  }
]