├── flask_squeeze ├── py.typed ├── __init__.py ├── log.py ├── utils.py ├── compress.py ├── models.py ├── cache.py ├── minify.py └── flask_squeeze.py ├── tests ├── test_app │ ├── static │ │ ├── empty.js │ │ ├── main.css │ │ ├── smallfile.js │ │ ├── text.txt │ │ ├── main.js │ │ └── jquery.min.js │ ├── main │ │ ├── __init__.py │ │ ├── routes.py │ │ └── templates │ │ │ └── index.html │ └── __init__.py ├── test_minify.py ├── test_compress.py └── test_flask_squeeze.py ├── assets ├── logo.png ├── logo-squeeze.afdesign └── coverage.svg ├── Flask-Squeeze.code-workspace ├── justfile ├── LICENSE ├── .gitignore ├── pyproject.toml ├── .github └── workflows │ └── test.yml └── README.md /flask_squeeze/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/static/empty.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/test_app/static/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #aaa; 3 | } -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkrd/Flask-Squeeze/HEAD/assets/logo.png -------------------------------------------------------------------------------- /flask_squeeze/__init__.py: -------------------------------------------------------------------------------- 1 | from .flask_squeeze import Squeeze 2 | 3 | __all__ = ["Squeeze"] 4 | -------------------------------------------------------------------------------- /tests/test_app/static/smallfile.js: -------------------------------------------------------------------------------- 1 | // Small JS file for testing min size threshold 2 | var x = 1; 3 | -------------------------------------------------------------------------------- /assets/logo-squeeze.afdesign: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mkrd/Flask-Squeeze/HEAD/assets/logo-squeeze.afdesign -------------------------------------------------------------------------------- /tests/test_app/main/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | bp = Blueprint("main", __name__, template_folder="templates") 4 | from test_app.main import routes # noqa: E402, F401 5 | -------------------------------------------------------------------------------- /tests/test_minify.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_squeeze.minify import minify 4 | 5 | 6 | def test_minify_invalid_minification() -> None: 7 | with pytest.raises(ValueError, match="Unsupported minification: unsupported"): 8 | minify(b"test data", "unsupported") # type: ignore[arg-type] 9 | -------------------------------------------------------------------------------- /tests/test_compress.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from flask_squeeze.compress import compress 4 | 5 | 6 | def test_compress_invalid_encoding() -> None: 7 | with pytest.raises(ValueError, match="Unsupported encoding: unsupported"): 8 | compress(b"test data", "unsupported", 5) # type: ignore[arg-type] 9 | -------------------------------------------------------------------------------- /tests/test_app/main/routes.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime, timezone 2 | 3 | # Flask 4 | from flask import render_template 5 | 6 | # Blueprint setup 7 | from test_app.main import bp 8 | 9 | 10 | @bp.route("/") 11 | @bp.route("/index") 12 | def hello() -> str: 13 | data = datetime.now(timezone.utc) 14 | return render_template("index.html", data=str(data)) 15 | -------------------------------------------------------------------------------- /Flask-Squeeze.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "[python]": { 9 | "editor.formatOnSave": true, 10 | "editor.defaultFormatter": "charliermarsh.ruff", 11 | "editor.codeActionsOnSave": { 12 | "source.fixAll": "never", 13 | "source.organizeImports": "explicit", 14 | }, 15 | }, 16 | "python.analysis.typeCheckingMode": "standard", 17 | "python.analysis.diagnosticMode": "workspace", 18 | "python.terminal.activateEnvironment": false, 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /flask_squeeze/log.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from flask import Request, current_app, request 4 | 5 | 6 | def _format_log( 7 | message: str, 8 | level: int, 9 | request: Request, 10 | color_code: int, 11 | ) -> str: 12 | log = f"Flask-Squeeze: {request.method} {request.path} | {2 * level * ' '}{message}" 13 | # ANSI escape code for color 14 | res = "\033[" 15 | res += f"{color_code}m{log}" 16 | res += "\033[0m" 17 | return res 18 | 19 | 20 | def log(level: int, s: str) -> None: 21 | if current_app.config["SQUEEZE_VERBOSE_LOGGING"]: 22 | print(_format_log(s, level, request, 92)) 23 | -------------------------------------------------------------------------------- /tests/test_app/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from flask import Flask 4 | 5 | from flask_squeeze import Squeeze 6 | 7 | 8 | def create_app(update_config: dict[str, str] | None = None) -> Flask: 9 | squeeze = Squeeze() 10 | app = Flask(__name__, instance_relative_config=True) 11 | config = { 12 | "ENV": "development", 13 | "DEBUG": True, 14 | "SECRET_KEY": "dev", 15 | "SQUEEZE_MIN_SIZE": 0, 16 | "SQUEEZE_VERBOSE_LOGGING": True, 17 | } 18 | if update_config: 19 | config.update(update_config) 20 | app.config.from_mapping(config) 21 | 22 | squeeze.init_app(app) 23 | 24 | from test_app import main 25 | 26 | app.register_blueprint(main.bp) 27 | 28 | return app 29 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | set dotenv-load := true 2 | 3 | # @ means: surpress printing the executed command 4 | 5 | default: 6 | @just --list 7 | 8 | alias r := run-test-app 9 | 10 | # Run test app 11 | run-test-app: 12 | export FLASK_DEBUG=1 13 | export PYTHONDONTWRITEBYTECODE=1 14 | cd tests && uv run flask --app "test_app:create_app()" run --host=localhost --port=5002 --debug --reload 15 | 16 | 17 | ruff: 18 | uv run ruff check 19 | 20 | alias m := mypy 21 | mypy: 22 | uv run mypy . 23 | 24 | alias t := test 25 | test: 26 | uv run pytest -p no:cacheprovider --capture=no --cov-report=term-missing --cov=flask_squeeze tests 27 | 28 | 29 | alias c := check 30 | check: ruff mypy test 31 | 32 | 33 | publish: 34 | uv build 35 | uv publish 36 | rm -rf dist 37 | rm -rf flask_squeeze.egg-info 38 | -------------------------------------------------------------------------------- /assets/coverage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | coverage 17 | coverage 18 | 99% 19 | 99% 20 | 21 | 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mkrd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /flask_squeeze/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import secrets 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from flask import Response 8 | 9 | from .models import Encoding 10 | 11 | 12 | def add_breach_exploit_protection_header(response: Response) -> None: 13 | """ 14 | Protect against BREACH attack by adding random padding to the response. 15 | """ 16 | padding_length = secrets.randbelow(128) + 1 17 | random_str = secrets.token_urlsafe(padding_length) 18 | response.headers["X-Flask-Squeeze-Breach-Protection"] = random_str 19 | 20 | 21 | def update_response_headers(response: Response, encode_choice: Encoding | None) -> None: 22 | """ 23 | Set the Content-Length header if it has changed. 24 | Set the Content-Encoding header if compressed data is served. 25 | """ 26 | assert not response.direct_passthrough # At least one of minify or compress was run 27 | 28 | if encode_choice is not None: 29 | response.headers["Content-Encoding"] = encode_choice.value 30 | vary = {x.strip() for x in response.headers.get("Vary", "").split(",")} 31 | response.headers["Vary"] = ",".join((vary | {"Accept-Encoding"}) - {""}) 32 | 33 | response.headers["Content-Length"] = str(response.content_length) 34 | -------------------------------------------------------------------------------- /flask_squeeze/compress.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import gzip 4 | import time 5 | import zlib 6 | from dataclasses import dataclass 7 | 8 | import brotli 9 | 10 | from .models import Encoding 11 | 12 | 13 | @dataclass(frozen=True) 14 | class CompressionInfo: 15 | encoding: Encoding 16 | quality: int 17 | duration: float 18 | ratio: float 19 | 20 | @property 21 | def headers(self) -> dict[str, str]: 22 | value = "; ".join( 23 | [ 24 | f"ratio={self.ratio:.1f}x", 25 | f"quality={self.quality}", 26 | f"duration={self.duration * 1000:.1f}ms", 27 | ], 28 | ) 29 | return {"X-Flask-Squeeze-Compress": value} 30 | 31 | 32 | def compress( 33 | data: bytes, 34 | encoding: Encoding, 35 | quality: int, 36 | ) -> tuple[bytes, CompressionInfo]: 37 | t0 = time.perf_counter() 38 | 39 | if encoding is Encoding.br: 40 | compressed_data = brotli.compress(data, quality=quality) 41 | elif encoding is Encoding.deflate: 42 | compressed_data = zlib.compress(data, level=quality) 43 | elif encoding is Encoding.gzip: 44 | compressed_data = gzip.compress(data, compresslevel=quality) 45 | else: 46 | msg = f"Unsupported encoding: {encoding}" 47 | raise ValueError(msg) 48 | 49 | ratio = len(data) / len(compressed_data) if len(compressed_data) > 0 else 1.0 50 | 51 | return compressed_data, CompressionInfo( 52 | encoding=encoding, 53 | quality=quality, 54 | duration=time.perf_counter() - t0, 55 | ratio=ratio, 56 | ) 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | .vscode -------------------------------------------------------------------------------- /flask_squeeze/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from flask import Config 8 | from werkzeug.datastructures import Headers 9 | 10 | 11 | class ResourceType(Enum): 12 | static = "static" 13 | dynamic = "dynamic" 14 | 15 | 16 | class Encoding(Enum): 17 | gzip = "gzip" 18 | deflate = "deflate" 19 | br = "br" 20 | 21 | @classmethod 22 | def get_from_headers_and_config( 23 | cls, 24 | headers: Headers, 25 | config: Config, 26 | ) -> Encoding | None: 27 | """ 28 | If the client supports brotli, gzip, or deflate, return the best encoding. 29 | If the client does not accept any of these encodings, or if the config 30 | variable SQUEEZE_COMPRESS is False, return None. 31 | """ 32 | if not config.get("SQUEEZE_COMPRESS"): 33 | return None 34 | encoding = headers.get("Accept-Encoding", "").lower() 35 | if "br" in encoding: 36 | return cls.br 37 | if "deflate" in encoding: 38 | return cls.deflate 39 | if "gzip" in encoding: 40 | return cls.gzip 41 | return None 42 | 43 | 44 | class Minification(Enum): 45 | js = "js" 46 | css = "css" 47 | html = "html" 48 | 49 | @classmethod 50 | def get_from_mimetype_and_config( 51 | cls, 52 | mimetype: str | None, 53 | config: Config, 54 | ) -> Minification | None: 55 | """ 56 | Based on the response mimetype: 57 | - `js` or `json`, and `SQUEEZE_MINIFY_JS=True`: return `Minification.js` 58 | - `css` and `SQUEEZE_MINIFY_CSS=True`: return `Minification.css` 59 | - `html` and `SQUEEZE_MINIFY_HTML=True`: return `Minification.html` 60 | - Otherwise, return `None` 61 | """ 62 | if mimetype is None: 63 | return None 64 | is_js_or_json = mimetype.endswith(("javascript", "json")) 65 | if is_js_or_json and config.get("SQUEEZE_MINIFY_JS"): 66 | return cls.js 67 | if mimetype.endswith("css") and config.get("SQUEEZE_MINIFY_CSS"): 68 | return cls.css 69 | if mimetype.endswith("html") and config.get("SQUEEZE_MINIFY_HTML"): 70 | return cls.html 71 | return None 72 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "flask-squeeze" 3 | version = "3.5.1" 4 | description = "Compress and minify Flask responses!" 5 | readme = "README.md" 6 | authors = [{ name = "Marcel Kröker", email = "kroeker.marcel@gmail.com" }] 7 | license = { file = "LICENSE" } 8 | classifiers=[ 9 | "Programming Language :: Python :: 3", 10 | "License :: OSI Approved :: MIT License", 11 | "Operating System :: OS Independent", 12 | "Intended Audience :: Developers", 13 | "Programming Language :: Python", 14 | "Topic :: Software Development :: Libraries :: Python Modules" 15 | ] 16 | requires-python = ">=3.9, <3.15" 17 | 18 | 19 | ######################################################################################## 20 | #### MARK: Dependencies 21 | 22 | 23 | dependencies = [ 24 | "flask>=1", 25 | "brotli~=1.1", 26 | "rjsmin~=1.2", 27 | "rcssmin~=1.2", 28 | ] 29 | 30 | [dependency-groups] 31 | dev = [ 32 | "mypy ~= 1.19.0", 33 | "pytest == 8.4.1", # 8.4.2 uses match statement (incompatible with python 3.9) 34 | "pytest-cov ~= 7.0.0", 35 | "ruff>=0.9.3", 36 | ] 37 | 38 | 39 | ######################################################################################## 40 | #### MARK: Packaging 41 | 42 | 43 | [tool.setuptools] 44 | packages = ["flask_squeeze"] 45 | license-files = [] # Workaround for https://github.com/astral-sh/uv/issues/9513 46 | 47 | [tool.uv] 48 | package = true 49 | 50 | 51 | ######################################################################################## 52 | #### MARK: Ruff 53 | 54 | 55 | [tool.ruff] 56 | cache-dir = ".cache/ruff" 57 | target-version = "py39" 58 | line-length = 120 59 | 60 | [tool.ruff.lint] 61 | select = ["ALL"] 62 | ignore = [ 63 | "ANN202", # Missing return type annotation for private function 64 | "D", # docstrings 65 | "E501", # line length 66 | "ERA001", # Found commented-out code 67 | "FBT001", # Boolean-typed positional argument in function definition 68 | "FIX002", # Line contains TODO, consider resolving the issue 69 | "INP001", # File is part of an implicit namespace package. Add an `__init__.py`. 70 | "PLC0415", # `import` should be at the top-level of a file 71 | "S101", # Use of `assert` detected 72 | "T201", # `print` found 73 | "TD002", # Missing author in TODO; try: `# TODO(): ...` 74 | "TD003", # Missing issue link on the line following this TODO 75 | "W191", # indentation contains tabs 76 | ] 77 | 78 | [tool.ruff.format] 79 | indent-style = "tab" 80 | quote-style = "double" 81 | 82 | 83 | ######################################################################################## 84 | #### MARK: Testing 85 | 86 | 87 | [tool.coverage.report] 88 | omit = ["tests/*"] 89 | exclude_also = ["if TYPE_CHECKING:"] 90 | 91 | 92 | ######################################################################################## 93 | #### MARK: Mypy 94 | 95 | 96 | [tool.mypy] 97 | cache_dir = ".cache/mypy" 98 | python_version = "3.9" 99 | ignore_missing_imports = true # brotli, rjsmin and rcssmin are missing type stubs 100 | strict = true 101 | -------------------------------------------------------------------------------- /flask_squeeze/cache.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import TYPE_CHECKING 5 | 6 | if TYPE_CHECKING: 7 | from pathlib import Path 8 | 9 | from .models import Encoding 10 | 11 | 12 | ######################################################################################## 13 | #### MARK: Disk utils 14 | 15 | 16 | def _save_cache_entry_to_disk( 17 | cache_dir: Path, 18 | cache_key: CacheKey, 19 | original_hash: str, 20 | data: bytes, 21 | ) -> None: 22 | # Save metadata 23 | with (cache_dir / f"{cache_key.normalized}.meta").open("w") as f: 24 | f.write(original_hash) 25 | 26 | # Save data 27 | with (cache_dir / f"{cache_key.normalized}.cache").open("wb") as f: 28 | f.write(data) 29 | 30 | 31 | def _read_cache_data_from_disk(cache_dir: Path) -> dict[str, tuple[str, bytes]]: 32 | data: dict[str, tuple[str, bytes]] = {} 33 | 34 | for meta_file in cache_dir.glob("*.meta"): 35 | cache_key = meta_file.stem 36 | with meta_file.open() as f: 37 | original_hash = f.read() 38 | 39 | cache_file = meta_file.with_suffix(".cache") 40 | if cache_file.exists(): 41 | with cache_file.open("rb") as f: 42 | cached_bytes = f.read() 43 | 44 | data[cache_key] = (original_hash, cached_bytes) 45 | 46 | return data 47 | 48 | 49 | ######################################################################################## 50 | #### MARK: Cache 51 | 52 | 53 | @dataclass(frozen=True) 54 | class CacheKey: 55 | path: str 56 | encoding: Encoding | None 57 | 58 | @property 59 | def normalized(self) -> str: 60 | flat_path = self.path.replace("/", "_") 61 | encoding = self.encoding.value if self.encoding else "none" 62 | return f"{flat_path}.{encoding}" 63 | 64 | 65 | @dataclass(frozen=True) 66 | class CachedData: 67 | original_hash: str 68 | data: bytes 69 | 70 | 71 | @dataclass 72 | class Cache: 73 | data: dict[str, tuple[str, bytes]] 74 | """ Maps request path to original data hash and compressed bytes """ 75 | 76 | cache_dir: Path | None = None 77 | """ Directory to store persistent cache files """ 78 | 79 | def __post_init__(self) -> None: 80 | """Initialize cache directory if specified.""" 81 | if self.cache_dir is not None: 82 | self.cache_dir.mkdir(parents=True, exist_ok=True) 83 | self.data.update(_read_cache_data_from_disk(self.cache_dir)) 84 | 85 | def get(self, cache_key: CacheKey) -> CachedData | None: 86 | """Get the cached hash and data for a given cache key.""" 87 | 88 | if cache_key.normalized not in self.data: 89 | return None 90 | 91 | original_hash, data = self.data[cache_key.normalized] 92 | return CachedData(original_hash, data) 93 | 94 | def set(self, cache_key: CacheKey, original_hash: str, data: bytes) -> None: 95 | """Set the cached data for a given cache key.""" 96 | 97 | self.data[cache_key.normalized] = (original_hash, data) 98 | 99 | # Save to disk if persistent caching is enabled 100 | if self.cache_dir is not None: 101 | _save_cache_entry_to_disk(self.cache_dir, cache_key, original_hash, data) 102 | -------------------------------------------------------------------------------- /flask_squeeze/minify.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import time 4 | from dataclasses import dataclass 5 | 6 | import rcssmin 7 | import rjsmin 8 | 9 | from .models import Minification 10 | 11 | 12 | @dataclass(frozen=True) 13 | class MinificationInfo: 14 | minification: Minification 15 | duration: float 16 | ratio: float 17 | 18 | @property 19 | def headers(self) -> dict[str, str]: 20 | value = "; ".join( 21 | [ 22 | f"ratio={self.ratio:.1f}x", 23 | f"duration={self.duration * 1000:.1f}ms", 24 | ], 25 | ) 26 | return {"X-Flask-Squeeze-Minify": value} 27 | 28 | 29 | def minify_html(html_bytes: bytes) -> bytes: 30 | """ 31 | Minifies HTML by removing white space and comments. 32 | Additionally it uses minify_css and minify_js functions 33 | to minify css in style tags and js in script tags 34 | respectively. 35 | """ 36 | 37 | # TODO: Find robust way to minify 38 | 39 | # html_text = html_bytes.decode("utf-8") 40 | 41 | # minified: list[str] = [] 42 | # parser = etree.HTMLParser(recover=False) 43 | # html_fragments: list[etree._Element] = etree.fromstring(html_text, 44 | # parser=parser 45 | # ) 46 | 47 | # for fragment in html_fragments: 48 | # print("fragment", fragment) 49 | # if isinstance(fragment, str): 50 | # minified.append(fragment) 51 | # continue 52 | 53 | # for element in fragment.iter(): 54 | # print("element", element, element.tag) 55 | # element: etree._Element = element 56 | # if element.tag in ["pre", "code", "textarea"]: 57 | # pass 58 | # elif element.tag == "style" and element.text: 59 | # element.text = minify_css(element.text) 60 | # elif element.tag == "script" and element.text: 61 | # element.text = minify_js(element.text) 62 | # else: 63 | # if element.text: 64 | # element.text = element.text.strip() 65 | # if element.tail: 66 | # element.tail = element.tail.strip() 67 | # element_bytes: bytes = etree.tostring(element, pretty_print=False) 68 | # minified.append(element_bytes.decode("utf-8")) 69 | 70 | # return "".join(minified) 71 | 72 | return html_bytes 73 | 74 | 75 | def minify_css(data: bytes) -> bytes: 76 | minified = rcssmin.cssmin(data) 77 | assert isinstance(minified, bytes) 78 | return minified 79 | 80 | 81 | def minify_js(data: bytes) -> bytes: 82 | minified = rjsmin.jsmin(data) 83 | assert isinstance(minified, bytes) 84 | return minified 85 | 86 | 87 | def minify(data: bytes, minification: Minification) -> tuple[bytes, MinificationInfo]: 88 | """ 89 | Run the minification using the correct minify function and return the minified data. 90 | """ 91 | 92 | t0 = time.perf_counter() 93 | 94 | if minification is Minification.html: 95 | minified_data = minify_html(data) 96 | elif minification is Minification.css: 97 | minified_data = minify_css(data) 98 | elif minification is Minification.js: 99 | minified_data = minify_js(data) 100 | else: 101 | msg = f"Unsupported minification: {minification}" 102 | raise ValueError(msg) 103 | 104 | ratio = len(data) / len(minified_data) if len(minified_data) > 0 else 1.0 105 | 106 | return minified_data, MinificationInfo( 107 | minification=minification, 108 | duration=time.perf_counter() - t0, 109 | ratio=ratio, 110 | ) 111 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | 6 | # JOB: Tests 7 | tests-job: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] 13 | 14 | steps: 15 | #---------------------------------------------- 16 | #---- Checkout and install uv and python 17 | #---------------------------------------------- 18 | 19 | - uses: actions/checkout@v4 20 | - name: Install uv 21 | uses: astral-sh/setup-uv@v6 22 | with: 23 | enable-cache: true 24 | - name: Set up Python ${{ matrix.python-version }} 25 | run: uv python install ${{ matrix.python-version }} 26 | 27 | #---------------------------------------------- 28 | #---- Install dependencies 29 | #---------------------------------------------- 30 | 31 | - name: uv install 32 | run: uv sync --all-extras --dev 33 | 34 | #---------------------------------------------- 35 | #---- Show installation details 36 | #---------------------------------------------- 37 | 38 | - name: uv --version 39 | run: uv --version 40 | - name: uv run python --version 41 | run: uv run python --version 42 | - name: ls -lah 43 | run: ls -lah 44 | - name: uv tree 45 | run: uv tree 46 | 47 | 48 | #---------------------------------------------- 49 | #---- Linting and Static Analysis 50 | #---------------------------------------------- 51 | 52 | - name: 🔎 Ruff checks 53 | run: uv run ruff check . 54 | 55 | - name: 🔎 Ruff format checks 56 | run: uv run ruff format --check . 57 | 58 | - name: 🐍 Mypy Static Type Checker 59 | run: uv run mypy . 60 | 61 | #---------------------------------------------- 62 | #---- Run tests with coverage report 63 | #---------------------------------------------- 64 | 65 | - name: 🚀 Run tests with code coverage report 66 | run: uv run pytest --cov=flask_squeeze --cov-report term-missing 67 | 68 | #---------------------------------------------- 69 | #---- Save coverage artifact 70 | #---------------------------------------------- 71 | 72 | - name: Show artifacts (ls -lah) 73 | run: ls -lah 74 | - uses: actions/upload-artifact@v4 75 | with: 76 | name: coverage-${{ matrix.python-version }} 77 | include-hidden-files: true 78 | if-no-files-found: error 79 | path: ".coverage" 80 | 81 | # JOB: Coverage Badge 82 | cov-badge-job: 83 | needs: tests-job 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v4 87 | 88 | #---------------------------------------------- 89 | #---- Download and debug artifact 90 | #---------------------------------------------- 91 | 92 | - uses: actions/download-artifact@v4 93 | with: 94 | name: coverage-3.12 95 | path: . 96 | 97 | 98 | #---------------------------------------------- 99 | #---- Generate coverage badge 100 | #---------------------------------------------- 101 | 102 | - name: Generate Coverage Badge 103 | uses: tj-actions/coverage-badge-py@v2 104 | with: 105 | output: assets/coverage.svg 106 | 107 | #---------------------------------------------- 108 | #---- Verify and commit changes 109 | #---------------------------------------------- 110 | 111 | - name: Verify Changed Files 112 | uses: tj-actions/verify-changed-files@v17 113 | id: changed_files 114 | with: 115 | files: assets/coverage.svg 116 | 117 | - name: Commit Files 118 | if: steps.changed_files.outputs.files_changed == 'true' 119 | run: | 120 | git config --local user.email "github-actions[bot]@users.noreply.github.com" 121 | git config --local user.name "github-actions[bot]" 122 | git add assets/coverage.svg 123 | git commit -m "Updated assets/coverage.svg" 124 | - name: Push Changes 125 | if: steps.changed_files.outputs.files_changed == 'true' 126 | uses: ad-m/github-push-action@master 127 | with: 128 | github_token: ${{ secrets.github_token }} 129 | branch: ${{ github.ref }} 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](https://github.com/mkrd/Flask-Squeeze/blob/master/assets/logo.png?raw=true) 2 | 3 | [![Downloads](https://pepy.tech/badge/flask-squeeze)](https://pepy.tech/project/flask-squeeze) 4 | ![Tests](https://github.com/mkrd/Flask-Squeeze/actions/workflows/test.yml/badge.svg) 5 | ![Coverage](https://github.com/mkrd/Flask-Squeeze/blob/master/assets/coverage.svg?raw=1) 6 | 7 | Flask-Squeeze is a Flask extension that automatically: 8 | - **Minifies** responses with JavaScript, CSS, and HTML content 9 | - **Compresses** all responses with brotli (preferred), gzip, or deflate compression based on browser support 10 | - **Protects** against the BREACH exploit by adding random padding to compressed responses 11 | - **Caches** static files so they don't need to be re-compressed, with both in-memory and persistent disk caching options 12 | - **Optimizes performance** with intelligent compression levels for static vs. dynamic content 13 | - **Works out-of-the-box** - no changes needed to your existing Flask routes or templates 14 | 15 | Files are considered static if the substring "/static/" is in their request path. 16 | 17 | 18 | Table of Contents 19 | ---------------------------------------------------------------------------------------- 20 | - [Compatibility](#compatibility) 21 | - [Installation](#installation) 22 | - [Quick Start](#quick-start) 23 | - [Contributing](#contributing) 24 | 25 | 26 | Compatibility 27 | ---------------------------------------------------------------------------------------- 28 | 29 | - Works with Python 3.9 to 3.14 30 | 31 | 32 | Installation 33 | ---------------------------------------------------------------------------------------- 34 | 35 | ``` 36 | pip install Flask-Squeeze 37 | ``` 38 | 39 | 40 | Quick Start 41 | ---------------------------------------------------------------------------------------- 42 | 43 | ```python 44 | from flask import Flask 45 | from flask_squeeze import Squeeze 46 | squeeze = Squeeze() 47 | 48 | def create_app(): 49 | app = Flask(__name__) 50 | 51 | # Init Flask-Squeeze 52 | squeeze.init_app(app) 53 | 54 | # Init all other extensions 55 | # AFTER Flask-Squeeze 56 | 57 | return app 58 | ``` 59 | 60 | Thats it! The responses of your Flask app will now get minified and compressed, if the browser supports it. 61 | To control how Flask-Squeeze behaves, the following options exist: 62 | 63 | 64 | ### Basic Options 65 | | Option | Default | Description | 66 | | --- | --- | --- | 67 | | `SQUEEZE_COMPRESS` | `True` | Enable/disable compression | 68 | | `SQUEEZE_MIN_SIZE` | `500` | Minimum file size (bytes) to compress | 69 | | `SQUEEZE_CACHE_DIR` | `None` | Directory for persistent cache (`None` = in-memory only) | 70 | | `SQUEEZE_VERBOSE_LOGGING` | `False` | Enable debug output | 71 | 72 | ### Minification Options 73 | | Option | Default | Description | 74 | | --- | --- | --- | 75 | | `SQUEEZE_MINIFY_CSS` | `True` | Enable CSS minification | 76 | | `SQUEEZE_MINIFY_JS` | `True` | Enable JavaScript minification | 77 | | `SQUEEZE_MINIFY_HTML` | `True` | Enable HTML minification | 78 | 79 | ### Compression Levels 80 | | Option | Default | Range | Description | 81 | | --- | --- | --- | --- | 82 | | `SQUEEZE_LEVEL_BROTLI_STATIC` | `11` | 0-11 | Brotli level for static files | 83 | | `SQUEEZE_LEVEL_BROTLI_DYNAMIC` | `1` | 0-11 | Brotli level for dynamic content | 84 | | `SQUEEZE_LEVEL_GZIP_STATIC` | `9` | 0-9 | Gzip level for static files | 85 | | `SQUEEZE_LEVEL_GZIP_DYNAMIC` | `1` | 0-9 | Gzip level for dynamic content | 86 | 87 | ### Example Configuration 88 | ```python 89 | app.config.update({ 90 | 'SQUEEZE_CACHE_DIR': './cache/flask_squeeze/', # Enable persistent caching 91 | 'SQUEEZE_MIN_SIZE': 1000, # Only compress files > 1KB 92 | 'SQUEEZE_VERBOSE_LOGGING': True, # Debug mode 93 | }) 94 | ``` 95 | 96 | 97 | Contributing 98 | ---------------------------------------------------------------------------------------- 99 | 100 | 1. **Report bugs** by opening an issue 101 | 2. **Submit pull requests** with improvements 102 | 3. **Improve documentation** 103 | 104 | ### Development Setup 105 | ```bash 106 | git clone https://github.com/mkrd/Flask-Squeeze.git 107 | cd Flask-Squeeze 108 | uv sync 109 | just test # Run tests 110 | just run-test-app # Run test app 111 | ``` 112 | 113 | 114 | License 115 | ---------------------------------------------------------------------------------------- 116 | 117 | MIT License - see [LICENSE](LICENSE) file for details. 118 | -------------------------------------------------------------------------------- /tests/test_app/main/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 10 | 11 | 27 | 28 | 29 |

