Repository:
[start of CONTRIBUTING.rst]
How to contribute to MarkupSafe
===============================

Thank you for considering contributing to MarkupSafe!


Support questions
-----------------

Please don't use the issue tracker for this. The issue tracker is a tool
to address bugs and feature requests in MarkupSafe itself. Use one of
the following resources for questions about using MarkupSafe or issues
with your own code:

-   The ``#get-help`` channel on our Discord chat:
    https://discord.gg/pallets
-   The mailing list flask@python.org for long term discussion or larger
    issues.
-   Ask on `Stack Overflow`_. Search with Google first using:
    ``site:stackoverflow.com markupsafe {search term, exception message, etc.}``

.. _Stack Overflow: https://stackoverflow.com/search?tab=relevance&q=markupsafe


Reporting issues
----------------

Include the following information in your post:

-   Describe what you expected to happen.
-   If possible, include a `minimal reproducible example`_ to help us
    identify the issue. This also helps check that the issue is not with
    your own code.
-   Describe what actually happened. Include the full traceback if there
    was an exception.
-   List your Python and MarkupSafe versions. If possible, check if this
    issue is already fixed in the latest releases or the latest code in
    the repository.

.. _minimal reproducible example: https://stackoverflow.com/help/minimal-reproducible-example


Submitting patches
------------------

If there is not an open issue for what you want to submit, prefer
opening one for discussion before working on a PR. You can work on any
issue that doesn't have an open PR linked to it or a maintainer assigned
to it. These show up in the sidebar. No need to ask if you can work on
an issue that interests you.

Include the following in your patch:

-   Use `Black`_ to format your code. This and other tools will run
    automatically if you install `pre-commit`_ using the instructions
    below.
-   Include tests if your patch adds or changes code. Make sure the test
    fails without your patch.
-   Update any relevant docs pages and docstrings. Docs pages and
    docstrings should be wrapped at 72 characters.
-   Add an entry in ``CHANGES.rst``. Use the same style as other
    entries. Also include ``.. versionchanged::`` inline changelogs in
    relevant docstrings.

.. _Black: https://black.readthedocs.io
.. _pre-commit: https://pre-commit.com


First time setup
~~~~~~~~~~~~~~~~

-   Download and install the `latest version of git`_.
-   Configure git with your `username`_ and `email`_.

    .. code-block:: text

        $ git config --global user.name 'your name'
        $ git config --global user.email 'your email'

-   Make sure you have a `GitHub account`_.
-   Fork MarkupSafe to your GitHub account by clicking the `Fork`_ button.
-   `Clone`_ the main repository locally.

    .. code-block:: text

        $ git clone https://github.com/pallets/markupsafe
        $ cd markupsafe

-   Add your fork as a remote to push your work to. Replace
    ``{username}`` with your username. This names the remote "fork", the
    default Pallets remote is "origin".

    .. code-block:: text

        $ git remote add fork https://github.com/{username}/markupsafe

-   Create a virtualenv.

    .. code-block:: text

        $ python3 -m venv env
        $ . env/bin/activate

    On Windows, activating is different.

    .. code-block:: text

        > env\\Scripts\\activate

-   Upgrade pip and setuptools.

    .. code-block:: text

        $ python -m pip install --upgrade pip setuptools

-   Install the development dependencies, then install MarkupSafe in
    editable mode.

    .. code-block:: text

        $ pip install -r requirements/dev.txt && pip install -e .

-   Install the pre-commit hooks.

    .. code-block:: text

        $ pre-commit install

.. _latest version of git: https://git-scm.com/downloads
.. _username: https://docs.github.com/en/github/using-git/setting-your-username-in-git
.. _email: https://docs.github.com/en/github/setting-up-and-managing-your-github-user-account/setting-your-commit-email-address
.. _GitHub account: https://github.com/join
.. _Fork: https://github.com/pallets/markupsafe/fork
.. _Clone: https://docs.github.com/en/github/getting-started-with-github/fork-a-repo#step-2-create-a-local-clone-of-your-fork


Start coding
~~~~~~~~~~~~

-   Create a branch to identify the issue you would like to work on. If
    you're submitting a bug or documentation fix, branch off of the
    latest ".x" branch.

    .. code-block:: text

        $ git fetch origin
        $ git checkout -b your-branch-name origin/2.0.x

    If you're submitting a feature addition or change, branch off of the
    "main" branch.

    .. code-block:: text

        $ git fetch origin
        $ git checkout -b your-branch-name origin/main

-   Using your favorite editor, make your changes,
    `committing as you go`_.
-   Include tests that cover any code changes you make. Make sure the
    test fails without your patch. Run the tests as described below.
-   Push your commits to your fork on GitHub and
    `create a pull request`_. Link to the issue being addressed with
    ``fixes #123`` in the pull request.

    .. code-block:: text

        $ git push --set-upstream fork your-branch-name

.. _committing as you go: https://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes
.. _create a pull request: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request


Running the tests
~~~~~~~~~~~~~~~~~

Run the basic test suite with pytest.

.. code-block:: text

    $ pytest

This runs the tests for the current environment, which is usually
sufficient. CI will run the full suite when you submit your pull
request. You can run the full test suite with tox if you don't want to
wait.

.. code-block:: text

    $ tox


Running test coverage
~~~~~~~~~~~~~~~~~~~~~

Generating a report of lines that do not have test coverage can indicate
where to start contributing. Run ``pytest`` using ``coverage`` and
generate a report.

.. code-block:: text

    $ pip install coverage
    $ coverage run -m pytest
    $ coverage html

Open ``htmlcov/index.html`` in your browser to explore the report.

Read more about `coverage <https://coverage.readthedocs.io>`__.


Building the docs
~~~~~~~~~~~~~~~~~

Build the docs in the ``docs`` directory using Sphinx.

.. code-block:: text

    $ cd docs
    $ make html

Open ``_build/html/index.html`` in your browser to view the docs.

Read more about `Sphinx <https://www.sphinx-doc.org/en/stable/>`__.

[end of CONTRIBUTING.rst]
[start of .pre-commit-config.yaml]
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.7.1
    hooks:
      - id: ruff
      - id: ruff-format
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v5.0.0
    hooks:
      - id: check-merge-conflict
      - id: debug-statements
      - id: fix-byte-order-marker
      - id: trailing-whitespace
      - id: end-of-file-fixer

[end of .pre-commit-config.yaml]
[start of pyproject.toml]
[project]
name = "MarkupSafe"
version = "3.1.0.dev"
description = "Safely add untrusted strings to HTML/XML markup."
readme = "README.md"
license = { file = "LICENSE.txt" }
maintainers = [{ name = "Pallets", email = "contact@palletsprojects.com" }]
classifiers = [
    "Development Status :: 5 - Production/Stable",
    "Environment :: Web Environment",
    "Intended Audience :: Developers",
    "License :: OSI Approved :: BSD License",
    "Operating System :: OS Independent",
    "Programming Language :: Python",
    "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
    "Topic :: Text Processing :: Markup :: HTML",
    "Typing :: Typed",
]
requires-python = ">=3.9"

