2 per 1 minute
+ $ curl localhost:5000/slow + 24 + $ curl localhost:5000/slow + +1 per 1 day
+ $ curl localhost:5000/ping + PONG + $ curl localhost:5000/ping + PONG + $ curl localhost:5000/ping + PONG + $ curl localhost:5000/ping + PONG + + + + diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/RECORD b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/RECORD new file mode 100644 index 0000000..23b8217 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/RECORD @@ -0,0 +1,35 @@ +flask_limiter-4.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask_limiter-4.0.0.dist-info/METADATA,sha256=z4cnwjhUEqIaZahFGB30-x1V4Lg_jy5q11Voh_bAMfQ,6190 +flask_limiter-4.0.0.dist-info/RECORD,, +flask_limiter-4.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_limiter-4.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +flask_limiter-4.0.0.dist-info/entry_points.txt,sha256=XP1DLGAtSzSTO-1e0l2FR9chlucKvsGCgh_wfCO9oj8,54 +flask_limiter-4.0.0.dist-info/licenses/LICENSE.txt,sha256=T6i7kq7F5gIPfcno9FCxU5Hcwm22Bjq0uHZV3ElcjsQ,1061 +flask_limiter/__init__.py,sha256=bRCHLQM_WY2FAIUOJhTtb3VQ6OXqq79lMz80uub6TNs,557 +flask_limiter/__pycache__/__init__.cpython-311.pyc,, +flask_limiter/__pycache__/_compat.cpython-311.pyc,, +flask_limiter/__pycache__/_extension.cpython-311.pyc,, +flask_limiter/__pycache__/_limits.cpython-311.pyc,, +flask_limiter/__pycache__/_manager.cpython-311.pyc,, +flask_limiter/__pycache__/_typing.cpython-311.pyc,, +flask_limiter/__pycache__/_version.cpython-311.pyc,, +flask_limiter/__pycache__/commands.cpython-311.pyc,, +flask_limiter/__pycache__/constants.cpython-311.pyc,, +flask_limiter/__pycache__/errors.cpython-311.pyc,, +flask_limiter/__pycache__/util.cpython-311.pyc,, +flask_limiter/_compat.py,sha256=jrUYRoIo4jOXp5JDWgpL77F6Cuj_0iX7ySsTOfYrPs8,379 +flask_limiter/_extension.py,sha256=QNu9R0u0J2x8Qr9YkOYys3fGA8sLPtX6ZnfZ6lgCG8U,48057 +flask_limiter/_limits.py,sha256=sJn-5OLYkeS2GfYVkbdSb5flbFidHJnNndCGgbAnYyg,15006 +flask_limiter/_manager.py,sha256=RJhFo30P8rfNiOtKiKZBUfL1ZYAmuPoXUbsZ4ILB1ew,10453 +flask_limiter/_typing.py,sha256=yrxK2Zu1sZ3ojvwJMfkJVVawGOzGVsfxAbUG8I5TkBo,401 +flask_limiter/_version.py,sha256=QKIQLQcx5S9GHF_rplWSVpBW_nVDWWvM96YNckz0xJI,704 +flask_limiter/_version.pyi,sha256=Y25n44pyE3vp92MiABKrcK3IWRyQ1JG1rZ4Ufqy2nC0,17 +flask_limiter/commands.py,sha256=meE7MIH0fezy7NnhKGUujsNFlVpCWEZqnu5qoXotseo,22463 +flask_limiter/constants.py,sha256=-e1Ff1g938ajdgR8f-oWVp3bjLSdy2pPOQdV3RsUHAs,2902 +flask_limiter/contrib/__init__.py,sha256=Yr06Iy3i_F1cwTSGcGWOxMHOZaQnySiRFBfsH8Syric,28 +flask_limiter/contrib/__pycache__/__init__.cpython-311.pyc,, +flask_limiter/contrib/__pycache__/util.cpython-311.pyc,, +flask_limiter/contrib/util.py,sha256=XKX5pqA7f-cGP7IQtg2tnyoQk2Eh5L4Hi3zDpWjq_3s,306 +flask_limiter/errors.py,sha256=mDP2C-SFxaP9ErSZCbSiNmM6RbEZG38EPPPkZELx4K4,1066 +flask_limiter/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_limiter/util.py,sha256=KwLYUluQR2M9dGZppqqczdtUjNiuv2zh_lB_gaDMzcw,975 diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/REQUESTED b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/WHEEL b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/entry_points.txt b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/entry_points.txt new file mode 100644 index 0000000..33d6392 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[flask.commands] +limiter = flask_limiter.commands:cli diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/licenses/LICENSE.txt b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..2261d32 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter-4.0.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2023 Ali-Akber Saifee + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/__init__.py b/buffteks/lib/python3.11/site-packages/flask_limiter/__init__.py new file mode 100644 index 0000000..14c5852 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/__init__.py @@ -0,0 +1,28 @@ +"""Flask-Limiter extension for rate limiting.""" + +from __future__ import annotations + +from . import _version +from ._extension import Limiter, RequestLimit +from ._limits import ( + ApplicationLimit, + Limit, + MetaLimit, + RouteLimit, +) +from .constants import ExemptionScope, HeaderNames +from .errors import RateLimitExceeded + +__all__ = [ + "ExemptionScope", + "HeaderNames", + "Limiter", + "Limit", + "RouteLimit", + "ApplicationLimit", + "MetaLimit", + "RateLimitExceeded", + "RequestLimit", +] + +__version__ = _version.__version__ diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_compat.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_compat.py new file mode 100644 index 0000000..a42b11c --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_compat.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import flask +from flask.ctx import RequestContext + +# flask.globals.request_ctx is only available in Flask >= 2.2.0 +try: + from flask.globals import request_ctx +except ImportError: + request_ctx = None + + +def request_context() -> RequestContext: + if request_ctx is None: + return flask._request_ctx_stack.top + return request_ctx diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_extension.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_extension.py new file mode 100644 index 0000000..b947f37 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_extension.py @@ -0,0 +1,1172 @@ +""" +Flask-Limiter Extension +""" + +from __future__ import annotations + +import dataclasses +import datetime +import functools +import itertools +import logging +import time +import warnings +import weakref +from collections import defaultdict +from functools import partial +from typing import overload + +import flask +import flask.wrappers +from limits import RateLimitItem, WindowStats +from limits.errors import ConfigurationError +from limits.storage import MemoryStorage, Storage, storage_from_string +from limits.strategies import STRATEGIES, RateLimiter +from ordered_set import OrderedSet +from werkzeug.http import http_date, parse_date + +from ._compat import request_context +from ._limits import ( + ApplicationLimit, + Limit, + MetaLimit, + RouteLimit, + RuntimeLimit, +) +from ._manager import LimitManager +from ._typing import ( + Callable, + P, + R, + Sequence, + cast, +) +from .constants import MAX_BACKEND_CHECKS, ConfigVars, ExemptionScope, HeaderNames +from .errors import RateLimitExceeded +from .util import get_qualified_name + + +class RequestLimit: + """ + Provides details of a rate limit within the context of a request + """ + + #: The instance of the rate limit + limit: RateLimitItem + + #: The full key for the request against which the rate limit is tested + key: str + + #: Whether the limit was breached within the context of this request + breached: bool + + #: Whether the limit is a shared limit + shared: bool + + def __init__( + self, + extension: Limiter, + limit: RateLimitItem, + request_args: list[str], + breached: bool, + shared: bool, + ) -> None: + self.extension: weakref.ProxyType[Limiter] = weakref.proxy(extension) + self.limit = limit + self.request_args = request_args + self.key = limit.key_for(*request_args) + self.breached = breached + self.shared = shared + self._window: WindowStats | None = None + + @property + def limiter(self) -> RateLimiter: + return cast(RateLimiter, self.extension.limiter) + + @property + def window(self) -> WindowStats: + if not self._window: + self._window = self.limiter.get_window_stats(self.limit, *self.request_args) + + return self._window + + @property + def reset_at(self) -> int: + """Timestamp at which the rate limit will be reset""" + + return int(self.window[0] + 1) + + @property + def remaining(self) -> int: + """Quantity remaining for this rate limit""" + + return self.window[1] + + +@dataclasses.dataclass +class LimiterContext: + view_rate_limit: RequestLimit | None = None + view_rate_limits: list[RequestLimit] = dataclasses.field(default_factory=list) + conditional_deductions: dict[RuntimeLimit, list[str]] = dataclasses.field(default_factory=dict) + seen_limits: OrderedSet[RuntimeLimit] = dataclasses.field(default_factory=OrderedSet) + + def reset(self) -> None: + self.view_rate_limit = None + self.view_rate_limits.clear() + self.conditional_deductions.clear() + self.seen_limits.clear() + + +class Limiter: + """ + The :class:`Limiter` class initializes the Flask-Limiter extension. + + :param key_func: a callable that returns the domain to rate limit by. + :param app: :class:`flask.Flask` instance to initialize the extension with. + :param default_limits: a list of strings, callables returning strings denoting default limits + or :class:`Limit` instances to apply to all routes that are not explicitely decorated with a + limit. :ref:`ratelimit-string` for more details. + :param default_limits_per_method: whether default limits are applied per method, per route or + as a combination of all method per route. + :param default_limits_exempt_when: a function that should return True/False to decide if the + default limits should be skipped + :param default_limits_deduct_when: a function that receives the current :class:`flask.Response` + object and returns True/False to decide if a deduction should be made from the default rate + limit(s) + :param default_limits_cost: The cost of a hit to the default limits as an integer or a function + that takes no parameters and returns an integer (Default: ``1``). + :param application_limits: a list of strings, callables returning strings for limits or + :class:`ApplicationLimit` that are applied to the entire application + (i.e a shared limit for all routes) + :param application_limits_per_method: whether application limits are applied per method, per + route or as a combination of all method per route. + :param application_limits_exempt_when: a function that should return True/False to decide if the + application limits should be skipped + :param application_limits_deduct_when: a function that receives the current + :class:`flask.Response` object and returns True/False to decide if a deduction should be made + from the application rate limit(s) + :param application_limits_cost: The cost of a hit to the global application limits as an integer + or a function that takes no parameters and returns an integer (Default: ``1``). + :param headers_enabled: whether ``X-RateLimit`` response headers are written. + :param header_name_mapping: Mapping of header names to use if :paramref:`Limiter.headers_enabled` + is ``True``. If no mapping is provided the default values will be used. + :param strategy: the strategy to use. Refer to :ref:`ratelimit-strategy` + :param storage_uri: the storage location. Refer to :data:`RATELIMIT_STORAGE_URI` + :param storage_options: kwargs to pass to the storage implementation upon instantiation. + :param swallow_errors: whether to swallow any errors when hitting a rate limit. An exception + will still be logged. default ``False`` + :param fail_on_first_breach: whether to stop processing remaining limits after the first breach. + default ``True`` + :param on_breach: a function that will be called when any limit in this extension is breached. + If the function returns an instance of :class:`flask.Response` that will be the response + embedded into the :exc:`RateLimitExceeded` exception raised. + :param meta_limits: a list of strings, callables returning strings for limits or + :class:`MetaLimit` that are used to control the upper limit of a requesting client hitting + any configured rate limit. Once a meta limit is exceeded all subsequent requests will + raise a :class:`~flask_limiter.RateLimitExceeded` for the duration of the meta limit window. + :param on_meta_breach: a function that will be called when a meta limit in this extension is + breached. If the function returns an instance of :class:`flask.Response` that will be the + response embedded into the :exc:`RateLimitExceeded` exception raised. + :param in_memory_fallback: a list of strings or callables returning strings denoting fallback + limits to apply when the storage is down. + :param in_memory_fallback_enabled: fall back to in memory storage when the main storage is down + and inherits the original limits. default ``False`` + :param retry_after: Allows configuration of how the value of the `Retry-After` header is + rendered. One of `http-date` or `delta-seconds`. + :param key_prefix: prefix prepended to rate limiter keys and app context global names. + :param request_identifier: a callable that returns the unique identity the current request. + Defaults to :attr:`flask.Request.endpoint` + :param enabled: Whether the extension is enabled or not + """ + + def __init__( + self, + key_func: Callable[[], str], + *, + app: flask.Flask | None = None, + default_limits: list[str | Callable[[], str] | Limit] | None = None, + default_limits_per_method: bool | None = None, + default_limits_exempt_when: Callable[[], bool] | None = None, + default_limits_deduct_when: Callable[[flask.wrappers.Response], bool] | None = None, + default_limits_cost: int | Callable[[], int] | None = None, + application_limits: list[str | Callable[[], str] | ApplicationLimit] | None = None, + application_limits_per_method: bool | None = None, + application_limits_exempt_when: Callable[[], bool] | None = None, + application_limits_deduct_when: Callable[[flask.wrappers.Response], bool] | None = None, + application_limits_cost: int | Callable[[], int] | None = None, + headers_enabled: bool | None = None, + header_name_mapping: dict[HeaderNames, str] | None = None, + strategy: str | None = None, + storage_uri: str | None = None, + storage_options: dict[str, str | int] | None = None, + swallow_errors: bool | None = None, + fail_on_first_breach: bool | None = None, + on_breach: Callable[[RequestLimit], flask.wrappers.Response | None] | None = None, + meta_limits: list[str | Callable[[], str] | MetaLimit] | None = None, + on_meta_breach: Callable[[RequestLimit], flask.wrappers.Response | None] | None = None, + in_memory_fallback: list[str] | None = None, + in_memory_fallback_enabled: bool | None = None, + retry_after: str | None = None, + key_prefix: str = "", + request_identifier: Callable[..., str] | None = None, + enabled: bool = True, + ) -> None: + self.app = app + self.logger = logging.getLogger("flask-limiter") + + self.enabled = enabled + self.initialized = False + self._default_limits_per_method = default_limits_per_method + self._default_limits_exempt_when = default_limits_exempt_when + self._default_limits_deduct_when = default_limits_deduct_when + self._default_limits_cost = default_limits_cost + self._application_limits_per_method = application_limits_per_method + self._application_limits_exempt_when = application_limits_exempt_when + self._application_limits_deduct_when = application_limits_deduct_when + self._application_limits_cost = application_limits_cost + self._in_memory_fallback = [] + self._in_memory_fallback_enabled = in_memory_fallback_enabled or ( + in_memory_fallback and len(in_memory_fallback) > 0 + ) + self._route_exemptions: dict[str, ExemptionScope] = {} + self._blueprint_exemptions: dict[str, ExemptionScope] = {} + self._request_filters: list[Callable[[], bool]] = [] + + self._headers_enabled = headers_enabled + self._header_mapping = header_name_mapping or {} + self._retry_after = retry_after + self._strategy = strategy + self._storage_uri = storage_uri + self._storage_options = storage_options or {} + self._swallow_errors = swallow_errors + self._fail_on_first_breach = fail_on_first_breach + self._on_breach = on_breach + self._on_meta_breach = on_meta_breach + + self._key_func = key_func + self._key_prefix = key_prefix + self._request_identifier = request_identifier + + _default_limits = ( + [ + Limit( + limit_provider=limit, + key_function=self._key_func, + finalized=False, + ).bind(self) + if not isinstance(limit, Limit) + else limit.bind(self) + for limit in default_limits + ] + if default_limits + else [] + ) + + _application_limits = ( + [ + ApplicationLimit( + limit_provider=limit, + finalized=False, + ).bind(self) + if not isinstance(limit, Limit) + else limit.bind(self) + for limit in application_limits + ] + if application_limits + else [] + ) + + self._meta_limits = ( + [ + MetaLimit( + limit_provider=limit, + ).bind(self) + if not isinstance(limit, Limit) + else limit.bind(self) + for limit in meta_limits + ] + if meta_limits + else [] + ) + + if in_memory_fallback: + for limit in in_memory_fallback: + self._in_memory_fallback.append( + Limit( + limit_provider=limit, + key_function=self._key_func, + ) + if not isinstance(limit, Limit) + else limit + ) + + self._storage: Storage | None = None + self._limiter: RateLimiter | None = None + self._storage_dead = False + self._fallback_limiter: RateLimiter | None = None + + self.__check_backend_count = 0 + self.__last_check_backend = time.time() + self._marked_for_limiting: set[str] = set() + + self.logger.addHandler(logging.NullHandler()) + + self.limit_manager = LimitManager( + application_limits=_application_limits, + default_limits=_default_limits, + decorated_limits={}, + blueprint_limits={}, + route_exemptions=self._route_exemptions, + blueprint_exemptions=self._blueprint_exemptions, + ) + + if app: + self.init_app(app) + + def init_app(self, app: flask.Flask) -> None: + """ + :param app: :class:`flask.Flask` instance to rate limit. + """ + config = app.config + self.enabled = config.setdefault(ConfigVars.ENABLED, self.enabled) + + if not self.enabled: + return + + if self._default_limits_per_method is None: + self._default_limits_per_method = bool( + config.get(ConfigVars.DEFAULT_LIMITS_PER_METHOD, False) + ) + self._default_limits_exempt_when = self._default_limits_exempt_when or config.get( + ConfigVars.DEFAULT_LIMITS_EXEMPT_WHEN + ) + self._default_limits_deduct_when = self._default_limits_deduct_when or config.get( + ConfigVars.DEFAULT_LIMITS_DEDUCT_WHEN + ) + self._default_limits_cost = self._default_limits_cost or config.get( + ConfigVars.DEFAULT_LIMITS_COST, 1 + ) + + if self._swallow_errors is None: + self._swallow_errors = bool(config.get(ConfigVars.SWALLOW_ERRORS, False)) + + if self._fail_on_first_breach is None: + self._fail_on_first_breach = bool(config.get(ConfigVars.FAIL_ON_FIRST_BREACH, True)) + + if self._headers_enabled is None: + self._headers_enabled = bool(config.get(ConfigVars.HEADERS_ENABLED, False)) + + self._storage_options.update(config.get(ConfigVars.STORAGE_OPTIONS, {})) + storage_uri_from_config = config.get(ConfigVars.STORAGE_URI, None) + + if not storage_uri_from_config: + if not self._storage_uri: + warnings.warn( + "Using the in-memory storage for tracking rate limits as no storage " + "was explicitly specified. This is not recommended for production use. " + "See: https://flask-limiter.readthedocs.io#configuring-a-storage-backend " + "for documentation about configuring the storage backend." + ) + storage_uri_from_config = "memory://" + self._storage = cast( + Storage, + storage_from_string( + self._storage_uri or storage_uri_from_config, **self._storage_options + ), + ) + self._strategy = self._strategy or config.setdefault(ConfigVars.STRATEGY, "fixed-window") + + if self._strategy not in STRATEGIES: + raise ConfigurationError("Invalid rate limiting strategy %s" % self._strategy) + self._limiter = STRATEGIES[self._strategy](self._storage) + + self._header_mapping = { + HeaderNames.RESET: self._header_mapping.get( + HeaderNames.RESET, + config.get(ConfigVars.HEADER_RESET, HeaderNames.RESET.value), + ), + HeaderNames.REMAINING: self._header_mapping.get( + HeaderNames.REMAINING, + config.get(ConfigVars.HEADER_REMAINING, HeaderNames.REMAINING.value), + ), + HeaderNames.LIMIT: self._header_mapping.get( + HeaderNames.LIMIT, + config.get(ConfigVars.HEADER_LIMIT, HeaderNames.LIMIT.value), + ), + HeaderNames.RETRY_AFTER: self._header_mapping.get( + HeaderNames.RETRY_AFTER, + config.get(ConfigVars.HEADER_RETRY_AFTER, HeaderNames.RETRY_AFTER.value), + ), + } + self._retry_after = self._retry_after or config.get(ConfigVars.HEADER_RETRY_AFTER_VALUE) + + self._key_prefix = self._key_prefix or config.get(ConfigVars.KEY_PREFIX, "") + self._request_identifier = self._request_identifier or config.get( + ConfigVars.REQUEST_IDENTIFIER, lambda: flask.request.endpoint or "" + ) + app_limits = config.get(ConfigVars.APPLICATION_LIMITS, None) + self._application_limits_cost = self._application_limits_cost or config.get( + ConfigVars.APPLICATION_LIMITS_COST, 1 + ) + + if self._application_limits_per_method is None: + self._application_limits_per_method = bool( + config.get(ConfigVars.APPLICATION_LIMITS_PER_METHOD, False) + ) + self._application_limits_exempt_when = self._application_limits_exempt_when or config.get( + ConfigVars.APPLICATION_LIMITS_EXEMPT_WHEN + ) + self._application_limits_deduct_when = self._application_limits_deduct_when or config.get( + ConfigVars.APPLICATION_LIMITS_DEDUCT_WHEN + ) + + if not self.limit_manager._application_limits and app_limits: + self.limit_manager.set_application_limits( + [ + ApplicationLimit( + limit_provider=app_limits, + per_method=self._application_limits_per_method, + exempt_when=self._application_limits_exempt_when, + deduct_when=self._application_limits_deduct_when, + cost=self._application_limits_cost, + ).bind(self) + ] + ) + else: + app_limits = self.limit_manager._application_limits + + for group in app_limits: + if not group.finalized: + group.cost = self._application_limits_cost + group.per_method = self._application_limits_per_method + group.exempt_when = self._application_limits_exempt_when + group.deduct_when = self._application_limits_deduct_when + group.finalized = True + self.limit_manager.set_application_limits(app_limits) + + conf_limits = config.get(ConfigVars.DEFAULT_LIMITS, None) + + if not self.limit_manager._default_limits and conf_limits: + self.limit_manager.set_default_limits( + [ + Limit( + limit_provider=conf_limits, + key_function=self._key_func, + per_method=self._default_limits_per_method, + exempt_when=self._default_limits_exempt_when, + deduct_when=self._default_limits_deduct_when, + cost=self._default_limits_cost, + ).bind(self) + ] + ) + else: + default_limit_groups = self.limit_manager._default_limits + + for default_group in default_limit_groups: + if not default_group.finalized: + default_group.cost = self._default_limits_cost + default_group.per_method = self._default_limits_per_method + default_group.exempt_when = self._default_limits_exempt_when + default_group.deduct_when = self._default_limits_deduct_when + default_group.finalized = True + self.limit_manager.set_default_limits(default_limit_groups) + + meta_limits = config.get(ConfigVars.META_LIMITS, None) + + if not self._meta_limits and meta_limits: + self._meta_limits = [ + MetaLimit( + limit_provider=meta_limits, + key_function=self._key_func, + ).bind(self) + if not isinstance(meta_limits, MetaLimit) + else meta_limits.bind(self) + ] + + self._on_breach = self._on_breach or config.get(ConfigVars.ON_BREACH, None) + self._on_meta_breach = self._on_meta_breach or config.get(ConfigVars.ON_META_BREACH, None) + + self.__configure_fallbacks(app, self._strategy) + + if self not in app.extensions.setdefault("limiter", set()): + app.before_request(self._check_request_limit) + app.after_request(partial(Limiter.__inject_headers, self)) + app.teardown_request(self.__release_context) + app.extensions["limiter"].add(self) + self.initialized = True + + @property + def context(self) -> LimiterContext: + """ + The context is meant to exist for the lifetime of a request/response cycle per instance of + the extension so as to keep track of any state used at different steps in the lifecycle + (for example to pass information from the before request hook to the after_request hook) + + :meta private: + """ + ctx = request_context() + + if not hasattr(ctx, "_limiter_request_context"): + ctx._limiter_request_context = defaultdict(LimiterContext) # type: ignore + + return cast( + dict[Limiter, LimiterContext], + ctx._limiter_request_context, # type: ignore + )[self] + + def limit( + self, + limit_value: str | Callable[[], str], + *, + key_func: Callable[[], str] | None = None, + per_method: bool = False, + methods: list[str] | None = None, + error_message: str | Callable[[], str] | None = None, + exempt_when: Callable[[], bool] | None = None, + override_defaults: bool = True, + deduct_when: Callable[[flask.wrappers.Response], bool] | None = None, + on_breach: None | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None, + cost: int | Callable[[], int] = 1, + scope: str | Callable[[str], str] | None = None, + meta_limits: Sequence[str | Callable[[], str] | MetaLimit] | None = None, + ) -> RouteLimit: + """ + Decorator to be used for rate limiting individual routes or blueprints. + + :param limit_value: rate limit string or a callable that returns a string. + :ref:`ratelimit-string` for more details. + :param key_func: function/lambda to extract the unique identifier for the rate limit. + :param per_method: whether the limit is sub categorized into the http method of the request. + :param methods: if specified, only the methods in this list will be rate limited + (default: ``None``). + :param error_message: string (or callable that returns one) to override the error message + used in the response. + :param exempt_when: function/lambda used to decide if the rate limit should skipped. + :param override_defaults: whether the decorated limit overrides the default limits + (Default: ``True``). + + .. note:: When used with a :class:`~flask.Blueprint` the meaning + of the parameter extends to any parents the blueprint instance is + registered under. For more details see :ref:`recipes:nested blueprints` + + :param deduct_when: a function that receives the current :class:`flask.Response` object and + returns True/False to decide if a deduction should be done from the rate limit + :param on_breach: a function that will be called when this limit is breached. If the + function returns an instance of :class:`flask.Response` that will be the response embedded + into the :exc:`RateLimitExceeded` exception raised. + :param cost: The cost of a hit or a function that takes no parameters and returns the cost + as an integer (Default: ``1``). + :param scope: a string or callable that returns a string for further categorizing the rate + limiting scope. This scope is combined with the current endpoint of the request. + + + Changes + - .. versionadded:: 2.9.0 + + The returned object can also be used as a context manager + for rate limiting a code block inside a view. For example:: + + @app.route("/") + def route(): + try: + with limiter.limit("10/second"): + # something expensive + except RateLimitExceeded: pass + + """ + + return RouteLimit( + limit_provider=limit_value, + limiter=self, + key_function=key_func or self._key_func, + shared=False, + scope=scope, + per_method=per_method, + methods=methods, + error_message=error_message, + exempt_when=exempt_when, + override_defaults=override_defaults, + deduct_when=deduct_when, + on_breach=on_breach, + cost=cost, + meta_limits=meta_limits, + ) + + def shared_limit( + self, + limit_value: str | Callable[[], str], + scope: str | Callable[[str], str], + *, + key_func: Callable[[], str] | None = None, + per_method: bool = False, + methods: list[str] | None = None, + error_message: str | Callable[[], str] | None = None, + exempt_when: Callable[[], bool] | None = None, + override_defaults: bool = True, + deduct_when: Callable[[flask.wrappers.Response], bool] | None = None, + on_breach: None | (Callable[[RequestLimit], flask.wrappers.Response | None]) = None, + cost: int | Callable[[], int] = 1, + meta_limits: Sequence[str | Callable[[], str] | MetaLimit] | None = None, + ) -> RouteLimit: + """ + decorator to be applied to multiple routes sharing the same rate limit. + + :param limit_value: rate limit string or a callable that returns a string. + :ref:`ratelimit-string` for more details. + :param scope: a string or callable that returns a string for defining the rate limiting scope. + :param key_func: function/lambda to extract the unique identifier for the rate limit. + :param per_method: whether the limit is sub categorized into the http method of the request. + :param methods: if specified, only the methods in this list will be rate limited + (default: ``None``). + :param error_message: string (or callable that returns one) to override the error message + used in the response. + :param function exempt_when: function/lambda used to decide if the rate limit should skipped. + :param override_defaults: whether the decorated limit overrides the default limits. + (default: ``True``) + + .. note:: When used with a :class:`~flask.Blueprint` the meaning + of the parameter extends to any parents the blueprint instance is + registered under. For more details see :ref:`recipes:nested blueprints` + :param deduct_when: a function that receives the current :class:`flask.Response` object and + returns True/False to decide if a deduction should be done from the rate limit + :param on_breach: a function that will be called when this limit is breached. + If the function returns an instance of :class:`flask.Response` that will be the response + embedded into the :exc:`RateLimitExceeded` exception raised. + :param cost: The cost of a hit or a function that takes no parameters and returns the cost + as an integer (default: ``1``). + """ + + return RouteLimit( + limit_provider=limit_value, + limiter=self, + key_function=key_func or self._key_func, + shared=True, + scope=scope, + per_method=per_method, + methods=methods, + error_message=error_message, + exempt_when=exempt_when, + override_defaults=override_defaults, + deduct_when=deduct_when, + on_breach=on_breach, + cost=cost, + meta_limits=meta_limits, + ) + + @overload + def exempt( + self, + obj: flask.Blueprint, + *, + flags: ExemptionScope = ExemptionScope.APPLICATION + | ExemptionScope.DEFAULT + | ExemptionScope.META, + ) -> flask.Blueprint: ... + + @overload + def exempt( + self, + obj: Callable[..., R], + *, + flags: ExemptionScope = ExemptionScope.APPLICATION + | ExemptionScope.DEFAULT + | ExemptionScope.META, + ) -> Callable[..., R]: ... + + @overload + def exempt( + self, + *, + flags: ExemptionScope = ExemptionScope.APPLICATION + | ExemptionScope.DEFAULT + | ExemptionScope.META, + ) -> ( + Callable[[Callable[P, R]], Callable[P, R]] | Callable[[flask.Blueprint], flask.Blueprint] + ): ... + + def exempt( + self, + obj: Callable[..., R] | flask.Blueprint | None = None, + *, + flags: ExemptionScope = ExemptionScope.APPLICATION + | ExemptionScope.DEFAULT + | ExemptionScope.META, + ) -> ( + Callable[..., R] + | flask.Blueprint + | Callable[[Callable[P, R]], Callable[P, R]] + | Callable[[flask.Blueprint], flask.Blueprint] + ): + """ + Mark a view function or all views in a blueprint as exempt from rate limits. + + :param obj: view function or blueprint to mark as exempt. + :param flags: Controls the scope of the exemption. By default application wide limits, + defaults configured on the extension and meta limits are opted out of. Additional flags + can be used to control the behavior when :paramref:`obj` is a Blueprint that is nested + under another Blueprint or has other Blueprints nested under it + (See :ref:`recipes:nested blueprints`) + + The method can be used either as a decorator without any arguments (the default + flags will apply and the route will be exempt from default and application limits:: + + @app.route("...") + @limiter.exempt + def route(...): + ... + + Specific exemption flags can be provided at decoration time:: + + @app.route("...") + @limiter.exempt(flags=ExemptionScope.APPLICATION) + def route(...): + ... + + If an entire blueprint (i.e. all routes under it) are to be exempted the method can be + called with the blueprint as the first parameter and any additional flags:: + + bp = Blueprint(...) + limiter.exempt(bp) + limiter.exempt( + bp, + flags=ExemptionScope.DEFAULT|ExemptionScope.APPLICATION|ExemptionScope.ANCESTORS + ) + + """ + + if isinstance(obj, flask.Blueprint): + self.limit_manager.add_blueprint_exemption(obj.name, flags) + elif obj: + self.limit_manager.add_route_exemption(get_qualified_name(obj), flags) + else: + return functools.partial(self.exempt, flags=flags) + + return obj + + def request_filter(self, fn: Callable[[], bool]) -> Callable[[], bool]: + """ + Decorator to mark a function as a filter to be executed to check if the request is exempt + from rate limiting. + + :param fn: The function will be called before evaluating any rate limits to decide whether + to perform rate limit or skip it. + """ + self._request_filters.append(fn) + + return fn + + def __configure_fallbacks(self, app: flask.Flask, strategy: str) -> None: + config = app.config + fallback_enabled = config.get(ConfigVars.IN_MEMORY_FALLBACK_ENABLED, False) + fallback_limits = config.get(ConfigVars.IN_MEMORY_FALLBACK, None) + + if not self._in_memory_fallback and fallback_limits: + self._in_memory_fallback = [ + Limit( + limit_provider=fallback_limits, + key_function=self._key_func, + scope=None, + per_method=False, + cost=1, + ).bind(self) + ] + + if not self._in_memory_fallback_enabled: + self._in_memory_fallback_enabled = fallback_enabled or len(self._in_memory_fallback) > 0 + + if self._in_memory_fallback_enabled: + self._fallback_storage = MemoryStorage() + self._fallback_limiter = STRATEGIES[strategy](self._fallback_storage) + + def __should_check_backend(self) -> bool: + if self.__check_backend_count > MAX_BACKEND_CHECKS: + self.__check_backend_count = 0 + + if time.time() - self.__last_check_backend > pow(2, self.__check_backend_count): + self.__last_check_backend = time.time() + self.__check_backend_count += 1 + + return True + + return False + + def reset(self) -> None: + """ + resets the storage if it supports being reset + """ + try: + self.storage.reset() + self.logger.info("Storage has been reset and all limits cleared") + except NotImplementedError: + self.logger.warning("This storage type does not support being reset") + + @property + def storage(self) -> Storage: + """ + The backend storage configured for the rate limiter + """ + assert self._storage + + return self._storage + + @property + def limiter(self) -> RateLimiter: + """ + Instance of the rate limiting strategy used for performing rate limiting. + """ + + if self._storage_dead and self._in_memory_fallback_enabled: + limiter = self._fallback_limiter + else: + limiter = self._limiter + assert limiter + + return limiter + + @property + def current_limit(self) -> RequestLimit | None: + """ + Get details for the most relevant rate limit used in this request. + + In a scenario where multiple rate limits are active for a single request and none are + breached, the rate limit which applies to the smallest time window will be returned. + + .. important:: The value of ``remaining`` in :class:`RequestLimit` is after + deduction for the current request. + + + For example:: + + @limit("1/second") + @limit("60/minute") + @limit("2/day") + def route(...): + ... + + - Request 1 at ``t=0`` (no breach): this will return the details for for ``1/second`` + - Request 2 at ``t=1`` (no breach): it will still return the details for ``1/second`` + - Request 3 at ``t=2`` (breach): it will return the details for ``2/day`` + """ + + return self.context.view_rate_limit + + @property + def current_limits(self) -> list[RequestLimit]: + """ + Get a list of all rate limits that were applicable and evaluated + within the context of this request. + + The limits are returned in a sorted order by smallest window size first. + """ + + return self.context.view_rate_limits + + def identify_request(self) -> str: + """ + Returns the identity of the request (by default this is the :attr:`flask.Request.endpoint` + associated by the view function that is handling the request). The behavior can be customized + by initializing the extension with a callable argument for + :paramref:`~flask_limiter.Limiter.request_identifier`. + """ + + if self.initialized and self.enabled: + assert self._request_identifier + + return self._request_identifier() + + return "" + + def __check_conditional_deductions(self, response: flask.wrappers.Response) -> None: + for lim, args in self.context.conditional_deductions.items(): + if lim.deduct_when and lim.deduct_when(response): + try: + self.limiter.hit(lim.limit, *args, cost=lim.deduction_amount) + except Exception as err: + if self._swallow_errors: + self.logger.exception("Failed to deduct rate limit. Swallowing error") + else: + raise err + + def __inject_headers(self, response: flask.wrappers.Response) -> flask.wrappers.Response: + self.__check_conditional_deductions(response) + header_limit = self.current_limit + + if self.enabled and self._headers_enabled and header_limit and self._header_mapping: + try: + reset_at = header_limit.reset_at + response.headers.add( + self._header_mapping[HeaderNames.LIMIT], + str(header_limit.limit.amount), + ) + response.headers.add( + self._header_mapping[HeaderNames.REMAINING], + str(header_limit.remaining), + ) + response.headers.add(self._header_mapping[HeaderNames.RESET], str(reset_at)) + + # response may have an existing retry after + existing_retry_after_header = response.headers.get("Retry-After") + + if existing_retry_after_header is not None: + # might be in http-date format + retry_after: float | datetime.datetime | None = parse_date( + existing_retry_after_header + ) + + # parse_date failure returns None + + if retry_after is None: + retry_after = time.time() + int(existing_retry_after_header) + + if isinstance(retry_after, datetime.datetime): + retry_after = time.mktime(retry_after.timetuple()) + + reset_at = max(int(retry_after), reset_at) + + # set the header instead of using add + response.headers.set( + self._header_mapping[HeaderNames.RETRY_AFTER], + str( + http_date(reset_at) + if self._retry_after == "http-date" + else int(reset_at - time.time()) + ), + ) + except Exception as e: # noqa: E722 + if self._in_memory_fallback_enabled and not self._storage_dead: + self.logger.warning( + "Rate limit storage unreachable - falling back to in-memory storage" + ) + self._storage_dead = True + response = self.__inject_headers(response) + else: + if self._swallow_errors: + self.logger.exception( + "Failed to update rate limit headers. Swallowing error" + ) + else: + raise e + + return response + + def __check_all_limits_exempt( + self, + endpoint: str | None, + ) -> bool: + return bool( + not endpoint + or not (self.enabled and self.initialized) + or endpoint.split(".")[-1] == "static" + or any(fn() for fn in self._request_filters) + ) + + def __filter_limits( + self, + endpoint: str | None, + blueprint: str | None, + callable_name: str | None, + in_middleware: bool = False, + ) -> list[RuntimeLimit]: + if callable_name: + name = callable_name + else: + view_func = flask.current_app.view_functions.get(endpoint or "", None) + name = get_qualified_name(view_func) if view_func else "" + + if self.__check_all_limits_exempt(endpoint): + return [] + + marked_for_limiting = name in self._marked_for_limiting or self.limit_manager.has_hints( + endpoint or "" + ) + fallback_limits = [] + + if self._storage_dead and self._fallback_limiter: + if in_middleware and name in self._marked_for_limiting: + pass + else: + if self.__should_check_backend() and self._storage and self._storage.check(): + self.logger.info("Rate limit storage recovered") + self._storage_dead = False + self.__check_backend_count = 0 + else: + fallback_limits = list(itertools.chain(*self._in_memory_fallback)) + + if fallback_limits: + return fallback_limits + + defaults, decorated = self.limit_manager.resolve_limits( + flask.current_app, + endpoint, + blueprint, + name, + in_middleware, + marked_for_limiting, + ) + limits = OrderedSet(defaults) - self.context.seen_limits + self.context.seen_limits.update(defaults) + + return list(limits) + list(decorated) + + def __evaluate_limits(self, endpoint: str, limits: list[RuntimeLimit]) -> None: + failed_limits: list[tuple[RuntimeLimit, list[str]]] = [] + limit_for_header: RequestLimit | None = None + view_limits: list[RequestLimit] = [] + meta_limits = [ + meta_limit + for meta_limit in itertools.chain( + *self._meta_limits, + *[limit.meta_limits for limit in limits if limit.meta_limits], + ) + if not meta_limit.is_exempt + ] + if not ( + ExemptionScope.META + & self.limit_manager.exemption_scope( + flask.current_app, endpoint, flask.request.blueprint + ) + ): + for lim in meta_limits: + limit_key, scope = lim.key_func(), lim.scope_for(endpoint, None) + args = [limit_key, scope] + on_breach = lim.on_breach or self._on_meta_breach + if not self.limiter.test(lim.limit, *args, cost=lim.deduction_amount): + breached_meta_limit = RequestLimit(self, lim.limit, args, True, lim.shared) + self.context.view_rate_limit = breached_meta_limit + self.context.view_rate_limits = [breached_meta_limit] + meta_breach_response = None + + if on_breach: + try: + cb_response = on_breach(breached_meta_limit) + + if isinstance(cb_response, flask.wrappers.Response): + meta_breach_response = cb_response + except Exception as err: # noqa + if self._swallow_errors: + self.logger.exception( + "on_meta_breach callback failed with error %s", err + ) + else: + raise err + raise RateLimitExceeded(lim, response=meta_breach_response) + + for lim in sorted(limits, key=lambda x: x.limit): + if lim.is_exempt or lim.method_exempt: + continue + + limit_scope = lim.scope_for(endpoint, flask.request.method) + limit_key = lim.key_func() + args = [limit_key, limit_scope] + kwargs = {} + + if not all(args): + self.logger.error(f"Skipping limit: {lim.limit}. Empty value found in parameters.") + + continue + + if self._key_prefix: + args = [self._key_prefix, *args] + + if lim.deduct_when: + self.context.conditional_deductions[lim] = args + method = self.limiter.test + else: + method = self.limiter.hit + kwargs["cost"] = lim.deduction_amount + + request_limit = RequestLimit(self, lim.limit, args, False, lim.shared) + view_limits.append(request_limit) + + if not method(lim.limit, *args, **kwargs): + self.logger.info( + "ratelimit %s (%s) exceeded at endpoint: %s", + lim.limit, + limit_key, + limit_scope, + ) + failed_limits.append((lim, args)) + view_limits[-1].breached = True + limit_for_header = view_limits[-1] + + if self._fail_on_first_breach: + break + + if not limit_for_header and view_limits: + # Pick a non shared limit over a shared one if possible + # when no rate limit has been hit. This should be the best hint + # for the client. + explicit = [limit for limit in view_limits if not limit.shared] + limit_for_header = explicit[0] if explicit else view_limits[0] + + self.context.view_rate_limit = limit_for_header or None + self.context.view_rate_limits = view_limits + + on_breach_response = None + + for limit in failed_limits: + request_limit = RequestLimit(self, limit[0].limit, limit[1], True, limit[0].shared) + + for cb in dict.fromkeys([self._on_breach, limit[0].on_breach]): + if cb: + try: + cb_response = cb(request_limit) + + if isinstance(cb_response, flask.wrappers.Response): + on_breach_response = cb_response + except Exception as err: # noqa + if self._swallow_errors: + self.logger.exception("on_breach callback failed with error %s", err) + else: + raise err + + if failed_limits: + meta_limits = [ + meta_limit + for meta_limit in itertools.chain( + *self._meta_limits, + *[list(lim[0].meta_limits) for lim in failed_limits if lim[0].meta_limits], + ) + if not meta_limit.is_exempt + ] + for lim in meta_limits: + limit_scope = lim.scope_for(endpoint, flask.request.method) + limit_key = lim.key_func() + args = [limit_key, limit_scope] + self.limiter.hit(lim.limit, *args) + raise RateLimitExceeded( + sorted(failed_limits, key=lambda x: x[0].limit)[0][0], + response=on_breach_response, + ) + + def _check_request_limit( + self, callable_name: str | None = None, in_middleware: bool = True + ) -> None: + endpoint = self.identify_request() + try: + all_limits = self.__filter_limits( + endpoint, + flask.request.blueprint, + callable_name, + in_middleware, + ) + self.__evaluate_limits(endpoint, all_limits) + except Exception as e: + if isinstance(e, RateLimitExceeded): + raise e + + if self._in_memory_fallback_enabled and not self._storage_dead: + self.logger.warning( + "Rate limit storage unreachable - falling back to in-memory storage" + ) + self._storage_dead = True + self.context.seen_limits.clear() + self._check_request_limit(callable_name=callable_name, in_middleware=in_middleware) + else: + if self._swallow_errors: + self.logger.exception("Failed to rate limit. Swallowing error") + else: + raise e + + def __release_context(self, _: BaseException | None = None) -> None: + self.context.reset() diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_limits.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_limits.py new file mode 100644 index 0000000..6651290 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_limits.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import dataclasses +import itertools +import traceback +import weakref +from functools import wraps +from types import TracebackType +from typing import TYPE_CHECKING, cast, overload + +import flask +from flask import request +from flask.wrappers import Response +from limits import RateLimitItem, parse_many + +from ._typing import Callable, Iterable, Iterator, P, R, Self, Sequence +from .util import get_qualified_name + +if TYPE_CHECKING: + from flask_limiter import Limiter, RequestLimit + + +@dataclasses.dataclass(eq=True, unsafe_hash=True) +class RuntimeLimit: + """ + Final representation of a rate limit before it is triggered during a request + """ + + limit: RateLimitItem + key_func: Callable[[], str] + scope: str | Callable[[str], str] | None + per_method: bool = False + methods: Sequence[str] | None = None + error_message: str | Callable[[], str] | None = None + exempt_when: Callable[[], bool] | None = None + override_defaults: bool | None = False + deduct_when: Callable[[Response], bool] | None = None + on_breach: Callable[[RequestLimit], Response | None] | None = None + cost: Callable[[], int] | int = 1 + shared: bool = False + meta_limits: tuple[RuntimeLimit, ...] | None = None + + def __post_init__(self) -> None: + if self.methods: + self.methods = tuple([k.lower() for k in self.methods]) + + @property + def is_exempt(self) -> bool: + """Check if the limit is exempt.""" + + if self.exempt_when: + return self.exempt_when() + + return False + + @property + def deduction_amount(self) -> int: + """How much to deduct from the limit""" + + return self.cost() if callable(self.cost) else self.cost + + @property + def method_exempt(self) -> bool: + """Check if the limit is not applicable for this method""" + + return self.methods is not None and request.method.lower() not in self.methods + + def scope_for(self, endpoint: str, method: str | None) -> str: + """ + Derive final bucket (scope) for this limit given the endpoint and request method. + If the limit is shared between multiple routes, the scope does not include the endpoint. + """ + limit_scope = self.scope(request.endpoint or "") if callable(self.scope) else self.scope + + if limit_scope: + if self.shared: + scope = limit_scope + else: + scope = f"{endpoint}:{limit_scope}" + else: + scope = endpoint + + if self.per_method: + assert method + scope += f":{method.upper()}" + + return scope + + +@dataclasses.dataclass(eq=True, unsafe_hash=True) +class Limit: + """ + The definition of a rate limit to be used by the extension as a default limit:: + + + def default_key_function(): + return request.remote_addr + + def username_key_function(): + return request.headers.get("username", "guest") + + limiter = flask_limiter.Limiter( + default_key_function, + default_limits = [ + # 10/second by username + flask_limiter.Limit("10/second", key_function=username_key_function), + # 100/second by ip (i.e. default_key_function) + flask_limiter.Limit("100/second), + + ] + ) + limit.init_app(app) + + - For application wide limits see :class:`ApplicationLimit` + - For meta limits see :class:`MetaLimit` + """ + + #: Rate limit string or a callable that returns a string. + #: :ref:`ratelimit-string` for more details. + limit_provider: Callable[[], str] | str + #: Callable to extract the unique identifier for the rate limit. + #: If not provided the key_function will default to the key function + #: that the :class:`Limiter` was initialized with (:paramref:`Limiter.key_func`) + key_function: Callable[[], str] | None = None + #: A string or callable that returns a unique scope for the rate limit. + #: The scope is combined with current endpoint of the request if + #: :paramref:`shared` is ``False`` + scope: str | Callable[[str], str] | None = None + #: The cost of a hit or a function that + #: takes no parameters and returns the cost as an integer (Default: ``1``). + cost: Callable[[], int] | int | None = None + #: If this a shared limit (i.e. to be used by different endpoints) + shared: bool = False + #: If specified, only the methods in this list will + #: be rate limited. + methods: Sequence[str] | None = None + #: Whether the limit is sub categorized into the + #: http method of the request. + per_method: bool = False + #: String (or callable that returns one) to override + #: the error message used in the response. + error_message: str | Callable[[], str] | None = None + #: Meta limits to trigger everytime this rate limit definition is exceeded + meta_limits: Iterable[Callable[[], str] | str | MetaLimit] | None = None + #: Callable used to decide if the rate + #: limit should skipped. + exempt_when: Callable[[], bool] | None = None + #: A function that receives the current + #: :class:`flask.Response` object and returns True/False to decide if a + #: deduction should be done from the rate limit + deduct_when: Callable[[Response], bool] | None = None + #: A function that will be called when this limit + #: is breached. If the function returns an instance of :class:`flask.Response` + #: that will be the response embedded into the :exc:`RateLimitExceeded` exception + #: raised. + on_breach: Callable[[RequestLimit], Response | None] | None = None + #: Whether the decorated limit overrides + #: the default limits (Default: ``True``). + #: + #: .. note:: When used with a :class:`~flask.Blueprint` the meaning + #: of the parameter extends to any parents the blueprint instance is + #: registered under. For more details see :ref:`recipes:nested blueprints` + #: + #: :meta private: + override_defaults: bool | None = dataclasses.field(default=False, init=False) + #: Weak reference to the limiter that this limit definition is bound to + #: + #: :meta private: + limiter: weakref.ProxyType[Limiter] = dataclasses.field( + init=False, hash=False, kw_only=True, repr=False + ) + #: :meta private: + finalized: bool = dataclasses.field(default=True) + + def __post_init__(self) -> None: + if self.methods: + self.methods = tuple([k.lower() for k in self.methods]) + + if self.meta_limits: + self.meta_limits = tuple(self.meta_limits) + + def __iter__(self) -> Iterator[RuntimeLimit]: + limit_str = self.limit_provider() if callable(self.limit_provider) else self.limit_provider + limit_items = parse_many(limit_str) if limit_str else [] + meta_limits: tuple[RuntimeLimit, ...] = () + + if self.meta_limits: + meta_limits = tuple( + itertools.chain( + *[ + list( + MetaLimit(meta_limit).bind_parent(self) + if not isinstance(meta_limit, MetaLimit) + else meta_limit + ) + for meta_limit in self.meta_limits + ] + ) + ) + + for limit in limit_items: + yield RuntimeLimit( + limit, + self.limit_by, + scope=self.scope, + per_method=self.per_method, + methods=self.methods, + error_message=self.error_message, + exempt_when=self.exempt_when, + deduct_when=self.deduct_when, + override_defaults=self.override_defaults, + on_breach=self.on_breach, + cost=self.cost or 1, + shared=self.shared, + meta_limits=meta_limits, + ) + + @property + def limit_by(self) -> Callable[[], str]: + return self.key_function or self.limiter._key_func + + def bind(self: Self, limiter: Limiter) -> Self: + """ + Returns an instance of the limit definition that binds to a weak reference of an instance + of :class:`Limiter`. + + :meta private: + """ + self.limiter = weakref.proxy(limiter) + [ + meta_limit.bind(limiter) + for meta_limit in self.meta_limits or () + if isinstance(meta_limit, MetaLimit) + ] + + return self + + +@dataclasses.dataclass(unsafe_hash=True, kw_only=True) +class RouteLimit(Limit): + """ + A variant of :class:`Limit` that can be used to to decorate a flask route or blueprint directly + instead of by using :meth:`Limiter.limit` or :meth:`Limiter.shared_limit`. + + Decorating individual routes:: + + limiter = flask_limiter.Limiter(.....) + limiter.init_app(app) + + @app.route("/") + @flask_limiter.RouteLimit("2/second", limiter=limiter) + def view_function(): + ... + + """ + + #: Whether the decorated limit overrides + #: the default limits (Default: ``True``). + #: + #: .. note:: When used with a :class:`~flask.Blueprint` the meaning + #: of the parameter extends to any parents the blueprint instance is + #: registered under. For more details see :ref:`recipes:nested blueprints` + override_defaults: bool | None = False + + limiter: dataclasses.InitVar[Limiter] = dataclasses.field(hash=False) + + def __post_init__(self, limiter: Limiter) -> None: + self.bind(limiter) + super().__post_init__() + + def __enter__(self) -> None: + tb = traceback.extract_stack(limit=2) + qualified_location = f"{tb[0].filename}:{tb[0].name}:{tb[0].lineno}" + + # TODO: if use as a context manager becomes interesting/valuable + # a less hacky approach than using the traceback and piggy backing + # on the limit manager's knowledge of decorated limits might be worth it. + self.limiter.limit_manager.add_decorated_limit(qualified_location, self, override=True) + + self.limiter.limit_manager.add_endpoint_hint( + self.limiter.identify_request(), qualified_location + ) + + self.limiter._check_request_limit(in_middleware=False, callable_name=qualified_location) + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: ... + + @overload + def __call__(self, obj: Callable[P, R]) -> Callable[P, R]: ... + + @overload + def __call__(self, obj: flask.Blueprint) -> None: ... + + def __call__(self, obj: Callable[P, R] | flask.Blueprint) -> Callable[P, R] | None: + if isinstance(obj, flask.Blueprint): + name = obj.name + else: + name = get_qualified_name(obj) + + if isinstance(obj, flask.Blueprint): + self.limiter.limit_manager.add_blueprint_limit(name, self) + + return None + else: + self.limiter._marked_for_limiting.add(name) + self.limiter.limit_manager.add_decorated_limit(name, self) + + @wraps(obj) + def __inner(*a: P.args, **k: P.kwargs) -> R: + if not getattr(obj, "__wrapper-limiter-instance", None) == self.limiter: + identity = self.limiter.identify_request() + + if identity: + view_func = flask.current_app.view_functions.get(identity, None) + + if view_func and not get_qualified_name(view_func) == name: + self.limiter.limit_manager.add_endpoint_hint(identity, name) + + self.limiter._check_request_limit(in_middleware=False, callable_name=name) + + return cast(R, flask.current_app.ensure_sync(obj)(*a, **k)) + + # mark this wrapper as wrapped by a decorator from the limiter + # from which the decorator was created. This ensures that stacked + # decorations only trigger rate limiting from the inner most + # decorator from each limiter instance (the weird need for + # keeping track of the instance is to handle cases where multiple + # limiter extensions are registered on the same application). + setattr(__inner, "__wrapper-limiter-instance", self.limiter) + + return __inner + + +@dataclasses.dataclass(kw_only=True, unsafe_hash=True) +class ApplicationLimit(Limit): + """ + Variant of :class:`Limit` to be used for declaring an application wide limit that can be passed + to :class:`Limiter` as one of the members of :paramref:`Limiter.application_limits` + """ + + #: The scope to use for the application wide limit + scope: str | Callable[[str], str] | None = dataclasses.field(default="global") + #: Application limits are always "shared" + #: + #: :meta private: + shared: bool = dataclasses.field(init=False, default=True) + + +@dataclasses.dataclass(kw_only=True, unsafe_hash=True) +class MetaLimit(Limit): + """ + Variant of :class:`Limit` to be used for declaring a meta limit that can be passed to + either :class:`Limiter` as one of the members of :paramref:`Limiter.meta_limits` or to another + instance of :class:`Limit` as a member of :paramref:`Limit.meta_limits` + """ + + #: The scope to use for the meta limit + scope: str | Callable[[str], str] | None = dataclasses.field(default="meta") + #: meta limits can't have meta limits - at least here :) + #: + #: :meta private: + meta_limits: Sequence[Callable[[], str] | str | MetaLimit] | None = dataclasses.field( + init=False, default=None + ) + #: The rate limit this meta limit is limiting. + #: + # :meta private: + parent_limit: Limit | None = dataclasses.field(init=False, default=None) + #: Meta limits are always "shared" + #: + #: :meta private: + shared: bool = dataclasses.field(init=False, default=True) + #: Meta limits can't have conditional deductions + #: + #: :meta private: + deduct_when: Callable[[Response], bool] | None = dataclasses.field(init=False, default=None) + #: Callable to extract the unique identifier for the rate limit. + #: If not provided the key_function will fallback to: + #: + #: - the key function of the parent limit this meta limit is declared for + #: - the key function for the :class:`Limiter` instance this meta limit + #: is eventually used with. + key_function: Callable[[], str] | None = None + + @property + def limit_by(self) -> Callable[[], str]: + return ( + self.key_function + or self.parent_limit + and self.parent_limit.key_function + or self.limiter._key_func + ) + + def bind_parent(self: Self, parent: Limit) -> Self: + """ + Binds this meta limit to be associated as a child of the ``parent`` limit. + + :meta private: + """ + self.parent_limit = parent + return self diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_manager.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_manager.py new file mode 100644 index 0000000..cfcd8a7 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_manager.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +import itertools +import logging +from collections.abc import Iterable +from typing import TYPE_CHECKING + +import flask +from ordered_set import OrderedSet + +from ._limits import ApplicationLimit, RuntimeLimit +from .constants import ExemptionScope +from .util import get_qualified_name + +if TYPE_CHECKING: + from . import Limit + + +class LimitManager: + def __init__( + self, + application_limits: list[ApplicationLimit], + default_limits: list[Limit], + decorated_limits: dict[str, OrderedSet[Limit]], + blueprint_limits: dict[str, OrderedSet[Limit]], + route_exemptions: dict[str, ExemptionScope], + blueprint_exemptions: dict[str, ExemptionScope], + ) -> None: + self._application_limits = application_limits + self._default_limits = default_limits + self._decorated_limits = decorated_limits + self._blueprint_limits = blueprint_limits + self._route_exemptions = route_exemptions + self._blueprint_exemptions = blueprint_exemptions + self._endpoint_hints: dict[str, OrderedSet[str]] = {} + self._logger = logging.getLogger("flask-limiter") + + @property + def application_limits(self) -> list[RuntimeLimit]: + return list(itertools.chain(*self._application_limits)) + + @property + def default_limits(self) -> list[RuntimeLimit]: + return list(itertools.chain(*self._default_limits)) + + def set_application_limits(self, limits: list[ApplicationLimit]) -> None: + self._application_limits = limits + + def set_default_limits(self, limits: list[Limit]) -> None: + self._default_limits = limits + + def add_decorated_limit(self, route: str, limit: Limit | None, override: bool = False) -> None: + if limit: + if not override: + self._decorated_limits.setdefault(route, OrderedSet()).add(limit) + else: + self._decorated_limits[route] = OrderedSet([limit]) + + def add_blueprint_limit(self, blueprint: str, limit: Limit | None) -> None: + if limit: + self._blueprint_limits.setdefault(blueprint, OrderedSet()).add(limit) + + def add_route_exemption(self, route: str, scope: ExemptionScope) -> None: + self._route_exemptions[route] = scope + + def add_blueprint_exemption(self, blueprint: str, scope: ExemptionScope) -> None: + self._blueprint_exemptions[blueprint] = scope + + def add_endpoint_hint(self, endpoint: str, callable: str) -> None: + self._endpoint_hints.setdefault(endpoint, OrderedSet()).add(callable) + + def has_hints(self, endpoint: str) -> bool: + return bool(self._endpoint_hints.get(endpoint)) + + def resolve_limits( + self, + app: flask.Flask, + endpoint: str | None = None, + blueprint: str | None = None, + callable_name: str | None = None, + in_middleware: bool = False, + marked_for_limiting: bool = False, + ) -> tuple[list[RuntimeLimit], ...]: + before_request_context = in_middleware and marked_for_limiting + decorated_limits = [] + hinted_limits = [] + if endpoint: + if not in_middleware: + if not callable_name: + view_func = app.view_functions.get(endpoint, None) + name = get_qualified_name(view_func) if view_func else "" + else: + name = callable_name + decorated_limits.extend(self.decorated_limits(name)) + + for hint in self._endpoint_hints.get(endpoint, OrderedSet()): + hinted_limits.extend(self.decorated_limits(hint)) + + if blueprint: + if not before_request_context and ( + not decorated_limits + or all(not limit.override_defaults for limit in decorated_limits) + ): + decorated_limits.extend(self.blueprint_limits(app, blueprint)) + exemption_scope = self.exemption_scope(app, endpoint, blueprint) + + all_limits = ( + self.application_limits + if in_middleware and not (exemption_scope & ExemptionScope.APPLICATION) + else [] + ) + # all_limits += decorated_limits + explicit_limits_exempt = all(limit.method_exempt for limit in decorated_limits) + + # all the decorated limits explicitly declared + # that they don't override the defaults - so, they should + # be included. + combined_defaults = all(not limit.override_defaults for limit in decorated_limits) + # previous requests to this endpoint have exercised decorated + # rate limits on callables that are not view functions. check + # if all of them declared that they don't override defaults + # and if so include the default limits. + hinted_limits_request_defaults = ( + all(not limit.override_defaults for limit in hinted_limits) if hinted_limits else False + ) + if ( + (explicit_limits_exempt or combined_defaults) + and (not (before_request_context or exemption_scope & ExemptionScope.DEFAULT)) + ) or hinted_limits_request_defaults: + all_limits += self.default_limits + return all_limits, decorated_limits + + def exemption_scope( + self, app: flask.Flask, endpoint: str | None, blueprint: str | None + ) -> ExemptionScope: + view_func = app.view_functions.get(endpoint or "", None) + name = get_qualified_name(view_func) if view_func else "" + route_exemption_scope = self._route_exemptions.get(name, ExemptionScope.NONE) + blueprint_instance = app.blueprints.get(blueprint) if blueprint else None + + if not blueprint_instance: + return route_exemption_scope + else: + assert blueprint + ( + blueprint_exemption_scope, + ancestor_exemption_scopes, + ) = self._blueprint_exemption_scope(app, blueprint) + if ( + blueprint_exemption_scope & ~(ExemptionScope.DEFAULT | ExemptionScope.APPLICATION) + or ancestor_exemption_scopes + ): + for exemption in ancestor_exemption_scopes.values(): + blueprint_exemption_scope |= exemption + return route_exemption_scope | blueprint_exemption_scope + + def decorated_limits(self, callable_name: str) -> list[RuntimeLimit]: + limits = [] + if not self._route_exemptions.get(callable_name, ExemptionScope.NONE): + if callable_name in self._decorated_limits: + for group in self._decorated_limits[callable_name]: + try: + for limit in group: + limits.append(limit) + except ValueError as e: + self._logger.error( + f"failed to load ratelimit for function {callable_name}: {e}", + ) + return limits + + def blueprint_limits(self, app: flask.Flask, blueprint: str) -> list[RuntimeLimit]: + limits: list[RuntimeLimit] = [] + + blueprint_instance = app.blueprints.get(blueprint) if blueprint else None + if blueprint_instance: + blueprint_name = blueprint_instance.name + blueprint_ancestory = set(blueprint.split(".") if blueprint else []) + + self_exemption, ancestor_exemptions = self._blueprint_exemption_scope(app, blueprint) + + if not (self_exemption & ~(ExemptionScope.DEFAULT | ExemptionScope.APPLICATION)): + blueprint_self_limits = self._blueprint_limits.get(blueprint_name, OrderedSet()) + blueprint_limits: Iterable[Limit] = ( + itertools.chain( + *( + self._blueprint_limits.get(member, []) + for member in blueprint_ancestory.intersection( + self._blueprint_limits + ).difference(ancestor_exemptions) + ) + ) + if not ( + blueprint_self_limits + and all(limit.override_defaults for limit in blueprint_self_limits) + ) + and not self._blueprint_exemptions.get(blueprint_name, ExemptionScope.NONE) + & ExemptionScope.ANCESTORS + else blueprint_self_limits + ) + if blueprint_limits: + for limit_group in blueprint_limits: + try: + limits.extend( + [ + RuntimeLimit( + limit.limit, + limit.key_func, + limit.scope, + limit.per_method, + limit.methods, + limit.error_message, + limit.exempt_when, + limit.override_defaults, + limit.deduct_when, + limit.on_breach, + limit.cost, + limit.shared, + ) + for limit in limit_group + ] + ) + except ValueError as e: + self._logger.error( + f"failed to load ratelimit for blueprint {blueprint_name}: {e}", + ) + return limits + + def _blueprint_exemption_scope( + self, app: flask.Flask, blueprint_name: str + ) -> tuple[ExemptionScope, dict[str, ExemptionScope]]: + name = app.blueprints[blueprint_name].name + exemption = self._blueprint_exemptions.get(name, ExemptionScope.NONE) & ~( + ExemptionScope.ANCESTORS + ) + + ancestory = set(blueprint_name.split(".")) + ancestor_exemption = { + k for k, f in self._blueprint_exemptions.items() if f & ExemptionScope.DESCENDENTS + }.intersection(ancestory) + + return exemption, { + k: self._blueprint_exemptions.get(k, ExemptionScope.NONE) for k in ancestor_exemption + } diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_typing.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_typing.py new file mode 100644 index 0000000..965427f --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_typing.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from collections.abc import Callable, Generator, Iterable, Iterator, Sequence +from typing import ( + ParamSpec, + TypeVar, + cast, +) + +from typing_extensions import Self + +R = TypeVar("R") +P = ParamSpec("P") + +__all__ = [ + "Callable", + "Generator", + "Iterable", + "Iterator", + "P", + "R", + "Sequence", + "Self", + "TypeVar", + "cast", +] diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_version.py b/buffteks/lib/python3.11/site-packages/flask_limiter/_version.py new file mode 100644 index 0000000..a9986cf --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_version.py @@ -0,0 +1,34 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] +else: + VERSION_TUPLE = object + COMMIT_ID = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID + +__version__ = version = '4.0.0' +__version_tuple__ = version_tuple = (4, 0, 0) + +__commit_id__ = commit_id = None diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/_version.pyi b/buffteks/lib/python3.11/site-packages/flask_limiter/_version.pyi new file mode 100644 index 0000000..bda5b5a --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/_version.pyi @@ -0,0 +1 @@ +__version__: str diff --git a/buffteks/lib/python3.11/site-packages/flask_limiter/commands.py b/buffteks/lib/python3.11/site-packages/flask_limiter/commands.py new file mode 100644 index 0000000..04f9ead --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/flask_limiter/commands.py @@ -0,0 +1,563 @@ +from __future__ import annotations + +import itertools +import time +from functools import partial +from typing import Any +from urllib.parse import urlparse + +import click +from flask import Flask, current_app +from flask.cli import with_appcontext +from limits.strategies import RateLimiter +from rich.console import Console, group +from rich.live import Live +from rich.pretty import Pretty +from rich.prompt import Confirm +from rich.table import Table +from rich.theme import Theme +from rich.tree import Tree +from typing_extensions import TypedDict +from werkzeug.exceptions import MethodNotAllowed, NotFound +from werkzeug.routing import Rule + +from ._extension import Limiter +from ._limits import RuntimeLimit +from ._typing import Callable, Generator, cast +from .constants import ConfigVars, ExemptionScope, HeaderNames +from .util import get_qualified_name + +limiter_theme = Theme( + { + "success": "bold green", + "danger": "bold red", + "error": "bold red", + "blueprint": "bold red", + "default": "magenta", + "callable": "cyan", + "entity": "magenta", + "exempt": "bold red", + "route": "yellow", + "http": "bold green", + "option": "bold yellow", + } +) + + +def render_func(func: Any) -> str | Pretty: + if callable(func): + if func.__name__ == "++ +Batch: + + $ markdown-it README.md README.footer.md > index.html +""" + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument("-v", "--version", action="version", version=version_str) + parser.add_argument( + "filenames", nargs="*", help="specify an optional list of files to convert" + ) + return parser.parse_args(args) + + +def print_heading() -> None: + print(f"{version_str} (interactive)") + print("Type Ctrl-D to complete input, or Ctrl-C to exit.") + + +if __name__ == "__main__": + exit_code = main(sys.argv[1:]) + sys.exit(exit_code) diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/common/__init__.py b/buffteks/lib/python3.11/site-packages/markdown_it/common/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/common/entities.py b/buffteks/lib/python3.11/site-packages/markdown_it/common/entities.py new file mode 100644 index 0000000..14d08ec --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/markdown_it/common/entities.py @@ -0,0 +1,5 @@ +"""HTML5 entities map: { name -> characters }.""" + +import html.entities + +entities = {name.rstrip(";"): chars for name, chars in html.entities.html5.items()} diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/common/html_blocks.py b/buffteks/lib/python3.11/site-packages/markdown_it/common/html_blocks.py new file mode 100644 index 0000000..8a3b0b7 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/markdown_it/common/html_blocks.py @@ -0,0 +1,69 @@ +"""List of valid html blocks names, according to commonmark spec +http://jgm.github.io/CommonMark/spec.html#html-blocks +""" + +# see https://spec.commonmark.org/0.31.2/#html-blocks +block_names = [ + "address", + "article", + "aside", + "base", + "basefont", + "blockquote", + "body", + "caption", + "center", + "col", + "colgroup", + "dd", + "details", + "dialog", + "dir", + "div", + "dl", + "dt", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "frame", + "frameset", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "iframe", + "legend", + "li", + "link", + "main", + "menu", + "menuitem", + "nav", + "noframes", + "ol", + "optgroup", + "option", + "p", + "param", + "search", + "section", + "summary", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "title", + "tr", + "track", + "ul", +] diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/common/html_re.py b/buffteks/lib/python3.11/site-packages/markdown_it/common/html_re.py new file mode 100644 index 0000000..ab822c5 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/markdown_it/common/html_re.py @@ -0,0 +1,39 @@ +"""Regexps to match html elements""" + +import re + +attr_name = "[a-zA-Z_:][a-zA-Z0-9:._-]*" + +unquoted = "[^\"'=<>`\\x00-\\x20]+" +single_quoted = "'[^']*'" +double_quoted = '"[^"]*"' + +attr_value = "(?:" + unquoted + "|" + single_quoted + "|" + double_quoted + ")" + +attribute = "(?:\\s+" + attr_name + "(?:\\s*=\\s*" + attr_value + ")?)" + +open_tag = "<[A-Za-z][A-Za-z0-9\\-]*" + attribute + "*\\s*\\/?>" + +close_tag = "<\\/[A-Za-z][A-Za-z0-9\\-]*\\s*>" +comment = "" +processing = "<[?][\\s\\S]*?[?]>" +declaration = "]*>" +cdata = "" + +HTML_TAG_RE = re.compile( + "^(?:" + + open_tag + + "|" + + close_tag + + "|" + + comment + + "|" + + processing + + "|" + + declaration + + "|" + + cdata + + ")" +) +HTML_OPEN_CLOSE_TAG_STR = "^(?:" + open_tag + "|" + close_tag + ")" +HTML_OPEN_CLOSE_TAG_RE = re.compile(HTML_OPEN_CLOSE_TAG_STR) diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/common/normalize_url.py b/buffteks/lib/python3.11/site-packages/markdown_it/common/normalize_url.py new file mode 100644 index 0000000..92720b3 --- /dev/null +++ b/buffteks/lib/python3.11/site-packages/markdown_it/common/normalize_url.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Callable +from contextlib import suppress +import re +from urllib.parse import quote, unquote, urlparse, urlunparse # noqa: F401 + +import mdurl + +from .. import _punycode + +RECODE_HOSTNAME_FOR = ("http:", "https:", "mailto:") + + +def normalizeLink(url: str) -> str: + """Normalize destination URLs in links + + :: + + [label]: destination 'title' + ^^^^^^^^^^^ + """ + parsed = mdurl.parse(url, slashes_denote_host=True) + + # Encode hostnames in urls like: + # `http://host/`, `https://host/`, `mailto:user@host`, `//host/` + # + # We don't encode unknown schemas, because it's likely that we encode + # something we shouldn't (e.g. `skype:name` treated as `skype:host`) + # + if parsed.hostname and ( + not parsed.protocol or parsed.protocol in RECODE_HOSTNAME_FOR + ): + with suppress(Exception): + parsed = parsed._replace(hostname=_punycode.to_ascii(parsed.hostname)) + + return mdurl.encode(mdurl.format(parsed)) + + +def normalizeLinkText(url: str) -> str: + """Normalize autolink content + + :: + +markdown input
+
` tags.
+ """
+ env = {} if env is None else env
+ return self.renderer.render(self.parseInline(src, env), self.options, env)
+
+ # link methods
+
+ def validateLink(self, url: str) -> bool:
+ """Validate if the URL link is allowed in output.
+
+ This validator can prohibit more than really needed to prevent XSS.
+ It's a tradeoff to keep code simple and to be secure by default.
+
+ Note: the url should be normalized at this point, and existing entities decoded.
+ """
+ return normalize_url.validateLink(url)
+
+ def normalizeLink(self, url: str) -> str:
+ """Normalize destination URLs in links
+
+ ::
+
+ [label]: destination 'title'
+ ^^^^^^^^^^^
+ """
+ return normalize_url.normalizeLink(url)
+
+ def normalizeLinkText(self, link: str) -> str:
+ """Normalize autolink content
+
+ ::
+
+
+ markdown input
)
+ "breaks": False, # Convert '\n' in paragraphs into
+ "langPrefix": "language-", # CSS language prefix for fenced blocks
+ # Highlighter function. Should return escaped HTML,
+ # or '' if the source string is not changed and should be escaped externally.
+ # If result starts with
)
+ "breaks": False, # Convert '\n' in paragraphs into
+ "langPrefix": "language-", # CSS language prefix for fenced blocks
+ # Highlighter function. Should return escaped HTML,
+ # or '' if the source string is not changed and should be escaped externally.
+ # If result starts with
)
+ "breaks": False, # Convert '\n' in paragraphs into
+ "langPrefix": "language-", # CSS language prefix for fenced blocks
+ # Highlighter function. Should return escaped HTML,
+ # or '' if the source string is not changed and should be escaped externally.
+ # If result starts with `.
+ #
+ needLf = False
+
+ result += ">\n" if needLf else ">"
+
+ return result
+
+ @staticmethod
+ def renderAttrs(token: Token) -> str:
+ """Render token attributes to string."""
+ result = ""
+
+ for key, value in token.attrItems():
+ result += " " + escapeHtml(key) + '="' + escapeHtml(str(value)) + '"'
+
+ return result
+
+ def renderInlineAsText(
+ self,
+ tokens: Sequence[Token] | None,
+ options: OptionsDict,
+ env: EnvType,
+ ) -> str:
+ """Special kludge for image `alt` attributes to conform CommonMark spec.
+
+ Don't try to use it! Spec requires to show `alt` content with stripped markup,
+ instead of simple escaping.
+
+ :param tokens: list on block tokens to render
+ :param options: params of parser instance
+ :param env: additional data from parsed input
+ """
+ result = ""
+
+ for token in tokens or []:
+ if token.type == "text":
+ result += token.content
+ elif token.type == "image":
+ if token.children:
+ result += self.renderInlineAsText(token.children, options, env)
+ elif token.type == "softbreak":
+ result += "\n"
+
+ return result
+
+ ###################################################
+
+ def code_inline(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ token = tokens[idx]
+ return (
+ "
"
+ + escapeHtml(tokens[idx].content)
+ + ""
+ )
+
+ def code_block(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: EnvType,
+ ) -> str:
+ token = tokens[idx]
+
+ return (
+ "
\n"
+ )
+
+ def fence(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: EnvType,
+ ) -> str:
+ token = tokens[idx]
+ info = unescapeAll(token.info).strip() if token.info else ""
+ langName = ""
+ langAttrs = ""
+
+ if info:
+ arr = info.split(maxsplit=1)
+ langName = arr[0]
+ if len(arr) == 2:
+ langAttrs = arr[1]
+
+ if options.highlight:
+ highlighted = options.highlight(
+ token.content, langName, langAttrs
+ ) or escapeHtml(token.content)
+ else:
+ highlighted = escapeHtml(token.content)
+
+ if highlighted.startswith(""
+ + escapeHtml(tokens[idx].content)
+ + "
\n"
+ )
+
+ return (
+ ""
+ + highlighted
+ + "
\n"
+ )
+
+ def image(
+ self,
+ tokens: Sequence[Token],
+ idx: int,
+ options: OptionsDict,
+ env: EnvType,
+ ) -> str:
+ token = tokens[idx]
+
+ # "alt" attr MUST be set, even if empty. Because it's mandatory and
+ # should be placed on proper position for tests.
+ if token.children:
+ token.attrSet("alt", self.renderInlineAsText(token.children, options, env))
+ else:
+ token.attrSet("alt", "")
+
+ return self.renderToken(tokens, idx, options, env)
+
+ def hardbreak(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ return ""
+ + highlighted
+ + "
\n" if options.xhtmlOut else "
\n"
+
+ def softbreak(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ return (
+ ("
\n" if options.xhtmlOut else "
\n") if options.breaks else "\n"
+ )
+
+ def text(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ return escapeHtml(tokens[idx].content)
+
+ def html_block(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ return tokens[idx].content
+
+ def html_inline(
+ self, tokens: Sequence[Token], idx: int, options: OptionsDict, env: EnvType
+ ) -> str:
+ return tokens[idx].content
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/ruler.py b/buffteks/lib/python3.11/site-packages/markdown_it/ruler.py
new file mode 100644
index 0000000..91ab580
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/ruler.py
@@ -0,0 +1,275 @@
+"""
+class Ruler
+
+Helper class, used by [[MarkdownIt#core]], [[MarkdownIt#block]] and
+[[MarkdownIt#inline]] to manage sequences of functions (rules):
+
+- keep rules in defined order
+- assign the name to each rule
+- enable/disable rules
+- add/replace rules
+- allow assign rules to additional named chains (in the same)
+- caching lists of active rules
+
+You will not need use this class directly until write plugins. For simple
+rules control use [[MarkdownIt.disable]], [[MarkdownIt.enable]] and
+[[MarkdownIt.use]].
+"""
+
+from __future__ import annotations
+
+from collections.abc import Iterable
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Generic, TypedDict, TypeVar
+import warnings
+
+from .utils import EnvType
+
+if TYPE_CHECKING:
+ from markdown_it import MarkdownIt
+
+
+class StateBase:
+ def __init__(self, src: str, md: MarkdownIt, env: EnvType):
+ self.src = src
+ self.env = env
+ self.md = md
+
+ @property
+ def src(self) -> str:
+ return self._src
+
+ @src.setter
+ def src(self, value: str) -> None:
+ self._src = value
+ self._srcCharCode: tuple[int, ...] | None = None
+
+ @property
+ def srcCharCode(self) -> tuple[int, ...]:
+ warnings.warn(
+ "StateBase.srcCharCode is deprecated. Use StateBase.src instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ if self._srcCharCode is None:
+ self._srcCharCode = tuple(ord(c) for c in self._src)
+ return self._srcCharCode
+
+
+class RuleOptionsType(TypedDict, total=False):
+ alt: list[str]
+
+
+RuleFuncTv = TypeVar("RuleFuncTv")
+"""A rule function, whose signature is dependent on the state type."""
+
+
+@dataclass(slots=True)
+class Rule(Generic[RuleFuncTv]):
+ name: str
+ enabled: bool
+ fn: RuleFuncTv = field(repr=False)
+ alt: list[str]
+
+
+class Ruler(Generic[RuleFuncTv]):
+ def __init__(self) -> None:
+ # List of added rules.
+ self.__rules__: list[Rule[RuleFuncTv]] = []
+ # Cached rule chains.
+ # First level - chain name, '' for default.
+ # Second level - diginal anchor for fast filtering by charcodes.
+ self.__cache__: dict[str, list[RuleFuncTv]] | None = None
+
+ def __find__(self, name: str) -> int:
+ """Find rule index by name"""
+ for i, rule in enumerate(self.__rules__):
+ if rule.name == name:
+ return i
+ return -1
+
+ def __compile__(self) -> None:
+ """Build rules lookup cache"""
+ chains = {""}
+ # collect unique names
+ for rule in self.__rules__:
+ if not rule.enabled:
+ continue
+ for name in rule.alt:
+ chains.add(name)
+ self.__cache__ = {}
+ for chain in chains:
+ self.__cache__[chain] = []
+ for rule in self.__rules__:
+ if not rule.enabled:
+ continue
+ if chain and (chain not in rule.alt):
+ continue
+ self.__cache__[chain].append(rule.fn)
+
+ def at(
+ self, ruleName: str, fn: RuleFuncTv, options: RuleOptionsType | None = None
+ ) -> None:
+ """Replace rule by name with new function & options.
+
+ :param ruleName: rule name to replace.
+ :param fn: new rule function.
+ :param options: new rule options (not mandatory).
+ :raises: KeyError if name not found
+ """
+ index = self.__find__(ruleName)
+ options = options or {}
+ if index == -1:
+ raise KeyError(f"Parser rule not found: {ruleName}")
+ self.__rules__[index].fn = fn
+ self.__rules__[index].alt = options.get("alt", [])
+ self.__cache__ = None
+
+ def before(
+ self,
+ beforeName: str,
+ ruleName: str,
+ fn: RuleFuncTv,
+ options: RuleOptionsType | None = None,
+ ) -> None:
+ """Add new rule to chain before one with given name.
+
+ :param beforeName: new rule will be added before this one.
+ :param ruleName: new rule will be added before this one.
+ :param fn: new rule function.
+ :param options: new rule options (not mandatory).
+ :raises: KeyError if name not found
+ """
+ index = self.__find__(beforeName)
+ options = options or {}
+ if index == -1:
+ raise KeyError(f"Parser rule not found: {beforeName}")
+ self.__rules__.insert(
+ index, Rule[RuleFuncTv](ruleName, True, fn, options.get("alt", []))
+ )
+ self.__cache__ = None
+
+ def after(
+ self,
+ afterName: str,
+ ruleName: str,
+ fn: RuleFuncTv,
+ options: RuleOptionsType | None = None,
+ ) -> None:
+ """Add new rule to chain after one with given name.
+
+ :param afterName: new rule will be added after this one.
+ :param ruleName: new rule will be added after this one.
+ :param fn: new rule function.
+ :param options: new rule options (not mandatory).
+ :raises: KeyError if name not found
+ """
+ index = self.__find__(afterName)
+ options = options or {}
+ if index == -1:
+ raise KeyError(f"Parser rule not found: {afterName}")
+ self.__rules__.insert(
+ index + 1, Rule[RuleFuncTv](ruleName, True, fn, options.get("alt", []))
+ )
+ self.__cache__ = None
+
+ def push(
+ self, ruleName: str, fn: RuleFuncTv, options: RuleOptionsType | None = None
+ ) -> None:
+ """Push new rule to the end of chain.
+
+ :param ruleName: new rule will be added to the end of chain.
+ :param fn: new rule function.
+ :param options: new rule options (not mandatory).
+
+ """
+ self.__rules__.append(
+ Rule[RuleFuncTv](ruleName, True, fn, (options or {}).get("alt", []))
+ )
+ self.__cache__ = None
+
+ def enable(
+ self, names: str | Iterable[str], ignoreInvalid: bool = False
+ ) -> list[str]:
+ """Enable rules with given names.
+
+ :param names: name or list of rule names to enable.
+ :param ignoreInvalid: ignore errors when rule not found
+ :raises: KeyError if name not found and not ignoreInvalid
+ :return: list of found rule names
+ """
+ if isinstance(names, str):
+ names = [names]
+ result: list[str] = []
+ for name in names:
+ idx = self.__find__(name)
+ if (idx < 0) and ignoreInvalid:
+ continue
+ if (idx < 0) and not ignoreInvalid:
+ raise KeyError(f"Rules manager: invalid rule name {name}")
+ self.__rules__[idx].enabled = True
+ result.append(name)
+ self.__cache__ = None
+ return result
+
+ def enableOnly(
+ self, names: str | Iterable[str], ignoreInvalid: bool = False
+ ) -> list[str]:
+ """Enable rules with given names, and disable everything else.
+
+ :param names: name or list of rule names to enable.
+ :param ignoreInvalid: ignore errors when rule not found
+ :raises: KeyError if name not found and not ignoreInvalid
+ :return: list of found rule names
+ """
+ if isinstance(names, str):
+ names = [names]
+ for rule in self.__rules__:
+ rule.enabled = False
+ return self.enable(names, ignoreInvalid)
+
+ def disable(
+ self, names: str | Iterable[str], ignoreInvalid: bool = False
+ ) -> list[str]:
+ """Disable rules with given names.
+
+ :param names: name or list of rule names to enable.
+ :param ignoreInvalid: ignore errors when rule not found
+ :raises: KeyError if name not found and not ignoreInvalid
+ :return: list of found rule names
+ """
+ if isinstance(names, str):
+ names = [names]
+ result = []
+ for name in names:
+ idx = self.__find__(name)
+ if (idx < 0) and ignoreInvalid:
+ continue
+ if (idx < 0) and not ignoreInvalid:
+ raise KeyError(f"Rules manager: invalid rule name {name}")
+ self.__rules__[idx].enabled = False
+ result.append(name)
+ self.__cache__ = None
+ return result
+
+ def getRules(self, chainName: str = "") -> list[RuleFuncTv]:
+ """Return array of active functions (rules) for given chain name.
+ It analyzes rules configuration, compiles caches if not exists and returns result.
+
+ Default chain name is `''` (empty string). It can't be skipped.
+ That's done intentionally, to keep signature monomorphic for high speed.
+
+ """
+ if self.__cache__ is None:
+ self.__compile__()
+ assert self.__cache__ is not None
+ # Chain can be empty, if rules disabled. But we still have to return Array.
+ return self.__cache__.get(chainName, []) or []
+
+ def get_all_rules(self) -> list[str]:
+ """Return all available rule names."""
+ return [r.name for r in self.__rules__]
+
+ def get_active_rules(self) -> list[str]:
+ """Return the active rule names."""
+ return [r.name for r in self.__rules__ if r.enabled]
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/__init__.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/__init__.py
new file mode 100644
index 0000000..517da23
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/__init__.py
@@ -0,0 +1,27 @@
+__all__ = (
+ "StateBlock",
+ "blockquote",
+ "code",
+ "fence",
+ "heading",
+ "hr",
+ "html_block",
+ "lheading",
+ "list_block",
+ "paragraph",
+ "reference",
+ "table",
+)
+
+from .blockquote import blockquote
+from .code import code
+from .fence import fence
+from .heading import heading
+from .hr import hr
+from .html_block import html_block
+from .lheading import lheading
+from .list import list_block
+from .paragraph import paragraph
+from .reference import reference
+from .state_block import StateBlock
+from .table import table
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/blockquote.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/blockquote.py
new file mode 100644
index 0000000..0c9081b
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/blockquote.py
@@ -0,0 +1,299 @@
+# Block quotes
+from __future__ import annotations
+
+import logging
+
+from ..common.utils import isStrSpace
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def blockquote(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug(
+ "entering blockquote: %s, %s, %s, %s", state, startLine, endLine, silent
+ )
+
+ oldLineMax = state.lineMax
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ max = state.eMarks[startLine]
+
+ if state.is_code_block(startLine):
+ return False
+
+ # check the block quote marker
+ try:
+ if state.src[pos] != ">":
+ return False
+ except IndexError:
+ return False
+ pos += 1
+
+ # we know that it's going to be a valid blockquote,
+ # so no point trying to find the end of it in silent mode
+ if silent:
+ return True
+
+ # set offset past spaces and ">"
+ initial = offset = state.sCount[startLine] + 1
+
+ try:
+ second_char: str | None = state.src[pos]
+ except IndexError:
+ second_char = None
+
+ # skip one optional space after '>'
+ if second_char == " ":
+ # ' > test '
+ # ^ -- position start of line here:
+ pos += 1
+ initial += 1
+ offset += 1
+ adjustTab = False
+ spaceAfterMarker = True
+ elif second_char == "\t":
+ spaceAfterMarker = True
+
+ if (state.bsCount[startLine] + offset) % 4 == 3:
+ # ' >\t test '
+ # ^ -- position start of line here (tab has width==1)
+ pos += 1
+ initial += 1
+ offset += 1
+ adjustTab = False
+ else:
+ # ' >\t test '
+ # ^ -- position start of line here + shift bsCount slightly
+ # to make extra space appear
+ adjustTab = True
+
+ else:
+ spaceAfterMarker = False
+
+ oldBMarks = [state.bMarks[startLine]]
+ state.bMarks[startLine] = pos
+
+ while pos < max:
+ ch = state.src[pos]
+
+ if isStrSpace(ch):
+ if ch == "\t":
+ offset += (
+ 4
+ - (offset + state.bsCount[startLine] + (1 if adjustTab else 0)) % 4
+ )
+ else:
+ offset += 1
+
+ else:
+ break
+
+ pos += 1
+
+ oldBSCount = [state.bsCount[startLine]]
+ state.bsCount[startLine] = (
+ state.sCount[startLine] + 1 + (1 if spaceAfterMarker else 0)
+ )
+
+ lastLineEmpty = pos >= max
+
+ oldSCount = [state.sCount[startLine]]
+ state.sCount[startLine] = offset - initial
+
+ oldTShift = [state.tShift[startLine]]
+ state.tShift[startLine] = pos - state.bMarks[startLine]
+
+ terminatorRules = state.md.block.ruler.getRules("blockquote")
+
+ oldParentType = state.parentType
+ state.parentType = "blockquote"
+
+ # Search the end of the block
+ #
+ # Block ends with either:
+ # 1. an empty line outside:
+ # ```
+ # > test
+ #
+ # ```
+ # 2. an empty line inside:
+ # ```
+ # >
+ # test
+ # ```
+ # 3. another tag:
+ # ```
+ # > test
+ # - - -
+ # ```
+
+ # for (nextLine = startLine + 1; nextLine < endLine; nextLine++) {
+ nextLine = startLine + 1
+ while nextLine < endLine:
+ # check if it's outdented, i.e. it's inside list item and indented
+ # less than said list item:
+ #
+ # ```
+ # 1. anything
+ # > current blockquote
+ # 2. checking this line
+ # ```
+ isOutdented = state.sCount[nextLine] < state.blkIndent
+
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ max = state.eMarks[nextLine]
+
+ if pos >= max:
+ # Case 1: line is not inside the blockquote, and this line is empty.
+ break
+
+ evaluatesTrue = state.src[pos] == ">" and not isOutdented
+ pos += 1
+ if evaluatesTrue:
+ # This line is inside the blockquote.
+
+ # set offset past spaces and ">"
+ initial = offset = state.sCount[nextLine] + 1
+
+ try:
+ next_char: str | None = state.src[pos]
+ except IndexError:
+ next_char = None
+
+ # skip one optional space after '>'
+ if next_char == " ":
+ # ' > test '
+ # ^ -- position start of line here:
+ pos += 1
+ initial += 1
+ offset += 1
+ adjustTab = False
+ spaceAfterMarker = True
+ elif next_char == "\t":
+ spaceAfterMarker = True
+
+ if (state.bsCount[nextLine] + offset) % 4 == 3:
+ # ' >\t test '
+ # ^ -- position start of line here (tab has width==1)
+ pos += 1
+ initial += 1
+ offset += 1
+ adjustTab = False
+ else:
+ # ' >\t test '
+ # ^ -- position start of line here + shift bsCount slightly
+ # to make extra space appear
+ adjustTab = True
+
+ else:
+ spaceAfterMarker = False
+
+ oldBMarks.append(state.bMarks[nextLine])
+ state.bMarks[nextLine] = pos
+
+ while pos < max:
+ ch = state.src[pos]
+
+ if isStrSpace(ch):
+ if ch == "\t":
+ offset += (
+ 4
+ - (
+ offset
+ + state.bsCount[nextLine]
+ + (1 if adjustTab else 0)
+ )
+ % 4
+ )
+ else:
+ offset += 1
+ else:
+ break
+
+ pos += 1
+
+ lastLineEmpty = pos >= max
+
+ oldBSCount.append(state.bsCount[nextLine])
+ state.bsCount[nextLine] = (
+ state.sCount[nextLine] + 1 + (1 if spaceAfterMarker else 0)
+ )
+
+ oldSCount.append(state.sCount[nextLine])
+ state.sCount[nextLine] = offset - initial
+
+ oldTShift.append(state.tShift[nextLine])
+ state.tShift[nextLine] = pos - state.bMarks[nextLine]
+
+ nextLine += 1
+ continue
+
+ # Case 2: line is not inside the blockquote, and the last line was empty.
+ if lastLineEmpty:
+ break
+
+ # Case 3: another tag found.
+ terminate = False
+
+ for terminatorRule in terminatorRules:
+ if terminatorRule(state, nextLine, endLine, True):
+ terminate = True
+ break
+
+ if terminate:
+ # Quirk to enforce "hard termination mode" for paragraphs;
+ # normally if you call `tokenize(state, startLine, nextLine)`,
+ # paragraphs will look below nextLine for paragraph continuation,
+ # but if blockquote is terminated by another tag, they shouldn't
+ state.lineMax = nextLine
+
+ if state.blkIndent != 0:
+ # state.blkIndent was non-zero, we now set it to zero,
+ # so we need to re-calculate all offsets to appear as
+ # if indent wasn't changed
+ oldBMarks.append(state.bMarks[nextLine])
+ oldBSCount.append(state.bsCount[nextLine])
+ oldTShift.append(state.tShift[nextLine])
+ oldSCount.append(state.sCount[nextLine])
+ state.sCount[nextLine] -= state.blkIndent
+
+ break
+
+ oldBMarks.append(state.bMarks[nextLine])
+ oldBSCount.append(state.bsCount[nextLine])
+ oldTShift.append(state.tShift[nextLine])
+ oldSCount.append(state.sCount[nextLine])
+
+ # A negative indentation means that this is a paragraph continuation
+ #
+ state.sCount[nextLine] = -1
+
+ nextLine += 1
+
+ oldIndent = state.blkIndent
+ state.blkIndent = 0
+
+ token = state.push("blockquote_open", "blockquote", 1)
+ token.markup = ">"
+ token.map = lines = [startLine, 0]
+
+ state.md.block.tokenize(state, startLine, nextLine)
+
+ token = state.push("blockquote_close", "blockquote", -1)
+ token.markup = ">"
+
+ state.lineMax = oldLineMax
+ state.parentType = oldParentType
+ lines[1] = state.line
+
+ # Restore original tShift; this might not be necessary since the parser
+ # has already been here, but just to make sure we can do that.
+ for i, item in enumerate(oldTShift):
+ state.bMarks[i + startLine] = oldBMarks[i]
+ state.tShift[i + startLine] = item
+ state.sCount[i + startLine] = oldSCount[i]
+ state.bsCount[i + startLine] = oldBSCount[i]
+
+ state.blkIndent = oldIndent
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/code.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/code.py
new file mode 100644
index 0000000..af8a41c
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/code.py
@@ -0,0 +1,36 @@
+"""Code block (4 spaces padded)."""
+
+import logging
+
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def code(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering code: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ if not state.is_code_block(startLine):
+ return False
+
+ last = nextLine = startLine + 1
+
+ while nextLine < endLine:
+ if state.isEmpty(nextLine):
+ nextLine += 1
+ continue
+
+ if state.is_code_block(nextLine):
+ nextLine += 1
+ last = nextLine
+ continue
+
+ break
+
+ state.line = last
+
+ token = state.push("code_block", "code", 0)
+ token.content = state.getLines(startLine, last, 4 + state.blkIndent, False) + "\n"
+ token.map = [startLine, state.line]
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/fence.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/fence.py
new file mode 100644
index 0000000..263f1b8
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/fence.py
@@ -0,0 +1,101 @@
+# fences (``` lang, ~~~ lang)
+import logging
+
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def fence(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering fence: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ haveEndMarker = False
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ if state.is_code_block(startLine):
+ return False
+
+ if pos + 3 > maximum:
+ return False
+
+ marker = state.src[pos]
+
+ if marker not in ("~", "`"):
+ return False
+
+ # scan marker length
+ mem = pos
+ pos = state.skipCharsStr(pos, marker)
+
+ length = pos - mem
+
+ if length < 3:
+ return False
+
+ markup = state.src[mem:pos]
+ params = state.src[pos:maximum]
+
+ if marker == "`" and marker in params:
+ return False
+
+ # Since start is found, we can report success here in validation mode
+ if silent:
+ return True
+
+ # search end of block
+ nextLine = startLine
+
+ while True:
+ nextLine += 1
+ if nextLine >= endLine:
+ # unclosed block should be autoclosed by end of document.
+ # also block seems to be autoclosed by end of parent
+ break
+
+ pos = mem = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if pos < maximum and state.sCount[nextLine] < state.blkIndent:
+ # non-empty line with negative indent should stop the list:
+ # - ```
+ # test
+ break
+
+ try:
+ if state.src[pos] != marker:
+ continue
+ except IndexError:
+ break
+
+ if state.is_code_block(nextLine):
+ continue
+
+ pos = state.skipCharsStr(pos, marker)
+
+ # closing code fence must be at least as long as the opening one
+ if pos - mem < length:
+ continue
+
+ # make sure tail has spaces only
+ pos = state.skipSpaces(pos)
+
+ if pos < maximum:
+ continue
+
+ haveEndMarker = True
+ # found!
+ break
+
+ # If a fence has heading spaces, they should be removed from its inner block
+ length = state.sCount[startLine]
+
+ state.line = nextLine + (1 if haveEndMarker else 0)
+
+ token = state.push("fence", "code", 0)
+ token.info = params
+ token.content = state.getLines(startLine + 1, nextLine, length, True)
+ token.markup = markup
+ token.map = [startLine, state.line]
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/heading.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/heading.py
new file mode 100644
index 0000000..afcf9ed
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/heading.py
@@ -0,0 +1,69 @@
+"""Atex heading (#, ##, ...)"""
+
+from __future__ import annotations
+
+import logging
+
+from ..common.utils import isStrSpace
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def heading(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering heading: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ if state.is_code_block(startLine):
+ return False
+
+ ch: str | None = state.src[pos]
+
+ if ch != "#" or pos >= maximum:
+ return False
+
+ # count heading level
+ level = 1
+ pos += 1
+ try:
+ ch = state.src[pos]
+ except IndexError:
+ ch = None
+ while ch == "#" and pos < maximum and level <= 6:
+ level += 1
+ pos += 1
+ try:
+ ch = state.src[pos]
+ except IndexError:
+ ch = None
+
+ if level > 6 or (pos < maximum and not isStrSpace(ch)):
+ return False
+
+ if silent:
+ return True
+
+ # Let's cut tails like ' ### ' from the end of string
+
+ maximum = state.skipSpacesBack(maximum, pos)
+ tmp = state.skipCharsStrBack(maximum, "#", pos)
+ if tmp > pos and isStrSpace(state.src[tmp - 1]):
+ maximum = tmp
+
+ state.line = startLine + 1
+
+ token = state.push("heading_open", "h" + str(level), 1)
+ token.markup = "########"[:level]
+ token.map = [startLine, state.line]
+
+ token = state.push("inline", "", 0)
+ token.content = state.src[pos:maximum].strip()
+ token.map = [startLine, state.line]
+ token.children = []
+
+ token = state.push("heading_close", "h" + str(level), -1)
+ token.markup = "########"[:level]
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/hr.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/hr.py
new file mode 100644
index 0000000..fca7d79
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/hr.py
@@ -0,0 +1,56 @@
+"""Horizontal rule
+
+At least 3 of these characters on a line * - _
+"""
+
+import logging
+
+from ..common.utils import isStrSpace
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def hr(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering hr: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ if state.is_code_block(startLine):
+ return False
+
+ try:
+ marker = state.src[pos]
+ except IndexError:
+ return False
+ pos += 1
+
+ # Check hr marker
+ if marker not in ("*", "-", "_"):
+ return False
+
+ # markers can be mixed with spaces, but there should be at least 3 of them
+
+ cnt = 1
+ while pos < maximum:
+ ch = state.src[pos]
+ pos += 1
+ if ch != marker and not isStrSpace(ch):
+ return False
+ if ch == marker:
+ cnt += 1
+
+ if cnt < 3:
+ return False
+
+ if silent:
+ return True
+
+ state.line = startLine + 1
+
+ token = state.push("hr", "hr", 0)
+ token.map = [startLine, state.line]
+ token.markup = marker * (cnt + 1)
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/html_block.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/html_block.py
new file mode 100644
index 0000000..3d43f6e
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/html_block.py
@@ -0,0 +1,90 @@
+# HTML block
+from __future__ import annotations
+
+import logging
+import re
+
+from ..common.html_blocks import block_names
+from ..common.html_re import HTML_OPEN_CLOSE_TAG_STR
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+# An array of opening and corresponding closing sequences for html tags,
+# last argument defines whether it can terminate a paragraph or not
+HTML_SEQUENCES: list[tuple[re.Pattern[str], re.Pattern[str], bool]] = [
+ (
+ re.compile(r"^<(script|pre|style|textarea)(?=(\s|>|$))", re.IGNORECASE),
+ re.compile(r"<\/(script|pre|style|textarea)>", re.IGNORECASE),
+ True,
+ ),
+ (re.compile(r"^"), True),
+ (re.compile(r"^<\?"), re.compile(r"\?>"), True),
+ (re.compile(r"^"), True),
+ (re.compile(r"^"), True),
+ (
+ re.compile("^?(" + "|".join(block_names) + ")(?=(\\s|/?>|$))", re.IGNORECASE),
+ re.compile(r"^$"),
+ True,
+ ),
+ (re.compile(HTML_OPEN_CLOSE_TAG_STR + "\\s*$"), re.compile(r"^$"), False),
+]
+
+
+def html_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug(
+ "entering html_block: %s, %s, %s, %s", state, startLine, endLine, silent
+ )
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ if state.is_code_block(startLine):
+ return False
+
+ if not state.md.options.get("html", None):
+ return False
+
+ if state.src[pos] != "<":
+ return False
+
+ lineText = state.src[pos:maximum]
+
+ html_seq = None
+ for HTML_SEQUENCE in HTML_SEQUENCES:
+ if HTML_SEQUENCE[0].search(lineText):
+ html_seq = HTML_SEQUENCE
+ break
+
+ if not html_seq:
+ return False
+
+ if silent:
+ # true if this sequence can be a terminator, false otherwise
+ return html_seq[2]
+
+ nextLine = startLine + 1
+
+ # If we are here - we detected HTML block.
+ # Let's roll down till block end.
+ if not html_seq[1].search(lineText):
+ while nextLine < endLine:
+ if state.sCount[nextLine] < state.blkIndent:
+ break
+
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+ lineText = state.src[pos:maximum]
+
+ if html_seq[1].search(lineText):
+ if len(lineText) != 0:
+ nextLine += 1
+ break
+ nextLine += 1
+
+ state.line = nextLine
+
+ token = state.push("html_block", "", 0)
+ token.map = [startLine, nextLine]
+ token.content = state.getLines(startLine, nextLine, state.blkIndent, True)
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/lheading.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/lheading.py
new file mode 100644
index 0000000..3522207
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/lheading.py
@@ -0,0 +1,86 @@
+# lheading (---, ==)
+import logging
+
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def lheading(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering lheading: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ level = None
+ nextLine = startLine + 1
+ ruler = state.md.block.ruler
+ terminatorRules = ruler.getRules("paragraph")
+
+ if state.is_code_block(startLine):
+ return False
+
+ oldParentType = state.parentType
+ state.parentType = "paragraph" # use paragraph to match terminatorRules
+
+ # jump line-by-line until empty one or EOF
+ while nextLine < endLine and not state.isEmpty(nextLine):
+ # this would be a code block normally, but after paragraph
+ # it's considered a lazy continuation regardless of what's there
+ if state.sCount[nextLine] - state.blkIndent > 3:
+ nextLine += 1
+ continue
+
+ # Check for underline in setext header
+ if state.sCount[nextLine] >= state.blkIndent:
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ if pos < maximum:
+ marker = state.src[pos]
+
+ if marker in ("-", "="):
+ pos = state.skipCharsStr(pos, marker)
+ pos = state.skipSpaces(pos)
+
+ # /* = */
+ if pos >= maximum:
+ level = 1 if marker == "=" else 2
+ break
+
+ # quirk for blockquotes, this line should already be checked by that rule
+ if state.sCount[nextLine] < 0:
+ nextLine += 1
+ continue
+
+ # Some tags can terminate paragraph without empty line.
+ terminate = False
+ for terminatorRule in terminatorRules:
+ if terminatorRule(state, nextLine, endLine, True):
+ terminate = True
+ break
+ if terminate:
+ break
+
+ nextLine += 1
+
+ if not level:
+ # Didn't find valid underline
+ return False
+
+ content = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
+
+ state.line = nextLine + 1
+
+ token = state.push("heading_open", "h" + str(level), 1)
+ token.markup = marker
+ token.map = [startLine, state.line]
+
+ token = state.push("inline", "", 0)
+ token.content = content
+ token.map = [startLine, state.line - 1]
+ token.children = []
+
+ token = state.push("heading_close", "h" + str(level), -1)
+ token.markup = marker
+
+ state.parentType = oldParentType
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/list.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/list.py
new file mode 100644
index 0000000..d8070d7
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/list.py
@@ -0,0 +1,345 @@
+# Lists
+import logging
+
+from ..common.utils import isStrSpace
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+# Search `[-+*][\n ]`, returns next pos after marker on success
+# or -1 on fail.
+def skipBulletListMarker(state: StateBlock, startLine: int) -> int:
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+
+ try:
+ marker = state.src[pos]
+ except IndexError:
+ return -1
+ pos += 1
+
+ if marker not in ("*", "-", "+"):
+ return -1
+
+ if pos < maximum:
+ ch = state.src[pos]
+
+ if not isStrSpace(ch):
+ # " -test " - is not a list item
+ return -1
+
+ return pos
+
+
+# Search `\d+[.)][\n ]`, returns next pos after marker on success
+# or -1 on fail.
+def skipOrderedListMarker(state: StateBlock, startLine: int) -> int:
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ pos = start
+ maximum = state.eMarks[startLine]
+
+ # List marker should have at least 2 chars (digit + dot)
+ if pos + 1 >= maximum:
+ return -1
+
+ ch = state.src[pos]
+ pos += 1
+
+ ch_ord = ord(ch)
+ # /* 0 */ /* 9 */
+ if ch_ord < 0x30 or ch_ord > 0x39:
+ return -1
+
+ while True:
+ # EOL -> fail
+ if pos >= maximum:
+ return -1
+
+ ch = state.src[pos]
+ pos += 1
+
+ # /* 0 */ /* 9 */
+ ch_ord = ord(ch)
+ if ch_ord >= 0x30 and ch_ord <= 0x39:
+ # List marker should have no more than 9 digits
+ # (prevents integer overflow in browsers)
+ if pos - start >= 10:
+ return -1
+
+ continue
+
+ # found valid marker
+ if ch in (")", "."):
+ break
+
+ return -1
+
+ if pos < maximum:
+ ch = state.src[pos]
+
+ if not isStrSpace(ch):
+ # " 1.test " - is not a list item
+ return -1
+
+ return pos
+
+
+def markTightParagraphs(state: StateBlock, idx: int) -> None:
+ level = state.level + 2
+
+ i = idx + 2
+ length = len(state.tokens) - 2
+ while i < length:
+ if state.tokens[i].level == level and state.tokens[i].type == "paragraph_open":
+ state.tokens[i + 2].hidden = True
+ state.tokens[i].hidden = True
+ i += 2
+ i += 1
+
+
+def list_block(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug("entering list: %s, %s, %s, %s", state, startLine, endLine, silent)
+
+ isTerminatingParagraph = False
+ tight = True
+
+ if state.is_code_block(startLine):
+ return False
+
+ # Special case:
+ # - item 1
+ # - item 2
+ # - item 3
+ # - item 4
+ # - this one is a paragraph continuation
+ if (
+ state.listIndent >= 0
+ and state.sCount[startLine] - state.listIndent >= 4
+ and state.sCount[startLine] < state.blkIndent
+ ):
+ return False
+
+ # limit conditions when list can interrupt
+ # a paragraph (validation mode only)
+ # Next list item should still terminate previous list item
+ #
+ # This code can fail if plugins use blkIndent as well as lists,
+ # but I hope the spec gets fixed long before that happens.
+ #
+ if (
+ silent
+ and state.parentType == "paragraph"
+ and state.sCount[startLine] >= state.blkIndent
+ ):
+ isTerminatingParagraph = True
+
+ # Detect list type and position after marker
+ posAfterMarker = skipOrderedListMarker(state, startLine)
+ if posAfterMarker >= 0:
+ isOrdered = True
+ start = state.bMarks[startLine] + state.tShift[startLine]
+ markerValue = int(state.src[start : posAfterMarker - 1])
+
+ # If we're starting a new ordered list right after
+ # a paragraph, it should start with 1.
+ if isTerminatingParagraph and markerValue != 1:
+ return False
+ else:
+ posAfterMarker = skipBulletListMarker(state, startLine)
+ if posAfterMarker >= 0:
+ isOrdered = False
+ else:
+ return False
+
+ # If we're starting a new unordered list right after
+ # a paragraph, first line should not be empty.
+ if (
+ isTerminatingParagraph
+ and state.skipSpaces(posAfterMarker) >= state.eMarks[startLine]
+ ):
+ return False
+
+ # We should terminate list on style change. Remember first one to compare.
+ markerChar = state.src[posAfterMarker - 1]
+
+ # For validation mode we can terminate immediately
+ if silent:
+ return True
+
+ # Start list
+ listTokIdx = len(state.tokens)
+
+ if isOrdered:
+ token = state.push("ordered_list_open", "ol", 1)
+ if markerValue != 1:
+ token.attrs = {"start": markerValue}
+
+ else:
+ token = state.push("bullet_list_open", "ul", 1)
+
+ token.map = listLines = [startLine, 0]
+ token.markup = markerChar
+
+ #
+ # Iterate list items
+ #
+
+ nextLine = startLine
+ prevEmptyEnd = False
+ terminatorRules = state.md.block.ruler.getRules("list")
+
+ oldParentType = state.parentType
+ state.parentType = "list"
+
+ while nextLine < endLine:
+ pos = posAfterMarker
+ maximum = state.eMarks[nextLine]
+
+ initial = offset = (
+ state.sCount[nextLine]
+ + posAfterMarker
+ - (state.bMarks[startLine] + state.tShift[startLine])
+ )
+
+ while pos < maximum:
+ ch = state.src[pos]
+
+ if ch == "\t":
+ offset += 4 - (offset + state.bsCount[nextLine]) % 4
+ elif ch == " ":
+ offset += 1
+ else:
+ break
+
+ pos += 1
+
+ contentStart = pos
+
+ # trimming space in "- \n 3" case, indent is 1 here
+ indentAfterMarker = 1 if contentStart >= maximum else offset - initial
+
+ # If we have more than 4 spaces, the indent is 1
+ # (the rest is just indented code block)
+ if indentAfterMarker > 4:
+ indentAfterMarker = 1
+
+ # " - test"
+ # ^^^^^ - calculating total length of this thing
+ indent = initial + indentAfterMarker
+
+ # Run subparser & write tokens
+ token = state.push("list_item_open", "li", 1)
+ token.markup = markerChar
+ token.map = itemLines = [startLine, 0]
+ if isOrdered:
+ token.info = state.src[start : posAfterMarker - 1]
+
+ # change current state, then restore it after parser subcall
+ oldTight = state.tight
+ oldTShift = state.tShift[startLine]
+ oldSCount = state.sCount[startLine]
+
+ # - example list
+ # ^ listIndent position will be here
+ # ^ blkIndent position will be here
+ #
+ oldListIndent = state.listIndent
+ state.listIndent = state.blkIndent
+ state.blkIndent = indent
+
+ state.tight = True
+ state.tShift[startLine] = contentStart - state.bMarks[startLine]
+ state.sCount[startLine] = offset
+
+ if contentStart >= maximum and state.isEmpty(startLine + 1):
+ # workaround for this case
+ # (list item is empty, list terminates before "foo"):
+ # ~~~~~~~~
+ # -
+ #
+ # foo
+ # ~~~~~~~~
+ state.line = min(state.line + 2, endLine)
+ else:
+ # NOTE in list.js this was:
+ # state.md.block.tokenize(state, startLine, endLine, True)
+ # but tokeniz does not take the final parameter
+ state.md.block.tokenize(state, startLine, endLine)
+
+ # If any of list item is tight, mark list as tight
+ if (not state.tight) or prevEmptyEnd:
+ tight = False
+
+ # Item become loose if finish with empty line,
+ # but we should filter last element, because it means list finish
+ prevEmptyEnd = (state.line - startLine) > 1 and state.isEmpty(state.line - 1)
+
+ state.blkIndent = state.listIndent
+ state.listIndent = oldListIndent
+ state.tShift[startLine] = oldTShift
+ state.sCount[startLine] = oldSCount
+ state.tight = oldTight
+
+ token = state.push("list_item_close", "li", -1)
+ token.markup = markerChar
+
+ nextLine = startLine = state.line
+ itemLines[1] = nextLine
+
+ if nextLine >= endLine:
+ break
+
+ contentStart = state.bMarks[startLine]
+
+ #
+ # Try to check if list is terminated or continued.
+ #
+ if state.sCount[nextLine] < state.blkIndent:
+ break
+
+ if state.is_code_block(startLine):
+ break
+
+ # fail if terminating block found
+ terminate = False
+ for terminatorRule in terminatorRules:
+ if terminatorRule(state, nextLine, endLine, True):
+ terminate = True
+ break
+
+ if terminate:
+ break
+
+ # fail if list has another type
+ if isOrdered:
+ posAfterMarker = skipOrderedListMarker(state, nextLine)
+ if posAfterMarker < 0:
+ break
+ start = state.bMarks[nextLine] + state.tShift[nextLine]
+ else:
+ posAfterMarker = skipBulletListMarker(state, nextLine)
+ if posAfterMarker < 0:
+ break
+
+ if markerChar != state.src[posAfterMarker - 1]:
+ break
+
+ # Finalize list
+ if isOrdered:
+ token = state.push("ordered_list_close", "ol", -1)
+ else:
+ token = state.push("bullet_list_close", "ul", -1)
+
+ token.markup = markerChar
+
+ listLines[1] = nextLine
+ state.line = nextLine
+
+ state.parentType = oldParentType
+
+ # mark paragraphs tight if needed
+ if tight:
+ markTightParagraphs(state, listTokIdx)
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/paragraph.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/paragraph.py
new file mode 100644
index 0000000..30ba877
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/paragraph.py
@@ -0,0 +1,66 @@
+"""Paragraph."""
+
+import logging
+
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def paragraph(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ LOGGER.debug(
+ "entering paragraph: %s, %s, %s, %s", state, startLine, endLine, silent
+ )
+
+ nextLine = startLine + 1
+ ruler = state.md.block.ruler
+ terminatorRules = ruler.getRules("paragraph")
+ endLine = state.lineMax
+
+ oldParentType = state.parentType
+ state.parentType = "paragraph"
+
+ # jump line-by-line until empty one or EOF
+ while nextLine < endLine:
+ if state.isEmpty(nextLine):
+ break
+ # this would be a code block normally, but after paragraph
+ # it's considered a lazy continuation regardless of what's there
+ if state.sCount[nextLine] - state.blkIndent > 3:
+ nextLine += 1
+ continue
+
+ # quirk for blockquotes, this line should already be checked by that rule
+ if state.sCount[nextLine] < 0:
+ nextLine += 1
+ continue
+
+ # Some tags can terminate paragraph without empty line.
+ terminate = False
+ for terminatorRule in terminatorRules:
+ if terminatorRule(state, nextLine, endLine, True):
+ terminate = True
+ break
+
+ if terminate:
+ break
+
+ nextLine += 1
+
+ content = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
+
+ state.line = nextLine
+
+ token = state.push("paragraph_open", "p", 1)
+ token.map = [startLine, state.line]
+
+ token = state.push("inline", "", 0)
+ token.content = content
+ token.map = [startLine, state.line]
+ token.children = []
+
+ token = state.push("paragraph_close", "p", -1)
+
+ state.parentType = oldParentType
+
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/reference.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/reference.py
new file mode 100644
index 0000000..ad94d40
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/reference.py
@@ -0,0 +1,235 @@
+import logging
+
+from ..common.utils import charCodeAt, isSpace, normalizeReference
+from .state_block import StateBlock
+
+LOGGER = logging.getLogger(__name__)
+
+
+def reference(state: StateBlock, startLine: int, _endLine: int, silent: bool) -> bool:
+ LOGGER.debug(
+ "entering reference: %s, %s, %s, %s", state, startLine, _endLine, silent
+ )
+
+ pos = state.bMarks[startLine] + state.tShift[startLine]
+ maximum = state.eMarks[startLine]
+ nextLine = startLine + 1
+
+ if state.is_code_block(startLine):
+ return False
+
+ if state.src[pos] != "[":
+ return False
+
+ string = state.src[pos : maximum + 1]
+
+ # string = state.getLines(startLine, nextLine, state.blkIndent, False).strip()
+ maximum = len(string)
+
+ labelEnd = None
+ pos = 1
+ while pos < maximum:
+ ch = charCodeAt(string, pos)
+ if ch == 0x5B: # /* [ */
+ return False
+ elif ch == 0x5D: # /* ] */
+ labelEnd = pos
+ break
+ elif ch == 0x0A: # /* \n */
+ if (lineContent := getNextLine(state, nextLine)) is not None:
+ string += lineContent
+ maximum = len(string)
+ nextLine += 1
+ elif ch == 0x5C: # /* \ */
+ pos += 1
+ if (
+ pos < maximum
+ and charCodeAt(string, pos) == 0x0A
+ and (lineContent := getNextLine(state, nextLine)) is not None
+ ):
+ string += lineContent
+ maximum = len(string)
+ nextLine += 1
+ pos += 1
+
+ if (
+ labelEnd is None or labelEnd < 0 or charCodeAt(string, labelEnd + 1) != 0x3A
+ ): # /* : */
+ return False
+
+ # [label]: destination 'title'
+ # ^^^ skip optional whitespace here
+ pos = labelEnd + 2
+ while pos < maximum:
+ ch = charCodeAt(string, pos)
+ if ch == 0x0A:
+ if (lineContent := getNextLine(state, nextLine)) is not None:
+ string += lineContent
+ maximum = len(string)
+ nextLine += 1
+ elif isSpace(ch):
+ pass
+ else:
+ break
+ pos += 1
+
+ # [label]: destination 'title'
+ # ^^^^^^^^^^^ parse this
+ destRes = state.md.helpers.parseLinkDestination(string, pos, maximum)
+ if not destRes.ok:
+ return False
+
+ href = state.md.normalizeLink(destRes.str)
+ if not state.md.validateLink(href):
+ return False
+
+ pos = destRes.pos
+
+ # save cursor state, we could require to rollback later
+ destEndPos = pos
+ destEndLineNo = nextLine
+
+ # [label]: destination 'title'
+ # ^^^ skipping those spaces
+ start = pos
+ while pos < maximum:
+ ch = charCodeAt(string, pos)
+ if ch == 0x0A:
+ if (lineContent := getNextLine(state, nextLine)) is not None:
+ string += lineContent
+ maximum = len(string)
+ nextLine += 1
+ elif isSpace(ch):
+ pass
+ else:
+ break
+ pos += 1
+
+ # [label]: destination 'title'
+ # ^^^^^^^ parse this
+ titleRes = state.md.helpers.parseLinkTitle(string, pos, maximum, None)
+ while titleRes.can_continue:
+ if (lineContent := getNextLine(state, nextLine)) is None:
+ break
+ string += lineContent
+ pos = maximum
+ maximum = len(string)
+ nextLine += 1
+ titleRes = state.md.helpers.parseLinkTitle(string, pos, maximum, titleRes)
+
+ if pos < maximum and start != pos and titleRes.ok:
+ title = titleRes.str
+ pos = titleRes.pos
+ else:
+ title = ""
+ pos = destEndPos
+ nextLine = destEndLineNo
+
+ # skip trailing spaces until the rest of the line
+ while pos < maximum:
+ ch = charCodeAt(string, pos)
+ if not isSpace(ch):
+ break
+ pos += 1
+
+ if pos < maximum and charCodeAt(string, pos) != 0x0A and title:
+ # garbage at the end of the line after title,
+ # but it could still be a valid reference if we roll back
+ title = ""
+ pos = destEndPos
+ nextLine = destEndLineNo
+ while pos < maximum:
+ ch = charCodeAt(string, pos)
+ if not isSpace(ch):
+ break
+ pos += 1
+
+ if pos < maximum and charCodeAt(string, pos) != 0x0A:
+ # garbage at the end of the line
+ return False
+
+ label = normalizeReference(string[1:labelEnd])
+ if not label:
+ # CommonMark 0.20 disallows empty labels
+ return False
+
+ # Reference can not terminate anything. This check is for safety only.
+ if silent:
+ return True
+
+ if "references" not in state.env:
+ state.env["references"] = {}
+
+ state.line = nextLine
+
+ # note, this is not part of markdown-it JS, but is useful for renderers
+ if state.md.options.get("inline_definitions", False):
+ token = state.push("definition", "", 0)
+ token.meta = {
+ "id": label,
+ "title": title,
+ "url": href,
+ "label": string[1:labelEnd],
+ }
+ token.map = [startLine, state.line]
+
+ if label not in state.env["references"]:
+ state.env["references"][label] = {
+ "title": title,
+ "href": href,
+ "map": [startLine, state.line],
+ }
+ else:
+ state.env.setdefault("duplicate_refs", []).append(
+ {
+ "title": title,
+ "href": href,
+ "label": label,
+ "map": [startLine, state.line],
+ }
+ )
+
+ return True
+
+
+def getNextLine(state: StateBlock, nextLine: int) -> None | str:
+ endLine = state.lineMax
+
+ if nextLine >= endLine or state.isEmpty(nextLine):
+ # empty line or end of input
+ return None
+
+ isContinuation = False
+
+ # this would be a code block normally, but after paragraph
+ # it's considered a lazy continuation regardless of what's there
+ if state.is_code_block(nextLine):
+ isContinuation = True
+
+ # quirk for blockquotes, this line should already be checked by that rule
+ if state.sCount[nextLine] < 0:
+ isContinuation = True
+
+ if not isContinuation:
+ terminatorRules = state.md.block.ruler.getRules("reference")
+ oldParentType = state.parentType
+ state.parentType = "reference"
+
+ # Some tags can terminate paragraph without empty line.
+ terminate = False
+ for terminatorRule in terminatorRules:
+ if terminatorRule(state, nextLine, endLine, True):
+ terminate = True
+ break
+
+ state.parentType = oldParentType
+
+ if terminate:
+ # terminated by another block
+ return None
+
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ maximum = state.eMarks[nextLine]
+
+ # max + 1 explicitly includes the newline
+ return state.src[pos : maximum + 1]
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/state_block.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/state_block.py
new file mode 100644
index 0000000..445ad26
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/state_block.py
@@ -0,0 +1,261 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Literal
+
+from ..common.utils import isStrSpace
+from ..ruler import StateBase
+from ..token import Token
+from ..utils import EnvType
+
+if TYPE_CHECKING:
+ from markdown_it.main import MarkdownIt
+
+
+class StateBlock(StateBase):
+ def __init__(
+ self, src: str, md: MarkdownIt, env: EnvType, tokens: list[Token]
+ ) -> None:
+ self.src = src
+
+ # link to parser instance
+ self.md = md
+
+ self.env = env
+
+ #
+ # Internal state variables
+ #
+
+ self.tokens = tokens
+
+ self.bMarks: list[int] = [] # line begin offsets for fast jumps
+ self.eMarks: list[int] = [] # line end offsets for fast jumps
+ # offsets of the first non-space characters (tabs not expanded)
+ self.tShift: list[int] = []
+ self.sCount: list[int] = [] # indents for each line (tabs expanded)
+
+ # An amount of virtual spaces (tabs expanded) between beginning
+ # of each line (bMarks) and real beginning of that line.
+ #
+ # It exists only as a hack because blockquotes override bMarks
+ # losing information in the process.
+ #
+ # It's used only when expanding tabs, you can think about it as
+ # an initial tab length, e.g. bsCount=21 applied to string `\t123`
+ # means first tab should be expanded to 4-21%4 === 3 spaces.
+ #
+ self.bsCount: list[int] = []
+
+ # block parser variables
+ self.blkIndent = 0 # required block content indent (for example, if we are
+ # inside a list, it would be positioned after list marker)
+ self.line = 0 # line index in src
+ self.lineMax = 0 # lines count
+ self.tight = False # loose/tight mode for lists
+ self.ddIndent = -1 # indent of the current dd block (-1 if there isn't any)
+ self.listIndent = -1 # indent of the current list block (-1 if there isn't any)
+
+ # can be 'blockquote', 'list', 'root', 'paragraph' or 'reference'
+ # used in lists to determine if they interrupt a paragraph
+ self.parentType = "root"
+
+ self.level = 0
+
+ # renderer
+ self.result = ""
+
+ # Create caches
+ # Generate markers.
+ indent_found = False
+
+ start = pos = indent = offset = 0
+ length = len(self.src)
+
+ for pos, character in enumerate(self.src):
+ if not indent_found:
+ if isStrSpace(character):
+ indent += 1
+
+ if character == "\t":
+ offset += 4 - offset % 4
+ else:
+ offset += 1
+ continue
+ else:
+ indent_found = True
+
+ if character == "\n" or pos == length - 1:
+ if character != "\n":
+ pos += 1
+ self.bMarks.append(start)
+ self.eMarks.append(pos)
+ self.tShift.append(indent)
+ self.sCount.append(offset)
+ self.bsCount.append(0)
+
+ indent_found = False
+ indent = 0
+ offset = 0
+ start = pos + 1
+
+ # Push fake entry to simplify cache bounds checks
+ self.bMarks.append(length)
+ self.eMarks.append(length)
+ self.tShift.append(0)
+ self.sCount.append(0)
+ self.bsCount.append(0)
+
+ self.lineMax = len(self.bMarks) - 1 # don't count last fake line
+
+ # pre-check if code blocks are enabled, to speed up is_code_block method
+ self._code_enabled = "code" in self.md["block"].ruler.get_active_rules()
+
+ def __repr__(self) -> str:
+ return (
+ f"{self.__class__.__name__}"
+ f"(line={self.line},level={self.level},tokens={len(self.tokens)})"
+ )
+
+ def push(self, ttype: str, tag: str, nesting: Literal[-1, 0, 1]) -> Token:
+ """Push new token to "stream"."""
+ token = Token(ttype, tag, nesting)
+ token.block = True
+ if nesting < 0:
+ self.level -= 1 # closing tag
+ token.level = self.level
+ if nesting > 0:
+ self.level += 1 # opening tag
+ self.tokens.append(token)
+ return token
+
+ def isEmpty(self, line: int) -> bool:
+ """."""
+ return (self.bMarks[line] + self.tShift[line]) >= self.eMarks[line]
+
+ def skipEmptyLines(self, from_pos: int) -> int:
+ """."""
+ while from_pos < self.lineMax:
+ try:
+ if (self.bMarks[from_pos] + self.tShift[from_pos]) < self.eMarks[
+ from_pos
+ ]:
+ break
+ except IndexError:
+ pass
+ from_pos += 1
+ return from_pos
+
+ def skipSpaces(self, pos: int) -> int:
+ """Skip spaces from given position."""
+ while True:
+ try:
+ current = self.src[pos]
+ except IndexError:
+ break
+ if not isStrSpace(current):
+ break
+ pos += 1
+ return pos
+
+ def skipSpacesBack(self, pos: int, minimum: int) -> int:
+ """Skip spaces from given position in reverse."""
+ if pos <= minimum:
+ return pos
+ while pos > minimum:
+ pos -= 1
+ if not isStrSpace(self.src[pos]):
+ return pos + 1
+ return pos
+
+ def skipChars(self, pos: int, code: int) -> int:
+ """Skip character code from given position."""
+ while True:
+ try:
+ current = self.srcCharCode[pos]
+ except IndexError:
+ break
+ if current != code:
+ break
+ pos += 1
+ return pos
+
+ def skipCharsStr(self, pos: int, ch: str) -> int:
+ """Skip character string from given position."""
+ while True:
+ try:
+ current = self.src[pos]
+ except IndexError:
+ break
+ if current != ch:
+ break
+ pos += 1
+ return pos
+
+ def skipCharsBack(self, pos: int, code: int, minimum: int) -> int:
+ """Skip character code reverse from given position - 1."""
+ if pos <= minimum:
+ return pos
+ while pos > minimum:
+ pos -= 1
+ if code != self.srcCharCode[pos]:
+ return pos + 1
+ return pos
+
+ def skipCharsStrBack(self, pos: int, ch: str, minimum: int) -> int:
+ """Skip character string reverse from given position - 1."""
+ if pos <= minimum:
+ return pos
+ while pos > minimum:
+ pos -= 1
+ if ch != self.src[pos]:
+ return pos + 1
+ return pos
+
+ def getLines(self, begin: int, end: int, indent: int, keepLastLF: bool) -> str:
+ """Cut lines range from source."""
+ line = begin
+ if begin >= end:
+ return ""
+
+ queue = [""] * (end - begin)
+
+ i = 1
+ while line < end:
+ lineIndent = 0
+ lineStart = first = self.bMarks[line]
+ last = (
+ self.eMarks[line] + 1
+ if line + 1 < end or keepLastLF
+ else self.eMarks[line]
+ )
+
+ while (first < last) and (lineIndent < indent):
+ ch = self.src[first]
+ if isStrSpace(ch):
+ if ch == "\t":
+ lineIndent += 4 - (lineIndent + self.bsCount[line]) % 4
+ else:
+ lineIndent += 1
+ elif first - lineStart < self.tShift[line]:
+ lineIndent += 1
+ else:
+ break
+ first += 1
+
+ if lineIndent > indent:
+ # partially expanding tabs in code blocks, e.g '\t\tfoobar'
+ # with indent=2 becomes ' \tfoobar'
+ queue[i - 1] = (" " * (lineIndent - indent)) + self.src[first:last]
+ else:
+ queue[i - 1] = self.src[first:last]
+
+ line += 1
+ i += 1
+
+ return "".join(queue)
+
+ def is_code_block(self, line: int) -> bool:
+ """Check if line is a code block,
+ i.e. the code block rule is enabled and text is indented by more than 3 spaces.
+ """
+ return self._code_enabled and (self.sCount[line] - self.blkIndent) >= 4
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/table.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/table.py
new file mode 100644
index 0000000..c52553d
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_block/table.py
@@ -0,0 +1,250 @@
+# GFM table, https://github.github.com/gfm/#tables-extension-
+from __future__ import annotations
+
+import re
+
+from ..common.utils import charStrAt, isStrSpace
+from .state_block import StateBlock
+
+headerLineRe = re.compile(r"^:?-+:?$")
+enclosingPipesRe = re.compile(r"^\||\|$")
+
+# Limit the amount of empty autocompleted cells in a table,
+# see https://github.com/markdown-it/markdown-it/issues/1000,
+# Both pulldown-cmark and commonmark-hs limit the number of cells this way to ~200k.
+# We set it to 65k, which can expand user input by a factor of x370
+# (256x256 square is 1.8kB expanded into 650kB).
+MAX_AUTOCOMPLETED_CELLS = 0x10000
+
+
+def getLine(state: StateBlock, line: int) -> str:
+ pos = state.bMarks[line] + state.tShift[line]
+ maximum = state.eMarks[line]
+
+ # return state.src.substr(pos, max - pos)
+ return state.src[pos:maximum]
+
+
+def escapedSplit(string: str) -> list[str]:
+ result: list[str] = []
+ pos = 0
+ max = len(string)
+ isEscaped = False
+ lastPos = 0
+ current = ""
+ ch = charStrAt(string, pos)
+
+ while pos < max:
+ if ch == "|":
+ if not isEscaped:
+ # pipe separating cells, '|'
+ result.append(current + string[lastPos:pos])
+ current = ""
+ lastPos = pos + 1
+ else:
+ # escaped pipe, '\|'
+ current += string[lastPos : pos - 1]
+ lastPos = pos
+
+ isEscaped = ch == "\\"
+ pos += 1
+
+ ch = charStrAt(string, pos)
+
+ result.append(current + string[lastPos:])
+
+ return result
+
+
+def table(state: StateBlock, startLine: int, endLine: int, silent: bool) -> bool:
+ tbodyLines = None
+
+ # should have at least two lines
+ if startLine + 2 > endLine:
+ return False
+
+ nextLine = startLine + 1
+
+ if state.sCount[nextLine] < state.blkIndent:
+ return False
+
+ if state.is_code_block(nextLine):
+ return False
+
+ # first character of the second line should be '|', '-', ':',
+ # and no other characters are allowed but spaces;
+ # basically, this is the equivalent of /^[-:|][-:|\s]*$/ regexp
+
+ pos = state.bMarks[nextLine] + state.tShift[nextLine]
+ if pos >= state.eMarks[nextLine]:
+ return False
+ first_ch = state.src[pos]
+ pos += 1
+ if first_ch not in ("|", "-", ":"):
+ return False
+
+ if pos >= state.eMarks[nextLine]:
+ return False
+ second_ch = state.src[pos]
+ pos += 1
+ if second_ch not in ("|", "-", ":") and not isStrSpace(second_ch):
+ return False
+
+ # if first character is '-', then second character must not be a space
+ # (due to parsing ambiguity with list)
+ if first_ch == "-" and isStrSpace(second_ch):
+ return False
+
+ while pos < state.eMarks[nextLine]:
+ ch = state.src[pos]
+
+ if ch not in ("|", "-", ":") and not isStrSpace(ch):
+ return False
+
+ pos += 1
+
+ lineText = getLine(state, startLine + 1)
+
+ columns = lineText.split("|")
+ aligns = []
+ for i in range(len(columns)):
+ t = columns[i].strip()
+ if not t:
+ # allow empty columns before and after table, but not in between columns;
+ # e.g. allow ` |---| `, disallow ` ---||--- `
+ if i == 0 or i == len(columns) - 1:
+ continue
+ else:
+ return False
+
+ if not headerLineRe.search(t):
+ return False
+ if charStrAt(t, len(t) - 1) == ":":
+ aligns.append("center" if charStrAt(t, 0) == ":" else "right")
+ elif charStrAt(t, 0) == ":":
+ aligns.append("left")
+ else:
+ aligns.append("")
+
+ lineText = getLine(state, startLine).strip()
+ if "|" not in lineText:
+ return False
+ if state.is_code_block(startLine):
+ return False
+ columns = escapedSplit(lineText)
+ if columns and columns[0] == "":
+ columns.pop(0)
+ if columns and columns[-1] == "":
+ columns.pop()
+
+ # header row will define an amount of columns in the entire table,
+ # and align row should be exactly the same (the rest of the rows can differ)
+ columnCount = len(columns)
+ if columnCount == 0 or columnCount != len(aligns):
+ return False
+
+ if silent:
+ return True
+
+ oldParentType = state.parentType
+ state.parentType = "table"
+
+ # use 'blockquote' lists for termination because it's
+ # the most similar to tables
+ terminatorRules = state.md.block.ruler.getRules("blockquote")
+
+ token = state.push("table_open", "table", 1)
+ token.map = tableLines = [startLine, 0]
+
+ token = state.push("thead_open", "thead", 1)
+ token.map = [startLine, startLine + 1]
+
+ token = state.push("tr_open", "tr", 1)
+ token.map = [startLine, startLine + 1]
+
+ for i in range(len(columns)):
+ token = state.push("th_open", "th", 1)
+ if aligns[i]:
+ token.attrs = {"style": "text-align:" + aligns[i]}
+
+ token = state.push("inline", "", 0)
+ # note in markdown-it this map was removed in v12.0.0 however, we keep it,
+ # since it is helpful to propagate to children tokens
+ token.map = [startLine, startLine + 1]
+ token.content = columns[i].strip()
+ token.children = []
+
+ token = state.push("th_close", "th", -1)
+
+ token = state.push("tr_close", "tr", -1)
+ token = state.push("thead_close", "thead", -1)
+
+ autocompleted_cells = 0
+ nextLine = startLine + 2
+ while nextLine < endLine:
+ if state.sCount[nextLine] < state.blkIndent:
+ break
+
+ terminate = False
+ for i in range(len(terminatorRules)):
+ if terminatorRules[i](state, nextLine, endLine, True):
+ terminate = True
+ break
+
+ if terminate:
+ break
+ lineText = getLine(state, nextLine).strip()
+ if not lineText:
+ break
+ if state.is_code_block(nextLine):
+ break
+ columns = escapedSplit(lineText)
+ if columns and columns[0] == "":
+ columns.pop(0)
+ if columns and columns[-1] == "":
+ columns.pop()
+
+ # note: autocomplete count can be negative if user specifies more columns than header,
+ # but that does not affect intended use (which is limiting expansion)
+ autocompleted_cells += columnCount - len(columns)
+ if autocompleted_cells > MAX_AUTOCOMPLETED_CELLS:
+ break
+
+ if nextLine == startLine + 2:
+ token = state.push("tbody_open", "tbody", 1)
+ token.map = tbodyLines = [startLine + 2, 0]
+
+ token = state.push("tr_open", "tr", 1)
+ token.map = [nextLine, nextLine + 1]
+
+ for i in range(columnCount):
+ token = state.push("td_open", "td", 1)
+ if aligns[i]:
+ token.attrs = {"style": "text-align:" + aligns[i]}
+
+ token = state.push("inline", "", 0)
+ # note in markdown-it this map was removed in v12.0.0 however, we keep it,
+ # since it is helpful to propagate to children tokens
+ token.map = [nextLine, nextLine + 1]
+ try:
+ token.content = columns[i].strip() if columns[i] else ""
+ except IndexError:
+ token.content = ""
+ token.children = []
+
+ token = state.push("td_close", "td", -1)
+
+ token = state.push("tr_close", "tr", -1)
+
+ nextLine += 1
+
+ if tbodyLines:
+ token = state.push("tbody_close", "tbody", -1)
+ tbodyLines[1] = nextLine
+
+ token = state.push("table_close", "table", -1)
+
+ tableLines[1] = nextLine
+ state.parentType = oldParentType
+ state.line = nextLine
+ return True
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/__init__.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/__init__.py
new file mode 100644
index 0000000..e7d7753
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/__init__.py
@@ -0,0 +1,19 @@
+__all__ = (
+ "StateCore",
+ "block",
+ "inline",
+ "linkify",
+ "normalize",
+ "replace",
+ "smartquotes",
+ "text_join",
+)
+
+from .block import block
+from .inline import inline
+from .linkify import linkify
+from .normalize import normalize
+from .replacements import replace
+from .smartquotes import smartquotes
+from .state_core import StateCore
+from .text_join import text_join
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/block.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/block.py
new file mode 100644
index 0000000..a6c3bb8
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/block.py
@@ -0,0 +1,13 @@
+from ..token import Token
+from .state_core import StateCore
+
+
+def block(state: StateCore) -> None:
+ if state.inlineMode:
+ token = Token("inline", "", 0)
+ token.content = state.src
+ token.map = [0, 1]
+ token.children = []
+ state.tokens.append(token)
+ else:
+ state.md.block.parse(state.src, state.md, state.env, state.tokens)
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/inline.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/inline.py
new file mode 100644
index 0000000..c3fd0b5
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/inline.py
@@ -0,0 +1,10 @@
+from .state_core import StateCore
+
+
+def inline(state: StateCore) -> None:
+ """Parse inlines"""
+ for token in state.tokens:
+ if token.type == "inline":
+ if token.children is None:
+ token.children = []
+ state.md.inline.parse(token.content, state.md, state.env, token.children)
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/linkify.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/linkify.py
new file mode 100644
index 0000000..efbc9d4
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/linkify.py
@@ -0,0 +1,149 @@
+from __future__ import annotations
+
+import re
+from typing import Protocol
+
+from ..common.utils import arrayReplaceAt, isLinkClose, isLinkOpen
+from ..token import Token
+from .state_core import StateCore
+
+HTTP_RE = re.compile(r"^http://")
+MAILTO_RE = re.compile(r"^mailto:")
+TEST_MAILTO_RE = re.compile(r"^mailto:", flags=re.IGNORECASE)
+
+
+def linkify(state: StateCore) -> None:
+ """Rule for identifying plain-text links."""
+ if not state.md.options.linkify:
+ return
+
+ if not state.md.linkify:
+ raise ModuleNotFoundError("Linkify enabled but not installed.")
+
+ for inline_token in state.tokens:
+ if inline_token.type != "inline" or not state.md.linkify.pretest(
+ inline_token.content
+ ):
+ continue
+
+ tokens = inline_token.children
+
+ htmlLinkLevel = 0
+
+ # We scan from the end, to keep position when new tags added.
+ # Use reversed logic in links start/end match
+ assert tokens is not None
+ i = len(tokens)
+ while i >= 1:
+ i -= 1
+ assert isinstance(tokens, list)
+ currentToken = tokens[i]
+
+ # Skip content of markdown links
+ if currentToken.type == "link_close":
+ i -= 1
+ while (
+ tokens[i].level != currentToken.level
+ and tokens[i].type != "link_open"
+ ):
+ i -= 1
+ continue
+
+ # Skip content of html tag links
+ if currentToken.type == "html_inline":
+ if isLinkOpen(currentToken.content) and htmlLinkLevel > 0:
+ htmlLinkLevel -= 1
+ if isLinkClose(currentToken.content):
+ htmlLinkLevel += 1
+ if htmlLinkLevel > 0:
+ continue
+
+ if currentToken.type == "text" and state.md.linkify.test(
+ currentToken.content
+ ):
+ text = currentToken.content
+ links: list[_LinkType] = state.md.linkify.match(text) or []
+
+ # Now split string to nodes
+ nodes = []
+ level = currentToken.level
+ lastPos = 0
+
+ # forbid escape sequence at the start of the string,
+ # this avoids http\://example.com/ from being linkified as
+ # http://example.com/
+ if (
+ links
+ and links[0].index == 0
+ and i > 0
+ and tokens[i - 1].type == "text_special"
+ ):
+ links = links[1:]
+
+ for link in links:
+ url = link.url
+ fullUrl = state.md.normalizeLink(url)
+ if not state.md.validateLink(fullUrl):
+ continue
+
+ urlText = link.text
+
+ # Linkifier might send raw hostnames like "example.com", where url
+ # starts with domain name. So we prepend http:// in those cases,
+ # and remove it afterwards.
+ if not link.schema:
+ urlText = HTTP_RE.sub(
+ "", state.md.normalizeLinkText("http://" + urlText)
+ )
+ elif link.schema == "mailto:" and TEST_MAILTO_RE.search(urlText):
+ urlText = MAILTO_RE.sub(
+ "", state.md.normalizeLinkText("mailto:" + urlText)
+ )
+ else:
+ urlText = state.md.normalizeLinkText(urlText)
+
+ pos = link.index
+
+ if pos > lastPos:
+ token = Token("text", "", 0)
+ token.content = text[lastPos:pos]
+ token.level = level
+ nodes.append(token)
+
+ token = Token("link_open", "a", 1)
+ token.attrs = {"href": fullUrl}
+ token.level = level
+ level += 1
+ token.markup = "linkify"
+ token.info = "auto"
+ nodes.append(token)
+
+ token = Token("text", "", 0)
+ token.content = urlText
+ token.level = level
+ nodes.append(token)
+
+ token = Token("link_close", "a", -1)
+ level -= 1
+ token.level = level
+ token.markup = "linkify"
+ token.info = "auto"
+ nodes.append(token)
+
+ lastPos = link.last_index
+
+ if lastPos < len(text):
+ token = Token("text", "", 0)
+ token.content = text[lastPos:]
+ token.level = level
+ nodes.append(token)
+
+ inline_token.children = tokens = arrayReplaceAt(tokens, i, nodes)
+
+
+class _LinkType(Protocol):
+ url: str
+ text: str
+ index: int
+ last_index: int
+ schema: str | None
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/normalize.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/normalize.py
new file mode 100644
index 0000000..3243924
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/normalize.py
@@ -0,0 +1,19 @@
+"""Normalize input string."""
+
+import re
+
+from .state_core import StateCore
+
+# https://spec.commonmark.org/0.29/#line-ending
+NEWLINES_RE = re.compile(r"\r\n?|\n")
+NULL_RE = re.compile(r"\0")
+
+
+def normalize(state: StateCore) -> None:
+ # Normalize newlines
+ string = NEWLINES_RE.sub("\n", state.src)
+
+ # Replace NULL characters
+ string = NULL_RE.sub("\ufffd", string)
+
+ state.src = string
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/replacements.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/replacements.py
new file mode 100644
index 0000000..bcc9980
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/replacements.py
@@ -0,0 +1,127 @@
+"""Simple typographic replacements
+
+* ``(c)``, ``(C)`` → ©
+* ``(tm)``, ``(TM)`` → ™
+* ``(r)``, ``(R)`` → ®
+* ``+-`` → ±
+* ``...`` → …
+* ``?....`` → ?..
+* ``!....`` → !..
+* ``????????`` → ???
+* ``!!!!!`` → !!!
+* ``,,,`` → ,
+* ``--`` → &ndash
+* ``---`` → &mdash
+"""
+
+from __future__ import annotations
+
+import logging
+import re
+
+from ..token import Token
+from .state_core import StateCore
+
+LOGGER = logging.getLogger(__name__)
+
+# TODO:
+# - fractionals 1/2, 1/4, 3/4 -> ½, ¼, ¾
+# - multiplication 2 x 4 -> 2 × 4
+
+RARE_RE = re.compile(r"\+-|\.\.|\?\?\?\?|!!!!|,,|--")
+
+# Workaround for phantomjs - need regex without /g flag,
+# or root check will fail every second time
+# SCOPED_ABBR_TEST_RE = r"\((c|tm|r)\)"
+
+SCOPED_ABBR_RE = re.compile(r"\((c|tm|r)\)", flags=re.IGNORECASE)
+
+PLUS_MINUS_RE = re.compile(r"\+-")
+
+ELLIPSIS_RE = re.compile(r"\.{2,}")
+
+ELLIPSIS_QUESTION_EXCLAMATION_RE = re.compile(r"([?!])…")
+
+QUESTION_EXCLAMATION_RE = re.compile(r"([?!]){4,}")
+
+COMMA_RE = re.compile(r",{2,}")
+
+EM_DASH_RE = re.compile(r"(^|[^-])---(?=[^-]|$)", flags=re.MULTILINE)
+
+EN_DASH_RE = re.compile(r"(^|\s)--(?=\s|$)", flags=re.MULTILINE)
+
+EN_DASH_INDENT_RE = re.compile(r"(^|[^-\s])--(?=[^-\s]|$)", flags=re.MULTILINE)
+
+
+SCOPED_ABBR = {"c": "©", "r": "®", "tm": "™"}
+
+
+def replaceFn(match: re.Match[str]) -> str:
+ return SCOPED_ABBR[match.group(1).lower()]
+
+
+def replace_scoped(inlineTokens: list[Token]) -> None:
+ inside_autolink = 0
+
+ for token in inlineTokens:
+ if token.type == "text" and not inside_autolink:
+ token.content = SCOPED_ABBR_RE.sub(replaceFn, token.content)
+
+ if token.type == "link_open" and token.info == "auto":
+ inside_autolink -= 1
+
+ if token.type == "link_close" and token.info == "auto":
+ inside_autolink += 1
+
+
+def replace_rare(inlineTokens: list[Token]) -> None:
+ inside_autolink = 0
+
+ for token in inlineTokens:
+ if (
+ token.type == "text"
+ and (not inside_autolink)
+ and RARE_RE.search(token.content)
+ ):
+ # +- -> ±
+ token.content = PLUS_MINUS_RE.sub("±", token.content)
+
+ # .., ..., ....... -> …
+ token.content = ELLIPSIS_RE.sub("…", token.content)
+
+ # but ?..... & !..... -> ?.. & !..
+ token.content = ELLIPSIS_QUESTION_EXCLAMATION_RE.sub("\\1..", token.content)
+ token.content = QUESTION_EXCLAMATION_RE.sub("\\1\\1\\1", token.content)
+
+ # ,, ,,, ,,,, -> ,
+ token.content = COMMA_RE.sub(",", token.content)
+
+ # em-dash
+ token.content = EM_DASH_RE.sub("\\1\u2014", token.content)
+
+ # en-dash
+ token.content = EN_DASH_RE.sub("\\1\u2013", token.content)
+ token.content = EN_DASH_INDENT_RE.sub("\\1\u2013", token.content)
+
+ if token.type == "link_open" and token.info == "auto":
+ inside_autolink -= 1
+
+ if token.type == "link_close" and token.info == "auto":
+ inside_autolink += 1
+
+
+def replace(state: StateCore) -> None:
+ if not state.md.options.typographer:
+ return
+
+ for token in state.tokens:
+ if token.type != "inline":
+ continue
+ if token.children is None:
+ continue
+
+ if SCOPED_ABBR_RE.search(token.content):
+ replace_scoped(token.children)
+
+ if RARE_RE.search(token.content):
+ replace_rare(token.children)
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/smartquotes.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/smartquotes.py
new file mode 100644
index 0000000..f9b8b45
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/smartquotes.py
@@ -0,0 +1,202 @@
+"""Convert straight quotation marks to typographic ones"""
+
+from __future__ import annotations
+
+import re
+from typing import Any
+
+from ..common.utils import charCodeAt, isMdAsciiPunct, isPunctChar, isWhiteSpace
+from ..token import Token
+from .state_core import StateCore
+
+QUOTE_TEST_RE = re.compile(r"['\"]")
+QUOTE_RE = re.compile(r"['\"]")
+APOSTROPHE = "\u2019" # ’
+
+
+def replaceAt(string: str, index: int, ch: str) -> str:
+ # When the index is negative, the behavior is different from the js version.
+ # But basically, the index will not be negative.
+ assert index >= 0
+ return string[:index] + ch + string[index + 1 :]
+
+
+def process_inlines(tokens: list[Token], state: StateCore) -> None:
+ stack: list[dict[str, Any]] = []
+
+ for i, token in enumerate(tokens):
+ thisLevel = token.level
+
+ j = 0
+ for j in range(len(stack))[::-1]:
+ if stack[j]["level"] <= thisLevel:
+ break
+ else:
+ # When the loop is terminated without a "break".
+ # Subtract 1 to get the same index as the js version.
+ j -= 1
+
+ stack = stack[: j + 1]
+
+ if token.type != "text":
+ continue
+
+ text = token.content
+ pos = 0
+ maximum = len(text)
+
+ while pos < maximum:
+ goto_outer = False
+ lastIndex = pos
+ t = QUOTE_RE.search(text[lastIndex:])
+ if not t:
+ break
+
+ canOpen = canClose = True
+ pos = t.start(0) + lastIndex + 1
+ isSingle = t.group(0) == "'"
+
+ # Find previous character,
+ # default to space if it's the beginning of the line
+ lastChar: None | int = 0x20
+
+ if t.start(0) + lastIndex - 1 >= 0:
+ lastChar = charCodeAt(text, t.start(0) + lastIndex - 1)
+ else:
+ for j in range(i)[::-1]:
+ if tokens[j].type == "softbreak" or tokens[j].type == "hardbreak":
+ break
+ # should skip all tokens except 'text', 'html_inline' or 'code_inline'
+ if not tokens[j].content:
+ continue
+
+ lastChar = charCodeAt(tokens[j].content, len(tokens[j].content) - 1)
+ break
+
+ # Find next character,
+ # default to space if it's the end of the line
+ nextChar: None | int = 0x20
+
+ if pos < maximum:
+ nextChar = charCodeAt(text, pos)
+ else:
+ for j in range(i + 1, len(tokens)):
+ # nextChar defaults to 0x20
+ if tokens[j].type == "softbreak" or tokens[j].type == "hardbreak":
+ break
+ # should skip all tokens except 'text', 'html_inline' or 'code_inline'
+ if not tokens[j].content:
+ continue
+
+ nextChar = charCodeAt(tokens[j].content, 0)
+ break
+
+ isLastPunctChar = lastChar is not None and (
+ isMdAsciiPunct(lastChar) or isPunctChar(chr(lastChar))
+ )
+ isNextPunctChar = nextChar is not None and (
+ isMdAsciiPunct(nextChar) or isPunctChar(chr(nextChar))
+ )
+
+ isLastWhiteSpace = lastChar is not None and isWhiteSpace(lastChar)
+ isNextWhiteSpace = nextChar is not None and isWhiteSpace(nextChar)
+
+ if isNextWhiteSpace: # noqa: SIM114
+ canOpen = False
+ elif isNextPunctChar and not (isLastWhiteSpace or isLastPunctChar):
+ canOpen = False
+
+ if isLastWhiteSpace: # noqa: SIM114
+ canClose = False
+ elif isLastPunctChar and not (isNextWhiteSpace or isNextPunctChar):
+ canClose = False
+
+ if nextChar == 0x22 and t.group(0) == '"': # 0x22: " # noqa: SIM102
+ if (
+ lastChar is not None and lastChar >= 0x30 and lastChar <= 0x39
+ ): # 0x30: 0, 0x39: 9
+ # special case: 1"" - count first quote as an inch
+ canClose = canOpen = False
+
+ if canOpen and canClose:
+ # Replace quotes in the middle of punctuation sequence, but not
+ # in the middle of the words, i.e.:
+ #
+ # 1. foo " bar " baz - not replaced
+ # 2. foo-"-bar-"-baz - replaced
+ # 3. foo"bar"baz - not replaced
+ canOpen = isLastPunctChar
+ canClose = isNextPunctChar
+
+ if not canOpen and not canClose:
+ # middle of word
+ if isSingle:
+ token.content = replaceAt(
+ token.content, t.start(0) + lastIndex, APOSTROPHE
+ )
+ continue
+
+ if canClose:
+ # this could be a closing quote, rewind the stack to get a match
+ for j in range(len(stack))[::-1]:
+ item = stack[j]
+ if stack[j]["level"] < thisLevel:
+ break
+ if item["single"] == isSingle and stack[j]["level"] == thisLevel:
+ item = stack[j]
+
+ if isSingle:
+ openQuote = state.md.options.quotes[2]
+ closeQuote = state.md.options.quotes[3]
+ else:
+ openQuote = state.md.options.quotes[0]
+ closeQuote = state.md.options.quotes[1]
+
+ # replace token.content *before* tokens[item.token].content,
+ # because, if they are pointing at the same token, replaceAt
+ # could mess up indices when quote length != 1
+ token.content = replaceAt(
+ token.content, t.start(0) + lastIndex, closeQuote
+ )
+ tokens[item["token"]].content = replaceAt(
+ tokens[item["token"]].content, item["pos"], openQuote
+ )
+
+ pos += len(closeQuote) - 1
+ if item["token"] == i:
+ pos += len(openQuote) - 1
+
+ text = token.content
+ maximum = len(text)
+
+ stack = stack[:j]
+ goto_outer = True
+ break
+ if goto_outer:
+ goto_outer = False
+ continue
+
+ if canOpen:
+ stack.append(
+ {
+ "token": i,
+ "pos": t.start(0) + lastIndex,
+ "single": isSingle,
+ "level": thisLevel,
+ }
+ )
+ elif canClose and isSingle:
+ token.content = replaceAt(
+ token.content, t.start(0) + lastIndex, APOSTROPHE
+ )
+
+
+def smartquotes(state: StateCore) -> None:
+ if not state.md.options.typographer:
+ return
+
+ for token in state.tokens:
+ if token.type != "inline" or not QUOTE_RE.search(token.content):
+ continue
+ if token.children is not None:
+ process_inlines(token.children, state)
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/state_core.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/state_core.py
new file mode 100644
index 0000000..a938041
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/state_core.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+from ..ruler import StateBase
+from ..token import Token
+from ..utils import EnvType
+
+if TYPE_CHECKING:
+ from markdown_it import MarkdownIt
+
+
+class StateCore(StateBase):
+ def __init__(
+ self,
+ src: str,
+ md: MarkdownIt,
+ env: EnvType,
+ tokens: list[Token] | None = None,
+ ) -> None:
+ self.src = src
+ self.md = md # link to parser instance
+ self.env = env
+ self.tokens: list[Token] = tokens or []
+ self.inlineMode = False
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/text_join.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/text_join.py
new file mode 100644
index 0000000..5379f6d
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_core/text_join.py
@@ -0,0 +1,35 @@
+"""Join raw text tokens with the rest of the text
+
+This is set as a separate rule to provide an opportunity for plugins
+to run text replacements after text join, but before escape join.
+
+For example, `\\:)` shouldn't be replaced with an emoji.
+"""
+
+from __future__ import annotations
+
+from ..token import Token
+from .state_core import StateCore
+
+
+def text_join(state: StateCore) -> None:
+ """Join raw text for escape sequences (`text_special`) tokens with the rest of the text"""
+
+ for inline_token in state.tokens[:]:
+ if inline_token.type != "inline":
+ continue
+
+ # convert text_special to text and join all adjacent text nodes
+ new_tokens: list[Token] = []
+ for child_token in inline_token.children or []:
+ if child_token.type == "text_special":
+ child_token.type = "text"
+ if (
+ child_token.type == "text"
+ and new_tokens
+ and new_tokens[-1].type == "text"
+ ):
+ new_tokens[-1].content += child_token.content
+ else:
+ new_tokens.append(child_token)
+ inline_token.children = new_tokens
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/__init__.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/__init__.py
new file mode 100644
index 0000000..d82ef8f
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/__init__.py
@@ -0,0 +1,31 @@
+__all__ = (
+ "StateInline",
+ "autolink",
+ "backtick",
+ "emphasis",
+ "entity",
+ "escape",
+ "fragments_join",
+ "html_inline",
+ "image",
+ "link",
+ "link_pairs",
+ "linkify",
+ "newline",
+ "strikethrough",
+ "text",
+)
+from . import emphasis, strikethrough
+from .autolink import autolink
+from .backticks import backtick
+from .balance_pairs import link_pairs
+from .entity import entity
+from .escape import escape
+from .fragments_join import fragments_join
+from .html_inline import html_inline
+from .image import image
+from .link import link
+from .linkify import linkify
+from .newline import newline
+from .state_inline import StateInline
+from .text import text
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/autolink.py b/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/autolink.py
new file mode 100644
index 0000000..6546e25
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it/rules_inline/autolink.py
@@ -0,0 +1,77 @@
+# Process autolinks '
)."""
+ breaks: bool
+ """Convert newlines in paragraphs into
."""
+ langPrefix: str
+ """CSS language prefix for fenced blocks."""
+ highlight: Callable[[str, str, str], str] | None
+ """Highlighter function: (content, lang, attrs) -> str."""
+ store_labels: NotRequired[bool]
+ """Store link label in link/image token's metadata (under Token.meta['label']).
+
+ This is a Python only option, and is intended for the use of round-trip parsing.
+ """
+
+
+class PresetType(TypedDict):
+ """Preset configuration for markdown-it."""
+
+ options: OptionsType
+ """Options for parsing."""
+ components: MutableMapping[str, MutableMapping[str, list[str]]]
+ """Components for parsing and rendering."""
+
+
+class OptionsDict(MutableMappingABC): # type: ignore
+ """A dictionary, with attribute access to core markdownit configuration options."""
+
+ # Note: ideally we would probably just remove attribute access entirely,
+ # but we keep it for backwards compatibility.
+
+ def __init__(self, options: OptionsType) -> None:
+ self._options = cast(OptionsType, dict(options))
+
+ def __getitem__(self, key: str) -> Any:
+ return self._options[key] # type: ignore[literal-required]
+
+ def __setitem__(self, key: str, value: Any) -> None:
+ self._options[key] = value # type: ignore[literal-required]
+
+ def __delitem__(self, key: str) -> None:
+ del self._options[key] # type: ignore
+
+ def __iter__(self) -> Iterable[str]: # type: ignore
+ return iter(self._options)
+
+ def __len__(self) -> int:
+ return len(self._options)
+
+ def __repr__(self) -> str:
+ return repr(self._options)
+
+ def __str__(self) -> str:
+ return str(self._options)
+
+ @property
+ def maxNesting(self) -> int:
+ """Internal protection, recursion limit."""
+ return self._options["maxNesting"]
+
+ @maxNesting.setter
+ def maxNesting(self, value: int) -> None:
+ self._options["maxNesting"] = value
+
+ @property
+ def html(self) -> bool:
+ """Enable HTML tags in source."""
+ return self._options["html"]
+
+ @html.setter
+ def html(self, value: bool) -> None:
+ self._options["html"] = value
+
+ @property
+ def linkify(self) -> bool:
+ """Enable autoconversion of URL-like texts to links."""
+ return self._options["linkify"]
+
+ @linkify.setter
+ def linkify(self, value: bool) -> None:
+ self._options["linkify"] = value
+
+ @property
+ def typographer(self) -> bool:
+ """Enable smartquotes and replacements."""
+ return self._options["typographer"]
+
+ @typographer.setter
+ def typographer(self, value: bool) -> None:
+ self._options["typographer"] = value
+
+ @property
+ def quotes(self) -> str:
+ """Quote characters."""
+ return self._options["quotes"]
+
+ @quotes.setter
+ def quotes(self, value: str) -> None:
+ self._options["quotes"] = value
+
+ @property
+ def xhtmlOut(self) -> bool:
+ """Use '/' to close single tags (
)."""
+ return self._options["xhtmlOut"]
+
+ @xhtmlOut.setter
+ def xhtmlOut(self, value: bool) -> None:
+ self._options["xhtmlOut"] = value
+
+ @property
+ def breaks(self) -> bool:
+ """Convert newlines in paragraphs into
."""
+ return self._options["breaks"]
+
+ @breaks.setter
+ def breaks(self, value: bool) -> None:
+ self._options["breaks"] = value
+
+ @property
+ def langPrefix(self) -> str:
+ """CSS language prefix for fenced blocks."""
+ return self._options["langPrefix"]
+
+ @langPrefix.setter
+ def langPrefix(self, value: str) -> None:
+ self._options["langPrefix"] = value
+
+ @property
+ def highlight(self) -> Callable[[str, str, str], str] | None:
+ """Highlighter function: (content, langName, langAttrs) -> escaped HTML."""
+ return self._options["highlight"]
+
+ @highlight.setter
+ def highlight(self, value: Callable[[str, str, str], str] | None) -> None:
+ self._options["highlight"] = value
+
+
+def read_fixture_file(path: str | Path) -> list[list[Any]]:
+ text = Path(path).read_text(encoding="utf-8")
+ tests = []
+ section = 0
+ last_pos = 0
+ lines = text.splitlines(keepends=True)
+ for i in range(len(lines)):
+ if lines[i].rstrip() == ".":
+ if section == 0:
+ tests.append([i, lines[i - 1].strip()])
+ section = 1
+ elif section == 1:
+ tests[-1].append("".join(lines[last_pos + 1 : i]))
+ section = 2
+ elif section == 2:
+ tests[-1].append("".join(lines[last_pos + 1 : i]))
+ section = 0
+
+ last_pos = i
+ return tests
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/INSTALLER b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/METADATA b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/METADATA
new file mode 100644
index 0000000..0f2b466
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/METADATA
@@ -0,0 +1,219 @@
+Metadata-Version: 2.4
+Name: markdown-it-py
+Version: 4.0.0
+Summary: Python port of markdown-it. Markdown parsing, done right!
+Keywords: markdown,lexer,parser,commonmark,markdown-it
+Author-email: Chris Sewell
+
Example
+
+
+
+Batch:
+
+ $ markdown-it README.md README.footer.md > index.html
+
+```
+
+## References / Thanks
+
+Big thanks to the authors of [markdown-it]:
+
+- Alex Kocharin [github/rlidwka](https://github.com/rlidwka)
+- Vitaly Puzrin [github/puzrin](https://github.com/puzrin)
+
+Also [John MacFarlane](https://github.com/jgm) for his work on the CommonMark spec and reference implementations.
+
+[github-ci]: https://github.com/executablebooks/markdown-it-py/actions/workflows/tests.yml/badge.svg?branch=master
+[github-link]: https://github.com/executablebooks/markdown-it-py
+[pypi-badge]: https://img.shields.io/pypi/v/markdown-it-py.svg
+[pypi-link]: https://pypi.org/project/markdown-it-py
+[conda-badge]: https://anaconda.org/conda-forge/markdown-it-py/badges/version.svg
+[conda-link]: https://anaconda.org/conda-forge/markdown-it-py
+[codecov-badge]: https://codecov.io/gh/executablebooks/markdown-it-py/branch/master/graph/badge.svg
+[codecov-link]: https://codecov.io/gh/executablebooks/markdown-it-py
+[install-badge]: https://img.shields.io/pypi/dw/markdown-it-py?label=pypi%20installs
+[install-link]: https://pypistats.org/packages/markdown-it-py
+
+[CommonMark spec]: http://spec.commonmark.org/
+[markdown-it]: https://github.com/markdown-it/markdown-it
+[markdown-it-readme]: https://github.com/markdown-it/markdown-it/blob/master/README.md
+[md-security]: https://markdown-it-py.readthedocs.io/en/latest/security.html
+[md-performance]: https://markdown-it-py.readthedocs.io/en/latest/performance.html
+[md-plugins]: https://markdown-it-py.readthedocs.io/en/latest/plugins.html
+
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/RECORD b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/RECORD
new file mode 100644
index 0000000..b28e7b4
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/RECORD
@@ -0,0 +1,142 @@
+../../../bin/markdown-it,sha256=qdnHxdU2bJo4LPAcjX834flA4-GPGTK_PQhpB_7xLCM,215
+markdown_it/__init__.py,sha256=R7fMvDxageYJ4Q6doBcimogy1ctcV1eBuCFu5Pr8bbA,114
+markdown_it/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/__pycache__/_compat.cpython-311.pyc,,
+markdown_it/__pycache__/_punycode.cpython-311.pyc,,
+markdown_it/__pycache__/main.cpython-311.pyc,,
+markdown_it/__pycache__/parser_block.cpython-311.pyc,,
+markdown_it/__pycache__/parser_core.cpython-311.pyc,,
+markdown_it/__pycache__/parser_inline.cpython-311.pyc,,
+markdown_it/__pycache__/renderer.cpython-311.pyc,,
+markdown_it/__pycache__/ruler.cpython-311.pyc,,
+markdown_it/__pycache__/token.cpython-311.pyc,,
+markdown_it/__pycache__/tree.cpython-311.pyc,,
+markdown_it/__pycache__/utils.cpython-311.pyc,,
+markdown_it/_compat.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
+markdown_it/_punycode.py,sha256=JvSOZJ4VKr58z7unFGM0KhfTxqHMk2w8gglxae2QszM,2373
+markdown_it/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+markdown_it/cli/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/cli/__pycache__/parse.cpython-311.pyc,,
+markdown_it/cli/parse.py,sha256=Un3N7fyGHhZAQouGVnRx-WZcpKwEK2OF08rzVAEBie8,2881
+markdown_it/common/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+markdown_it/common/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/common/__pycache__/entities.cpython-311.pyc,,
+markdown_it/common/__pycache__/html_blocks.cpython-311.pyc,,
+markdown_it/common/__pycache__/html_re.cpython-311.pyc,,
+markdown_it/common/__pycache__/normalize_url.cpython-311.pyc,,
+markdown_it/common/__pycache__/utils.cpython-311.pyc,,
+markdown_it/common/entities.py,sha256=EYRCmUL7ZU1FRGLSXQlPx356lY8EUBdFyx96eSGc6d0,157
+markdown_it/common/html_blocks.py,sha256=QXbUDMoN9lXLgYFk2DBYllnLiFukL6dHn2X98Y6Wews,986
+markdown_it/common/html_re.py,sha256=FggAEv9IL8gHQqsGTkHcf333rTojwG0DQJMH9oVu0fU,926
+markdown_it/common/normalize_url.py,sha256=avOXnLd9xw5jU1q5PLftjAM9pvGx8l9QDEkmZSyrMgg,2568
+markdown_it/common/utils.py,sha256=pMgvMOE3ZW-BdJ7HfuzlXNKyD1Ivk7jHErc2J_B8J5M,8734
+markdown_it/helpers/__init__.py,sha256=YH2z7dS0WUc_9l51MWPvrLtFoBPh4JLGw58OuhGRCK0,253
+markdown_it/helpers/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/helpers/__pycache__/parse_link_destination.cpython-311.pyc,,
+markdown_it/helpers/__pycache__/parse_link_label.cpython-311.pyc,,
+markdown_it/helpers/__pycache__/parse_link_title.cpython-311.pyc,,
+markdown_it/helpers/parse_link_destination.py,sha256=u-xxWVP3g1s7C1bQuQItiYyDrYoYHJzXaZXPgr-o6mY,1906
+markdown_it/helpers/parse_link_label.py,sha256=PIHG6ZMm3BUw0a2m17lCGqNrl3vaz911tuoGviWD3I4,1037
+markdown_it/helpers/parse_link_title.py,sha256=jkLoYQMKNeX9bvWQHkaSroiEo27HylkEUNmj8xBRlp4,2273
+markdown_it/main.py,sha256=vzuT23LJyKrPKNyHKKAbOHkNWpwIldOGUM-IGsv2DHM,12732
+markdown_it/parser_block.py,sha256=-MyugXB63Te71s4NcSQZiK5bE6BHkdFyZv_bviuatdI,3939
+markdown_it/parser_core.py,sha256=SRmJjqe8dC6GWzEARpWba59cBmxjCr3Gsg8h29O8sQk,1016
+markdown_it/parser_inline.py,sha256=y0jCig8CJxQO7hBz0ZY3sGvPlAKTohOwIgaqnlSaS5A,5024
+markdown_it/port.yaml,sha256=jt_rdwOnfocOV5nc35revTybAAQMIp_-1fla_527sVE,2447
+markdown_it/presets/__init__.py,sha256=22vFtwJEY7iqFRtgVZ-pJthcetfpr1Oig8XOF9x1328,970
+markdown_it/presets/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/presets/__pycache__/commonmark.cpython-311.pyc,,
+markdown_it/presets/__pycache__/default.cpython-311.pyc,,
+markdown_it/presets/__pycache__/zero.cpython-311.pyc,,
+markdown_it/presets/commonmark.py,sha256=ygfb0R7WQ_ZoyQP3df-B0EnYMqNXCVOSw9SAdMjsGow,2869
+markdown_it/presets/default.py,sha256=FfKVUI0HH3M-_qy6RwotLStdC4PAaAxE7Dq0_KQtRtc,1811
+markdown_it/presets/zero.py,sha256=okXWTBEI-2nmwx5XKeCjxInRf65oC11gahtRl-QNtHM,2113
+markdown_it/py.typed,sha256=8PjyZ1aVoQpRVvt71muvuq5qE-jTFZkK-GLHkhdebmc,26
+markdown_it/renderer.py,sha256=Lzr0glqd5oxFL10DOfjjW8kg4Gp41idQ4viEQaE47oA,9947
+markdown_it/ruler.py,sha256=eMAtWGRAfSM33aiJed0k5923BEkuMVsMq1ct8vU-ql4,9142
+markdown_it/rules_block/__init__.py,sha256=SQpg0ocmsHeILPAWRHhzgLgJMKIcNkQyELH13o_6Ktc,553
+markdown_it/rules_block/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/blockquote.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/code.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/fence.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/heading.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/hr.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/html_block.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/lheading.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/list.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/paragraph.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/reference.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/state_block.cpython-311.pyc,,
+markdown_it/rules_block/__pycache__/table.cpython-311.pyc,,
+markdown_it/rules_block/blockquote.py,sha256=7uymS36dcrned3DsIaRcqcbFU1NlymhvsZpEXTD3_n8,8887
+markdown_it/rules_block/code.py,sha256=iTAxv0U1-MDhz88M1m1pi2vzOhEMSEROsXMo2Qq--kU,860
+markdown_it/rules_block/fence.py,sha256=BJgU-PqZ4vAlCqGcrc8UtdLpJJyMeRWN-G-Op-zxrMc,2537
+markdown_it/rules_block/heading.py,sha256=4Lh15rwoVsQjE1hVhpbhidQ0k9xKHihgjAeYSbwgO5k,1745
+markdown_it/rules_block/hr.py,sha256=QCoY5kImaQRvF7PyP8OoWft6A8JVH1v6MN-0HR9Ikpg,1227
+markdown_it/rules_block/html_block.py,sha256=wA8pb34LtZr1BkIATgGKQBIGX5jQNOkwZl9UGEqvb5M,2721
+markdown_it/rules_block/lheading.py,sha256=fWoEuUo7S2svr5UMKmyQMkh0hheYAHg2gMM266Mogs4,2625
+markdown_it/rules_block/list.py,sha256=gIodkAJFyOIyKCZCj5lAlL7jIj5kAzrDb-K-2MFNplY,9668
+markdown_it/rules_block/paragraph.py,sha256=9pmCwA7eMu4LBdV4fWKzC4EdwaOoaGw2kfeYSQiLye8,1819
+markdown_it/rules_block/reference.py,sha256=ue1qZbUaUP0GIvwTjh6nD1UtCij8uwsIMuYW1xBkckc,6983
+markdown_it/rules_block/state_block.py,sha256=HowsQyy5hGUibH4HRZWKfLIlXeDUnuWL7kpF0-rSwoM,8422
+markdown_it/rules_block/table.py,sha256=8nMd9ONGOffER7BXmc9kbbhxkLjtpX79dVLR0iatGnM,7682
+markdown_it/rules_core/__init__.py,sha256=QFGBe9TUjnRQJDU7xY4SQYpxyTHNwg8beTSwXpNGRjE,394
+markdown_it/rules_core/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/block.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/inline.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/linkify.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/normalize.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/replacements.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/smartquotes.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/state_core.cpython-311.pyc,,
+markdown_it/rules_core/__pycache__/text_join.cpython-311.pyc,,
+markdown_it/rules_core/block.py,sha256=0_JY1CUy-H2OooFtIEZAACtuoGUMohgxo4Z6A_UinSg,372
+markdown_it/rules_core/inline.py,sha256=9oWmeBhJHE7x47oJcN9yp6UsAZtrEY_A-VmfoMvKld4,325
+markdown_it/rules_core/linkify.py,sha256=mjQqpk_lHLh2Nxw4UFaLxa47Fgi-OHnmDamlgXnhmv0,5141
+markdown_it/rules_core/normalize.py,sha256=AJm4femtFJ_QBnM0dzh0UNqTTJk9K6KMtwRPaioZFqM,403
+markdown_it/rules_core/replacements.py,sha256=CH75mie-tdzdLKQtMBuCTcXAl1ijegdZGfbV_Vk7st0,3471
+markdown_it/rules_core/smartquotes.py,sha256=izK9fSyuTzA-zAUGkRkz9KwwCQWo40iRqcCKqOhFbEE,7443
+markdown_it/rules_core/state_core.py,sha256=HqWZCUr5fW7xG6jeQZDdO0hE9hxxyl3_-bawgOy57HY,570
+markdown_it/rules_core/text_join.py,sha256=rLXxNuLh_es5RvH31GsXi7en8bMNO9UJ5nbJMDBPltY,1173
+markdown_it/rules_inline/__init__.py,sha256=qqHZk6-YE8Rc12q6PxvVKBaxv2wmZeeo45H1XMR_Vxs,696
+markdown_it/rules_inline/__pycache__/__init__.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/autolink.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/backticks.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/balance_pairs.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/emphasis.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/entity.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/escape.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/fragments_join.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/html_inline.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/image.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/link.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/linkify.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/newline.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/state_inline.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/strikethrough.cpython-311.pyc,,
+markdown_it/rules_inline/__pycache__/text.cpython-311.pyc,,
+markdown_it/rules_inline/autolink.py,sha256=pPoqJY8i99VtFn7KgUzMackMeq1hytzioVvWs-VQPRo,2065
+markdown_it/rules_inline/backticks.py,sha256=J7bezjjNxiXlKqvHc0fJkHZwH7-2nBsXVjcKydk8E4M,2037
+markdown_it/rules_inline/balance_pairs.py,sha256=5zgBiGidqdiWmt7Io_cuZOYh5EFEfXrYRce8RXg5m7o,4852
+markdown_it/rules_inline/emphasis.py,sha256=7aDLZx0Jlekuvbu3uEUTDhJp00Z0Pj6g4C3-VLhI8Co,3123
+markdown_it/rules_inline/entity.py,sha256=CE8AIGMi5isEa24RNseo0wRmTTaj5YLbgTFdDmBesAU,1651
+markdown_it/rules_inline/escape.py,sha256=KGulwrP5FnqZM7GXY8lf7pyVv0YkR59taZDeHb5cmKg,1659
+markdown_it/rules_inline/fragments_join.py,sha256=_3JbwWYJz74gRHeZk6T8edVJT2IVSsi7FfmJJlieQlA,1493
+markdown_it/rules_inline/html_inline.py,sha256=SBg6HR0HRqCdrkkec0dfOYuQdAqyfeLRFLeQggtgjvg,1130
+markdown_it/rules_inline/image.py,sha256=Wbsg7jgnOtKXIwXGNJOlG7ORThkMkBVolxItC0ph6C0,4141
+markdown_it/rules_inline/link.py,sha256=2oD-fAdB0xyxDRtZLTjzLeWbzJ1k9bbPVQmohb58RuI,4258
+markdown_it/rules_inline/linkify.py,sha256=ifH6sb5wE8PGMWEw9Sr4x0DhMVfNOEBCfFSwKll2O-s,1706
+markdown_it/rules_inline/newline.py,sha256=329r0V3aDjzNtJcvzA3lsFYjzgBrShLAV5uf9hwQL_M,1297
+markdown_it/rules_inline/state_inline.py,sha256=d-menFzbz5FDy1JNgGBF-BASasnVI-9RuOxWz9PnKn4,5003
+markdown_it/rules_inline/strikethrough.py,sha256=pwcPlyhkh5pqFVxRCSrdW5dNCIOtU4eDit7TVDTPIVA,3214
+markdown_it/rules_inline/text.py,sha256=FQqaQRUqbnMLO9ZSWPWQUMEKH6JqWSSSmlZ5Ii9P48o,1119
+markdown_it/token.py,sha256=cWrt9kodfPdizHq_tYrzyIZNtJYNMN1813DPNlunwTg,6381
+markdown_it/tree.py,sha256=56Cdbwu2Aiks7kNYqO_fQZWpPb_n48CUllzjQQfgu1Y,11111
+markdown_it/utils.py,sha256=lVLeX7Af3GaNFfxmMgUbsn5p7cXbwhLq7RSf56UWuRE,5687
+markdown_it_py-4.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+markdown_it_py-4.0.0.dist-info/METADATA,sha256=6fyqHi2vP5bYQKCfuqo5T-qt83o22Ip7a2tnJIfGW_s,7288
+markdown_it_py-4.0.0.dist-info/RECORD,,
+markdown_it_py-4.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
+markdown_it_py-4.0.0.dist-info/entry_points.txt,sha256=T81l7fHQ3pllpQ4wUtQK6a8g_p6wxQbnjKVHCk2WMG4,58
+markdown_it_py-4.0.0.dist-info/licenses/LICENSE,sha256=SiJg1uLND1oVGh6G2_59PtVSseK-q_mUHBulxJy85IQ,1078
+markdown_it_py-4.0.0.dist-info/licenses/LICENSE.markdown-it,sha256=eSxIxahJoV_fnjfovPnm0d0TsytGxkKnSKCkapkZ1HM,1073
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/WHEEL b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/WHEEL
new file mode 100644
index 0000000..d8b9936
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: flit 3.12.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/entry_points.txt b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/entry_points.txt
new file mode 100644
index 0000000..7d829cd
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/entry_points.txt
@@ -0,0 +1,3 @@
+[console_scripts]
+markdown-it=markdown_it.cli.parse:main
+
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE
new file mode 100644
index 0000000..582ddf5
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 ExecutableBookProject
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE.markdown-it b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE.markdown-it
new file mode 100644
index 0000000..7ffa058
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/markdown_it_py-4.0.0.dist-info/licenses/LICENSE.markdown-it
@@ -0,0 +1,22 @@
+Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin.
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/INSTALLER b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/INSTALLER
new file mode 100644
index 0000000..a1b589e
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/LICENSE b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/LICENSE
new file mode 100644
index 0000000..2a920c5
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/LICENSE
@@ -0,0 +1,46 @@
+Copyright (c) 2015 Vitaly Puzrin, Alex Kocharin.
+Copyright (c) 2021 Taneli Hukkinen
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
+
+--------------------------------------------------------------------------------
+
+.parse() is based on Joyent's node.js `url` code:
+
+Copyright Joyent, Inc. and other Node contributors. All rights reserved.
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to
+deal in the Software without restriction, including without limitation the
+rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+sell copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+IN THE SOFTWARE.
diff --git a/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/METADATA b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/METADATA
new file mode 100644
index 0000000..b4670e8
--- /dev/null
+++ b/buffteks/lib/python3.11/site-packages/mdurl-0.1.2.dist-info/METADATA
@@ -0,0 +1,32 @@
+Metadata-Version: 2.1
+Name: mdurl
+Version: 0.1.2
+Summary: Markdown URL utilities
+Keywords: markdown,commonmark
+Author-email: Taneli Hukkinen