Index Page

30 |

31 | This is a dynamically generated page, and thus should not be cached. 32 | The current date is passed to the template and gets printed, to verify that no cache problems appear 33 |

34 |

Date when this template was generated

35 |

{{ data }}

36 |

37 | 38 | {% for i in range(1000) %} 39 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 40 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 41 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 42 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 43 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 44 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 45 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 46 | {% endfor %} 47 |

48 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /flask_squeeze/flask_squeeze.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import hashlib 4 | from pathlib import Path 5 | from typing import cast 6 | 7 | from flask import Flask, Response, request 8 | 9 | from .cache import Cache, CacheKey 10 | from .compress import CompressionInfo, compress 11 | from .log import log 12 | from .minify import MinificationInfo, minify 13 | from .models import Encoding, Minification, ResourceType 14 | from .utils import add_breach_exploit_protection_header, update_response_headers 15 | 16 | 17 | class Squeeze: 18 | __slots__ = "app", "cache_static" 19 | app: Flask 20 | 21 | cache_static: Cache 22 | """ (request.path, encoding) -> (original file sha256 hash, compressed bytes) """ 23 | 24 | def __init__(self, app: Flask | None = None) -> None: 25 | """Initialize Flask-Squeeze with or without app.""" 26 | if app is None: 27 | return 28 | self.app = app 29 | self.init_app(app) 30 | 31 | def init_app(self, app: Flask) -> None: 32 | """Initialize Flask-Squeeze with app""" 33 | self.app = app 34 | # Compression options 35 | app.config.setdefault("SQUEEZE_COMPRESS", True) 36 | app.config.setdefault("SQUEEZE_MIN_SIZE", 500) 37 | # Compression levels 38 | app.config.setdefault("SQUEEZE_LEVEL_GZIP_STATIC", 9) 39 | app.config.setdefault("SQUEEZE_LEVEL_GZIP_DYNAMIC", 1) 40 | app.config.setdefault("SQUEEZE_LEVEL_BROTLI_STATIC", 11) 41 | app.config.setdefault("SQUEEZE_LEVEL_BROTLI_DYNAMIC", 1) 42 | app.config.setdefault("SQUEEZE_LEVEL_DEFLATE_STATIC", 9) 43 | app.config.setdefault("SQUEEZE_LEVEL_DEFLATE_DYNAMIC", 1) 44 | # Minification options 45 | app.config.setdefault("SQUEEZE_MINIFY_JS", True) 46 | app.config.setdefault("SQUEEZE_MINIFY_CSS", True) 47 | app.config.setdefault("SQUEEZE_MINIFY_HTML", True) 48 | # Caching options 49 | app.config.setdefault("SQUEEZE_CACHE_DIR", None) 50 | # Logging options 51 | app.config.setdefault("SQUEEZE_VERBOSE_LOGGING", False) 52 | 53 | # Initialize cache 54 | 55 | cache_dir = app.config.get("SQUEEZE_CACHE_DIR") 56 | self.cache_static = Cache( 57 | data={}, 58 | cache_dir=Path(cache_dir) if cache_dir else None, 59 | ) 60 | 61 | # Initialize after_request hook 62 | 63 | if ( 64 | app.config["SQUEEZE_COMPRESS"] 65 | or app.config["SQUEEZE_MINIFY_JS"] 66 | or app.config["SQUEEZE_MINIFY_CSS"] 67 | or app.config["SQUEEZE_MINIFY_HTML"] 68 | ): 69 | app.after_request(self.after_request) 70 | 71 | #################################################################################### 72 | #### MARK: Utils 73 | 74 | def get_configured_quality(self, encode_choice: Encoding, resource_type: ResourceType) -> int: 75 | options = { 76 | (Encoding.br, ResourceType.static): "SQUEEZE_LEVEL_BROTLI_STATIC", 77 | (Encoding.br, ResourceType.dynamic): "SQUEEZE_LEVEL_BROTLI_DYNAMIC", 78 | (Encoding.deflate, ResourceType.static): "SQUEEZE_LEVEL_DEFLATE_STATIC", 79 | (Encoding.deflate, ResourceType.dynamic): "SQUEEZE_LEVEL_DEFLATE_DYNAMIC", 80 | (Encoding.gzip, ResourceType.static): "SQUEEZE_LEVEL_GZIP_STATIC", 81 | (Encoding.gzip, ResourceType.dynamic): "SQUEEZE_LEVEL_GZIP_DYNAMIC", 82 | } 83 | 84 | option = options[(encode_choice, resource_type)] 85 | 86 | return cast("int", self.app.config[option]) 87 | 88 | #################################################################################### 89 | #### MARK: Dynamic 90 | 91 | def run_dynamic( 92 | self, 93 | response: Response, 94 | encode_choice: Encoding | None, 95 | minify_choice: Minification | None, 96 | ) -> None: 97 | assert encode_choice or minify_choice 98 | 99 | data, minification_info, compression_info = self.squeeze( 100 | response.get_data(as_text=False), 101 | ResourceType.dynamic, 102 | minify_choice, 103 | encode_choice, 104 | ) 105 | 106 | response.set_data(data) 107 | 108 | if minification_info: 109 | response.headers.update(minification_info.headers) 110 | 111 | if compression_info: 112 | response.headers.update(compression_info.headers) 113 | add_breach_exploit_protection_header(response) 114 | 115 | #################################################################################### 116 | #### MARK: Static 117 | 118 | def run_static( 119 | self, 120 | response: Response, 121 | encode_choice: Encoding | None, 122 | minify_choice: Minification | None, 123 | ) -> None: 124 | """ 125 | If the hash of the current response matches the hash of the cached response, 126 | return the cached response. Otherwise, compress and minify the response and 127 | cache the compressed response. 128 | """ 129 | assert encode_choice or minify_choice 130 | 131 | data = response.get_data(as_text=False) 132 | data_hash = hashlib.sha256(data).hexdigest() 133 | 134 | cache_key = CacheKey(request.path, encode_choice) 135 | cached = self.cache_static.get(cache_key) 136 | 137 | if cached is not None and data_hash == cached.original_hash: 138 | log(2, "Found in cache, hashes match. RETURN") 139 | response.set_data(cached.data) 140 | response.headers["X-Flask-Squeeze-Cache"] = "HIT" 141 | return 142 | 143 | # Not in cache, compress and minify 144 | 145 | log(2, "Not in cache or hashes don't match. Squeeze and cache.") 146 | data, minification_info, compression_info = self.squeeze( 147 | data, 148 | ResourceType.static, 149 | minify_choice, 150 | encode_choice, 151 | ) 152 | 153 | response.set_data(data) 154 | 155 | if minification_info: 156 | response.headers.update(minification_info.headers) 157 | 158 | if compression_info: 159 | response.headers.update(compression_info.headers) 160 | 161 | response.headers["X-Flask-Squeeze-Cache"] = "MISS" 162 | 163 | # Cache the compressed data, with the dash of the original data 164 | self.cache_static.set(cache_key, data_hash, data) 165 | 166 | def squeeze( 167 | self, 168 | data: bytes, 169 | resource_type: ResourceType, 170 | minify_choice: Minification | None, 171 | encode_choice: Encoding | None, 172 | ) -> tuple[bytes, MinificationInfo | None, CompressionInfo | None]: 173 | assert encode_choice or minify_choice 174 | 175 | minification_info = None 176 | if minify_choice is not None: 177 | data, minification_info = minify(data, minify_choice) 178 | 179 | compression_info = None 180 | if encode_choice is not None: 181 | data, compression_info = compress( 182 | data, 183 | encode_choice, 184 | self.get_configured_quality(encode_choice, resource_type), 185 | ) 186 | 187 | return data, minification_info, compression_info 188 | 189 | #################################################################################### 190 | #### MARK: After Request 191 | 192 | def after_request(self, response: Response) -> Response: 193 | log(1, f"Enter after_request({response})") 194 | 195 | if response.status_code is None or response.content_length is None: 196 | log(1, "Response status code or content length is None. RETURN") 197 | return response 198 | 199 | if response.status_code not in range(200, 300): 200 | log(1, "Response status code is not ok. RETURN") 201 | return response 202 | 203 | if response.content_length < self.app.config["SQUEEZE_MIN_SIZE"]: 204 | log(1, "Response size is smaller than the defined minimum. RETURN") 205 | return response 206 | 207 | if "Content-Encoding" in response.headers: 208 | log(1, "Response already encoded. RETURN") 209 | return response 210 | 211 | # Assert: The response is ok, the size is above threshold, and the response is 212 | # not already encoded. 213 | 214 | encode_choice = Encoding.get_from_headers_and_config( 215 | request.headers, 216 | self.app.config, 217 | ) 218 | 219 | minify_choice = Minification.get_from_mimetype_and_config( 220 | response.mimetype, 221 | self.app.config, 222 | ) 223 | 224 | if encode_choice is None and minify_choice is None: 225 | log(1, "No compression or minification requested. RETURN") 226 | return response 227 | 228 | # At least one of minify or compress is requested 229 | 230 | response.direct_passthrough = False # In both cases, we need to read the data 231 | 232 | if request.path.startswith("/static/"): 233 | self.run_static(response, encode_choice, minify_choice) 234 | else: 235 | self.run_dynamic(response, encode_choice, minify_choice) 236 | 237 | update_response_headers(response, encode_choice) 238 | 239 | log(1, f"Static cache: {self.cache_static.data.keys()}") 240 | return response 241 | -------------------------------------------------------------------------------- /tests/test_app/static/text.txt: -------------------------------------------------------------------------------- 1 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 2 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 3 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 4 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 5 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 6 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 7 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 8 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 9 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 10 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 11 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 12 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 13 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 14 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 15 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 16 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 17 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 18 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 19 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 20 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 21 | -------------------------------------------------------------------------------- /tests/test_flask_squeeze.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | import tempfile 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING, Any 7 | 8 | import pytest 9 | from test_app import create_app 10 | 11 | if TYPE_CHECKING: 12 | from collections.abc import Generator 13 | 14 | from flask.testing import FlaskClient 15 | from werkzeug.wrappers import Response 16 | 17 | STATUS_CODE_NOT_FOUND_404 = 404 18 | STATUS_CODE_OK_200 = 200 19 | 20 | ######################################################################################## 21 | #### MARK: Fixtures 22 | 23 | 24 | @pytest.fixture 25 | def client() -> Generator[FlaskClient, Any, None]: 26 | app = create_app() 27 | app.testing = True 28 | with app.test_client() as test_client: 29 | yield test_client 30 | 31 | 32 | @pytest.fixture(params=["", "gzip", "br", "deflate"]) 33 | def use_encoding(request: pytest.FixtureRequest) -> str: 34 | return str(request.param) 35 | 36 | 37 | @pytest.fixture(params=[False, True]) 38 | def use_minify_js(request: pytest.FixtureRequest) -> bool: 39 | return bool(request.param) 40 | 41 | 42 | @pytest.fixture(params=[False, True]) 43 | def use_minify_css(request: pytest.FixtureRequest) -> bool: 44 | return bool(request.param) 45 | 46 | 47 | ######################################################################################## 48 | #### MARK: Utilities 49 | 50 | 51 | def almost_equal(a: float, b: float, percent: float = 0.01) -> bool: 52 | diff = abs(int(a) - int(b)) 53 | return diff < percent * int(a) and diff < percent * int(b) 54 | 55 | 56 | def content_length_correct(r: Response) -> bool: 57 | return r.headers.get("Content-Length", 0) == str(len(r.data)) 58 | 59 | 60 | ######################################################################################## 61 | #### MARK: Tests 62 | 63 | 64 | def test_init_app_with_existing_app() -> None: 65 | app = create_app() 66 | 67 | from flask_squeeze import Squeeze 68 | 69 | _squeeze = Squeeze(app) 70 | 71 | 72 | def test_get_index(client: FlaskClient, use_encoding: str) -> None: 73 | print("test_get_index") 74 | r = client.get("/", headers={"Accept-Encoding": use_encoding}) 75 | assert content_length_correct(r) 76 | length = int(r.headers.get("Content-Length", "0")) 77 | encoding = r.headers.get("Content-Encoding", "") 78 | 79 | assert use_encoding == encoding 80 | 81 | sizes = { 82 | "": 3_932_146, 83 | "br": 8_164, 84 | "deflate": 83_554, 85 | "gzip": 83_566, 86 | } 87 | 88 | assert almost_equal(length, sizes[use_encoding]) 89 | 90 | 91 | def test_get_css_file(client: FlaskClient, use_encoding: str, use_minify_css: bool) -> None: 92 | print("test_get_css_file with", use_encoding, "minify:", use_minify_css) 93 | client.application.config.update({"SQUEEZE_MINIFY_CSS": use_minify_css}) 94 | url = "/static/fomantic.css" 95 | r = client.get(url, headers={"Accept-Encoding": use_encoding}) 96 | assert content_length_correct(r) 97 | response_length = int(r.headers.get("Content-Length", "0")) 98 | encoding = r.headers.get("Content-Encoding", "") 99 | 100 | assert use_encoding == encoding 101 | 102 | sizes = { 103 | ("", True): 1_377_522, 104 | ("br", True): 117_261, 105 | ("deflate", True): 157_184, 106 | ("gzip", True): 157_196, 107 | ("", False): 1_642_530, 108 | ("br", False): 130_781, 109 | ("deflate", False): 179_986, 110 | ("gzip", False): 179_998, 111 | } 112 | 113 | assert almost_equal(response_length, sizes[(use_encoding, use_minify_css)]) 114 | 115 | 116 | def test_get_js_file(client: FlaskClient, use_encoding: str, use_minify_js: bool) -> None: 117 | print("test_get_js_file with", use_encoding, "minify:", use_minify_js) 118 | client.application.config.update({"SQUEEZE_MINIFY_JS": use_minify_js}) 119 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": use_encoding}) 120 | assert content_length_correct(r) 121 | assert use_encoding == r.headers.get("Content-Encoding", "") 122 | 123 | 124 | def test_get_jquery_no_minify(client: FlaskClient) -> None: 125 | client.application.config.update({"SQUEEZE_MINIFY_JS": False}) 126 | r_orig = client.get("/static/jquery.js", headers={}) 127 | assert content_length_correct(r_orig) 128 | assert "Content-Encoding" not in r_orig.headers 129 | assert r_orig.headers["Content-Length"] == "292458" 130 | 131 | 132 | def test_get_from_cache(client: FlaskClient, use_encoding: str) -> None: 133 | client.application.config.update({"SQUEEZE_MINIFY_JS": False}) 134 | r = client.get("/static/jquery.min.js", headers={"Accept-Encoding": use_encoding}) 135 | r_2 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": use_encoding}) 136 | assert r.data == r_2.data 137 | 138 | 139 | def test_get_unknown_url(client: FlaskClient) -> None: 140 | r = client.get("/static/unknown.js", headers={"Accept-Encoding": "gzip"}) 141 | assert r.status_code == STATUS_CODE_NOT_FOUND_404 142 | 143 | 144 | def test_get_same_repeatedly(client: FlaskClient) -> None: 145 | client.application.config.update({"SQUEEZE_MINIFY_JS": True}) 146 | for i in range(100): 147 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "br"}) 148 | if i == 0: 149 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 150 | else: 151 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 152 | for i in range(100): 153 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 154 | if i == 0: 155 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 156 | else: 157 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 158 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "br"}) 159 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 160 | 161 | 162 | def response_has_breach_header(r: Response) -> bool: 163 | return "X-Flask-Squeeze-Breach-Protection" in r.headers 164 | 165 | 166 | def response_has_vary_header(r: Response) -> bool: 167 | return "Vary" in r.headers and "Accept-Encoding" in r.headers["Vary"] 168 | 169 | 170 | ######################################################################################## 171 | #### MARK: Additional Tests 172 | 173 | 174 | def test_disable_compression(client: FlaskClient) -> None: 175 | """Test that compression can be disabled.""" 176 | client.application.config.update({"SQUEEZE_COMPRESS": False}) 177 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 178 | assert "Content-Encoding" not in r.headers 179 | assert not response_has_breach_header(r) 180 | 181 | 182 | def test_disable_minification(client: FlaskClient) -> None: 183 | """Test that minification can be disabled.""" 184 | client.application.config.update( 185 | { 186 | "SQUEEZE_MINIFY_JS": False, 187 | "SQUEEZE_MINIFY_CSS": False, 188 | "SQUEEZE_MINIFY_HTML": False, 189 | }, 190 | ) 191 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 192 | assert "X-Flask-Squeeze-Minify" not in r.headers 193 | 194 | 195 | def test_html_response_minification(client: FlaskClient) -> None: 196 | """Test minification of HTML responses.""" 197 | client.application.config.update({"SQUEEZE_MINIFY_HTML": True}) 198 | r = client.get("/", headers={"Accept-Encoding": "gzip"}) 199 | assert "X-Flask-Squeeze-Minify" in r.headers, r.headers 200 | 201 | 202 | def test_breach_header_presence(client: FlaskClient) -> None: 203 | """Test the presence of the breach exploit protection header.""" 204 | r = client.get("/", headers={"Accept-Encoding": "br"}) 205 | assert response_has_breach_header(r) 206 | 207 | 208 | def test_vary_header_presence(client: FlaskClient) -> None: 209 | """Test the presence of the Vary header after compression.""" 210 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 211 | assert response_has_vary_header(r) 212 | 213 | 214 | def test_minification_and_compression_together(client: FlaskClient) -> None: 215 | """Test both minification and compression are applied together.""" 216 | client.application.config.update({"SQUEEZE_MINIFY_CSS": True}) 217 | r = client.get("/static/fomantic.css", headers={"Accept-Encoding": "gzip"}) 218 | assert "Content-Encoding" in r.headers 219 | assert "X-Flask-Squeeze-Minify" in r.headers 220 | assert content_length_correct(r) 221 | 222 | 223 | def test_no_minify_no_compress(client: FlaskClient) -> None: 224 | client.application.config.update( 225 | { 226 | "SQUEEZE_MINIFY_JS": False, 227 | "SQUEEZE_MINIFY_CSS": False, 228 | "SQUEEZE_MINIFY_HTML": False, 229 | "SQUEEZE_COMPRESS": False, 230 | }, 231 | ) 232 | 233 | # Static file with no minification or compression 234 | 235 | r = client.get("/static/fomantic.css", headers={"Accept-Encoding": "gzip"}) 236 | assert "Content-Encoding" not in r.headers 237 | assert "X-Flask-Squeeze-Minify" not in r.headers 238 | assert "X-Flask-Squeeze-Compress" not in r.headers 239 | 240 | # Dynamic file with no minification or compression 241 | 242 | r = client.get("/", headers={"Accept-Encoding": "gzip"}) 243 | assert "Content-Encoding" not in r.headers 244 | assert "X-Flask-Squeeze-Minify" not in r.headers 245 | assert "X-Flask-Squeeze-Compress" not in r.headers 246 | 247 | 248 | def test_static_file_cache_behavior( 249 | client: FlaskClient, 250 | use_encoding: str, 251 | use_minify_css: bool, 252 | ) -> None: 253 | """Test cache behavior for a temporary static file.""" 254 | 255 | # Skip if encoding is not set 256 | if not use_encoding: 257 | return 258 | 259 | client.application.config.update({"SQUEEZE_MINIFY_CSS": use_minify_css}) 260 | 261 | # Ensure the static directory is set 262 | if (static_dir := client.application.static_folder) is None: 263 | msg = "Static directory not found" 264 | raise ValueError(msg) 265 | 266 | # Use a temporary file within the static directory 267 | with tempfile.NamedTemporaryFile(dir=static_dir, suffix=".css", delete=True) as temp_file: 268 | file_path = temp_file.name 269 | url_path = "/static/" + Path(file_path).name 270 | temp_file.write(b"body { color: #000; } " * 1000) 271 | temp_file.flush() # Ensure content is written to disk 272 | 273 | # First request: MISS 274 | r = client.get(url_path, headers={"Accept-Encoding": use_encoding}) 275 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 276 | 277 | # Second request: HIT 278 | r = client.get(url_path, headers={"Accept-Encoding": use_encoding}) 279 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 280 | 281 | # Modify the file content 282 | temp_file.write(b"body { color: #123456; } " * 1000) 283 | temp_file.flush() # Ensure the new content is written 284 | 285 | # Third request: MISS 286 | r = client.get(url_path, headers={"Accept-Encoding": use_encoding}) 287 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 288 | 289 | # Fourth request: HIT 290 | r = client.get(url_path, headers={"Accept-Encoding": use_encoding}) 291 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 292 | 293 | 294 | def test_malformed_accept_encoding(client: FlaskClient) -> None: 295 | # Case 1: Missing Accept-Encoding 296 | r = client.get("/static/jquery.js") 297 | assert "Content-Encoding" not in r.headers 298 | 299 | # Case 2: Malformed Accept-Encoding 300 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "invalid-encoding"}) 301 | assert "Content-Encoding" not in r.headers 302 | 303 | # Case 3: Conflicting Accept-Encoding values 304 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip, br"}) 305 | assert r.headers.get("Content-Encoding") in {"gzip", "br"} 306 | 307 | 308 | def test_cache_invalidation_on_compression_disable(client: FlaskClient) -> None: 309 | # Enable compression and fetch the file 310 | client.application.config.update({"SQUEEZE_COMPRESS": True}) 311 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 312 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 313 | 314 | # Fetch again to ensure it is cached 315 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 316 | assert r.headers.get("X-Flask-Squeeze-Cache") == "HIT" 317 | 318 | # Disable compression 319 | client.application.config.update({"SQUEEZE_COMPRESS": False}) 320 | 321 | # Fetch again, cache should be invalidated 322 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 323 | assert r.headers.get("X-Flask-Squeeze-Cache") == "MISS" 324 | assert "Content-Encoding" not in r.headers 325 | 326 | 327 | def test_small_response_below_min_size(client: FlaskClient) -> None: 328 | min_size = 1000 329 | client.application.config.update({"SQUEEZE_MIN_SIZE": min_size}) 330 | 331 | # A small response 332 | r = client.get("/static/smallfile.js", headers={"Accept-Encoding": "gzip"}) 333 | assert r.content_length < min_size # Ensure response is small 334 | assert "Content-Encoding" not in r.headers 335 | assert "X-Flask-Squeeze-Minify" not in r.headers 336 | 337 | 338 | def test_persistent_cache_basic(client: FlaskClient) -> None: 339 | """Test basic persistent caching functionality.""" 340 | with tempfile.TemporaryDirectory() as temp_dir: 341 | cache_dir = Path(temp_dir) / "flask_squeeze_cache" 342 | 343 | # Configure persistent caching 344 | client.application.config.update({"SQUEEZE_CACHE_DIR": str(cache_dir)}) 345 | 346 | # Reinitialize the squeeze instance with the new config 347 | from flask_squeeze import Squeeze 348 | 349 | squeeze = Squeeze() 350 | squeeze.init_app(client.application) 351 | 352 | # First request - should be a cache miss 353 | r1 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 354 | assert r1.headers.get("X-Flask-Squeeze-Cache") == "MISS" 355 | 356 | # Second request - should be a cache hit from memory 357 | r2 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 358 | assert r2.headers.get("X-Flask-Squeeze-Cache") == "HIT" 359 | assert r1.data == r2.data 360 | 361 | # Verify cache files were created 362 | cache_files = list(cache_dir.glob("*.cache")) 363 | meta_files = list(cache_dir.glob("*.meta")) 364 | assert len(cache_files) > 0 365 | assert len(meta_files) > 0 366 | 367 | 368 | def test_persistent_cache_across_restarts(client: FlaskClient) -> None: 369 | """Test that cache persists across application restarts.""" 370 | with tempfile.TemporaryDirectory() as temp_dir: 371 | cache_dir = Path(temp_dir) / "flask_squeeze_cache" 372 | 373 | # Configure persistent caching 374 | client.application.config.update({"SQUEEZE_CACHE_DIR": str(cache_dir)}) 375 | 376 | # Create first squeeze instance 377 | from flask_squeeze import Squeeze 378 | 379 | squeeze1 = Squeeze() 380 | squeeze1.init_app(client.application) 381 | 382 | # First request 383 | r1 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 384 | assert r1.headers.get("X-Flask-Squeeze-Cache") == "MISS" 385 | 386 | # Create a second squeeze instance to simulate a restart 387 | 388 | app_2 = create_app() 389 | app_2.testing = True 390 | client_2 = app_2.test_client() 391 | client_2.application.config.update({"SQUEEZE_CACHE_DIR": str(cache_dir)}) 392 | squeeze2 = Squeeze() 393 | squeeze2.init_app(client_2.application) 394 | 395 | # Second request after "restart" - should be cache hit from disk 396 | r2 = client_2.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 397 | assert r2.headers.get("X-Flask-Squeeze-Cache") == "HIT" 398 | assert r1.data == r2.data 399 | 400 | 401 | def test_persistent_cache_disabled(client: FlaskClient) -> None: 402 | """Test that without cache directory, only in-memory cache is used.""" 403 | # Don't set SQUEEZE_CACHE_DIR 404 | from flask_squeeze import Squeeze 405 | 406 | squeeze = Squeeze() 407 | squeeze.init_app(client.application) 408 | 409 | # First request 410 | r1 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 411 | assert r1.headers.get("X-Flask-Squeeze-Cache") == "MISS" 412 | 413 | # Second request - should be cache hit from memory 414 | r2 = client.get("/static/jquery.min.js", headers={"Accept-Encoding": "gzip"}) 415 | assert r2.headers.get("X-Flask-Squeeze-Cache") == "HIT" 416 | 417 | 418 | def test_mimetype_detection_comprehensive(client: FlaskClient) -> None: 419 | """Test comprehensive mimetype detection for various file types.""" 420 | test_cases = [ 421 | ("/static/main.js", "text/javascript", True), 422 | ("/static/main.css", "text/css", True), 423 | ("/", "text/html", True), # HTML from template 424 | ] 425 | 426 | for url, expected_mimetype, should_process in test_cases: 427 | r = client.get(url, headers={"Accept-Encoding": "gzip"}) 428 | assert expected_mimetype in (r.mimetype or "") 429 | if should_process: 430 | assert "Content-Encoding" in r.headers or "X-Flask-Squeeze-Minify" in r.headers 431 | 432 | 433 | def test_encoding_priority_selection(client: FlaskClient) -> None: 434 | """Test that encodings are selected in correct priority order.""" 435 | # Test brotli preferred over gzip 436 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip, br, deflate"}) 437 | assert r.headers.get("Content-Encoding") == "br" 438 | 439 | # Test deflate preferred over none when br/gzip unavailable 440 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "deflate"}) 441 | assert r.headers.get("Content-Encoding") == "deflate" 442 | 443 | # Test gzip when only gzip available 444 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 445 | assert r.headers.get("Content-Encoding") == "gzip" 446 | 447 | 448 | def test_quality_levels_configuration(client: FlaskClient) -> None: 449 | """Test different compression quality levels.""" 450 | configs = [ 451 | {"SQUEEZE_LEVEL_GZIP_STATIC": 1, "SQUEEZE_LEVEL_BROTLI_STATIC": 1}, 452 | {"SQUEEZE_LEVEL_GZIP_STATIC": 9, "SQUEEZE_LEVEL_BROTLI_STATIC": 11}, 453 | ] 454 | 455 | for config in configs: 456 | client.application.config.update(config) 457 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 458 | assert content_length_correct(r) 459 | assert "Content-Encoding" in r.headers 460 | 461 | 462 | def test_cache_file_corruption_recovery(client: FlaskClient) -> None: 463 | """Test recovery from corrupted cache files.""" 464 | with tempfile.TemporaryDirectory() as temp_dir: 465 | cache_dir = Path(temp_dir) / "cache" 466 | client.application.config.update({"SQUEEZE_CACHE_DIR": str(cache_dir)}) 467 | 468 | from flask_squeeze import Squeeze 469 | 470 | squeeze = Squeeze() 471 | squeeze.init_app(client.application) 472 | 473 | # Create initial cache entry 474 | r1 = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 475 | assert r1.headers.get("X-Flask-Squeeze-Cache") == "MISS" 476 | 477 | # Corrupt cache files 478 | for cache_file in cache_dir.glob("*.cache"): 479 | with cache_file.open("wb") as f: 480 | f.write(b"corrupted data") 481 | 482 | # Should recover gracefully 483 | r2 = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 484 | assert content_length_correct(r2) 485 | 486 | 487 | def test_very_large_files(client: FlaskClient) -> None: 488 | """Test handling of very large files.""" 489 | # Create a large CSS file 490 | large_content = "body { color: #000; } " * 10000 # ~200KB 491 | 492 | static_dir = client.application.static_folder 493 | if static_dir is None: 494 | pytest.skip("Static directory not configured") 495 | 496 | large_file_path = Path(static_dir) / "large_test.css" 497 | try: 498 | with large_file_path.open("w") as f: 499 | f.write(large_content) 500 | 501 | # Test compression of large file 502 | r = client.get("/static/large_test.css", headers={"Accept-Encoding": "gzip"}) 503 | assert content_length_correct(r) 504 | assert "Content-Encoding" in r.headers 505 | 506 | # Verify significant compression ratio 507 | original_size = len(large_content.encode()) 508 | compressed_size = len(r.data) 509 | compression_ratio = original_size / compressed_size 510 | minimum_compression_ratio = 2.0 # Expect at least 2:1 compression ratio 511 | assert compression_ratio > minimum_compression_ratio 512 | 513 | finally: 514 | large_file_path.unlink(missing_ok=True) 515 | 516 | 517 | def test_binary_file_handling(client: FlaskClient) -> None: 518 | """Test handling of binary files that shouldn't be processed.""" 519 | # Create a small binary file 520 | static_dir = client.application.static_folder 521 | if static_dir is None: 522 | pytest.skip("Static directory not configured") 523 | 524 | binary_file_path = Path(static_dir) / "test.bin" 525 | try: 526 | with binary_file_path.open("wb") as f: 527 | f.write(b"\x00\x01\x02\x03" * 100) 528 | 529 | r = client.get("/static/test.bin", headers={"Accept-Encoding": "gzip"}) 530 | assert "Content-Encoding" in r.headers 531 | assert "X-Flask-Squeeze-Minify" not in r.headers 532 | 533 | finally: 534 | binary_file_path.unlink(missing_ok=True) 535 | 536 | 537 | def test_empty_file_handling(client: FlaskClient) -> None: 538 | """Test handling of empty files.""" 539 | r = client.get("/static/empty.js", headers={"Accept-Encoding": "gzip"}) 540 | assert r.status_code == STATUS_CODE_OK_200 541 | 542 | 543 | def test_malformed_content_handling(client: FlaskClient) -> None: 544 | """Test handling of malformed CSS/JS content.""" 545 | static_dir = client.application.static_folder 546 | if static_dir is None: 547 | pytest.skip("Static directory not configured") 548 | 549 | # Test malformed CSS 550 | malformed_css_path = Path(static_dir) / "malformed.css" 551 | try: 552 | with malformed_css_path.open("w") as f: 553 | f.write("body { color: #000; /* unclosed comment") 554 | 555 | client.application.config.update({"SQUEEZE_MINIFY_CSS": True}) 556 | r = client.get("/static/malformed.css", headers={"Accept-Encoding": "gzip"}) 557 | # Should handle gracefully, might not minify but shouldn't crash 558 | assert r.status_code == STATUS_CODE_OK_200 559 | assert content_length_correct(r) 560 | 561 | finally: 562 | malformed_css_path.unlink(missing_ok=True) 563 | 564 | 565 | def test_security_headers_comprehensive(client: FlaskClient) -> None: 566 | """Test comprehensive security header behavior.""" 567 | # Test BREACH protection header 568 | r = client.get("/", headers={"Accept-Encoding": "gzip"}) 569 | breach_header = r.headers.get("X-Flask-Squeeze-Breach-Protection") 570 | assert breach_header is not None 571 | assert len(breach_header) > 0 572 | 573 | # Test that BREACH protection varies between requests 574 | r2 = client.get("/", headers={"Accept-Encoding": "gzip"}) 575 | breach_header2 = r2.headers.get("X-Flask-Squeeze-Breach-Protection") 576 | assert breach_header != breach_header2 # Should be different 577 | 578 | # Test Vary header 579 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 580 | vary_header = r.headers.get("Vary", "") 581 | assert "Accept-Encoding" in vary_header 582 | 583 | 584 | def test_content_length_edge_cases(client: FlaskClient) -> None: 585 | """Test edge cases around content length handling.""" 586 | # Test exactly at min size threshold 587 | min_size_threshold = 100 588 | client.application.config.update({"SQUEEZE_MIN_SIZE": min_size_threshold}) 589 | 590 | static_dir = client.application.static_folder 591 | if static_dir is None: 592 | pytest.skip("Static directory not configured") 593 | 594 | # Create file exactly one above the threshold 595 | 596 | threshold_file_path = Path(static_dir) / "threshold.js" 597 | try: 598 | with threshold_file_path.open("w") as f: 599 | f.write("x" * (min_size_threshold + 1)) # Create a file of size min_size_threshold + 1 600 | 601 | r = client.get("/static/threshold.js", headers={"Accept-Encoding": "gzip"}) 602 | 603 | assert "Content-Encoding" in r.headers 604 | 605 | finally: 606 | threshold_file_path.unlink(missing_ok=True) 607 | 608 | # Create file exactly one below the threshold 609 | 610 | below_threshold_file_path = Path(static_dir) / "below_threshold.js" 611 | try: 612 | with below_threshold_file_path.open("w") as f: 613 | f.write("x" * (min_size_threshold - 1)) # Create a file of size min_size_threshold 614 | 615 | r = client.get("/static/below_threshold.js", headers={"Accept-Encoding": "gzip"}) 616 | 617 | assert "Content-Encoding" not in r.headers 618 | finally: 619 | below_threshold_file_path.unlink(missing_ok=True) 620 | 621 | 622 | def test_configuration_validation(client: FlaskClient) -> None: 623 | """Test various configuration combinations.""" 624 | configs_to_test: list[dict[str, bool | int]] = [ 625 | # All disabled 626 | { 627 | "SQUEEZE_COMPRESS": False, 628 | "SQUEEZE_MINIFY_JS": False, 629 | "SQUEEZE_MINIFY_CSS": False, 630 | "SQUEEZE_MINIFY_HTML": False, 631 | }, 632 | # Only compression 633 | { 634 | "SQUEEZE_COMPRESS": True, 635 | "SQUEEZE_MINIFY_JS": False, 636 | "SQUEEZE_MINIFY_CSS": False, 637 | "SQUEEZE_MINIFY_HTML": False, 638 | }, 639 | # Only minification 640 | { 641 | "SQUEEZE_COMPRESS": False, 642 | "SQUEEZE_MINIFY_JS": True, 643 | "SQUEEZE_MINIFY_CSS": True, 644 | "SQUEEZE_MINIFY_HTML": True, 645 | }, 646 | # Extreme quality settings 647 | { 648 | "SQUEEZE_LEVEL_GZIP_STATIC": 9, 649 | "SQUEEZE_LEVEL_BROTLI_STATIC": 11, 650 | "SQUEEZE_LEVEL_DEFLATE_STATIC": 9, 651 | }, 652 | ] 653 | 654 | for config in configs_to_test: 655 | client.application.config.update(config) 656 | 657 | # Test should not crash with any config 658 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 659 | assert r.status_code == STATUS_CODE_OK_200 660 | assert content_length_correct(r) 661 | 662 | 663 | def test_cache_key_normalization(client: FlaskClient) -> None: 664 | """Test that cache keys are properly normalized.""" 665 | # Test paths with special characters 666 | paths_to_test = [ 667 | "/static/file-with-dashes.js", 668 | "/static/file_with_underscores.js", 669 | "/static/file.with.dots.js", 670 | "/static/deeply/nested/file.js", 671 | ] 672 | 673 | static_dir = client.application.static_folder 674 | if static_dir is None: 675 | pytest.skip("Static directory not configured") 676 | 677 | created_files: list[Path] = [] 678 | try: 679 | for path in paths_to_test: 680 | file_path = Path(static_dir) / Path(path).name 681 | with file_path.open("w") as f: 682 | f.write("var test = 1;") 683 | created_files.append(file_path) 684 | 685 | # Test that these can be cached without issues 686 | r = client.get(path, headers={"Accept-Encoding": "gzip"}) 687 | if r.status_code == STATUS_CODE_OK_200: # File exists 688 | assert content_length_correct(r) 689 | 690 | finally: 691 | for file_path in created_files: 692 | file_path.unlink(missing_ok=True) 693 | 694 | 695 | def test_memory_usage_stability(client: FlaskClient) -> None: 696 | """Test that memory usage remains stable with many requests.""" 697 | import gc 698 | 699 | # Get initial memory baseline 700 | gc.collect() 701 | initial_objects = len(gc.get_objects()) 702 | 703 | # Make many requests 704 | for _ in range(100): 705 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 706 | assert r.status_code == STATUS_CODE_OK_200 707 | 708 | # Check memory didn't grow significantly 709 | gc.collect() 710 | final_objects = len(gc.get_objects()) 711 | growth = final_objects - initial_objects 712 | 713 | # Allow some growth but not excessive (arbitrary threshold) 714 | max_objects_growth = 1100 715 | assert growth < max_objects_growth, f"Excessive memory growth: {growth} objects" 716 | 717 | 718 | def test_error_recovery_after_failures(client: FlaskClient) -> None: 719 | """Test that the system recovers properly after various failures.""" 720 | # Test recovery after cache directory becomes unavailable 721 | with tempfile.TemporaryDirectory() as temp_dir: 722 | cache_dir = Path(temp_dir) / "cache" 723 | client.application.config.update({"SQUEEZE_CACHE_DIR": str(cache_dir)}) 724 | 725 | from flask_squeeze import Squeeze 726 | 727 | squeeze = Squeeze() 728 | squeeze.init_app(client.application) 729 | 730 | # First request should work 731 | r1 = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 732 | assert r1.status_code == STATUS_CODE_OK_200 733 | 734 | # Simulate cache directory becoming unavailable 735 | try: 736 | cache_dir.chmod(0o000) # Remove all permissions 737 | 738 | # Should still work, just without caching 739 | r2 = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 740 | assert r2.status_code == STATUS_CODE_OK_200 741 | assert content_length_correct(r2) 742 | 743 | finally: 744 | # Restore permissions for cleanup 745 | with contextlib.suppress(Exception): 746 | cache_dir.chmod(0o755) 747 | 748 | 749 | def test_header_preservation(client: FlaskClient) -> None: 750 | """Test that important headers are preserved during processing.""" 751 | r = client.get("/static/jquery.js", headers={"Accept-Encoding": "gzip"}) 752 | 753 | # Content-Length should be accurate 754 | assert content_length_correct(r) 755 | 756 | # ETag should be preserved if present 757 | if "ETag" in r.headers: 758 | assert r.headers["ETag"] 759 | 760 | # Last-Modified should be preserved if present 761 | if "Last-Modified" in r.headers: 762 | assert r.headers["Last-Modified"] 763 | -------------------------------------------------------------------------------- /tests/test_app/static/main.js: -------------------------------------------------------------------------------- 1 | let content = `Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 2 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 3 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 4 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 5 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 6 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 7 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 8 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 9 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 10 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 11 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 12 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 13 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 14 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 15 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 16 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 17 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 18 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 19 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 20 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 21 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 22 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 23 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 24 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 25 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 26 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 27 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 28 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 29 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 30 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 31 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 32 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 33 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 34 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 35 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 36 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 37 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 38 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 39 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 40 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 41 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 42 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 43 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 44 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 45 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 46 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 47 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 48 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 49 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 50 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 51 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 52 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 53 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 54 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 55 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 56 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 57 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 58 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 59 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 60 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 61 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 62 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 63 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 64 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 65 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 66 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 67 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 68 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 69 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 70 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 71 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 72 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 73 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 74 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 75 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 76 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 77 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 78 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 79 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 80 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 81 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 82 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 83 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 84 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 85 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 86 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 87 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 88 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 89 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 90 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 91 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 92 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 93 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. 94 | Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. 95 | Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo consequat. 96 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis. 97 | At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua. est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat. 98 | Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus. 99 | Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. 100 | Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore magna aliquam erat volutpat. ` 101 | 102 | console.log(`main.js has str on length: ${content.length}`) -------------------------------------------------------------------------------- /tests/test_app/static/jquery.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v3.6.4 | (c) OpenJS Foundation and other contributors | jquery.org/license */ 2 | !function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,y=n.hasOwnProperty,a=y.toString,l=a.call(Object),v={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.4",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&v(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!y||!y.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ve(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace(B,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ye(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ve(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.cssHas=ce(function(){try{return C.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],y=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&y.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||y.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||y.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||y.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||y.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||y.push(".#.+[+~]"),e.querySelectorAll("\\\f"),y.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&y.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&y.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&y.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),y.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),d.cssHas||y.push(":has"),y=y.length&&new RegExp(y.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),v=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&v(p,e)?-1:t==C||t.ownerDocument==p&&v(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!y||!y.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),v.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",v.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",v.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ye(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ve(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var Ut,Xt=[],Vt=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=Xt.pop()||S.expando+"_"+Ct.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Vt.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Vt.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Vt,"$1"+r):!1!==e.jsonp&&(e.url+=(Et.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,Xt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),v.createHTMLDocument=((Ut=E.implementation.createHTMLDocument("").body).innerHTML="
",2===Ut.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(v.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return B(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=_e(v.pixelPosition,function(e,t){if(t)return t=Be(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return B(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0