[project.urls]
Donate = "https://palletsprojects.com/donate"
Documentation = "https://markupsafe.palletsprojects.com/"
Changes = "https://markupsafe.palletsprojects.com/changes/"
Source = "https://github.com/pallets/markupsafe/"
Chat = "https://discord.gg/pallets"

[build-system]
requires = ["setuptools>=70.1"]
build-backend = "setuptools.build_meta"

[tool.pytest.ini_options]
testpaths = ["tests"]
filterwarnings = [
    "error",
]

[tool.coverage.run]
branch = true
source = ["markupsafe", "tests"]

[tool.coverage.paths]
source = ["src", "*/site-packages"]

[tool.mypy]
python_version = "3.9"
files = ["src/markupsafe", "tests"]
show_error_codes = true
pretty = true
strict = true

[[tool.mypy.overrides]]
module = [
    "argcomplete.*",
]
ignore_missing_imports = true

[tool.pyright]
pythonVersion = "3.9"
include = ["src/markupsafe", "tests"]
typeCheckingMode = "basic"

[tool.ruff]
src = ["src"]
fix = true
show-fixes = true
output-format = "full"

[tool.ruff.lint]
select = [
    "B",  # flake8-bugbear
    "E",  # pycodestyle error
    "F",  # pyflakes
    "I",  # isort
    "UP",  # pyupgrade
    "W",  # pycodestyle warning
]

[tool.ruff.lint.isort]
force-single-line = true
order-by-type = false

[tool.gha-update]
tag-only = [
    "slsa-framework/slsa-github-generator",
]

[end of pyproject.toml]
[start of MANIFEST.in]
include CHANGES.rst
include tox.ini
include requirements/*.txt
graft docs
prune docs/_build
graft tests
include src/markupsafe/py.typed
include src/markupsafe/*.pyi
global-exclude *.pyc

[end of MANIFEST.in]
[start of .editorconfig]
root = true

[*]
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
charset = utf-8
max_line_length = 88

[*.{css,html,js,json,jsx,scss,ts,tsx,yaml,yml}]
indent_size = 2

[end of .editorconfig]
[start of bench.py]
import subprocess
import sys

for name, s in (
    ("short escape", '"<strong>Hello, World!</strong>"'),
    ("long escape", '"Hello, World!" * 1000'),
    ("short plain", '"Hello, World!"'),
    ("long plain", '"Hello, World!" * 1000'),
    ("long suffix", '"<strong>Hello, World!</strong>" + "x" * 100_000'),
):
    for mod in "native", "speedups":
        subprocess.run(
            [
                sys.executable,
                "-m",
                "pyperf",
                "timeit",
                "--name",
                f"{name} {mod}",
                "-s",
                (
                    "import markupsafe\
"
                    f"from markupsafe._{mod} import _escape_inner\
"
                    "markupsafe._escape_inner = _escape_inner\
"
                    "from markupsafe import escape\
"
                    f"s = {s}"
                ),
                "escape(s)",
            ]
        )

[end of bench.py]
[start of README.md]
# MarkupSafe

MarkupSafe implements a text object that escapes characters so it is
safe to use in HTML and XML. Characters that have special meanings are
replaced so that they display as the actual characters. This mitigates
injection attacks, meaning untrusted user input can safely be displayed
on a page.


## Examples

```pycon
>>> from markupsafe import Markup, escape

>>> # escape replaces special characters and wraps in Markup
>>> escape("<script>alert(document.cookie);</script>")
Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')

>>> # wrap in Markup to mark text "safe" and prevent escaping
>>> Markup("<strong>Hello</strong>")
Markup('<strong>hello</strong>')

>>> escape(Markup("<strong>Hello</strong>"))
Markup('<strong>hello</strong>')

>>> # Markup is a str subclass
>>> # methods and operators escape their arguments
>>> template = Markup("Hello <em>{name}</em>")
>>> template.format(name='"World"')
Markup('Hello <em>&#34;World&#34;</em>')
```

## Donate

The Pallets organization develops and supports MarkupSafe and other
popular packages. In order to grow the community of contributors and
users, and allow the maintainers to devote more time to the projects,
[please donate today][].

[please donate today]: https://palletsprojects.com/donate

[end of README.md]
[start of setup.py]
import os
import platform
import sys

from setuptools import Extension
from setuptools import setup
from setuptools.command.build_ext import build_ext
from setuptools.errors import CCompilerError
from setuptools.errors import ExecError
from setuptools.errors import PlatformError

ext_modules = [Extension("markupsafe._speedups", ["src/markupsafe/_speedups.c"])]


class BuildFailed(Exception):
    pass


class ve_build_ext(build_ext):
    """This class allows C extension building to fail."""

    def run(self):
        try:
            super().run()
        except PlatformError as e:
            raise BuildFailed() from e

    def build_extension(self, ext):
        try:
            super().build_extension(ext)
        except (CCompilerError, ExecError, PlatformError) as e:
            raise BuildFailed() from e
        except ValueError as e:
            # this can happen on Windows 64 bit, see Python issue 7511
            if "'path'" in str(sys.exc_info()[1]):  # works with Python 2 and 3
                raise BuildFailed() from e
            raise


def run_setup(with_binary):
    setup(
        cmdclass={"build_ext": ve_build_ext},
        ext_modules=ext_modules if with_binary else [],
    )


def show_message(*lines):
    print("=" * 74)
    for line in lines:
        print(line)
    print("=" * 74)


supports_speedups = platform.python_implementation() not in {
    "PyPy",
    "Jython",
    "GraalVM",
}

if os.environ.get("CIBUILDWHEEL", "0") == "1" and supports_speedups:
    run_setup(True)
elif supports_speedups:
    try:
        run_setup(True)
    except BuildFailed:
        show_message(
            "WARNING: The C extension could not be compiled, speedups"
            " are not enabled.",
            "Failure information, if any, is above.",
            "Retrying the build without the C extension now.",
        )
        run_setup(False)
        show_message(
            "WARNING: The C extension could not be compiled, speedups"
            " are not enabled.",
            "Plain-Python build succeeded.",
        )
else:
    run_setup(False)
    show_message(
        "WARNING: C extensions are not supported on this Python"
        " platform, speedups are not enabled.",
        "Plain-Python build succeeded.",
    )

[end of setup.py]
[start of tox.ini]
[tox]
envlist =
    py3{13,12,11,10,9}
    pypy310
    style
    typing
    docs
skip_missing_interpreters = true

[testenv]
package = wheel
pass_env = PYTHON_GIL
constrain_package_deps = true
use_frozen_constraints = true
deps = -r requirements/tests.txt
commands = pytest -v --tb=short --basetemp={envtmpdir} {posargs}

[testenv:style]
deps = pre-commit
skip_install = true
commands = pre-commit run --all-files

[testenv:typing]
deps = -r requirements/typing.txt
commands = mypy

[testenv:docs]
deps = -r requirements/docs.txt
commands = sphinx-build -E -W -b dirhtml docs docs/_build/dirhtml

[testenv:update-actions]
labels = update
deps = gha-update
commands = gha-update

[testenv:update-pre_commit]
labels = update
deps = pre-commit
skip_install = true
commands = pre-commit autoupdate -j4

[testenv:update-requirements]
labels = update
deps = pip-tools
skip_install = true
change_dir = requirements
commands =
    pip-compile build.in -q {posargs:-U}
    pip-compile docs.in -q {posargs:-U}
    pip-compile tests.in -q {posargs:-U}
    pip-compile typing.in -q {posargs:-U}
    pip-compile dev.in -q {posargs:-U}

[end of tox.ini]
[start of LICENSE.txt]
Copyright 2010 Pallets

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:

1.  Redistributions of source code must retain the above copyright
    notice, this list of conditions and the following disclaimer.

2.  Redistributions in binary form must reproduce the above copyright
    notice, this list of conditions and the following disclaimer in the
    documentation and/or other materials provided with the distribution.

3.  Neither the name of the copyright holder nor the names of its
    contributors may be used to endorse or promote products derived from
    this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

[end of LICENSE.txt]
[start of CHANGES.rst]
Version 3.1.0
-------------

Unreleased


Version 3.0.2
-------------

Released 2024-10-18

-   Fix compatibility when ``__str__`` returns a ``str`` subclass. :issue:`472`
-   Build requires setuptools >= 70.1. :issue:`475`


Version 3.0.1
-------------

Released 2024-10-08

-   Address compiler warnings that became errors in GCC 14. :issue:`466`
-   Fix compatibility with proxy objects. :issue:`467`


Version 3.0.0
-------------

Released 2024-10-07

-   Support Python 3.13 and its experimental free-threaded build. :pr:`461`
-   Drop support for Python 3.7 and 3.8.
-   Use modern packaging metadata with ``pyproject.toml`` instead of ``setup.cfg``.
    :pr:`348`
-   Change ``distutils`` imports to ``setuptools``. :pr:`399`
-   Use deferred evaluation of annotations. :pr:`400`
-   Update signatures for ``Markup`` methods to match ``str`` signatures. Use
    positional-only arguments. :pr:`400`
-   Some ``str`` methods on ``Markup`` no longer escape their argument:
    ``strip``, ``lstrip``, ``rstrip``, ``removeprefix``, ``removesuffix``,
    ``partition``, and ``rpartition``; ``replace`` only escapes its ``new``
    argument. These methods are conceptually linked to search methods such as
    ``in``, ``find``, and ``index``, which already do not escape their argument.
    :issue:`401`
-   The ``__version__`` attribute is deprecated. Use feature detection, or
    ``importlib.metadata.version("markupsafe")``, instead. :pr:`402`
-   Speed up escaping plain strings by 40%. :pr:`434`
-   Simplify speedups implementation. :pr:`437`


Version 2.1.5
-------------

Released 2024-02-02

-   Fix ``striptags`` not collapsing spaces. :issue:`417`


Version 2.1.4
-------------

Released 2024-01-19

-   Don't use regular expressions for ``striptags``, avoiding a performance
    issue. :pr:`413`


Version 2.1.3
-------------

Released 2023-06-02

-   Implement ``format_map``, ``casefold``, ``removeprefix``, and ``removesuffix``
    methods. :issue:`370`
-   Fix static typing for basic ``str`` methods on ``Markup``. :issue:`358`
-   Use ``Self`` for annotating return types. :pr:`379`


Version 2.1.2
-------------

Released 2023-01-17

-   Fix ``striptags`` not stripping tags containing newlines.
    :issue:`310`


Version 2.1.1
-------------

Released 2022-03-14

-   Avoid ambiguous regex matches in ``striptags``. :pr:`293`


Version 2.1.0
-------------

Released 2022-02-17

-   Drop support for Python 3.6. :pr:`262`
-   Remove ``soft_unicode``, which was previously deprecated. Use
    ``soft_str`` instead. :pr:`261`
-   Raise error on missing single placeholder during string
    interpolation. :issue:`225`
-   Disable speedups module for GraalPython. :issue:`277`


Version 2.0.1
-------------

Released 2021-05-18

-   Mark top-level names as exported so type checking understands
    imports in user projects. :pr:`215`
-   Fix some types that weren't available in Python 3.6.0. :pr:`215`


Version 2.0.0
-------------

Released 2021-05-11

-   Drop Python 2.7, 3.4, and 3.5 support.
-   ``Markup.unescape`` uses :func:`html.unescape` to support HTML5
    character references. :pr:`117`
-   Add type annotations for static typing tools. :pr:`149`


Version 1.1.1
-------------

Released 2019-02-23

-   Fix segfault when ``__html__`` method raises an exception when using
    the C speedups. The exception is now propagated correctly. :pr:`109`


Version 1.1.0
-------------

Released 2018-11-05

-   Drop support for Python 2.6 and 3.3.
-   Build wheels for Linux, Mac, and Windows, allowing systems without
    a compiler to take advantage of the C extension speedups. :pr:`104`
-   Use newer CPython API on Python 3, resulting in a 1.5x speedup.
    :pr:`64`
-   ``escape`` wraps ``__html__`` result in ``Markup``, consistent with
    documented behavior. :pr:`69`


Version 1.0
-----------

Released 2017-03-07

-   Fixed custom types not invoking ``__unicode__`` when used with
    ``format()``.
-   Added ``__version__`` module attribute.
-   Improve unescape code to leave lone ampersands alone.


Version 0.18
------------

Released 2013-05-22

-   Fixed ``__mul__`` and string splitting on Python 3.


Version 0.17
------------

Released 2013-05-21

-   Fixed a bug with broken interpolation on tuples.


Version 0.16
------------

Released 2013-05-20

-   Improved Python 3 Support and removed 2to3.
-   Removed support for Python 3.2 and 2.5.


Version 0.15
------------

Released 2011-07-20

-   Fixed a typo that caused the library to fail to install on pypy and
    jython.


Version 0.14
------------

Released 2011-07-20

-   Release fix for 0.13.


Version 0.13
------------

Released 2011-07-20

-   Do not attempt to compile extension for PyPy or Jython.
-   Work around some 64bit Windows issues.


Version 0.12
------------

Released 2011-02-17

-   Improved PyPy compatibility.

[end of CHANGES.rst]
[start of .readthedocs.yaml]
version: 2
build:
  os: ubuntu-24.04
  tools:
    python: '3.12'
python:
  install:
    - requirements: requirements/docs.txt
    - method: pip
      path: .
sphinx:
  builder: dirhtml
  fail_on_warning: true

[end of .readthedocs.yaml]
[start of requirements/dev.txt]
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
#    pip-compile dev.in
#
alabaster==1.0.0
    # via
    #   -r docs.txt
    #   sphinx
babel==2.16.0
    # via
    #   -r docs.txt
    #   sphinx
cachetools==5.5.0
    # via tox
certifi==2024.8.30
    # via
    #   -r docs.txt
    #   requests
cfgv==3.4.0
    # via pre-commit
chardet==5.2.0
    # via tox
charset-normalizer==3.4.0
    # via
    #   -r docs.txt
    #   requests
colorama==0.4.6
    # via tox
distlib==0.3.9
    # via virtualenv
docutils==0.21.2
    # via
    #   -r docs.txt
    #   sphinx
filelock==3.16.1
    # via
    #   tox
    #   virtualenv
identify==2.6.1
    # via pre-commit
idna==3.10
    # via
    #   -r docs.txt
    #   requests
imagesize==1.4.1
    # via
    #   -r docs.txt
    #   sphinx
iniconfig==2.0.0
    # via
    #   -r tests.txt
    #   -r typing.txt
    #   pytest
jinja2==3.1.4
    # via
    #   -r docs.txt
    #   sphinx
markupsafe==3.0.2
    # via
    #   -r docs.txt
    #   jinja2
mypy==1.13.0
    # via -r typing.txt
mypy-extensions==1.0.0
    # via
    #   -r typing.txt
    #   mypy
nodeenv==1.9.1
    # via
    #   -r typing.txt
    #   pre-commit
    #   pyright
packaging==24.1
    # via
    #   -r docs.txt
    #   -r tests.txt
    #   -r typing.txt
    #   pallets-sphinx-themes
    #   pyproject-api
    #   pytest
    #   sphinx
    #   tox
pallets-sphinx-themes==2.3.0
    # via -r docs.txt
platformdirs==4.3.6
    # via
    #   tox
    #   virtualenv
pluggy==1.5.0
    # via
    #   -r tests.txt
    #   -r typing.txt
    #   pytest
    #   tox
pre-commit==4.0.1
    # via -r dev.in
pygments==2.18.0
    # via
    #   -r docs.txt
    #   sphinx
pyproject-api==1.8.0
    # via tox
pyright==1.1.386
    # via -r typing.txt
pytest==8.3.3
    # via
    #   -r tests.txt
    #   -r typing.txt
pyyaml==6.0.2
    # via pre-commit
requests==2.32.3
    # via
    #   -r docs.txt
    #   sphinx
snowballstemmer==2.2.0
    # via
    #   -r docs.txt
    #   sphinx
sphinx==8.1.3
    # via
    #   -r docs.txt
    #   pallets-sphinx-themes
    #   sphinx-notfound-page
    #   sphinxcontrib-log-cabinet
sphinx-notfound-page==1.0.4
    # via
    #   -r docs.txt
    #   pallets-sphinx-themes
sphinxcontrib-applehelp==2.0.0
    # via
    #   -r docs.txt
    #   sphinx
sphinxcontrib-devhelp==2.0.0
    # via
    #   -r docs.txt
    #   sphinx
sphinxcontrib-htmlhelp==2.1.0
    # via
    #   -r docs.txt
    #   sphinx
sphinxcontrib-jsmath==1.0.1
    # via
    #   -r docs.txt
    #   sphinx
sphinxcontrib-log-cabinet==1.0.1
    # via -r docs.txt
sphinxcontrib-qthelp==2.0.0
    # via
    #   -r docs.txt
    #   sphinx
sphinxcontrib-serializinghtml==2.0.0
    # via
    #   -r docs.txt
    #   sphinx
tox==4.23.2
    # via -r dev.in
typing-extensions==4.12.2
    # via
    #   -r typing.txt
    #   mypy
    #   pyright
urllib3==2.2.3
    # via
    #   -r docs.txt
    #   requests
virtualenv==20.27.0
    # via
    #   pre-commit
    #   tox

[end of requirements/dev.txt]
[start of requirements/docs.in]
pallets-sphinx-themes
sphinx
sphinxcontrib-log-cabinet

[end of requirements/docs.in]
[start of requirements/build.in]
build

[end of requirements/build.in]
[start of requirements/typing.txt]
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
#    pip-compile typing.in
#
iniconfig==2.0.0
    # via pytest
mypy==1.13.0
    # via -r typing.in
mypy-extensions==1.0.0
    # via mypy
nodeenv==1.9.1
    # via pyright
packaging==24.1
    # via pytest
pluggy==1.5.0
    # via pytest
pyright==1.1.386
    # via -r typing.in
pytest==8.3.3
    # via -r typing.in
typing-extensions==4.12.2
    # via
    #   mypy
    #   pyright

[end of requirements/typing.txt]
[start of requirements/build.txt]
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
#    pip-compile build.in
#
build==1.2.2.post1
    # via -r build.in
packaging==24.1
    # via build
pyproject-hooks==1.2.0
    # via build

[end of requirements/build.txt]
[start of requirements/typing.in]
mypy
pyright
pytest

[end of requirements/typing.in]
[start of requirements/dev.in]
-r docs.txt
-r tests.txt
-r typing.txt
pre-commit
tox

[end of requirements/dev.in]
[start of requirements/docs.txt]
#
# This file is autogenerated by pip-compile with Python 3.13
# by the following command:
#
#    pip-compile docs.in
#
alabaster==1.0.0
    # via sphinx
babel==2.16.0
    # via sphinx
certifi==2024.8.30
    # via requests
charset-normalizer==3.4.0
    # via requests
docutils==0.21.2
    # via sphinx
idna==3.10
    # via requests
imagesize==1.4.1
    # via sphinx
jinja2==3.1.4
    # via sphinx
markupsafe==3.0.2
    # via jinja2
packaging==24.1
    # via
    #   pallets-sphinx-themes
    #   sphinx
pallets-sphinx-themes==2.3.0
    # via -r docs.in
pygments==2.18.0
    # via sphinx
requests==2.32.3
    # via sphinx
snowballstemmer==2.2.0
    # via sphinx
sphinx==8.1.3
    # via
    #   -r docs.in
    #   pallets-sphinx-themes
    #   sphinx-notfound-page
    #   sphinxcontrib-log-cabinet
sphinx-notfound-page==1.0.4
    # via pallets-sphinx-themes
sphinxcontrib-applehelp==2.0.0
    # via sphinx
sphinxcontrib-devhelp==2.0.0
    # via sphinx
sphinxcontrib-htmlhelp==2.1.0
    # via sphinx
sphinxcontrib-jsmath==1.0.1
    # via sphinx
sphinxcontrib-log-cabinet==1.0.1
    # via -r docs.in
sphinxcontrib-qthelp==2.0.0
    # via sphinx
sphinxcontrib-serializinghtml==2.0.0
    # via sphinx
urllib3==2.2.3
    # via requests

[end of requirements/docs.txt]
[start of docs/formatting.rst]
.. currentmodule:: markupsafe

String Formatting
=================

The :class:`Markup` class can be used as a format string. Objects
formatted into a markup string will be escaped first.


Format Method
-------------

The ``format`` method extends the standard :meth:`str.format` behavior
to use an ``__html_format__`` method.

#.  If an object has an ``__html_format__`` method, it is called as a
    replacement for the ``__format__`` method. It is passed a format
    specifier if it's given. The method must return a string or
    :class:`Markup` instance.

#.  If an object has an ``__html__`` method, it is called. If a format
    specifier was passed and the class defined ``__html__`` but not
    ``__html_format__``, a ``ValueError`` is raised.

#.  Otherwise Python's default format behavior is used and the result
    is escaped.

For example, to implement a ``User`` that wraps its ``name`` in a
``span`` tag, and adds a link when using the ``"link"`` format
specifier:

.. code-block:: python

    class User(object):
        def __init__(self, id, name):
            self.id = id
            self.name = name

        def __html_format__(self, format_spec):
            if format_spec == "link":
                return Markup(
                    '<a href="/user/{}">{}</a>'
                ).format(self.id, self.__html__())
            elif format_spec:
                raise ValueError("Invalid format spec")
            return self.__html__()

        def __html__(self):
            return Markup(
                '<span class="user">{0}</span>'
            ).format(self.name)


.. code-block:: pycon

    >>> user = User(3, "<script>")
    >>> escape(user)
    Markup('<span class="user">&lt;script&gt;</span>')
    >>> Markup("<p>User: {user:link}").format(user=user)
    Markup('<p>User: <a href="/user/3"><span class="user">&lt;script&gt;</span></a>

See Python's docs on :ref:`format string syntax <python:formatstrings>`.


printf-style Formatting
-----------------------

Besides escaping, there's no special behavior involved with percent
formatting.

.. code-block:: pycon

    >>> user = User(3, "<script>")
    >>> Markup('<a href="/user/%d">%s</a>') % (user.id, user.name)
    Markup('<a href="/user/3">&lt;script&gt;</a>')

See Python's docs on :ref:`printf-style formatting <python:old-string-formatting>`.

[end of docs/formatting.rst]
[start of docs/index.rst]
.. currentmodule:: markupsafe

MarkupSafe
==========

MarkupSafe escapes characters so text is safe to use in HTML and XML.
Characters that have special meanings are replaced so that they display
as the actual characters. This mitigates injection attacks, meaning
untrusted user input can safely be displayed on a page.

The :func:`escape` function escapes text and returns a :class:`Markup`
object. The object won't be escaped anymore, but any text that is used
with it will be, ensuring that the result remains safe to use in HTML.

>>> from markupsafe import escape
>>> hello = escape("<em>Hello</em>")
>>> hello
Markup('&lt;em&gt;Hello&lt;/em&gt;')
>>> escape(hello)
Markup('&lt;em&gt;Hello&lt;/em&gt;')
>>> hello + " <strong>World</strong>"
Markup('&lt;em&gt;Hello&lt;/em&gt; &lt;strong&gt;World&lt;/strong&gt;')


Installing
----------

Install and update using `pip`_:

.. code-block:: text

    pip install -U MarkupSafe

.. _pip: https://pip.pypa.io/en/stable/quickstart/


Table of Contents
-----------------

.. toctree::
    :maxdepth: 2

    escaping
    html
    formatting
    license
    changes

[end of docs/index.rst]
[start of docs/escaping.rst]
.. module:: markupsafe

Working With Safe Text
======================

.. autofunction:: escape

.. autoclass:: Markup
    :members: escape, unescape, striptags


Optional Values
---------------

.. autofunction:: escape_silent


Convert an Object to a String
-----------------------------

.. autofunction:: soft_str

[end of docs/escaping.rst]
[start of docs/Makefile]
# Minimal makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
SOURCEDIR     = .
BUILDDIR      = _build

# Put it first so that "make" without argument is like "make help".
help:
    @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

.PHONY: help Makefile

# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
    @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

[end of docs/Makefile]
[start of docs/conf.py]
from pallets_sphinx_themes import get_version
from pallets_sphinx_themes import ProjectLink

# Project --------------------------------------------------------------

project = "MarkupSafe"
copyright = "2010 Pallets"
author = "Pallets"
release, version = get_version("MarkupSafe")

# General --------------------------------------------------------------

default_role = "code"
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.extlinks",
    "sphinx.ext.intersphinx",
    "sphinxcontrib.log_cabinet",
    "pallets_sphinx_themes",
]
autodoc_member_order = "bysource"
autodoc_typehints = "description"
autodoc_preserve_defaults = True
extlinks = {
    "issue": ("https://github.com/pallets/markupsafe/issues/%s", "#%s"),
    "pr": ("https://github.com/pallets/markupsafe/pull/%s", "#%s"),
}
intersphinx_mapping = {
    "python": ("https://docs.python.org/3/", None),
}

# HTML -----------------------------------------------------------------

html_theme = "jinja"
html_theme_options = {"index_sidebar_logo": False}
html_context = {
    "project_links": [
        ProjectLink("Donate", "https://palletsprojects.com/donate"),
        ProjectLink("PyPI Releases", "https://pypi.org/project/MarkupSafe/"),
        ProjectLink("Source Code", "https://github.com/pallets/markupsafe/"),
        ProjectLink("Issue Tracker", "https://github.com/pallets/markupsafe/issues/"),
        ProjectLink("Chat", "https://discord.gg/pallets"),
    ]
}
html_sidebars = {
    "index": ["project.html", "localtoc.html", "searchbox.html", "ethicalads.html"],
    "**": ["localtoc.html", "relations.html", "searchbox.html", "ethicalads.html"],
}
singlehtml_sidebars = {"index": ["project.html", "localtoc.html", "ethicalads.html"]}
html_title = f"MarkupSafe Documentation ({version})"
html_show_sourcelink = False

[end of docs/conf.py]
[start of docs/html.rst]
.. currentmodule:: markupsafe

HTML Representations
====================

In many frameworks, if a class implements an ``__html__`` method it
will be used to get the object's representation in HTML. MarkupSafe's
:func:`escape` function and :class:`Markup` class understand and
implement this method. If an object has an ``__html__`` method it will
be called rather than converting the object to a string, and the result
will be assumed safe and not escaped.

For example, an ``Image`` class might automatically generate an
``<img>`` tag:

.. code-block:: python

    class Image:
        def __init__(self, url):
            self.url = url

        def __html__(self):
            return f'<img src="{self.url}">'

.. code-block:: pycon

    >>> img = Image("/static/logo.png")
    >>> Markup(img)
    Markup('<img src="/static/logo.png">')

Since this bypasses escaping, you need to be careful about using
user-provided data in the output. For example, a user's display name
should still be escaped:

.. code-block:: python

    class User:
        def __init__(self, id, name):
            self.id = id
            self.name = name

        def __html__(self):
            return f'<a href="/user/{self.id}">{escape(self.name)}</a>'

.. code-block:: pycon

    >>> user = User(3, "<script>")
    >>> escape(user)
    Markup('<a href="/users/3">&lt;script&gt;</a>')

[end of docs/html.rst]
[start of docs/make.bat]
@ECHO OFF

pushd %~dp0

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
    set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=.
set BUILDDIR=_build

if "%1" == "" goto help

%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
    echo.
    echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
    echo.installed, then set the SPHINXBUILD environment variable to point
    echo.to the full path of the 'sphinx-build' executable. Alternatively you
    echo.may add the Sphinx directory to PATH.
    echo.
    echo.If you don't have Sphinx installed, grab it from
    echo.http://sphinx-doc.org/
    exit /b 1
)

%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
goto end

:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%

:end
popd

[end of docs/make.bat]
[start of docs/license.rst]
BSD-3-Clause License
====================

.. literalinclude:::: ../LICENSE.txt
    :language: text

[end of docs/license.rst]
[start of docs/changes.rst]
Changes
=======

.. include:: ../CHANGES.rst

[end of docs/changes.rst]
[start of .devcontainer/on-create-command.sh]
#!/bin/bash
set -e
python3 -m venv --upgrade-deps .venv
. .venv/bin/activate
pip install -r requirements/dev.txt
pip install -e .
pre-commit install --install-hooks

[end of .devcontainer/on-create-command.sh]
[start of .devcontainer/devcontainer.json]
{
  "name": "pallets/markupsafe",
  "image": "mcr.microsoft.com/devcontainers/python:3",
  "customizations": {
    "vscode": {
      "settings": {
        "python.defaultInterpreterPath": "${workspaceFolder}/.venv",
        "python.terminal.activateEnvInCurrentTerminal": true,
        "python.terminal.launchArgs": [
          "-X",
          "dev"
        ]
      }
    }
  },
  "onCreateCommand": ".devcontainer/on-create-command.sh"
}

[end of .devcontainer/devcontainer.json]
[start of src/markupsafe/_speedups.c]
#include <Python.h>

#define GET_DELTA(inp, inp_end, delta) \\
    while (inp < inp_end) { \\
        switch (*inp++) { \\
        case '"': \\
        case '\\'': \\
        case '&': \\
            delta += 4; \\
            break; \\
        case '<': \\
        case '>': \\
            delta += 3; \\
            break; \\
        } \\
    }

#define DO_ESCAPE(inp, inp_end, outp) \\
    { \\
        Py_ssize_t ncopy = 0; \\
        while (inp < inp_end) { \\
            switch (*inp) { \\
            case '"': \\
                memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
                outp += ncopy; ncopy = 0; \\
                *outp++ = '&'; \\
                *outp++ = '#'; \\
                *outp++ = '3'; \\
                *outp++ = '4'; \\
                *outp++ = ';'; \\
                break; \\
            case '\\'': \\
                memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
                outp += ncopy; ncopy = 0; \\
                *outp++ = '&'; \\
                *outp++ = '#'; \\
                *outp++ = '3'; \\
                *outp++ = '9'; \\
                *outp++ = ';'; \\
                break; \\
            case '&': \\
                memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
                outp += ncopy; ncopy = 0; \\
                *outp++ = '&'; \\
                *outp++ = 'a'; \\
                *outp++ = 'm'; \\
                *outp++ = 'p'; \\
                *outp++ = ';'; \\
                break; \\
            case '<': \\
                memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
                outp += ncopy; ncopy = 0; \\
                *outp++ = '&'; \\
                *outp++ = 'l'; \\
                *outp++ = 't'; \\
                *outp++ = ';'; \\
                break; \\
            case '>': \\
                memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
                outp += ncopy; ncopy = 0; \\
                *outp++ = '&'; \\
                *outp++ = 'g'; \\
                *outp++ = 't'; \\
                *outp++ = ';'; \\
                break; \\
            default: \\
                ncopy++; \\
            } \\
            inp++; \\
        } \\
        memcpy(outp, inp-ncopy, sizeof(*outp)*ncopy); \\
    }

static PyObject*
escape_unicode_kind1(PyUnicodeObject *in)
{
    Py_UCS1 *inp = PyUnicode_1BYTE_DATA(in);
    Py_UCS1 *inp_end = inp + PyUnicode_GET_LENGTH(in);
    Py_UCS1 *outp;
    PyObject *out;
    Py_ssize_t delta = 0;

    GET_DELTA(inp, inp_end, delta);
    if (!delta) {
        Py_INCREF(in);
        return (PyObject*)in;
    }

    out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta,
                        PyUnicode_IS_ASCII(in) ? 127 : 255);
    if (!out)
        return NULL;

    inp = PyUnicode_1BYTE_DATA(in);
    outp = PyUnicode_1BYTE_DATA(out);
    DO_ESCAPE(inp, inp_end, outp);
    return out;
}

static PyObject*
escape_unicode_kind2(PyUnicodeObject *in)
{
    Py_UCS2 *inp = PyUnicode_2BYTE_DATA(in);
    Py_UCS2 *inp_end = inp + PyUnicode_GET_LENGTH(in);
    Py_UCS2 *outp;
    PyObject *out;
    Py_ssize_t delta = 0;

    GET_DELTA(inp, inp_end, delta);
    if (!delta) {
        Py_INCREF(in);
        return (PyObject*)in;
    }

    out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 65535);
    if (!out)
        return NULL;

    inp = PyUnicode_2BYTE_DATA(in);
    outp = PyUnicode_2BYTE_DATA(out);
    DO_ESCAPE(inp, inp_end, outp);
    return out;
}


static PyObject*
escape_unicode_kind4(PyUnicodeObject *in)
{
    Py_UCS4 *inp = PyUnicode_4BYTE_DATA(in);
    Py_UCS4 *inp_end = inp + PyUnicode_GET_LENGTH(in);
    Py_UCS4 *outp;
    PyObject *out;
    Py_ssize_t delta = 0;

    GET_DELTA(inp, inp_end, delta);
    if (!delta) {
        Py_INCREF(in);
        return (PyObject*)in;
    }

    out = PyUnicode_New(PyUnicode_GET_LENGTH(in) + delta, 1114111);
    if (!out)
        return NULL;

    inp = PyUnicode_4BYTE_DATA(in);
    outp = PyUnicode_4BYTE_DATA(out);
    DO_ESCAPE(inp, inp_end, outp);
    return out;
}

static PyObject*
escape_unicode(PyObject *self, PyObject *s)
{
    if (!PyUnicode_Check(s))
        return NULL;

    // This check is no longer needed in Python 3.12.
    if (PyUnicode_READY(s))
        return NULL;

    switch (PyUnicode_KIND(s)) {
    case PyUnicode_1BYTE_KIND:
        return escape_unicode_kind1((PyUnicodeObject*) s);
    case PyUnicode_2BYTE_KIND:
        return escape_unicode_kind2((PyUnicodeObject*) s);
    case PyUnicode_4BYTE_KIND:
        return escape_unicode_kind4((PyUnicodeObject*) s);
    }
    assert(0);  /* shouldn't happen */
    return NULL;
}

static PyMethodDef module_methods[] = {
    {"_escape_inner", (PyCFunction)escape_unicode, METH_O, NULL},
    {NULL, NULL, 0, NULL}  /* Sentinel */
};

static struct PyModuleDef module_definition = {
    PyModuleDef_HEAD_INIT,
    "markupsafe._speedups",
    NULL,
    -1,
    module_methods,
    NULL,
    NULL,
    NULL,
    NULL
};

PyMODINIT_FUNC
PyInit__speedups(void)
{
    PyObject *m = PyModule_Create(&module_definition);

    if (m == NULL) {
        return NULL;
    }

    #ifdef Py_GIL_DISABLED
    PyUnstable_Module_SetGIL(m, Py_MOD_GIL_NOT_USED);
    #endif

    return m;
}

[end of src/markupsafe/_speedups.c]
[start of src/markupsafe/__init__.py]
from __future__ import annotations

import collections.abc as cabc
import string
import typing as t

try:
    from ._speedups import _escape_inner
except ImportError:
    from ._native import _escape_inner

if t.TYPE_CHECKING:
    import typing_extensions as te


class _HasHTML(t.Protocol):
    def __html__(self, /) -> str: ...


class _TPEscape(t.Protocol):
    def __call__(self, s: t.Any, /) -> Markup: ...


def escape(s: t.Any, /) -> Markup:
    """Replace the characters ``&``, ``<``, ``>``, ``'``, and ``"`` in
    the string with HTML-safe sequences. Use this if you need to display
    text that might contain such characters in HTML.

    If the object has an ``__html__`` method, it is called and the
    return value is assumed to already be safe for HTML.

    :param s: An object to be converted to a string and escaped.
    :return: A :class:`Markup` string with the escaped text.
    """
    # If the object is already a plain string, skip __html__ check and string
    # conversion. This is the most common use case.
    # Use type(s) instead of s.__class__ because a proxy object may be reporting
    # the __class__ of the proxied value.
    if type(s) is str:
        return Markup(_escape_inner(s))

    if hasattr(s, "__html__"):
        return Markup(s.__html__())

    return Markup(_escape_inner(str(s)))


def escape_silent(s: t.Any | None, /) -> Markup:
    """Like :func:`escape` but treats ``None`` as the empty string.
    Useful with optional values, as otherwise you get the string
    ``'None'`` when the value is ``None``.

    >>> escape(None)
    Markup('None')
    >>> escape_silent(None)
    Markup('')
    """
    if s is None:
        return Markup()

    return escape(s)


def soft_str(s: t.Any, /) -> str:
    """Convert an object to a string if it isn't already. This preserves
    a :class:`Markup` string rather than converting it back to a basic
    string, so it will still be marked as safe and won't be escaped
    again.

    >>> value = escape("<User 1>")
    >>> value
    Markup('&lt;User 1&gt;')
    >>> escape(str(value))
    Markup('&amp;lt;User 1&amp;gt;')
    >>> escape(soft_str(value))
    Markup('&lt;User 1&gt;')
    """
    if not isinstance(s, str):
        return str(s)

    return s


class Markup(str):
    """A string that is ready to be safely inserted into an HTML or XML
    document, either because it was escaped or because it was marked
    safe.

    Passing an object to the constructor converts it to text and wraps
    it to mark it safe without escaping. To escape the text, use the
    :meth:`escape` class method instead.

    >>> Markup("Hello, <em>World</em>!")
    Markup('Hello, <em>World</em>!')
    >>> Markup(42)
    Markup('42')
    >>> Markup.escape("Hello, <em>World</em>!")
    Markup('Hello &lt;em&gt;World&lt;/em&gt;!')

    This implements the ``__html__()`` interface that some frameworks
    use. Passing an object that implements ``__html__()`` will wrap the
    output of that method, marking it safe.

    >>> class Foo:
    ...     def __html__(self):
    ...         return '<a href="/foo">foo</a>'
    ...
    >>> Markup(Foo())
    Markup('<a href="/foo">foo</a>')

    This is a subclass of :class:`str`. It has the same methods, but
    escapes their arguments and returns a ``Markup`` instance.

    >>> Markup("<em>%s</em>") % ("foo & bar",)
    Markup('<em>foo &amp; bar</em>')
    >>> Markup("<em>Hello</em> ") + "<foo>"
    Markup('<em>Hello</em> &lt;foo&gt;')
    """

    __slots__ = ()

    def __new__(
        cls, object: t.Any = "", encoding: str | None = None, errors: str = "strict"
    ) -> te.Self:
        if hasattr(object, "__html__"):
            object = object.__html__()

        if encoding is None:
            return super().__new__(cls, object)

        return super().__new__(cls, object, encoding, errors)

    def __html__(self, /) -> te.Self:
        return self

    def __add__(self, value: str | _HasHTML, /) -> te.Self:
        if isinstance(value, str) or hasattr(value, "__html__"):
            return self.__class__(super().__add__(self.escape(value)))

        return NotImplemented

    def __radd__(self, value: str | _HasHTML, /) -> te.Self:
        if isinstance(value, str) or hasattr(value, "__html__"):
            return self.escape(value).__add__(self)

        return NotImplemented

    def __mul__(self, value: t.SupportsIndex, /) -> te.Self:
        return self.__class__(super().__mul__(value))

    def __rmul__(self, value: t.SupportsIndex, /) -> te.Self:
        return self.__class__(super().__mul__(value))

    def __mod__(self, value: t.Any, /) -> te.Self:
        if isinstance(value, tuple):
            # a tuple of arguments, each wrapped
            value = tuple(_MarkupEscapeHelper(x, self.escape) for x in value)
        elif hasattr(type(value), "__getitem__") and not isinstance(value, str):
            # a mapping of arguments, wrapped
            value = _MarkupEscapeHelper(value, self.escape)
        else:
            # a single argument, wrapped with the helper and a tuple
            value = (_MarkupEscapeHelper(value, self.escape),)

        return self.__class__(super().__mod__(value))

    def __repr__(self, /) -> str:
        return f"{self.__class__.__name__}({super().__repr__()})"

    def join(self, iterable: cabc.Iterable[str | _HasHTML], /) -> te.Self:
        return self.__class__(super().join(map(self.escape, iterable)))

    def split(  # type: ignore[override]
        self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1
    ) -> list[te.Self]:
        return [self.__class__(v) for v in super().split(sep, maxsplit)]

    def rsplit(  # type: ignore[override]
        self, /, sep: str | None = None, maxsplit: t.SupportsIndex = -1
    ) -> list[te.Self]:
        return [self.__class__(v) for v in super().rsplit(sep, maxsplit)]

    def splitlines(  # type: ignore[override]
        self, /, keepends: bool = False
    ) -> list[te.Self]:
        return [self.__class__(v) for v in super().splitlines(keepends)]

    def unescape(self, /) -> str:
        """Convert escaped markup back into a text string. This replaces
        HTML entities with the characters they represent.

        >>> Markup("Main &raquo; <em>About</em>").unescape()
        'Main \u00bb <em>About</em>'
        """
        from html import unescape

        return unescape(str(self))

    def striptags(self, /) -> str:
        """:meth:`unescape` the markup, remove tags, and normalize
        whitespace to single spaces.

        >>> Markup("Main &raquo;\    <em>About</em>").striptags()
        'Main \u00bb About'
        """
        value = str(self)

        # Look for comments then tags separately. Otherwise, a comment that
        # contains a tag would end early, leaving some of the comment behind.

        # keep finding comment start marks
        while (start := value.find("<!--")) != -1:
            # find a comment end mark beyond the start, otherwise stop
            if (end := value.find("-->", start)) == -1:
                break

            value = f"{value[:start]}{value[end + 3:]}"

        # remove tags using the same method
        while (start := value.find("<")) != -1:
            if (end := value.find(">", start)) == -1:
                break

            value = f"{value[:start]}{value[end + 1:]}"

        # collapse spaces
        value = " ".join(value.split())
        return self.__class__(value).unescape()

    @classmethod
    def escape(cls, s: t.Any, /) -> te.Self:
        """Escape a string. Calls :func:`escape` and ensures that for
        subclasses the correct type is returned.
        """
        rv = escape(s)

        if rv.__class__ is not cls:
            return cls(rv)

        return rv  # type: ignore[return-value]

    def __getitem__(self, key: t.SupportsIndex | slice, /) -> te.Self:
        return self.__class__(super().__getitem__(key))

    def capitalize(self, /) -> te.Self:
        return self.__class__(super().capitalize())

    def title(self, /) -> te.Self:
        return self.__class__(super().title())

    def lower(self, /) -> te.Self:
        return self.__class__(super().lower())

    def upper(self, /) -> te.Self:
        return self.__class__(super().upper())

    def replace(self, old: str, new: str, count: t.SupportsIndex = -1, /) -> te.Self:
        return self.__class__(super().replace(old, self.escape(new), count))

    def ljust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
        return self.__class__(super().ljust(width, self.escape(fillchar)))

    def rjust(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
        return self.__class__(super().rjust(width, self.escape(fillchar)))

    def lstrip(self, chars: str | None = None, /) -> te.Self:
        return self.__class__(super().lstrip(chars))

    def rstrip(self, chars: str | None = None, /) -> te.Self:
        return self.__class__(super().rstrip(chars))

    def center(self, width: t.SupportsIndex, fillchar: str = " ", /) -> te.Self:
        return self.__class__(super().center(width, self.escape(fillchar)))

    def strip(self, chars: str | None = None, /) -> te.Self:
        return self.__class__(super().strip(chars))

    def translate(
        self,
        table: cabc.Mapping[int, str | int | None],  # type: ignore[override]
        /,
    ) -> str:
        return self.__class__(super().translate(table))

    def expandtabs(self, /, tabsize: t.SupportsIndex = 8) -> te.Self:
        return self.__class__(super().expandtabs(tabsize))

    def swapcase(self, /) -> te.Self:
        return self.__class__(super().swapcase())

    def zfill(self, width: t.SupportsIndex, /) -> te.Self:
        return self.__class__(super().zfill(width))

    def casefold(self, /) -> te.Self:
        return self.__class__(super().casefold())

    def removeprefix(self, prefix: str, /) -> te.Self:
        return self.__class__(super().removeprefix(prefix))

    def removesuffix(self, suffix: str) -> te.Self:
        return self.__class__(super().removesuffix(suffix))

    def partition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:
        left, sep, right = super().partition(sep)
        cls = self.__class__
        return cls(left), cls(sep), cls(right)

    def rpartition(self, sep: str, /) -> tuple[te.Self, te.Self, te.Self]:
        left, sep, right = super().rpartition(sep)
        cls = self.__class__
        return cls(left), cls(sep), cls(right)

    def format(self, *args: t.Any, **kwargs: t.Any) -> te.Self:
        formatter = EscapeFormatter(self.escape)
        return self.__class__(formatter.vformat(self, args, kwargs))

    def format_map(
        self,
        mapping: cabc.Mapping[str, t.Any],  # type: ignore[override]
        /,
    ) -> te.Self:
        formatter = EscapeFormatter(self.escape)
        return self.__class__(formatter.vformat(self, (), mapping))

    def __html_format__(self, format_spec: str, /) -> te.Self:
        if format_spec:
            raise ValueError("Unsupported format specification for Markup.")

        return self


class EscapeFormatter(string.Formatter):
    __slots__ = ("escape",)

    def __init__(self, escape: _TPEscape) -> None:
        self.escape: _TPEscape = escape
        super().__init__()

    def format_field(self, value: t.Any, format_spec: str) -> str:
        if hasattr(value, "__html_format__"):
            rv = value.__html_format__(format_spec)
        elif hasattr(value, "__html__"):
            if format_spec:
                raise ValueError(
                    f"Format specifier {format_spec} given, but {type(value)} does not"
                    " define __html_format__. A class that defines __html__ must define"
                    " __html_format__ to work with format specifiers."
                )
            rv = value.__html__()
        else:
            # We need to make sure the format spec is str here as
            # otherwise the wrong callback methods are invoked.
            rv = super().format_field(value, str(format_spec))
        return str(self.escape(rv))


class _MarkupEscapeHelper:
    """Helper for :meth:`Markup.__mod__`."""

    __slots__ = ("obj", "escape")

    def __init__(self, obj: t.Any, escape: _TPEscape) -> None:
        self.obj: t.Any = obj
        self.escape: _TPEscape = escape

    def __getitem__(self, key: t.Any, /) -> te.Self:
        return self.__class__(self.obj[key], self.escape)

    def __str__(self, /) -> str:
        return str(self.escape(self.obj))

    def __repr__(self, /) -> str:
        return str(self.escape(repr(self.obj)))

    def __int__(self, /) -> int:
        return int(self.obj)

    def __float__(self, /) -> float:
        return float(self.obj)


def __getattr__(name: str) -> t.Any:
    if name == "__version__":
        import importlib.metadata
        import warnings

        warnings.warn(
            "The '__version__' attribute is deprecated and will be removed in"
            " MarkupSafe 3.1. Use feature detection, or"
            ' `importlib.metadata.version("markupsafe")`, instead.',
            stacklevel=2,
        )
        return importlib.metadata.version("markupsafe")

    raise AttributeError(name)

[end of src/markupsafe/__init__.py]
[start of src/markupsafe/_native.py]
def _escape_inner(s: str, /) -> str:
    return (
        s.replace("&", "&amp;")
        .replace(">", "&gt;")
        .replace("<", "&lt;")
        .replace("'", "&#39;")
        .replace('"', "&#34;")
    )

[end of src/markupsafe/_native.py]
[start of src/markupsafe/_speedups.pyi]
def _escape_inner(s: str, /) -> str: ...

[end of src/markupsafe/_speedups.pyi]
[start of src/markupsafe/py.typed]

[end of src/markupsafe/py.typed]

        