pax_global_header00006660000000000000000000000064146251773460014531gustar00rootroot0000000000000052 comment=7f938cf4e87b22abc5efd4155b78ea8127eefc4b zeal-0.7.1/000077500000000000000000000000001462517734600124715ustar00rootroot00000000000000zeal-0.7.1/.editorconfig000066400000000000000000000005211462517734600151440ustar00rootroot00000000000000# EditorConfig (https://editorconfig.org) root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true indent_size = 4 indent_style = space max_line_length = 100 trim_trailing_whitespace = true [{*.cmake,CMakeLists.txt}] # TODO: indent_size = 2 indent_size = 4 [*.json] indent_size = 2 [*.{yaml,yml}] indent_size = 2 zeal-0.7.1/.gitattributes000066400000000000000000000005561462517734600153720ustar00rootroot00000000000000# Autodetect text files * text=auto # Enforce text mode *.c text *.cmake text *.conf text *.cpp text *.css text *.desktop text *.h text *.html text *.in text *.md text *.pri text *.pro text *.qrc text *.ui text *.yml text *.yaml text CMakeLists.txt text COPYING text # Binary files *.icns binary *.ico binary *.png binary *.rtf binary *.woff binary *.woff2 binary zeal-0.7.1/.github/000077500000000000000000000000001462517734600140315ustar00rootroot00000000000000zeal-0.7.1/.github/CODEOWNERS000066400000000000000000000002061462517734600154220ustar00rootroot00000000000000# This file allows automatic assignment of pull requests. # See https://help.github.com/articles/about-codeowners/ * @trollixx zeal-0.7.1/.github/dependabot.yml000066400000000000000000000002611462517734600166600ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" commit-message: prefix: ci(github) labels: [] zeal-0.7.1/.github/workflows/000077500000000000000000000000001462517734600160665ustar00rootroot00000000000000zeal-0.7.1/.github/workflows/analyze-codeql.yaml000066400000000000000000000023561462517734600216700ustar00rootroot00000000000000name: CodeQL Scan on: push: branches: [main] pull_request: # The branches below must be a subset of the branches above. branches: [main] schedule: - cron: '0 8 * * 6' concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: analyze-codeql: name: Analyze runs-on: ubuntu-22.04 permissions: security-events: write steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v3 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ libarchive-dev \ libgl1-mesa-dev \ libqt6opengl6-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ qt6-base-private-dev \ qt6-webengine-dev \ qt6-webengine-dev-tools - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ninja-multi buildPreset: ninja-multi-release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 zeal-0.7.1/.github/workflows/analyze-coverity.yaml000066400000000000000000000030661462517734600222640ustar00rootroot00000000000000name: Coverity Scan on: push: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: analyze-coverity: name: Analyze if: github.repository == 'zealdocs/zeal' runs-on: ubuntu-22.04 steps: - name: Checkout Repository uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ git \ libarchive-dev \ libgl1-mesa-dev \ libqt6opengl6-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ qt6-base-private-dev \ qt6-webengine-dev \ qt6-webengine-dev-tools - name: Configure run: cmake -B build -G Ninja - name: Retrieve Application Version run: | zeal_version=$(> $GITHUB_ENV - name: Coverity Scan uses: vapier/coverity-scan-action@v1 with: command: ninja -C build version: ${{ env.ZEAL_VERSION }} email: ${{ secrets.COVERITY_SCAN_EMAIL }} token: ${{ secrets.COVERITY_SCAN_TOKEN }} - name: Upload Build Log uses: actions/upload-artifact@v4 with: name: build-log path: cov-int/build-log.txt if-no-files-found: ignore zeal-0.7.1/.github/workflows/appimage/000077500000000000000000000000001462517734600176515ustar00rootroot00000000000000zeal-0.7.1/.github/workflows/appimage/Dockerfile000066400000000000000000000022471462517734600216500ustar00rootroot00000000000000FROM ubuntu:jammy RUN apt-get update -q -y \ # Install appimage-builder and appimagetool dependencies. && DEBIAN_FRONTEND="noninteractive" apt-get install -q -y --no-install-recommends \ appstream curl desktop-file-utils fakeroot file git gnupg patchelf squashfs-tools zsync \ python3-pip python3-setuptools python3-wheel \ && \ # Install appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling. cd /tmp && \ curl -sLO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage && \ chmod +x appimagetool-x86_64.AppImage && \ ./appimagetool-x86_64.AppImage --appimage-extract && \ mv squashfs-root/ /opt/appimagetool.AppDir && \ ln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool && \ rm appimagetool-x86_64.AppImage && \ cd - && \ # Install appimage-builder. pip3 install git+https://github.com/AppImageCrafters/appimage-builder.git@669213cb730e007d5b316ed19b39691fbdcd41c4 && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # Set entrypoint. COPY entrypoint.sh /entrypoint.sh ENTRYPOINT ["/entrypoint.sh"] zeal-0.7.1/.github/workflows/appimage/action.yaml000066400000000000000000000006551462517734600220200ustar00rootroot00000000000000name: 'AppImage Builder' description: 'Create an AppImage with appimage-builder.' inputs: recipe: description: 'Path to the appimage-builder recipe.' required: true default: 'AppImageBuilder.yml' apt_dependencies: description: 'List of packages to install with apt-get.' required: false runs: using: 'docker' image: 'Dockerfile' args: - ${{ inputs.recipe }} - ${{ inputs.apt_dependencies }} zeal-0.7.1/.github/workflows/appimage/entrypoint.sh000077500000000000000000000006251462517734600224260ustar00rootroot00000000000000#!/bin/bash # Should be in .gitignore. export APPIMAGE_BUILD_DIR=build.appimage # Install dependencies if [ ! -z ${INPUT_APT_DEPENDENCIES+x} ]; then apt-get update -q -y apt-get install -q -y --no-install-recommends ${INPUT_APT_DEPENDENCIES} fi # Run appimage-builder appimage-builder --skip-test --build-dir ${APPIMAGE_BUILD_DIR} --appdir ${APPIMAGE_BUILD_DIR}/AppDir --recipe ${INPUT_RECIPE} zeal-0.7.1/.github/workflows/build-check.yaml000066400000000000000000000155661462517734600211410ustar00rootroot00000000000000name: Build Check on: push: branches: [main] pull_request: branches: [main] concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-ubuntu: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - { name: "Ubuntu 20.04 / Qt 5", os: ubuntu-20.04, qt_packages: "libqt5x11extras5-dev qt5-default qtwebengine5-dev", configurePreset: "ninja-multi", buildPreset: "ninja-multi-release" } - { name: "Ubuntu 20.04 / Qt 5 / Portable", os: ubuntu-20.04, qt_packages: "libqt5x11extras5-dev qt5-default qtwebengine5-dev", configurePreset: "ninja-multi-portable", buildPreset: "ninja-multi-portable-release" } - { name: "Ubuntu 22.04 / Qt 6", os: ubuntu-22.04, qt_packages: "libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools", configurePreset: "ninja-multi", buildPreset: "ninja-multi-release" } - { name: "Ubuntu 22.04 / Qt 6 / Portable", os: ubuntu-22.04, qt_packages: "libgl1-mesa-dev libqt6opengl6-dev qt6-base-private-dev qt6-webengine-dev qt6-webengine-dev-tools", configurePreset: "ninja-multi-portable", buildPreset: "ninja-multi-portable-release" } steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install Dependencies run: | sudo apt-get -y -qq update sudo apt-get -y -qq --no-install-recommends install \ cmake \ extra-cmake-modules \ libarchive-dev \ libsqlite3-dev \ libvulkan-dev \ libxcb-keysyms1-dev \ ninja-build \ ${{ matrix.config.qt_packages }} - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} build-windows: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - { name: "Windows Server 2019 / Qt 5", os: windows-2019, qt_modules: "qtwebengine", qt_version: "5.15.2", configurePreset: "ninja-multi-vcpkg", buildPreset: "ninja-multi-vcpkg-release", publishArtifacts: false } - { name: "Windows Server 2019 / Qt 5 / Portable", os: windows-2019, qt_modules: "qtwebengine", qt_version: "5.15.2", configurePreset: ninja-multi-vcpkg-portable, buildPreset: ninja-multi-vcpkg-portable-release, publishArtifacts: false } - { name: "Windows Server 2022 / Qt 6", os: windows-2022, qt_modules: "qtwebengine qtwebchannel qtpositioning", qt_version: "6.6.3", configurePreset: ninja-multi-vcpkg, buildPreset: ninja-multi-vcpkg-release, publishArtifacts: true } - { name: "Windows Server 2022 / Qt 6 / Portable", os: windows-2022, qt_modules: "qtwebengine qtwebchannel qtpositioning", qt_version: "6.6.3", configurePreset: ninja-multi-vcpkg-portable, buildPreset: ninja-multi-vcpkg-portable-release, publishArtifacts: true } env: VCPKG_DEFAULT_TRIPLET: x64-windows-release steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Prepare vcpkg uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: ${{ runner.workspace }}/vcpkg vcpkgGitCommitId: fba75d09065fcc76a25dcf386b1d00d33f5175af # 2024.02.14 - name: Install Qt uses: jurplel/install-qt-action@v3 with: arch: win64_msvc2019_64 modules: ${{ matrix.config.qt_modules }} version: ${{ matrix.config.qt_version }} cache: true extra: --external 7z - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} - name: Retrieve Application Version run: | $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version Write-Output "Zeal Version: $zeal_version" "ZEAL_VERSION=$zeal_version" >> $env:GITHUB_ENV - name: Package if: matrix.config.publishArtifacts run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package env: CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }} CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }} - name: Upload ZIP Artifacts if: matrix.config.publishArtifacts uses: actions/upload-artifact@v4 with: name: zeal-${{ env.ZEAL_VERSION }}${{ matrix.config.configurePreset == 'ninja-multi-vcpkg-portable' && '-portable' || '' }}-windows-x64.zip path: | build/${{ matrix.config.configurePreset }}/zeal-*.zip build/${{ matrix.config.configurePreset }}/zeal-*.zip.sha256 - name: Upload MSI Artifacts if: matrix.config.publishArtifacts && matrix.config.configurePreset == 'ninja-multi-vcpkg' uses: actions/upload-artifact@v4 with: name: zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi path: | build/${{ matrix.config.configurePreset }}/zeal-*.msi build/${{ matrix.config.configurePreset }}/zeal-*.msi.sha256 build-appimage: name: AppImage runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build AppImage uses: ./.github/workflows/appimage/ with: recipe: pkg/appimage/appimage-amd64.yaml apt_dependencies: >- build-essential cmake extra-cmake-modules libappindicator-dev libarchive-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev appstream - name: Upload AppImage uses: actions/upload-artifact@v4 with: name: zeal-dev-x86_64.AppImage # TODO: Provide real version. path: zeal-*.AppImage zeal-0.7.1/.github/workflows/lock.yaml000066400000000000000000000006731462517734600177100ustar00rootroot00000000000000name: Lock Issues on: schedule: - cron: "0 0 * * *" workflow_dispatch: permissions: issues: write pull-requests: write concurrency: group: lock jobs: lock-issues: name: Lock Old Issues if: github.repository == 'zealdocs/zeal' runs-on: ubuntu-latest steps: - uses: dessant/lock-threads@v5 with: issue-inactive-days: 180 issue-lock-reason: "" process-only: issues zeal-0.7.1/.github/workflows/release.yaml000066400000000000000000000102141462517734600203700ustar00rootroot00000000000000name: Release on: push: tags: - "v*.*.*" # Required for creating GitHub release. permissions: contents: write concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: build-windows: name: ${{ matrix.config.name }} runs-on: ${{ matrix.config.os }} strategy: fail-fast: false matrix: config: - { name: "Windows Server 2022 / Qt 6", os: windows-2022, qt_modules: "qtwebengine qtwebchannel qtpositioning", qt_version: "6.6.3", configurePreset: ninja-multi-vcpkg, buildPreset: ninja-multi-vcpkg-release, buildSourcePackage: true } - { name: "Windows Server 2022 / Qt 6 / Portable", os: windows-2022, qt_modules: "qtwebengine qtwebchannel qtpositioning", qt_version: "6.6.3", configurePreset: ninja-multi-vcpkg-portable, buildPreset: ninja-multi-vcpkg-portable-release } env: VCPKG_DEFAULT_TRIPLET: x64-windows-release ZEAL_RELEASE_BUILD: ON steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Prepare vcpkg uses: lukka/run-vcpkg@v11 with: vcpkgDirectory: ${{ runner.workspace }}/vcpkg vcpkgGitCommitId: fba75d09065fcc76a25dcf386b1d00d33f5175af # 2024.02.14 - name: Install Qt uses: jurplel/install-qt-action@v3 with: arch: win64_msvc2019_64 modules: ${{ matrix.config.qt_modules }} version: ${{ matrix.config.qt_version }} cache: true extra: --external 7z - name: Configure & Build uses: lukka/run-cmake@v10 with: configurePreset: ${{ matrix.config.configurePreset }} buildPreset: ${{ matrix.config.buildPreset }} - name: Retrieve Application Version run: | $zeal_version = Get-Content build/${{ matrix.config.configurePreset }}/zeal_version Write-Output "Zeal Version: $zeal_version" "ZEAL_VERSION=$zeal_version" >> $env:GITHUB_ENV - name: Package run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package env: CODESIGN_CERTIFICATE_BASE64: ${{ secrets.CODESIGN_CERTIFICATE_BASE64 }} CODESIGN_PASSWORD: ${{ secrets.CODESIGN_PASSWORD }} - name: Package Source if: matrix.config.buildSourcePackage run: cmake --build build --preset ${{ matrix.config.buildPreset }} --target package_source - name: Update GitHub Release uses: softprops/action-gh-release@v2 with: draft: true # Only upload the following artifacts: # - Source packages. # - Non-portable MSI package. # - Portable 7-Zip and ZIP packages. files: | build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-portable-windows-x64.* build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}-windows-x64.msi* build/${{ matrix.config.configurePreset }}/zeal-${{ env.ZEAL_VERSION }}.* build-appimage: name: AppImage runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Build AppImage uses: ./.github/workflows/appimage/ with: recipe: pkg/appimage/appimage-amd64.yaml apt_dependencies: >- build-essential cmake extra-cmake-modules libappindicator-dev libarchive-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev - name: Generate Digest Files run: for file in zeal-*.AppImage*; do sha256sum $file > $file.sha256; done - name: Update GitHub Release uses: softprops/action-gh-release@v2 with: draft: true fail_on_unmatched_files: true files: | zeal-*.AppImage* zeal-0.7.1/.gitignore000066400000000000000000000005711462517734600144640ustar00rootroot00000000000000# C++ objects and libs *.a *.dll *.dylib *.la *.lai *.lo *.o *.slo *.so # CMake build.*/ build/ CMakeLists.txt.user CMakeUserPresets.json # AppImage Builder *.AppImage *.AppImage.zsync appimage-builder-cache/ squashfs-root/ # WiX Toolset *.msi *.wixobj *.wixpdb # Qt Creator *.autosave # VS Code .vscode/ # Linux appdata /assets/freedesktop/org.zealdocs.zeal.appdata.xml zeal-0.7.1/CHANGELOG.md000066400000000000000000000001571462517734600143050ustar00rootroot00000000000000# Changelog The version history is available on [GitHub Releases](https://github.com/zealdocs/zeal/releases). zeal-0.7.1/CMakeLists.txt000066400000000000000000000042101462517734600152260ustar00rootroot00000000000000cmake_minimum_required(VERSION 3.16.3) # CMake options. set(CMAKE_ERROR_DEPRECATED TRUE) set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") project(Zeal VERSION 0.7.1 DESCRIPTION "A simple documentation browser." HOMEPAGE_URL "https://zealdocs.org" LANGUAGES CXX ) # Set to TRUE for a tagged release. # NOTE: Don't forget to add a new release entry in the AppStream metadata! if(NOT ZEAL_RELEASE_BUILD AND DEFINED ENV{ZEAL_RELEASE_BUILD}) set(ZEAL_RELEASE_BUILD $ENV{ZEAL_RELEASE_BUILD}) endif() # Project information. set(PROJECT_COMPANY_NAME "Oleg Shparber") set(PROJECT_COPYRIGHT "© 2013-2023 Oleg Shparber and other contributors") # Find available major Qt version. It will be stored in QT_VERSION_MAJOR. find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core) if(QT_VERSION_MAJOR EQUAL 6) set(QT_MINIMUM_VERSION 6.2.0) else() set(QT_MINIMUM_VERSION 5.9.5) endif() # Determine version for dev builds. if(NOT ZEAL_RELEASE_BUILD) message(NOTICE "Building unreleased code. Proceed at your own risk!") # TODO: Add support for metadata passed from env, e.g. aur, appimage, etc. include(GetVersionFromGit) if(Zeal_GIT_VERSION_SHA) # Extra check in case we forgot to bump version in project() directive. if(NOT PROJECT_VERSION_PATCH EQUAL Zeal_GIT_VERSION_PATCH_NEXT) message(WARNING "Incorrect patch version! Forgot to bump?") endif() set(ZEAL_VERSION_SUFFIX "-dev.${Zeal_GIT_VERSION_AHEAD}+${Zeal_GIT_VERSION_SHA}") else() set(ZEAL_VERSION_SUFFIX "-dev") endif() endif() set(ZEAL_VERSION_FULL "${Zeal_VERSION}${ZEAL_VERSION_SUFFIX}") message(NOTICE "Calculated Zeal version: ${ZEAL_VERSION_FULL}") file(WRITE "${CMAKE_BINARY_DIR}/zeal_version" ${ZEAL_VERSION_FULL}) # A custom target to print the full version. # Usage: cmake --build build --preset ninja-multi-vcpkg-release --target zeal_version add_custom_target(zeal_version COMMAND ${CMAKE_COMMAND} -E echo "Zeal version: ${ZEAL_VERSION_FULL}" VERBATIM ) if(${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.24.0") set(CMAKE_COMPILE_WARNING_AS_ERROR ON) endif() add_subdirectory(assets) add_subdirectory(src) zeal-0.7.1/CMakePresets.json000066400000000000000000000055661462517734600157260ustar00rootroot00000000000000{ "version": 3, "cmakeMinimumRequired": { "major": 3, "minor": 21, "patch": 0 }, "configurePresets": [ { "name": "ninja-multi", "generator": "Ninja Multi-Config", "binaryDir": "${sourceDir}/build/${presetName}", "warnings": { "deprecated": true, "dev": true, "uninitialized": true, "unusedCli": true } }, { "name": "ninja-multi-portable", "inherits": [ "ninja-multi", "config-portable" ] }, { "name": "ninja-multi-vcpkg", "condition": { "type": "equals", "lhs": "${hostSystemName}", "rhs": "Windows" }, "inherits": [ "ninja-multi" ], "cacheVariables": { "CMAKE_TOOLCHAIN_FILE": { "type": "FILEPATH", "value": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake" }, "X_VCPKG_APPLOCAL_DEPS_INSTALL": "ON" } }, { "name": "ninja-multi-vcpkg-portable", "inherits": [ "ninja-multi-vcpkg", "config-portable" ] }, { "name": "config-portable", "hidden": true, "cacheVariables": { "ZEAL_PORTABLE_BUILD": { "type": "BOOL", "value": "ON" } } }, { "name": "config-release", "hidden": true, "cacheVariables": { "ZEAL_RELEASE_BUILD": { "type": "BOOL", "value": "ON" } } } ], "buildPresets": [ { "name": "ninja-multi-debug", "configurePreset": "ninja-multi", "configuration": "Debug" }, { "name": "ninja-multi-release", "configurePreset": "ninja-multi", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-debug-portable", "configurePreset": "ninja-multi-portable", "configuration": "Debug" }, { "name": "ninja-multi-portable-release", "configurePreset": "ninja-multi-portable", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-vcpkg-debug", "configurePreset": "ninja-multi-vcpkg", "configuration": "Debug" }, { "name": "ninja-multi-vcpkg-release", "configurePreset": "ninja-multi-vcpkg", "configuration": "RelWithDebInfo" }, { "name": "ninja-multi-vcpkg-portable-debug", "configurePreset": "ninja-multi-vcpkg-portable", "configuration": "Debug" }, { "name": "ninja-multi-vcpkg-portable-release", "configurePreset": "ninja-multi-vcpkg-portable", "configuration": "RelWithDebInfo" } ], "testPresets": [ { "name": "ninja-multi-vcpkg-debug", "configurePreset": "ninja-multi-vcpkg", "configuration": "Debug" }, { "name": "ninja-multi-vcpkg-release", "configurePreset": "ninja-multi-vcpkg", "configuration": "RelWithDebInfo" } ] } zeal-0.7.1/COPYING000066400000000000000000000772471462517734600135450ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. zeal-0.7.1/README.md000066400000000000000000000061071462517734600137540ustar00rootroot00000000000000# Zeal [![Changelog](https://img.shields.io/github/release/zealdocs/zeal.svg?style=flat-square)](https://github.com/zealdocs/zeal/releases) [![Gitter](https://img.shields.io/gitter/room/zealdocs/zeal.svg?style=flat-square)](https://gitter.im/zealdocs/zeal) [![IRC](https://img.shields.io/badge/chat-on%20irc-blue.svg?style=flat-square)](https://web.libera.chat/#zealdocs) [![Telegram Channel](https://img.shields.io/badge/follow-on%20telegram-179cde.svg?style=flat-square)](https://telegram.me/zealdocs) [![Twitter](https://img.shields.io/badge/follow-on%20twitter-1da1f2.svg?style=flat-square)](https://twitter.com/zealdocs) [![Build Check](https://img.shields.io/github/actions/workflow/status/zealdocs/zeal/build-check.yaml?style=flat-square)](https://github.com/zealdocs/zeal/actions/workflows/build-check.yaml) [![Coverity Scan](https://img.shields.io/coverity/scan/4271.svg?style=flat-square)](https://scan.coverity.com/projects/4271) Zeal is a simple offline documentation browser inspired by [Dash](https://kapeli.com/dash). ![Screenshot](https://github.com/zealdocs/zeal/assets/714940/e8443bb4-ccb9-469b-89d6-b5b3bfc7e239) ## Download Get binary builds for Windows and Linux from the [download page](https://zealdocs.org/download.html). ## How to use After installing Zeal go to `Tools->Docsets`, select the ones you want, and click the `Download` button. ## How to compile ### Build dependencies * [CMake](https://cmake.org/). * [Qt](https://www.qt.io/) version 5.9.5 or above. Required module: Qt WebEngine Widgets. * [libarchive](https://libarchive.org/). * [SQLite](https://sqlite.org/). * X11 platforms only: Qt X11 Extras and `xcb-util-keysyms`. ### Build instructions ```sh cmake -B build cmake --build build ``` More detailed instructions are available in the [wiki](https://github.com/zealdocs/zeal/wiki). ## Query & Filter docsets You can limit the search scope by using ':' to indicate the desired docsets: `java:BaseDAO` You can also search multiple docsets separating them with a comma: `python,django:string` ## Command line If you prefer, you can start Zeal with a query from the command line: `zeal python:pprint` ## Create your own docsets Follow instructions in the [Dash docset generation guide](https://kapeli.com/docsets). ## Contact and Support We want your feedback! Here's a list of different ways to contact developers and request help: * Report bugs and submit feature requests to [GitHub issues](https://github.com/zealdocs/zeal/issues). * Reach developers and other Zeal users in `#zealdocs` IRC channel on [Libera Chat](https://libera.chat) ([web client](https://web.libera.chat/#zealdocs)). * Ask any questions in our [GitHub discussions](https://github.com/zealdocs/zeal/discussions). * Do not forget to follow [@zealdocs](https://twitter.com/zealdocs) on Twitter! * Finally, for private communication shoot an email to support@zealdocs.org. ## License This software is licensed under the terms of the GNU General Public License version 3 (GPLv3). Full text of the license is available in the [COPYING](COPYING) file and [online](https://www.gnu.org/licenses/gpl-3.0.html). zeal-0.7.1/assets/000077500000000000000000000000001462517734600137735ustar00rootroot00000000000000zeal-0.7.1/assets/CMakeLists.txt000066400000000000000000000000361462517734600165320ustar00rootroot00000000000000add_subdirectory(freedesktop) zeal-0.7.1/assets/freedesktop/000077500000000000000000000000001462517734600163065ustar00rootroot00000000000000zeal-0.7.1/assets/freedesktop/128-apps-zeal.png000066400000000000000000000551501462517734600212260ustar00rootroot00000000000000PNG  IHDR>aZ/IDATxweU ~i} UՕi !  ,<=~01c<A&8`1&0I \9pUH}k]]pqqqqqqqqqq_|MN*Gy-ow^~(59'Ҥ01Q(֒R$ҦXr)BjPZc"Z@Nr 1kJˇɉyrf?mq؊v!@kvѣ 1̰MgP* ń,q{2 bF,e hr_Ob>2f]u7 CGN* {C?0Ήŷ΁g/~˯.͓2/]差<'O[s|S~#|wwŗ]?GswU|)?̣unco22GW,_}W_u ?znYq/7Ox[̃r9ǫnMx{?7ξڠF+*fu;_ٖ~_Ox窲59C&S9KL3h]]9 [AUccO3} ^F ?'1(Wa8 yv;Q֡%n^}!&w8Ɵ|m] Go;nf '>|YA9.t=E\8\?z&&Uغx[&]U7/}xӛ~4?&Y6,Zi2:B$2YG!bya,ޓIM視x1;!@N++17FrNʁRD(k)cFW׳#֠8[,FU[ܣLGSFU&n/|C BTe Lc- 2EH9 I.s/ HYE4I>`T¬N awyC{˗[.eoI5M?RU*8gH9c=1%B y흷&o D۵5AR&k֊$,%eJu q$9BA1%'>3(I(IZ"} :%9(9\b{jrl@r(9JV΃ jȠc*M$ő(&S[I`;?H?_x&5zw_)|_xBrk gX5PN4 RJ>=$o>|w޲u"nw, hdrjb>Mm'3>''eQ&O>` 9C# ,f:A)HGJGybxhVc ?QAe!;Rr /e**Q&$RʐC!C@䘆({0#!pa}!zg9?w yo:w]_~am}O~tDΙ埻ʴ9K@դ'N3-?w`%EO }-nu"k (NBΠ!uշ=!f0 ]T9DUUAA` Xn:$ǡG.ʠFöG7*DrE)MΑ`V&瀱NsT =iNENYm ˟$Almp% )sÞü偧xNP;% 7$y>!tnmRRʤ$7hB)¬'Djt]*#H0%bBD][RΘ9+BjLDŽEkR1$5`5 C{0lI\OG3%?$M1"Jr ne,7O|sɘ۱#?kO=c('. FK hYKk4$C$(!E[{n6"R"L\$NLY2$fT*-wwhe PZ?f-~sUW" E.;vPIzO*%RTUO;QhcH>RZoVOyL96$ݮ$yƠDh{1>A>ܻm)"ԣ=?'?  C9xTE]K^,2C7xR4́3/'gI>+3tm%8);,m`Eѵ1L&$uME e5*f|ۓX! ;'¨".:XܾD$amVsm2<*ghnQ1;/5$LmI>Ƒ|Ϫv!n`Y_`'Vr/޵ߚ*_yOB3XS SaĘhFW)uXYmbAЩ 7H$)2ZHa%De" Uq%9hI Rr%*rӖwRFI(C6D$_$m3rޭ62$3+;ta։ۏ-aq$ͯ0Qp' [dWk ۘƱ<'ԇolb!b/'L(p}`-HyE[ p<~43̀2K[$Z\ B\ (QVAuk@kHXtRY#'vd1N\,Cns!ĹG7{tnjRL1fd n*p8DV@.<2ئ"+P4佀dF!T .4  rAQ;.z9}ĭMZBJT ihqcr ;Q4(yO-.?>DUXkh5ʈ&J̐ijK c46?ɰoyivcF g%-v ux){)3O,=eK#9oI>IL4Z.?k]ϊ`t)f |I@ԅB+X9ژZ&0]a W$9-h!ggx(EĮ>FJ( y>ͳ(i*`J1$49gB4}FKCH&ƌ/5"EzR@`JU/ƬAUcƷR- g٢Umw)VG>S7i0ƀQʉwP&e¬[if[b KʎJ mǫ< $ d%DL kY=јfTQ|ҧcMdy|ss|Z+cN:iC@)]эcXl)PڢMK =L>/q૿ >pqf|@<5O?_O~$%Њz"ׯE;߿~g oAʑ3Y|/1*q[%l-@k|~m a[yfx$޶~Wr Ukþ{uAuKǙ6'Hs 'N¯筏ȉ##nR ĮRq$z\gW1#'uɥ~fʡDO?c"^p KITDn! =.RzM^o sRgqxʖNxCBh#b mq€VƢNͤZ#3B $EF):Wˋؓ;Ο׎Ynτ H8YxqFKu%7*#y|>ƂRW?'oϤUҞ@ąK1lԻW8q0rNYq=YF7>0|L!Q,6Zm6@eP1NVovNBc=v:HTNP[8Rt?9G0l,*JZiS[g=d<$TƏEMaE"lrLkcVOu9xBawYvESFZi|~`*WeGJbbEpao[%*N+PcK 8[&!L Vrl4Aǔ3\]Y$GZ>t-_*] eQP9iVwϻq1㩟y)V9%wSerPH"l4qH%އ2l jb b?@cO}x/8@+X 13OCX_[X0D>P׆n>fC)ucc䋧=r^m(J-q;`kG&TZNʼ,n -ʔ~cFi> )1o#xH1aWFp~2 :S 7!$ؒؑŸMTm0VSϗH N*P(R&pn[ #通0\3X.9O%\MC)3O3CH4I{^.L7_ϗV6.#SI#0DZ;b'J2%07n[=L{vS()g9Af:";b)vYLrLmr5Ac|Wyw_9NϣFk=}$lva`L7aōݒf3j% /l)AJ @#§l5zI}>u,kSZaNksR(:S5' ݚ/܌[KPL-$X )4a /,ʐB)c֤Y$[ QAxir X@}fn?B'ɟG<TR(9iLeȹT U`Gj, kiZ) 0IhP4&Hp*tGq>f ~ F]y;XiF1_,P$W'( TUR*0R]xэ!)P"($h!s"^Zd 4q1zBYg',b& TL6UB8H/AՆu9ri AX ‹HPr`Y 03 *BϘ f{vR9Bu:&QX%qDGh أ|-ד7NXW21Apb,nn,X Ppɘ~{H9 1?g4rB줦XV|UeaH] -/y~)1]l'#Oرܸ"U [8@JCB(%h{ 4:'7R2#ٌFWn)QUrZg[]g45 V kBP &TPb7HɅdҠKfj)҇T.l j hJMlxncYn) CQ Ɖשh+y2Z/? u}!9cƇFh~gJxLJ&[me ]nxmϝU:vvIg)C `NJʒi ,TFl%}(AhޅD3e*FYUngϫ?Y`#<3'. A(WXWB}ҙj@UNnؕ{Q$! `F>Ҋ)m+($~\O۶2'ɠj@!T)SC{o}D`me7J5R-B- hWFd!|+ QE a`j0"cj?_Вt21{_\PM(]W tXk UFƔQ^ 78$pQJe[f'~kA31hkWsYl.Y=qe:؇T2{y?%=}o(zĈYmU0kq#;]]B֝GV8VJ:ʌ\T$FSNAYU/ᆿ-mlJŸX5*ZJfYk03Z5[$k!羴9$|_}Oe&xULc22JPkI g5ua(զx+4T GFVIZS9C=ꈮ+ԦJhUoӄn'aUF6Pz15d:(E% }7Oߌ?? )wT"!6ܡ9K" UZKd8q24mV}B9VF[NC$ӄ.6=$^ya膫hΡB nz 6ʒ24Rs/M,=% _E2K}D9_xl 3 8d'Ͳq34wq7}?{>M_l.><>bGV}jsI!㯦 CWR0iP@WF# 1$*:O Cn{)^A3ŅtBFΐ| Bl]%B kE2Pc+zz(~8#>J2n2֯/}G<~Ӵ;o2z{ۑ-)co1N-O׆+g>yh%1*&T!,pk#r?X¬I+OX,G>uUaCu5U^ƽw4^FvZE;C;!QU & :y5\Gx wu/%:_NLeWk28!2 ML ^Bjn.,y'!]Ѝ4~%B5]+6FJ(Zy/ 8"=4%hNpF KVZT<$: q@lhK2ZgHK *r ~:+D ɤfFӌ*L!l~@2~lNym>H:}oQ|! #%}Uk3›_ w4zN>ɤ2 7O_5\'m'Iw7A0 Ƣ+Q7e\cD+ɉ؄ :v; J'Y$ZZa Gh5X|XJR L7#k y/6 Hu$r!@p !den@kVk"3 =mڍ ꧴%9DHNI>aW9ᵄ?Q0.n`r4f yU^ƭU%jѦNCee4Đ=u0f%=n\;6DLѵ(b [(ԉs(6Za ]ĭB0j9Sf>@nivI'/q5!]EB0ҧFdbLԍ %_J!DvC|OAHcRҽ¦4T#-^5zZBD/5xuĘŅ _,m3}dv[H>Qnn&l" )y R2QNHy\I@I*t"SŌJv '{ͅTO&l=# PXDkp5WY B%%ɟ[uҝ"qʙTeks!@QZ]QXf_Φ"S2a4 GZnɬ2n o$NWxJc*LP`j!2)3$&) ru-Զ!Qk슓"b0CtI1Di=L#. %ڂU/x_ k$%TZd3;I8poIޱA) .> 4ϖj`%ZK NIzb嫹G/*}W3!), %A'>@H.'Q6ZԺLr-R"A.w}!+vwcE2exJD38XL-"0\1#aRJ JkV_vnb_pr ("f>P WƹBLĘqEgvbO=rϝFbMV_%: >쟂R[~S,}6hm<~yG?ߒlkm1cʹ!}ig'$ =v4DP`% ' 3rR94;Z[:mG7>~+kwaTq:̊=ì[w@&ܓ`4WSfeX̱iʅ} WW^yqw}9q%'xXoYmHA$UX̩mlmeKFT" ;E'!IY$6F]!c#*aVI@GI"ݤ^At'>8?-}#~#鑤>;7g5qf3CYRqr(8f"4JY1)TN ĺqy&vᬦ/wIЊڄ~",D_x(+K6#>`_kO?S?rg+Z!,"-JbV\:iBe8T%0jUJ8D)mL, No4+5GrV 3iӊ8DXlmcԆEt d\x." %(2VG҈g5 ZK(0RH2U3"tN3HH,RJݻw3OhXfErp*%eWfe i{q90 =_MakER[tI]ؙcVOQ\DA(S)R7PXQ;1 󡨉I{X+7jGȡ j8j!ߺXʈ)SM\|jb$wQ; %"ʴTqDڴ?1Lcx`S.dPJ3jZŞ.Lj{6ӥhtҜ!!7~ŀ[#opǮHZ uiJ+ГUƷM=͡{j{sW~ gO0{AO>ą?y7 3'}{\\_xnދ!S7!gyͯc8߸/0`Jܬ&,r牬2˭)NP0ʅt!3PAL#w2.&79iy3ηĽϷ?%Ra!LV|H`OOq~8Ss? 2T+c峣@3 `ކW"M׉[H*Z XkEw.naD,qUeƜH^H| `f8Eh<`$dfvD d'+~@?,L )ԴO䤤'$O@- DXr݌DsV&zߡuw}JDj#/? MK4մ19`"9kMEb!ROkY/ X/?jM<{*'\ky n a3UeF)USڳ=$ q!qڶ2Ʒ^<ŠuWE\#dUSJYd"T#I6A(}GHcl7iz(ܣֆ;1YnŒW@yYMϷixb C.Bl\L9& 9 U-C][1z~wc=݅JOb6rB-:vaD2D< jK? e1*" R2Ӟ_P5' rAIwrh5hΥ;2f~!2,"n=H(YǸK+bb'- gGD/RR&ؑ+Tk51F]dЖ~ՀxuT!{H LsJ #EY%(niEΙ~&b V_wl_Zx, M^e|81l ATmy뢓c'KPTb"1ۆQ*@h4dR&PzCPݕ$̹:I%#>ʤ ide]_s2&RV"g q)_JzEe; aLdX"D-zj(4t,(EWT/a]]̘vW tt}d/7J9e\"dT}ۧ~*HP&,]/S+M m̱%j!`BjH*9YBDLWb ['b- Tq3մߒjd.v+m-Ŗfb}*K-D!qw5)ʙk ~RIu0E v[%6]RkQ%FDE/hR [%qM cbWџx m4+7nY-ĎrhXciƕpK(c❟<ϟoF6Z&F<Te5zi' nLc4}eR[ODxT5~{Jfst: 4ui^.Nhcm* ]٧dD﨎-v'6bFznfLiE{M-ZMhŨJ⷏{{|wW6N5M]4j9;3|bz|\_$wv2[Nr!D^!{ӚѸ yX1^Ü||>>gu99Jfqڔ7Q5!-K t˲3Af9R(˜(z{>]ZC>d:~^2A Ҁ$.G֔3)QE 9{~J08DpRF q$̽h£L!a' ]=̠Va1Uc!SBgYt 5 BL z APb'B4+E-Jqϼ,|h|ʅ"/bd"(+.QHjÌ$P~"# i+֒|F-l4ɩڲ ٰ ;7t[mvT'(f0E(ńdL];1dEE2DDqWx-QDp9.,e9SUOa }UW[əc6IO杭j TFA Zڍ^j l(+֪<wOD P1d*[ 70=i\]Dd*^\l6z(Ű$h2 e&բ HHB525{ ٬%áh4+6ʐVQ" XY@Naj"V.<;P5F>9ԍG]m0jp˙~]&,܊%,ĕ02VMddEԗ4n$z@ pQ-bV;'xJ8;E]y=QEkHhk*MRi% S5(p dQԖj;ZC+IgZ.+UՉ^T<`u52>H_-졹vM]1_,D ;V( )ƐJ.|$V2}s֑o\m>Ra^󝯺ۯgX h_ %nA:kpVA{ѡ,e܂O?F_Y6r::_(qyYFO&C;P5Ea{z-{ kR"OF2psV8NJFZw|ivDLm/1zCZjTSYo2$)*/1/[O2h+Ҏ<Yf1+e? V+ ]c}?"MqWf(qec"TW"XUGQSrb*VF8~a|Q~ ||g͎QPđh 0mQk7:I#Z|Vgn$E. -%KO}Z.6e~hE#1e\cٖAвJ[# -TjW#PwF1:PeܡV&*M;TaXFQZګ^EUz!;(:W6E4ɪRT1Il~>v`Z[TU"~:RJ g:c)D2iS!Z泎E SX,J,<{rJY1#i֤XB_BN-=nbD9 ;=bfaj*a In6VAWiEvC4miA/I XE?:{Nb!s;i. K9mCXb Oͷ{7C#F]I S|ѳڞ#Jx9:֕X36H 4XB Kv$+iT%KsaaC̲o"`=n$hF20,DU>6mcdAN Ȕ[)e'Ҋz0#MQ IH^md*ɍ, a% U99 M,)e\sP}Đ aX\>B\YJ*#SL.$BrN/jݙ,zYhVZUVaFf>eh÷fjmPe=bÖZ7'( K)=.8@ c峖?-mVX{㷾E|!r]e"rI"Ϧ^4X4] 2Z1 7uz6dX7yñT-Aw^WtѰ7N:W<Ú-o9o8jvfaξmΞ7N{x`vނtp0ߠ{:%eV ޸2ǃM$~C}sau]ۖ;J+hzx/06q%`B/+W{>jDyE^xW`yMEo=}o;(15>nV05Tc!DOkNwqa&ApitPD"ݱǨei=Ns~ةMΞo)\n&|{ѣ)wi5 ]㾓o"9%)Ⱥjvdejmo=ۦ]Rؚj7-DzQ-'sŬ8>YOA `aҷ֔=.Q/g<:RjB^vvv"e)[,oeD1l} nz] Iȥۋl.^jf-oww8qhÕ4ƪsVZͮ{Y۵'NdscD#//X'uBXSR9KeYK{*YVKŘ34AiOsS'O|7i /ï2el/+H!cxɯqVӶ7x##.x<[ػw/uSSU5a<S7 .@Kj.ϳ#P.R@ \;{ pE_biI/dz.4r@*w>)ˮ?c"X, COӌ軁hێ~nXz%LBHM[_}yEӌL&L5u^= SNX 2焈 {.rP"e+<ع/`;! 4wC& 8ZcA e7#(cǎ1 (~t++1NǸ9c.](Wzy%c<+gt/u)ΪXڹY1O伌-v~>/R߹sHtmO-hùJΧWL_q''eʼoo:d2eeedJ4"a)Bp<'>DP9Ky~n^ CiS\JI$FpIK){>Eʽ+2PH9'C &TvX]'Sٳ嬯30iF#r#.qF)W%42\aA/S/(/˄1+@v~𬿿郞g.sESbѶ8k\E۶4MmE]W]'e_R8&HxPy)v`?U{1}=Fcf* '"J.y^:l;/pe >` s e \ 21c 1Fs /.PK5]{ܹzСCr篬0*gsכr_k39@NHeSL0t=uSc IB^|1Kt/ 8r9dn W2Uq"x?ksBU@]עӬ4M0ϤeRݔfݬvŖr|ُҳ]1q}]dzwIZZe#XJEL =US~ǝ]rw]GF Ѳϸ*7OgOei5ɴbg馛v#C/7bU0,3< 92 $h? !X]^ |. WҶ;\2z<)qwivM*OG_8t0Ǐ[~?mw>}#rchJຨƺѲsӄ?=nVZ(Vsqsn8|8?~L_ =yɘkH FD1`dnbBOsO=Ι_1YwՌcA `ᄑo6779rs!3ĈI 9q;5ajFC<3,>|[{EGyロ7|2yO AVgO7"rUXޛ(x껞{penV6%ϧgD.%~ʏq ,z$Dvؓ#pJMQLfd֬}゚ xK6n~^U8x=++<#9} |RkSiDvV)VTJQK_3^guuh91zo~zD|wo/h3N9[[p:<`DFM#F+q1q9~vwͯ%zW( bSUӕV+TUp(\WlPM] @+:6.lqng6{~zkq/wƣM5p՞@򃭭<|ߥ=wlCW>W[Ak|ew};څqp\n9_g?' sqq)(IENDB`zeal-0.7.1/assets/freedesktop/16-apps-zeal.png000066400000000000000000000015501462517734600211350ustar00rootroot00000000000000PNG  IHDR(-S|PLTE&&&uSAiLjHjIjHjIlIxWrQ94+hFwfIrbFvspfa\?.ʬq//////--./2!1-&c=f9h:gd9b6a4b4)$";52UA_6ܐvǼஞఢ⸬㵥_7[1[/[/!2.&V2[2`9eCkMӈsؕb?\5X/X,W+png1,&2.'T1X0^9dEޮsWcC]9[5Y1T+R(zws4/'Q>W4sXޮhNcG`A]?{alNP*M#sqkA84O/jLĻ⽲ڣՒ҈u҈tՑ~⼱V4K%Hvrn"+'!I)M(O/R3W9eK҅pԎ|זbFM,G I!pnhG2@AC!E%I*M.N0I)B >Q*)% <93L1EE F H#I%I$H%G"N'O@kh`?:3=70MFQJUNWQZRYRSLQLID=HC<~FA;WSL454STUķrtRNSβ/v+>JIDATcd`3aedf0cO_ * OK T$%7;x_-gL;wF(DJ&D*3Eƕ=^Yh!X=%NmZ"H2Wa2~DQoF՞8=27ȍ"@[Y[}N<|F\ؿ+_yKޘ?E1W~P7Uu3bi64xի|eَ vУ{µ?oPV YZѸmU]eMPV;~OY﫛^12EV5S) 7U6Nl?m/e`'4BRfB& >WnC1yq\x?iR*U-SgԽۆwT5QQ;D\Ĉi'}|ލKSwe jX?rcO}5C~=h}R7jn6[v!|ǽO~U32,Gln_D]-M/k -}{X.i:뻷U~/+Q )jUu!f4(-Mn]@UR\BH(H5SAJgPZؚjS IA?A@EH& @i!":|H4֓wC_H~H?x?![ܹΎzlˇ>i:m[UJ!@Jz٬>j> -:w:e26txjNSQ)k`_ul6-t}x| 4Z럽p DهD ~uIIENDB`zeal-0.7.1/assets/freedesktop/32-apps-zeal.png000066400000000000000000000033371462517734600211400ustar00rootroot00000000000000PNG  IHDR szzIDATXÝM\Gofvgwvm L8 (>$DHȑS!8Y EB. !@B/l뵽ޝ7m9yZ}?5O 2UHBG@@C+=fא݃1`/9N`޽a/ӤHC;Qf<Z@u1biDwK³gwϮfܺɆU>P+U揝Ȕmvvb 3>Fp'Y{Y+y0ENROfwiDt`-KT`D#Tk畷&/Գז9{(Q$D63*?XS&"} s}O>/~/pWU`e+KVb$ )! c],4RdB瘚ՕUvNxej/Z"Zx,,EX(e}6j!^PMU 9m;%ejI^'q sk bƸ1Fiʠ?dߣv%B.̰ݻm2jl67$\s.""t@$42Ժu4(ggGĩj{=VkfD$Ɍ&.%1Y >Z$ EU !>j8 KSa0TLJ1Ḽ0MvP)cN2:Z!/=OڭU С79l"ZuLdJrew#?]v!p}W/xppvt ?=pYV'z x`/0^@#z?Z9t)0q=_?Qr2|e(g}1InIENDB`zeal-0.7.1/assets/freedesktop/64-apps-zeal.png000066400000000000000000000134431462517734600211440ustar00rootroot00000000000000PNG  IHDR@@iqIDATx{eWuks=33 Hh$"$$^i vpى1ʳrIJSNI9 eJ8ı)0{EzZo!1oLwϻ_q+}ν3D* N߹}Z{o}\?d5o{]z'ZΜ?̞m uh9nnrz'3Ӂ{k 53[8S4oʼn @#1@b^DA E`_I!spԜz Bc1B "ޡ (瓳i~qE6?,<9Id}0%1&FEE>m" nv/8DTCBD"(R`+8!Z$Y==J]rًYJbs""u3(hZK`Ƨϫ*x!ĀFE@ ;ĥtqr$lA@]:)M5/E@P=y [;XDJU\ׂM[v@ H-JrdFED 3{4(ͭG ,)Nϒ:aNJIqJPb煯8ge[zI٢bC@y/m%gh(F8Nulz/оZj&O͐9 ݣ #)A{/}5r˟"?}P(Āy$jI?k*×x[./uFkٱ28H0Jx_孆HԊBPZ_aهm rg)OX1/ d]@#ۻv\k ?Yza Xa2O!`zaB 8DG?uȲ5xv{/.yw-8nf+W bP4#h 1aS"|O}bq^#``拟H9-9B(Eė'0:iĢ5]B?-33+@3SfUd\hg RT#CN-őޘYIcUZ;/g>Bk߼'LJRzaIO*RMW)IRDА%oOe8['E`<1bN-Y97&Gƨ8.Vv@%../ٛl?Șj]e/+>4G;IP/P+꼔M>;wNw6׏\Uc:N"ZYI{GwE޿Vr0ZK(i %,/r~[P#$T|=qFIo@sE2BcvY\bt.7ӄ\)G%ְ_c{X|A6bg 4QҪ)aÓr6-/7'IR~!V퀲*+_b!'W@EԻpFw1}7=J#C*-!^8pbgB(#hNo[HX5P2n@,~&ƘgeyCχZbVzr5*-)MJ&=}{1(踥ˎ&h/U2u*AIr3?|Ĩl1F韽i`y3~/Lq.a2eWlzǻ9? *mb<U!눀s : tbw5 Q&N L66/r񩝭5Nr1V4Fsxb5WFHYo1zf?TOcZʕ=/޶&Vn>zukֱ5t;Y;Bˢc4Q#"ç͉]WGh^r^uyDOiDn@+ɉ%<2c 78»ɼC;/;4o}"zDܡ:vkF'x,mnngvJI,xq0JZ~6;`3 Л=FyC2"5wѫs~K J%^8!WNྕ13l0N{I{@gݽm4l,MԹMeg'֦3WkPߴ׼ўo (cW` wp/U漅p39{3WmP j=!y<(U04tZ1'طH*өAsT%[Qk&P6NOl1"VDNLZZCe+MT:I vewl G"%ciTP*GENZg{ϛۑȼljK@4沷/h131̴ # 䡢ض]F%oi.1KBmxO-Oq]>)1Qߟ zjmo%I'r$ef$ [r[KԚ( ʾ.e$QmYLU+^2Q#nju) L0r4pl Z,U|ϗ$Z]#W˰MLMcǠ6N |r5&QP9c\9 ǞS/f}$ Q2k.T᭭b IK-1#e řdvi|b|Ct,Av[y! .)u[]<үo3uk(9?g8xﵻilJw(MϞď#>;{mtgҘF,r<:ڲi\I;{Ʀ-NR_WoН9BcVshlފ9qylzќ\ox,Ι.HtBS| #1MBTyAף:8b"Pr>#eZ6;vd͚5l6jdYFexoK("r:UйHt#9" .RjBee,el5vfڵGFn\u|byst^\jR4Zٖ!XcgR'ᅲ[~={7,U'c9 H(R;< " 5rQ!}Ucot[|]h4젴O`f.Xi. UU]Plӈ:RGe5z@R"v>/-9r|w~?F {̛r P*}mXϯYQ33'ȋV|hRyxQj:##0K!|Qxxq11D}?ѹ#Lm^Oq_9:s_1?FVq!:eߺm!#dx׃^{W})iwx-XvX?Mvٗ,[[^2+~Tɫpq@r@~z+ /W߽ce_0q5+5a0ځ7F;IENDB`zeal-0.7.1/assets/freedesktop/CMakeLists.txt000066400000000000000000000026641462517734600210560ustar00rootroot00000000000000if(UNIX AND NOT APPLE) find_package(ECM 1.0.0 REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(ECMInstallIcons) if(QT_VERSION_MAJOR EQUAL 5) include(KDEInstallDirs) else() # Workaround until KDEInstallDirs6 is ready to use. include(GNUInstallDirs) set(KDE_INSTALL_APPDIR "${CMAKE_INSTALL_DATAROOTDIR}/applications") set(KDE_INSTALL_ICONDIR "${CMAKE_INSTALL_DATAROOTDIR}/icons") set(KDE_INSTALL_METAINFODIR "${CMAKE_INSTALL_DATAROOTDIR}/metainfo") endif() ecm_install_icons(ICONS "16-apps-zeal.png" "24-apps-zeal.png" "32-apps-zeal.png" "64-apps-zeal.png" "128-apps-zeal.png" DESTINATION ${KDE_INSTALL_ICONDIR} ) # For development builds insert an extra release in the AppStream metadata. if(NOT ZEAL_RELEASE_BUILD) string(TIMESTAMP ZEAL_APPSTREAM_DEV_RELEASE "\n ") endif() configure_file( org.zealdocs.zeal.appdata.xml.in org.zealdocs.zeal.appdata.xml ) install(FILES ${CMAKE_BINARY_DIR}/assets/freedesktop/org.zealdocs.zeal.appdata.xml DESTINATION ${KDE_INSTALL_METAINFODIR} ) install(FILES "org.zealdocs.zeal.desktop" DESTINATION ${KDE_INSTALL_APPDIR} ) endif() zeal-0.7.1/assets/freedesktop/org.zealdocs.zeal.appdata.xml.in000066400000000000000000000033041462517734600243720ustar00rootroot00000000000000 org.zealdocs.zeal org.zealdocs.zeal.desktop Zeal CC0-1.0 GPL-3.0-or-later Oleg Shparber Documentation browser

Zeal is a simple offline documentation browser inspired by Dash. It offers access to over 200 docsets covering various libraries and APIs.

Development https://zealdocs.org/ https://github.com/zealdocs/zeal/issues https://zealdocs.org/usage.html https://go.zealdocs.org/l/contact The main window https://i.imgur.com/FvGEguY.png zeal.desktop @ZEAL_APPSTREAM_DEV_RELEASE@ https://github.com/zealdocs/zeal/releases/tag/v0.7.1 https://github.com/zealdocs/zeal/releases/tag/v0.7.0 https://github.com/zealdocs/zeal/releases/tag/v0.6.1 support@zealdocs.org
zeal-0.7.1/assets/freedesktop/org.zealdocs.zeal.desktop000066400000000000000000000004371462517734600232310ustar00rootroot00000000000000[Desktop Entry] Version=1.0 Name=Zeal GenericName=Documentation Browser Comment=Simple API documentation browser Exec=zeal %u Icon=zeal Terminal=false Type=Application Categories=Development;Documentation; MimeType=x-scheme-handler/dash;x-scheme-handler/dash-plugin; StartupWMClass=Zeal zeal-0.7.1/cmake/000077500000000000000000000000001462517734600135515ustar00rootroot00000000000000zeal-0.7.1/cmake/CodeSign.cmake000066400000000000000000000162071462517734600162540ustar00rootroot00000000000000# # CodeSign.cmake # # Copyright (c) 2023 Oleg Shparber and contributors. All rights reserved. # Licensed under the MIT License. # include_guard() # codesign(FILES ... # [DESCRIPTION] # [URL] # [CERTIFICATE_FILE] # [PASSWORD] # [TIMESTAMP_URL] # [QUIET] # [VERBOSE] # [DEBUG]) function(codesign) if(NOT WIN32) message(FATAL_ERROR "Code signing is only supported on Windows.") endif() cmake_parse_arguments(_ARG "QUIET;VERBOSE;DEBUG" # Options. "DESCRIPTION;URL;CERTIFICATE_FILE;PASSWORD;TIMESTAMP_URL" # Single-value keywords. "FILES" # Multi-value keywords. ${ARGN} ) if(NOT _ARG_FILES) message(FATAL_ERROR "FILES argument is required.") endif() # Find signtool executable. # TODO: Add option for path to signtool.exe. # Add Windows 10 SDK paths. get_filename_component(_w10sdk_root_path "[HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots;KitsRoot10]" ABSOLUTE CACHE ) if(_w10sdk_root_path) file(GLOB _w10sdk_paths "${_w10sdk_root_path}/bin/10.*") list(REVERSE _w10sdk_paths) # Newest version first. # Detect target architecture. # https://learn.microsoft.com/en-us/windows/win32/winprog64/wow64-implementation-details#environment-variables if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") set(_w10sdk_arch "x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "IA64") set(_w10sdk_arch "x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "X86") set(_w10sdk_arch "x86") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") set(_w10sdk_arch "arm64") else() message(DEBUG "Unknown architecture: ${CMAKE_SYSTEM_PROCESSOR}.") endif() endif() # TODO: Add microsoft.windows.sdk.buildtools path. find_program(_cmd NAMES signtool PATHS ${_w10sdk_paths} PATH_SUFFIXES ${_w10sdk_arch} ) if(NOT _cmd) message(NOTICE "signtool.exe was not found, no binaries will be signed.") return() endif() message(DEBUG "Found signtool.exe: ${_cmd}") # Start constructing command. set(_cmd_args "sign") list(APPEND _cmd_args "/fd" "sha256") # Set certificate file. if(NOT _ARG_CERTIFICATE_FILE) if(CODESIGN_CERTIFICATE_FILE) set(_ARG_CERTIFICATE_FILE ${CODESIGN_CERTIFICATE_FILE}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE_FILE}) set(_ARG_CERTIFICATE_FILE $ENV{CODESIGN_CERTIFICATE_FILE}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE}) # Read the whole certificate from environment variable and store it # in a temporary file for signtool to use. # Determine temporary file location. Try to keep it local to the build. if(CMAKE_BINARY_DIR) set(_temp_path ${CMAKE_BINARY_DIR}) elseif(CPACK_TEMPORARY_DIRECTORY) set(_temp_path ${CPACK_TEMPORARY_DIRECTORY}) else() set(_temp_path $ENV{TEMP}) endif() set(_certificate_file "${_temp_path}/codesign.tmp") file(WRITE ${_certificate_file} $ENV{CODESIGN_CERTIFICATE}) set(_ARG_CERTIFICATE_FILE ${_certificate_file}) elseif(DEFINED ENV{CODESIGN_CERTIFICATE_BASE64}) # Read base64-encoded certificate from environment variable, # decode with `certutil.exe`, and store in a temporary file # for signtool to use. # # This is useful for GitHub Actions, which cannot handle unencoded # multiline secrets. # Determine temporary file location. Try to keep it local to the build. if(CMAKE_BINARY_DIR) set(_temp_path ${CMAKE_BINARY_DIR}) elseif(CPACK_TEMPORARY_DIRECTORY) set(_temp_path ${CPACK_TEMPORARY_DIRECTORY}) else() set(_temp_path $ENV{TEMP}) endif() # Save base64-encoded certificate to file. set(_certificate_file "${_temp_path}/codesign.tmp") set(_certificate_base64_file "${_certificate_file}.base64") file(WRITE ${_certificate_base64_file} $ENV{CODESIGN_CERTIFICATE_BASE64}) # Decode certificate. set(_cmd_certutil_args "-decode" ${_certificate_base64_file} ${_certificate_file}) execute_process(COMMAND "certutil.exe" ${_cmd_certutil_args} RESULT_VARIABLE _rc OUTPUT_VARIABLE _stdout # ERROR_VARIABLE _stderr ) # Remove temporary file first. file(REMOVE ${_certificate_base64_file}) if(NOT _rc EQUAL 0) # For some reason certutil prints errors to stdout. message(NOTICE "Failed to decode certificate: ${_stdout}") return() endif() set(_ARG_CERTIFICATE_FILE ${_certificate_file}) else() message(NOTICE "Certificate is not provided, no binaries will be signed.") return() endif() endif() list(APPEND _cmd_args "/f" ${_ARG_CERTIFICATE_FILE}) # Set password. if(NOT _ARG_PASSWORD) if(CODESIGN_PASSWORD) set(_ARG_PASSWORD ${CODESIGN_PASSWORD}) elseif(DEFINED ENV{CODESIGN_PASSWORD}) set(_ARG_PASSWORD $ENV{CODESIGN_PASSWORD}) endif() endif() if(_ARG_PASSWORD) list(APPEND _cmd_args "/p" ${_ARG_PASSWORD}) endif() # Set description. if(NOT _ARG_DESCRIPTION AND PROJECT_DESCRIPTION) set(_ARG_DESCRIPTION ${PROJECT_DESCRIPTION}) endif() if(_ARG_DESCRIPTION) list(APPEND _cmd_args "/d" ${_ARG_DESCRIPTION}) endif() # Set project URL. if(NOT _ARG_URL AND PROJECT_HOMEPAGE_URL) set(_ARG_URL ${PROJECT_HOMEPAGE_URL}) endif() if(_ARG_URL) list(APPEND _cmd_args "/du" ${_ARG_URL}) endif() # Set timestamp server. if(NOT _ARG_TIMESTAMP_URL) set(_ARG_TIMESTAMP_URL "http://timestamp.digicert.com") endif() if(_ARG_TIMESTAMP_URL) list(APPEND _cmd_args "/t" ${_ARG_TIMESTAMP_URL}) endif() # Set quiet, verbose, or debug options. if(_ARG_QUIET) list(APPEND _cmd_args "/q") endif() if(_ARG_VERBOSE) list(APPEND _cmd_args "/v") endif() if(_ARG_DEBUG) list(APPEND _cmd_args "/debug") endif() foreach(_file ${_ARG_FILES}) message(STATUS "Signing ${_file}...") execute_process(COMMAND ${_cmd} ${_cmd_args} ${_file} RESULT_VARIABLE _rc OUTPUT_VARIABLE _stdout ERROR_VARIABLE _stderr ) if(NOT _rc EQUAL 0) message(NOTICE "Failed to sign: ${_stderr}") endif() if(NOT _ARG_QUIET) message(NOTICE ${_stdout}) endif() endforeach() # Cleanup if(_certificate_file) file(REMOVE ${_certificate_file}) endif() endfunction() zeal-0.7.1/cmake/GetVersionFromGit.cmake000066400000000000000000000131161462517734600201320ustar00rootroot00000000000000# Based on https://github.com/fakenmc/cmake-git-semver by Nuno Fachada. # This module is public domain, use it as it fits you best. # # This cmake module sets the project version and partial version # variables by analysing the git tag and commit history. It expects git # tags defined with semantic versioning 2.0.0 (http://semver.org/). # # The module expects the PROJECT_NAME variable to be set, and recognizes # the GIT_FOUND, GIT_EXECUTABLE and VERSION_UPDATE_FROM_GIT variables. # If Git is found and VERSION_UPDATE_FROM_GIT is set to boolean TRUE, # the project version will be updated using information fetched from the # most recent git tag and commit. Otherwise, the module will try to read # a VERSION file containing the full and partial versions. The module # will update this file each time the project version is updated. # # Once done, this module will define the following variables: # # ${PROJECT_NAME}_GIT_VERSION_STRING - Version string without metadata # such as "v2.0.0" or "v1.2.41-beta.1". This should correspond to the # most recent git tag. # ${PROJECT_NAME}_GIT_VERSION_STRING_FULL - Version string with metadata # such as "v2.0.0+3.a23fbc" or "v1.3.1-alpha.2+4.9c4fd1" # ${PROJECT_NAME}_GIT_VERSION_MAJOR - Major version integer (e.g. 2 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_MINOR - Minor version integer (e.g. 3 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_PATCH - Patch version integer (e.g. 1 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_TWEAK - Tweak version string (e.g. "RC.2" in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_AHEAD - How many commits ahead of last tag (e.g. 21 in v2.3.1-RC.2+21.ef12c8) # ${PROJECT_NAME}_GIT_VERSION_SHA - The git sha1 of the most recent commit (e.g. the "ef12c8" in v2.3.1-RC.2+21.ef12c8) # Only if VERSION_UPDATE_FROM_GIT is TRUE: # ${PROJECT_NAME}_VERSION - Same as ${PROJECT_NAME}_GIT_VERSION_STRING, # without the preceding 'v', e.g. "2.0.0" or "1.2.41-beta.1" # Check if .git directory is present. if(NOT IS_DIRECTORY "${CMAKE_SOURCE_DIR}/.git") message(NOTICE "Cannot find Git metadata, using static version string.") return() endif() # Check if Git executable is present. find_package(Git) if(NOT GIT_FOUND) message(NOTICE "Cannot find Git executable, using static version string.") return() endif() # Check if Git executable version is >= 2.15. Required for --is-shallow-repository argument. # See https://stackoverflow.com/a/37533086. if(GIT_VERSION_STRING VERSION_LESS "2.15") message(NOTICE "Git executable is too old (< 2.15), using static version string.") return() endif() # Detect shallow clone. execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --is-shallow-repository WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} RESULT_VARIABLE IS_SHALLOW_RESULT OUTPUT_VARIABLE IS_SHALLOW_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ERROR_QUIET) if(IS_SHALLOW_RESULT AND NOT IS_SHALLOW_RESULT EQUAL 0) message(NOTICE "Cannot perform shallow clone detection, using static version string.") unset(IS_SHALLOW_RESULT) unset(IS_SHALLOW_OUTPUT) return() endif() unset(IS_SHALLOW_RESULT) if(NOT "${IS_SHALLOW_OUTPUT}" STREQUAL "false") message(NOTICE "Shallow clone detected, using static version string.") unset(IS_SHALLOW_OUTPUT) return() endif() unset(IS_SHALLOW_OUTPUT) # Get last tag from git execute_process(COMMAND ${GIT_EXECUTABLE} describe --abbrev=0 --tags WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_STRING OUTPUT_STRIP_TRAILING_WHITESPACE) # How many commits since the last tag execute_process(COMMAND ${GIT_EXECUTABLE} rev-list ${${PROJECT_NAME}_GIT_VERSION_STRING}^..HEAD --count WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_AHEAD OUTPUT_STRIP_TRAILING_WHITESPACE) # Get current commit SHA from git execute_process(COMMAND ${GIT_EXECUTABLE} rev-parse --short HEAD WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} OUTPUT_VARIABLE ${PROJECT_NAME}_GIT_VERSION_SHA OUTPUT_STRIP_TRAILING_WHITESPACE) # Get partial versions into a list string(REGEX MATCHALL "-.*$|[0-9]+" ${PROJECT_NAME}_PARTIAL_VERSION_LIST ${${PROJECT_NAME}_GIT_VERSION_STRING}) # Set the version numbers list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 0 ${PROJECT_NAME}_GIT_VERSION_MAJOR) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 1 ${PROJECT_NAME}_GIT_VERSION_MINOR) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 2 ${PROJECT_NAME}_GIT_VERSION_PATCH) # Calculate next patch version. math(EXPR ${PROJECT_NAME}_GIT_VERSION_PATCH_NEXT ${${PROJECT_NAME}_GIT_VERSION_PATCH}+1) # The tweak part is optional, so check if the list contains it list(LENGTH ${PROJECT_NAME}_PARTIAL_VERSION_LIST ${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN) if (${PROJECT_NAME}_PARTIAL_VERSION_LIST_LEN GREATER 3) list(GET ${PROJECT_NAME}_PARTIAL_VERSION_LIST 3 ${PROJECT_NAME}_GIT_VERSION_TWEAK) string(SUBSTRING ${${PROJECT_NAME}_GIT_VERSION_TWEAK} 1 -1 ${PROJECT_NAME}_GIT_VERSION_TWEAK) endif() # Unset the list unset(${PROJECT_NAME}_PARTIAL_VERSION_LIST) # Set full project version string set(${PROJECT_NAME}_GIT_VERSION_STRING_FULL ${${PROJECT_NAME}_GIT_VERSION_STRING}+${${PROJECT_NAME}_GIT_VERSION_AHEAD}.${${PROJECT_NAME}_GIT_VERSION_SHA}) if(VERSION_UPDATE_FROM_GIT) # Set project version (without the preceding 'v') set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_GIT_VERSION_MAJOR}.${${PROJECT_NAME}_GIT_VERSION_MINOR}.${${PROJECT_NAME}_GIT_VERSION_PATCH}) if (${PROJECT_NAME}_GIT_VERSION_TWEAK) set(${PROJECT_NAME}_VERSION ${${PROJECT_NAME}_VERSION}-${${PROJECT_NAME}_GIT_VERSION_TWEAK}) endif() endif() zeal-0.7.1/cmake/MacOSXBundleInfo.plist.in000066400000000000000000000025611462517734600202770ustar00rootroot00000000000000 CFBundleDevelopmentRegion English CFBundleExecutable ${MACOSX_BUNDLE_EXECUTABLE_NAME} CFBundleIconFile ${MACOSX_BUNDLE_ICON_FILE} CFBundleIdentifier ${MACOSX_BUNDLE_GUI_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleLongVersionString ${MACOSX_BUNDLE_LONG_VERSION_STRING} CFBundleName ${MACOSX_BUNDLE_BUNDLE_NAME} CFBundlePackageType APPL CFBundleShortVersionString ${MACOSX_BUNDLE_SHORT_VERSION_STRING} CFBundleSignature ???? CFBundleVersion ${MACOSX_BUNDLE_BUNDLE_VERSION} CSResourcesFileMapped NSHumanReadableCopyright ${MACOSX_BUNDLE_COPYRIGHT} NSPrincipalClass NSApplication NSHighResolutionCapable NSSupportsAutomaticGraphicsSwitching zeal-0.7.1/pkg/000077500000000000000000000000001462517734600132525ustar00rootroot00000000000000zeal-0.7.1/pkg/appimage/000077500000000000000000000000001462517734600150355ustar00rootroot00000000000000zeal-0.7.1/pkg/appimage/README.md000066400000000000000000000027221462517734600163170ustar00rootroot00000000000000# AppImage Package ## Local Testing Run Docker container: ```shell docker run -it --rm -v $(pwd):/src --entrypoint /bin/bash ubuntu:jammy ``` Install `appimage-builder` and `appimagetool` dependencies: ```shell apt-get update -q -y DEBIAN_FRONTEND="noninteractive" apt-get install -q -y --no-install-recommends appstream curl desktop-file-utils fakeroot file git gnupg patchelf squashfs-tools zsync python3-pip python3-setuptools python3-wheel ``` Install appimagetool, it has to be extracted because FUSE doesn't work in containers without extra fiddling. ```shell cd /tmp curl -sLO https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage chmod +x appimagetool-x86_64.AppImage ./appimagetool-x86_64.AppImage --appimage-extract mv squashfs-root/ /opt/appimagetool.AppDir ln -s /opt/appimagetool.AppDir/AppRun /usr/local/bin/appimagetool cd - ``` Install appimage-builder. ```shell pip3 install git+https://github.com/AppImageCrafters/appimage-builder.git@669213cb730e007d5b316ed19b39691fbdcd41c4 ``` Install build dependencies: ```shell apt-get install -q -y --no-install-recommends build-essential cmake extra-cmake-modules libappindicator-dev libarchive-dev libqt5x11extras5-dev libsqlite3-dev libxcb-keysyms1-dev ninja-build qtbase5-dev qtwebengine5-dev ``` Run `appimage-builder`: ```shell cd /src appimage-builder --skip-test --build-dir build.appimage --appdir build.appimage/AppDir --recipe pkg/appimage/appimage-amd64.yaml ``` zeal-0.7.1/pkg/appimage/appimage-amd64.yaml000066400000000000000000000041461462517734600204220ustar00rootroot00000000000000version: 1 script: - cmake -B $BUILD_DIR/cmake-build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo - cmake --build $BUILD_DIR/cmake-build - cmake --install $BUILD_DIR/cmake-build --prefix $TARGET_APPDIR/usr - appstreamcli validate $TARGET_APPDIR/usr/share/metainfo/org.zealdocs.zeal.appdata.xml AppDir: app_info: id: org.zealdocs.zeal name: zeal icon: zeal version: 0.7.1 # TODO: Use version from CMake. exec: usr/bin/zeal exec_args: $@ runtime: env: APPDIR_LIBRARY_PATH: $APPDIR/lib/x86_64-linux-gnu:$APPDIR/usr/lib/x86_64-linux-gnu QTWEBENGINE_DISABLE_SANDBOX: 1 apt: arch: amd64 sources: - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy main restricted universe multiverse key_url: https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x871920d1991bc93c - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-updates main restricted universe multiverse - sourceline: deb [arch=amd64] http://archive.ubuntu.com/ubuntu/ jammy-backports main restricted universe multiverse include: # Required Qt packages. - libqt5concurrent5 - libqt5gui5 - libqt5network5 - libqt5webchannel5 - libqt5webengine5 - libqt5webenginewidgets5 - libqt5widgets5 - libqt5x11extras5 - qt5-gtk-platformtheme - qtwayland5 # Other dependencies. - libsqlite3-0 - libarchive13 - libfontconfig1 - libfreetype6 exclude: - "*dbgsym*" - adwaita-icon-theme - dconf-service - gcc-* - gnupg - humanity-icon-theme - libsystemd0 - libwacom* - perl - perl-* - sound-theme-freedesktop - systemd - systemd-* files: exclude: - etc/systemd - lib/systemd - usr/bin/*-linux-gnu-* - usr/bin/dpkg* - usr/bin/systemd* - usr/include - usr/lib/x86_64-linux-gnu/gconv - usr/share/doc - usr/share/man AppImage: arch: x86_64 update-information: gh-releases-zsync|zealdocs|zeal|latest|zeal-*x86_64.AppImage.zsync sign-key: None zeal-0.7.1/pkg/wix/000077500000000000000000000000001462517734600140615ustar00rootroot00000000000000zeal-0.7.1/pkg/wix/cpack_post_build.cmake000066400000000000000000000005251462517734600203720ustar00rootroot00000000000000if(CPACK_SOURCE_INSTALLED_DIRECTORIES) message(DEBUG "Skipping package signing for source package generator.") return() endif() if(NOT CPACK_GENERATOR STREQUAL "WIX") message(DEBUG "Skipping package signing for ${CPACK_GENERATOR} generator.") return() endif() include(CodeSign) codesign(FILES ${CPACK_PACKAGE_FILES} QUIET) zeal-0.7.1/pkg/wix/cpack_pre_build.cmake000066400000000000000000000006021462517734600201670ustar00rootroot00000000000000if(CPACK_SOURCE_INSTALLED_DIRECTORIES) message(DEBUG "Skipping package signing for source package generator.") return() endif() # TODO: Automatically generate list. set(_file_list "zeal.exe" "archive.dll" "zlib1.dll" "sqlite3.dll" ) include(CodeSign) foreach(_file ${_file_list}) codesign(FILES "${CPACK_TEMPORARY_DIRECTORY}/${_file}" QUIET) endforeach() zeal-0.7.1/pkg/wix/exitdialog.wxs000066400000000000000000000042601462517734600167570ustar00rootroot00000000000000 zeal-0.7.1/pkg/wix/patch.xml000066400000000000000000000004101462517734600156750ustar00rootroot00000000000000 zeal-0.7.1/pkg/wix/template.xml000066400000000000000000000075411462517734600164250ustar00rootroot00000000000000 ProductIcon.ico LAUNCHAPPONEXIT WIX_UPGRADE_DETECTED zeal-0.7.1/pkg/wix/ui.wxs000066400000000000000000000110261462517734600152410ustar00rootroot00000000000000 1 "1"]]> 1 NOT Installed Installed AND PATCH 1 LicenseAccepted = "1" 1 1 NOT WIXUI_DONTVALIDATEPATH "1"]]> WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID="1" 1 1 NOT Installed Installed AND NOT PATCH Installed AND PATCH 1 1 1 1 zeal-0.7.1/pkg/wix/zeal.wxs000066400000000000000000000304431462517734600155630ustar00rootroot00000000000000 CURRENTVERSIONDETECTED zeal-0.7.1/src/000077500000000000000000000000001462517734600132605ustar00rootroot00000000000000zeal-0.7.1/src/CMakeLists.txt000066400000000000000000000013271462517734600160230ustar00rootroot00000000000000set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find includes in corresponding build directories. set(CMAKE_INCLUDE_CURRENT_DIR ON) ## Build options option(ZEAL_PORTABLE_BUILD "Build portable version") if(ZEAL_PORTABLE_BUILD) add_definitions(-DPORTABLE_BUILD) endif() ## Macros. add_definitions(-DZEAL_VERSION="${ZEAL_VERSION_FULL}") # QString options add_definitions(-DQT_USE_QSTRINGBUILDER) add_definitions(-DQT_RESTRICTED_CAST_FROM_ASCII) add_definitions(-DQT_NO_CAST_TO_ASCII) add_definitions(-DQT_NO_URL_CAST_FROM_STRING) ## Handle moc, uic, and rcc files. set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) set(CMAKE_AUTORCC ON) include_directories(libs) add_subdirectory(libs) add_subdirectory(app) zeal-0.7.1/src/app/000077500000000000000000000000001462517734600140405ustar00rootroot00000000000000zeal-0.7.1/src/app/CMakeLists.txt000066400000000000000000000140241462517734600166010ustar00rootroot00000000000000find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) if (Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_LESS QT_MINIMUM_VERSION) message(FATAL_ERROR "Qt version >= ${QT_MINIMUM_VERSION} is required.") endif() # Define output binary name. if(APPLE) set(_project_output_name ${CMAKE_PROJECT_NAME}) else() string(TOLOWER ${CMAKE_PROJECT_NAME} _project_output_name) endif() set(PROJECT_EXECUTABLE_NAME "${_project_output_name}${CMAKE_EXECUTABLE_SUFFIX}") message(STATUS "Project executable name: ${PROJECT_EXECUTABLE_NAME}") # Only support installing runtime dependencies with Qt >=6.5.1 (see QTBUG-111741). if(Qt${QT_VERSION_MAJOR}Widgets_VERSION VERSION_GREATER_EQUAL "6.5.1") set(_use_qt_cmake_commands TRUE) qt_standard_project_setup() endif() if(APPLE) list(APPEND App_RESOURCES resources/zeal.icns) elseif(WIN32) configure_file(versioninfo.rc.in ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc) list(APPEND App_RESOURCES ${CMAKE_CURRENT_BINARY_DIR}/versioninfo.rc) else() set(App_RESOURCES) # Silence CMake warning. endif() if(QT_VERSION_MAJOR EQUAL 6) qt_add_executable(App WIN32 main.cpp zeal.qrc ${App_RESOURCES} ) else() add_executable(App WIN32 main.cpp zeal.qrc ${App_RESOURCES} ) endif() target_link_libraries(App PRIVATE Core Util Qt${QT_VERSION_MAJOR}::Widgets) set_target_properties(App PROPERTIES OUTPUT_NAME ${_project_output_name} RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR} ) # Install Qt runtime dependencies on Windows. if(WIN32 AND _use_qt_cmake_commands) qt_generate_deploy_script( TARGET App OUTPUT_SCRIPT _qt_deploy_script CONTENT " # TODO: Run windeployqt after build. # Override deployment script's working directory. # set(QT_DEPLOY_PREFIX \"$\") qt_deploy_runtime_dependencies( EXECUTABLE \"$\" BIN_DIR . NO_TRANSLATIONS NO_COMPILER_RUNTIME )") endif() if(APPLE) set_target_properties(App PROPERTIES MACOSX_BUNDLE TRUE MACOSX_BUNDLE_BUNDLE_NAME ${CMAKE_PROJECT_NAME} MACOSX_BUNDLE_GUI_IDENTIFIER "org.zealdocs.zeal" MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} MACOSX_BUNDLE_LONG_VERSION_STRING ${PROJECT_VERSION} MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}" MACOSX_BUNDLE_ICON_FILE "zeal.icns" MACOSX_BUNDLE_COPYRIGHT ${PROJECT_COPYRIGHT} RESOURCE "resources/zeal.icns" ) elseif(WIN32) install(TARGETS App RUNTIME DESTINATION .) if(_use_qt_cmake_commands) # Install Qt runtime dependencies. install(SCRIPT ${_qt_deploy_script}) unset(_qt_deploy_script) unset(_use_qt_cmake_commands) endif() elseif(UNIX) include(GNUInstallDirs) install(TARGETS App DESTINATION ${CMAKE_INSTALL_BINDIR}) endif() # # CPack configuration. # set(CPACK_GENERATOR "7Z;ZIP") set(CPACK_VERBATIM_VARIABLES YES) # Usage: cmake --build --preset --target package # E.g. cmake --build build --preset ninja-multi-vcpkg-release --target package set(CPACK_PACKAGE_NAME ${CMAKE_PROJECT_NAME}) set(CPACK_PACKAGE_DESCRIPTION ${CMAKE_PROJECT_DESCRIPTION}) set(CPACK_PACKAGE_VENDOR ${PROJECT_COMPANY_NAME}) set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION}) set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico") # Set binary package file name. if(WIN32) if(CMAKE_SYSTEM_PROCESSOR STREQUAL "AMD64") set(_package_file_name_suffix "-windows-x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "IA64") set(_package_file_name_suffix "-windows-x64") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "X86") set(_package_file_name_suffix "-windows-x86") elseif(CMAKE_SYSTEM_PROCESSOR STREQUAL "ARM64") set(_package_file_name_suffix "-windows-arm64") else() set(_package_file_name_suffix "-windows-unknown") endif() endif() if(ZEAL_PORTABLE_BUILD) string(PREPEND _package_file_name_suffix "-portable") endif() set(CPACK_PACKAGE_FILE_NAME "${_project_output_name}-${ZEAL_VERSION_FULL}${_package_file_name_suffix}") set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME}) set(CPACK_PACKAGE_EXECUTABLES ${_project_output_name} ${CPACK_PACKAGE_NAME}) set(CPACK_CREATE_DESKTOP_LINKS ${_project_output_name} ${CPACK_PACKAGE_NAME}) # Allow CPack to do text to RTF conversion. configure_file("${CMAKE_SOURCE_DIR}/COPYING" "${CMAKE_CURRENT_BINARY_DIR}/license.txt" COPYONLY) set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_BINARY_DIR}/license.txt") set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md") set(CPACK_PACKAGE_CHECKSUM SHA256) if(WIN32) # CPack WiX configuration. set(CPACK_WIX_UPGRADE_GUID "5C4B6030-A1B4-4EFE-A5AF-28F6FA2E7978") set(CPACK_WIX_PROPERTY_ARPURLINFOABOUT ${CMAKE_PROJECT_HOMEPAGE_URL}) set(CPACK_WIX_PRODUCT_ICON "${CMAKE_SOURCE_DIR}/src/app/resources/zeal.ico") #set(CPACK_WIX_UI_BANNER "${CMAKE_SOURCE_DIR}/pkg/wix/banner.png") #set(CPACK_WIX_UI_DIALOG "${CMAKE_SOURCE_DIR}/pkg/wix/dialog.png") set(CPACK_WIX_EXTENSIONS "WixUtilExtension.dll") set(CPACK_WIX_UI_REF "Zeal_InstallDir") set(CPACK_WIX_TEMPLATE "${CMAKE_SOURCE_DIR}/pkg/wix/template.xml") set(CPACK_WIX_PATCH_FILE "${CMAKE_SOURCE_DIR}/pkg/wix/patch.xml") set(CPACK_WIX_EXTRA_SOURCES "${CMAKE_SOURCE_DIR}/pkg/wix/ui.wxs" "${CMAKE_SOURCE_DIR}/pkg/wix/exitdialog.wxs" ) set(CPACK_PRE_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/pkg/wix/cpack_pre_build.cmake") if(NOT ZEAL_PORTABLE_BUILD) list(APPEND CPACK_GENERATOR "WIX") set(CPACK_POST_BUILD_SCRIPTS "${CMAKE_SOURCE_DIR}/pkg/wix/cpack_post_build.cmake") endif() endif() # Set options for the source package. # Usage: cmake --build --target package_source set(CPACK_SOURCE_GENERATOR "TGZ;TXZ;ZIP") set(CPACK_SOURCE_PACKAGE_FILE_NAME "${_project_output_name}-${ZEAL_VERSION_FULL}") set(CPACK_SOURCE_IGNORE_FILES # Directories. ".git/" ".github/" ".vscode/" "build/" # Files. ".editorconfig" ".gitattributes" ".gitignore" ) include(CPack) zeal-0.7.1/src/app/main.cpp000066400000000000000000000221401462517734600154670ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef Q_OS_WIN32 #include #include #endif #include using namespace Zeal; struct CommandLineParameters { bool force; bool preventActivation; Registry::SearchQuery query; #ifdef Q_OS_WIN32 bool registerProtocolHandlers; bool unregisterProtocolHandlers; #endif }; QString stripParameterUrl(const QString &url, const QString &scheme) { QString str = url.mid(scheme.length() + 1); if (str.startsWith(QLatin1String("//"))) { str = str.mid(2); } if (str.endsWith(QLatin1Char('/'))) { str = str.left(str.length() - 1); } return str; } CommandLineParameters parseCommandLine(const QStringList &arguments) { QCommandLineParser parser; parser.setApplicationDescription(QObject::tr("Zeal - Offline documentation browser.")); parser.addHelpOption(); parser.addVersionOption(); parser.addOption({{QStringLiteral("f"), QStringLiteral("force")}, QObject::tr("Force the application run.")}); #ifdef Q_OS_WIN32 parser.addOption(QCommandLineOption({QStringLiteral("register")}, QObject::tr("Register protocol handlers"))); parser.addOption(QCommandLineOption({QStringLiteral("unregister")}, QObject::tr("Unregister protocol handlers"))); #endif parser.addPositionalArgument(QStringLiteral("url"), QObject::tr("dash[-plugin]:// URL")); parser.process(arguments); CommandLineParameters clParams; clParams.force = parser.isSet(QStringLiteral("force")); clParams.preventActivation = false; #ifdef Q_OS_WIN32 clParams.registerProtocolHandlers = parser.isSet(QStringLiteral("register")); clParams.unregisterProtocolHandlers = parser.isSet(QStringLiteral("unregister")); if (clParams.registerProtocolHandlers && clParams.unregisterProtocolHandlers) { QTextStream(stderr) << QObject::tr("Parameter conflict: --register and --unregister.\n"); ::exit(EXIT_FAILURE); } #endif // TODO: Support dash-feed:// protocol const QString arg = QUrl::fromPercentEncoding(parser.positionalArguments().value(0).toUtf8()); if (arg.startsWith(QLatin1String("dash:"))) { clParams.query.setQuery(stripParameterUrl(arg, QStringLiteral("dash"))); } else if (arg.startsWith(QLatin1String("dash-plugin:"))) { const QUrlQuery urlQuery(stripParameterUrl(arg, QStringLiteral("dash-plugin"))); const QString keys = urlQuery.queryItemValue(QStringLiteral("keys")); if (!keys.isEmpty()) clParams.query.setKeywords(keys.split(QLatin1Char(','))); clParams.query.setQuery(urlQuery.queryItemValue(QStringLiteral("query"))); const QString preventActivation = urlQuery.queryItemValue(QStringLiteral("prevent_activation")); clParams.preventActivation = preventActivation == QLatin1String("true"); } else { clParams.query.setQuery(arg); } return clParams; } #ifdef Q_OS_WIN32 void registerProtocolHandler(const QString &scheme, const QString &description) { const QString appPath = QDir::toNativeSeparators(QCoreApplication::applicationFilePath()); const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes\\") + scheme; QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); reg->setValue(QStringLiteral("Default"), description); reg->setValue(QStringLiteral("URL Protocol"), QString()); reg->beginGroup(QStringLiteral("DefaultIcon")); reg->setValue(QStringLiteral("Default"), QString("%1,1").arg(appPath)); reg->endGroup(); reg->beginGroup(QStringLiteral("shell")); reg->beginGroup(QStringLiteral("open")); reg->beginGroup(QStringLiteral("command")); reg->setValue(QStringLiteral("Default"), QVariant(appPath + QLatin1String(" %1"))); } void registerProtocolHandlers(const QHash &protocols, bool force = false) { const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes"); QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); const QStringList groups = reg->childGroups(); for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) { if (force || !groups.contains(it.key())) registerProtocolHandler(it.key(), it.value()); } } void unregisterProtocolHandlers(const QHash &protocols) { const QString regPath = QStringLiteral("HKEY_CURRENT_USER\\Software\\Classes"); QScopedPointer reg(new QSettings(regPath, QSettings::NativeFormat)); for (auto it = protocols.cbegin(); it != protocols.cend(); ++it) { reg->remove(it.key()); } } #endif int main(int argc, char *argv[]) { // Do not allow Qt version lower than the app was compiled with. QT_REQUIRE_VERSION(argc, argv, QT_VERSION_STR) QCoreApplication::setApplicationName(QStringLiteral("Zeal")); QCoreApplication::setApplicationVersion(ZEAL_VERSION); QCoreApplication::setOrganizationDomain(QStringLiteral("zealdocs.org")); QCoreApplication::setOrganizationName(QStringLiteral("Zeal")); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); QCoreApplication::setAttribute(Qt::AA_UseHighDpiPixmaps); #endif // Use Fusion style on Windows 10 & 11. This enables proper darl mode support. // See https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5. // TODO: Make style configurable, detect -style argument. #if defined(Q_OS_WIN) && (QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)) const auto osName = QSysInfo::prettyProductName(); if (osName.startsWith("Windows 10") || osName.startsWith("Windows 11")) { QApplication::setStyle("fusion"); } #endif QScopedPointer qapp(new QApplication(argc, argv)); const CommandLineParameters clParams = parseCommandLine(qapp->arguments()); #ifdef Q_OS_WIN32 const static QHash protocols = { {QStringLiteral("dash"), QStringLiteral("Dash Protocol")}, {QStringLiteral("dash-plugin"), QStringLiteral("Dash Plugin Protocol")} }; if (clParams.unregisterProtocolHandlers) { unregisterProtocolHandlers(protocols); return EXIT_SUCCESS; } else { registerProtocolHandlers(protocols, clParams.registerProtocolHandlers); if (clParams.registerProtocolHandlers) return EXIT_SUCCESS; } #endif QScopedPointer appSingleton(new Core::ApplicationSingleton()); if (appSingleton->isSecondary()) { #ifdef Q_OS_WIN32 ::AllowSetForegroundWindow(appSingleton->primaryPid()); #endif QByteArray ba; QDataStream out(&ba, QIODevice::WriteOnly); out << clParams.query << clParams.preventActivation; // TODO: Check for a possible error. appSingleton->sendMessage(ba); return EXIT_SUCCESS; } // Set application-wide window icon. All message boxes and other windows will use it by default. qapp->setDesktopFileName(QStringLiteral("org.zealdocs.zeal")); qapp->setWindowIcon(QIcon::fromTheme(QStringLiteral("zeal"), QIcon(QStringLiteral(":/zeal.ico")))); QDir::setSearchPaths(QStringLiteral("typeIcon"), {QStringLiteral(":/icons/type")}); QScopedPointer app(new Core::Application()); QObject::connect(appSingleton.data(), &Core::ApplicationSingleton::messageReceived, [&app](const QByteArray &data) { Registry::SearchQuery query; bool preventActivation; QDataStream in(data); in >> query >> preventActivation; app->executeQuery(query, preventActivation); }); if (!clParams.query.isEmpty()) { QTimer::singleShot(0, app.data(), [&app, clParams] { app->executeQuery(clParams.query, clParams.preventActivation); }); } return qapp->exec(); } zeal-0.7.1/src/app/resources/000077500000000000000000000000001462517734600160525ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/browser/000077500000000000000000000000001462517734600175355ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/browser/404.html000066400000000000000000000051351462517734600207360ustar00rootroot00000000000000 Welcome

File not found

Encountered a problem? Please report!

Get in touch

Gitter

Chat with developers and other users

GitHub

Contribute to the project

zeal-0.7.1/src/app/resources/browser/assets/000077500000000000000000000000001462517734600210375ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/browser/assets/css/000077500000000000000000000000001462517734600216275ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/browser/assets/css/darkmode.css000066400000000000000000000003451462517734600241310ustar00rootroot00000000000000/* Dark mode (see #466). */ html { background-color: #262626 !important; filter: invert(1) hue-rotate(180deg) contrast(70%) !important; } html img { filter: invert(1) hue-rotate(180deg) contrast(120%) !important; } zeal-0.7.1/src/app/resources/browser/assets/css/fa-brands.min.css000066400000000000000000000012431462517734600247600ustar00rootroot00000000000000/*! * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ @font-face{font-family:"Font Awesome 5 Brands";font-style:normal;font-weight:400;font-display:block;src:url(../webfonts/fa-brands-400.eot);src:url(../webfonts/fa-brands-400.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-brands-400.woff2) format("woff2"),url(../webfonts/fa-brands-400.woff) format("woff"),url(../webfonts/fa-brands-400.ttf) format("truetype"),url(../webfonts/fa-brands-400.svg#fontawesome) format("svg")}.fab{font-family:"Font Awesome 5 Brands";font-weight:400}zeal-0.7.1/src/app/resources/browser/assets/css/fa-solid.min.css000066400000000000000000000012351462517734600246220ustar00rootroot00000000000000/*! * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ @font-face{font-family:"Font Awesome 5 Free";font-style:normal;font-weight:900;font-display:block;src:url(../webfonts/fa-solid-900.eot);src:url(../webfonts/fa-solid-900.eot?#iefix) format("embedded-opentype"),url(../webfonts/fa-solid-900.woff2) format("woff2"),url(../webfonts/fa-solid-900.woff) format("woff"),url(../webfonts/fa-solid-900.ttf) format("truetype"),url(../webfonts/fa-solid-900.svg#fontawesome) format("svg")}.fa,.fas{font-family:"Font Awesome 5 Free";font-weight:900}zeal-0.7.1/src/app/resources/browser/assets/css/fontawesome.min.css000066400000000000000000001574721462517734600254720ustar00rootroot00000000000000/*! * Font Awesome Free 5.13.0 by @fontawesome - https://fontawesome.com * License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) */ .fa,.fab,.fad,.fal,.far,.fas{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;display:inline-block;font-style:normal;font-variant:normal;text-rendering:auto;line-height:1}.fa-lg{font-size:1.33333em;line-height:.75em;vertical-align:-.0667em}.fa-xs{font-size:.75em}.fa-sm{font-size:.875em}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-fw{text-align:center;width:1.25em}.fa-ul{list-style-type:none;margin-left:2.5em;padding-left:0}.fa-ul>li{position:relative}.fa-li{left:-2em;position:absolute;text-align:center;width:2em;line-height:inherit}.fa-border{border:.08em solid #eee;border-radius:.1em;padding:.2em .25em .15em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left,.fab.fa-pull-left,.fal.fa-pull-left,.far.fa-pull-left,.fas.fa-pull-left{margin-right:.3em}.fa.fa-pull-right,.fab.fa-pull-right,.fal.fa-pull-right,.far.fa-pull-right,.fas.fa-pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s linear infinite;animation:fa-spin 2s linear infinite}.fa-pulse{-webkit-animation:fa-spin 1s steps(8) infinite;animation:fa-spin 1s steps(8) infinite}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scaleX(-1);transform:scaleX(-1)}.fa-flip-vertical{-webkit-transform:scaleY(-1);transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical,.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{-webkit-transform:scale(-1);transform:scale(-1)}:root .fa-flip-both,:root .fa-flip-horizontal,:root .fa-flip-vertical,:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270{-webkit-filter:none;filter:none}.fa-stack{display:inline-block;height:2em;line-height:2em;position:relative;vertical-align:middle;width:2.5em}.fa-stack-1x,.fa-stack-2x{left:0;position:absolute;text-align:center;width:100%}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-500px:before{content:"\f26e"}.fa-accessible-icon:before{content:"\f368"}.fa-accusoft:before{content:"\f369"}.fa-acquisitions-incorporated:before{content:"\f6af"}.fa-ad:before{content:"\f641"}.fa-address-book:before{content:"\f2b9"}.fa-address-card:before{content:"\f2bb"}.fa-adjust:before{content:"\f042"}.fa-adn:before{content:"\f170"}.fa-adobe:before{content:"\f778"}.fa-adversal:before{content:"\f36a"}.fa-affiliatetheme:before{content:"\f36b"}.fa-air-freshener:before{content:"\f5d0"}.fa-airbnb:before{content:"\f834"}.fa-algolia:before{content:"\f36c"}.fa-align-center:before{content:"\f037"}.fa-align-justify:before{content:"\f039"}.fa-align-left:before{content:"\f036"}.fa-align-right:before{content:"\f038"}.fa-alipay:before{content:"\f642"}.fa-allergies:before{content:"\f461"}.fa-amazon:before{content:"\f270"}.fa-amazon-pay:before{content:"\f42c"}.fa-ambulance:before{content:"\f0f9"}.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-amilia:before{content:"\f36d"}.fa-anchor:before{content:"\f13d"}.fa-android:before{content:"\f17b"}.fa-angellist:before{content:"\f209"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-down:before{content:"\f107"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angry:before{content:"\f556"}.fa-angrycreative:before{content:"\f36e"}.fa-angular:before{content:"\f420"}.fa-ankh:before{content:"\f644"}.fa-app-store:before{content:"\f36f"}.fa-app-store-ios:before{content:"\f370"}.fa-apper:before{content:"\f371"}.fa-apple:before{content:"\f179"}.fa-apple-alt:before{content:"\f5d1"}.fa-apple-pay:before{content:"\f415"}.fa-archive:before{content:"\f187"}.fa-archway:before{content:"\f557"}.fa-arrow-alt-circle-down:before{content:"\f358"}.fa-arrow-alt-circle-left:before{content:"\f359"}.fa-arrow-alt-circle-right:before{content:"\f35a"}.fa-arrow-alt-circle-up:before{content:"\f35b"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-down:before{content:"\f063"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrows-alt:before{content:"\f0b2"}.fa-arrows-alt-h:before{content:"\f337"}.fa-arrows-alt-v:before{content:"\f338"}.fa-artstation:before{content:"\f77a"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asterisk:before{content:"\f069"}.fa-asymmetrik:before{content:"\f372"}.fa-at:before{content:"\f1fa"}.fa-atlas:before{content:"\f558"}.fa-atlassian:before{content:"\f77b"}.fa-atom:before{content:"\f5d2"}.fa-audible:before{content:"\f373"}.fa-audio-description:before{content:"\f29e"}.fa-autoprefixer:before{content:"\f41c"}.fa-avianex:before{content:"\f374"}.fa-aviato:before{content:"\f421"}.fa-award:before{content:"\f559"}.fa-aws:before{content:"\f375"}.fa-baby:before{content:"\f77c"}.fa-baby-carriage:before{content:"\f77d"}.fa-backspace:before{content:"\f55a"}.fa-backward:before{content:"\f04a"}.fa-bacon:before{content:"\f7e5"}.fa-bahai:before{content:"\f666"}.fa-balance-scale:before{content:"\f24e"}.fa-balance-scale-left:before{content:"\f515"}.fa-balance-scale-right:before{content:"\f516"}.fa-ban:before{content:"\f05e"}.fa-band-aid:before{content:"\f462"}.fa-bandcamp:before{content:"\f2d5"}.fa-barcode:before{content:"\f02a"}.fa-bars:before{content:"\f0c9"}.fa-baseball-ball:before{content:"\f433"}.fa-basketball-ball:before{content:"\f434"}.fa-bath:before{content:"\f2cd"}.fa-battery-empty:before{content:"\f244"}.fa-battery-full:before{content:"\f240"}.fa-battery-half:before{content:"\f242"}.fa-battery-quarter:before{content:"\f243"}.fa-battery-three-quarters:before{content:"\f241"}.fa-battle-net:before{content:"\f835"}.fa-bed:before{content:"\f236"}.fa-beer:before{content:"\f0fc"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-bell:before{content:"\f0f3"}.fa-bell-slash:before{content:"\f1f6"}.fa-bezier-curve:before{content:"\f55b"}.fa-bible:before{content:"\f647"}.fa-bicycle:before{content:"\f206"}.fa-biking:before{content:"\f84a"}.fa-bimobject:before{content:"\f378"}.fa-binoculars:before{content:"\f1e5"}.fa-biohazard:before{content:"\f780"}.fa-birthday-cake:before{content:"\f1fd"}.fa-bitbucket:before{content:"\f171"}.fa-bitcoin:before{content:"\f379"}.fa-bity:before{content:"\f37a"}.fa-black-tie:before{content:"\f27e"}.fa-blackberry:before{content:"\f37b"}.fa-blender:before{content:"\f517"}.fa-blender-phone:before{content:"\f6b6"}.fa-blind:before{content:"\f29d"}.fa-blog:before{content:"\f781"}.fa-blogger:before{content:"\f37c"}.fa-blogger-b:before{content:"\f37d"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-bold:before{content:"\f032"}.fa-bolt:before{content:"\f0e7"}.fa-bomb:before{content:"\f1e2"}.fa-bone:before{content:"\f5d7"}.fa-bong:before{content:"\f55c"}.fa-book:before{content:"\f02d"}.fa-book-dead:before{content:"\f6b7"}.fa-book-medical:before{content:"\f7e6"}.fa-book-open:before{content:"\f518"}.fa-book-reader:before{content:"\f5da"}.fa-bookmark:before{content:"\f02e"}.fa-bootstrap:before{content:"\f836"}.fa-border-all:before{content:"\f84c"}.fa-border-none:before{content:"\f850"}.fa-border-style:before{content:"\f853"}.fa-bowling-ball:before{content:"\f436"}.fa-box:before{content:"\f466"}.fa-box-open:before{content:"\f49e"}.fa-box-tissue:before{content:"\f95b"}.fa-boxes:before{content:"\f468"}.fa-braille:before{content:"\f2a1"}.fa-brain:before{content:"\f5dc"}.fa-bread-slice:before{content:"\f7ec"}.fa-briefcase:before{content:"\f0b1"}.fa-briefcase-medical:before{content:"\f469"}.fa-broadcast-tower:before{content:"\f519"}.fa-broom:before{content:"\f51a"}.fa-brush:before{content:"\f55d"}.fa-btc:before{content:"\f15a"}.fa-buffer:before{content:"\f837"}.fa-bug:before{content:"\f188"}.fa-building:before{content:"\f1ad"}.fa-bullhorn:before{content:"\f0a1"}.fa-bullseye:before{content:"\f140"}.fa-burn:before{content:"\f46a"}.fa-buromobelexperte:before{content:"\f37f"}.fa-bus:before{content:"\f207"}.fa-bus-alt:before{content:"\f55e"}.fa-business-time:before{content:"\f64a"}.fa-buy-n-large:before{content:"\f8a6"}.fa-buysellads:before{content:"\f20d"}.fa-calculator:before{content:"\f1ec"}.fa-calendar:before{content:"\f133"}.fa-calendar-alt:before{content:"\f073"}.fa-calendar-check:before{content:"\f274"}.fa-calendar-day:before{content:"\f783"}.fa-calendar-minus:before{content:"\f272"}.fa-calendar-plus:before{content:"\f271"}.fa-calendar-times:before{content:"\f273"}.fa-calendar-week:before{content:"\f784"}.fa-camera:before{content:"\f030"}.fa-camera-retro:before{content:"\f083"}.fa-campground:before{content:"\f6bb"}.fa-canadian-maple-leaf:before{content:"\f785"}.fa-candy-cane:before{content:"\f786"}.fa-cannabis:before{content:"\f55f"}.fa-capsules:before{content:"\f46b"}.fa-car:before{content:"\f1b9"}.fa-car-alt:before{content:"\f5de"}.fa-car-battery:before{content:"\f5df"}.fa-car-crash:before{content:"\f5e1"}.fa-car-side:before{content:"\f5e4"}.fa-caravan:before{content:"\f8ff"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-caret-square-down:before{content:"\f150"}.fa-caret-square-left:before{content:"\f191"}.fa-caret-square-right:before{content:"\f152"}.fa-caret-square-up:before{content:"\f151"}.fa-caret-up:before{content:"\f0d8"}.fa-carrot:before{content:"\f787"}.fa-cart-arrow-down:before{content:"\f218"}.fa-cart-plus:before{content:"\f217"}.fa-cash-register:before{content:"\f788"}.fa-cat:before{content:"\f6be"}.fa-cc-amazon-pay:before{content:"\f42d"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-apple-pay:before{content:"\f416"}.fa-cc-diners-club:before{content:"\f24c"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-cc-visa:before{content:"\f1f0"}.fa-centercode:before{content:"\f380"}.fa-centos:before{content:"\f789"}.fa-certificate:before{content:"\f0a3"}.fa-chair:before{content:"\f6c0"}.fa-chalkboard:before{content:"\f51b"}.fa-chalkboard-teacher:before{content:"\f51c"}.fa-charging-station:before{content:"\f5e7"}.fa-chart-area:before{content:"\f1fe"}.fa-chart-bar:before{content:"\f080"}.fa-chart-line:before{content:"\f201"}.fa-chart-pie:before{content:"\f200"}.fa-check:before{content:"\f00c"}.fa-check-circle:before{content:"\f058"}.fa-check-double:before{content:"\f560"}.fa-check-square:before{content:"\f14a"}.fa-cheese:before{content:"\f7ef"}.fa-chess:before{content:"\f439"}.fa-chess-bishop:before{content:"\f43a"}.fa-chess-board:before{content:"\f43c"}.fa-chess-king:before{content:"\f43f"}.fa-chess-knight:before{content:"\f441"}.fa-chess-pawn:before{content:"\f443"}.fa-chess-queen:before{content:"\f445"}.fa-chess-rook:before{content:"\f447"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-down:before{content:"\f078"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-chevron-up:before{content:"\f077"}.fa-child:before{content:"\f1ae"}.fa-chrome:before{content:"\f268"}.fa-chromecast:before{content:"\f838"}.fa-church:before{content:"\f51d"}.fa-circle:before{content:"\f111"}.fa-circle-notch:before{content:"\f1ce"}.fa-city:before{content:"\f64f"}.fa-clinic-medical:before{content:"\f7f2"}.fa-clipboard:before{content:"\f328"}.fa-clipboard-check:before{content:"\f46c"}.fa-clipboard-list:before{content:"\f46d"}.fa-clock:before{content:"\f017"}.fa-clone:before{content:"\f24d"}.fa-closed-captioning:before{content:"\f20a"}.fa-cloud:before{content:"\f0c2"}.fa-cloud-download-alt:before{content:"\f381"}.fa-cloud-meatball:before{content:"\f73b"}.fa-cloud-moon:before{content:"\f6c3"}.fa-cloud-moon-rain:before{content:"\f73c"}.fa-cloud-rain:before{content:"\f73d"}.fa-cloud-showers-heavy:before{content:"\f740"}.fa-cloud-sun:before{content:"\f6c4"}.fa-cloud-sun-rain:before{content:"\f743"}.fa-cloud-upload-alt:before{content:"\f382"}.fa-cloudscale:before{content:"\f383"}.fa-cloudsmith:before{content:"\f384"}.fa-cloudversify:before{content:"\f385"}.fa-cocktail:before{content:"\f561"}.fa-code:before{content:"\f121"}.fa-code-branch:before{content:"\f126"}.fa-codepen:before{content:"\f1cb"}.fa-codiepie:before{content:"\f284"}.fa-coffee:before{content:"\f0f4"}.fa-cog:before{content:"\f013"}.fa-cogs:before{content:"\f085"}.fa-coins:before{content:"\f51e"}.fa-columns:before{content:"\f0db"}.fa-comment:before{content:"\f075"}.fa-comment-alt:before{content:"\f27a"}.fa-comment-dollar:before{content:"\f651"}.fa-comment-dots:before{content:"\f4ad"}.fa-comment-medical:before{content:"\f7f5"}.fa-comment-slash:before{content:"\f4b3"}.fa-comments:before{content:"\f086"}.fa-comments-dollar:before{content:"\f653"}.fa-compact-disc:before{content:"\f51f"}.fa-compass:before{content:"\f14e"}.fa-compress:before{content:"\f066"}.fa-compress-alt:before{content:"\f422"}.fa-compress-arrows-alt:before{content:"\f78c"}.fa-concierge-bell:before{content:"\f562"}.fa-confluence:before{content:"\f78d"}.fa-connectdevelop:before{content:"\f20e"}.fa-contao:before{content:"\f26d"}.fa-cookie:before{content:"\f563"}.fa-cookie-bite:before{content:"\f564"}.fa-copy:before{content:"\f0c5"}.fa-copyright:before{content:"\f1f9"}.fa-cotton-bureau:before{content:"\f89e"}.fa-couch:before{content:"\f4b8"}.fa-cpanel:before{content:"\f388"}.fa-creative-commons:before{content:"\f25e"}.fa-creative-commons-by:before{content:"\f4e7"}.fa-creative-commons-nc:before{content:"\f4e8"}.fa-creative-commons-nc-eu:before{content:"\f4e9"}.fa-creative-commons-nc-jp:before{content:"\f4ea"}.fa-creative-commons-nd:before{content:"\f4eb"}.fa-creative-commons-pd:before{content:"\f4ec"}.fa-creative-commons-pd-alt:before{content:"\f4ed"}.fa-creative-commons-remix:before{content:"\f4ee"}.fa-creative-commons-sa:before{content:"\f4ef"}.fa-creative-commons-sampling:before{content:"\f4f0"}.fa-creative-commons-sampling-plus:before{content:"\f4f1"}.fa-creative-commons-share:before{content:"\f4f2"}.fa-creative-commons-zero:before{content:"\f4f3"}.fa-credit-card:before{content:"\f09d"}.fa-critical-role:before{content:"\f6c9"}.fa-crop:before{content:"\f125"}.fa-crop-alt:before{content:"\f565"}.fa-cross:before{content:"\f654"}.fa-crosshairs:before{content:"\f05b"}.fa-crow:before{content:"\f520"}.fa-crown:before{content:"\f521"}.fa-crutch:before{content:"\f7f7"}.fa-css3:before{content:"\f13c"}.fa-css3-alt:before{content:"\f38b"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-cut:before{content:"\f0c4"}.fa-cuttlefish:before{content:"\f38c"}.fa-d-and-d:before{content:"\f38d"}.fa-d-and-d-beyond:before{content:"\f6ca"}.fa-dailymotion:before{content:"\f952"}.fa-dashcube:before{content:"\f210"}.fa-database:before{content:"\f1c0"}.fa-deaf:before{content:"\f2a4"}.fa-delicious:before{content:"\f1a5"}.fa-democrat:before{content:"\f747"}.fa-deploydog:before{content:"\f38e"}.fa-deskpro:before{content:"\f38f"}.fa-desktop:before{content:"\f108"}.fa-dev:before{content:"\f6cc"}.fa-deviantart:before{content:"\f1bd"}.fa-dharmachakra:before{content:"\f655"}.fa-dhl:before{content:"\f790"}.fa-diagnoses:before{content:"\f470"}.fa-diaspora:before{content:"\f791"}.fa-dice:before{content:"\f522"}.fa-dice-d20:before{content:"\f6cf"}.fa-dice-d6:before{content:"\f6d1"}.fa-dice-five:before{content:"\f523"}.fa-dice-four:before{content:"\f524"}.fa-dice-one:before{content:"\f525"}.fa-dice-six:before{content:"\f526"}.fa-dice-three:before{content:"\f527"}.fa-dice-two:before{content:"\f528"}.fa-digg:before{content:"\f1a6"}.fa-digital-ocean:before{content:"\f391"}.fa-digital-tachograph:before{content:"\f566"}.fa-directions:before{content:"\f5eb"}.fa-discord:before{content:"\f392"}.fa-discourse:before{content:"\f393"}.fa-disease:before{content:"\f7fa"}.fa-divide:before{content:"\f529"}.fa-dizzy:before{content:"\f567"}.fa-dna:before{content:"\f471"}.fa-dochub:before{content:"\f394"}.fa-docker:before{content:"\f395"}.fa-dog:before{content:"\f6d3"}.fa-dollar-sign:before{content:"\f155"}.fa-dolly:before{content:"\f472"}.fa-dolly-flatbed:before{content:"\f474"}.fa-donate:before{content:"\f4b9"}.fa-door-closed:before{content:"\f52a"}.fa-door-open:before{content:"\f52b"}.fa-dot-circle:before{content:"\f192"}.fa-dove:before{content:"\f4ba"}.fa-download:before{content:"\f019"}.fa-draft2digital:before{content:"\f396"}.fa-drafting-compass:before{content:"\f568"}.fa-dragon:before{content:"\f6d5"}.fa-draw-polygon:before{content:"\f5ee"}.fa-dribbble:before{content:"\f17d"}.fa-dribbble-square:before{content:"\f397"}.fa-dropbox:before{content:"\f16b"}.fa-drum:before{content:"\f569"}.fa-drum-steelpan:before{content:"\f56a"}.fa-drumstick-bite:before{content:"\f6d7"}.fa-drupal:before{content:"\f1a9"}.fa-dumbbell:before{content:"\f44b"}.fa-dumpster:before{content:"\f793"}.fa-dumpster-fire:before{content:"\f794"}.fa-dungeon:before{content:"\f6d9"}.fa-dyalog:before{content:"\f399"}.fa-earlybirds:before{content:"\f39a"}.fa-ebay:before{content:"\f4f4"}.fa-edge:before{content:"\f282"}.fa-edit:before{content:"\f044"}.fa-egg:before{content:"\f7fb"}.fa-eject:before{content:"\f052"}.fa-elementor:before{content:"\f430"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-ello:before{content:"\f5f1"}.fa-ember:before{content:"\f423"}.fa-empire:before{content:"\f1d1"}.fa-envelope:before{content:"\f0e0"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-text:before{content:"\f658"}.fa-envelope-square:before{content:"\f199"}.fa-envira:before{content:"\f299"}.fa-equals:before{content:"\f52c"}.fa-eraser:before{content:"\f12d"}.fa-erlang:before{content:"\f39d"}.fa-ethereum:before{content:"\f42e"}.fa-ethernet:before{content:"\f796"}.fa-etsy:before{content:"\f2d7"}.fa-euro-sign:before{content:"\f153"}.fa-evernote:before{content:"\f839"}.fa-exchange-alt:before{content:"\f362"}.fa-exclamation:before{content:"\f12a"}.fa-exclamation-circle:before{content:"\f06a"}.fa-exclamation-triangle:before{content:"\f071"}.fa-expand:before{content:"\f065"}.fa-expand-alt:before{content:"\f424"}.fa-expand-arrows-alt:before{content:"\f31e"}.fa-expeditedssl:before{content:"\f23e"}.fa-external-link-alt:before{content:"\f35d"}.fa-external-link-square-alt:before{content:"\f360"}.fa-eye:before{content:"\f06e"}.fa-eye-dropper:before{content:"\f1fb"}.fa-eye-slash:before{content:"\f070"}.fa-facebook:before{content:"\f09a"}.fa-facebook-f:before{content:"\f39e"}.fa-facebook-messenger:before{content:"\f39f"}.fa-facebook-square:before{content:"\f082"}.fa-fan:before{content:"\f863"}.fa-fantasy-flight-games:before{content:"\f6dc"}.fa-fast-backward:before{content:"\f049"}.fa-fast-forward:before{content:"\f050"}.fa-faucet:before{content:"\f905"}.fa-fax:before{content:"\f1ac"}.fa-feather:before{content:"\f52d"}.fa-feather-alt:before{content:"\f56b"}.fa-fedex:before{content:"\f797"}.fa-fedora:before{content:"\f798"}.fa-female:before{content:"\f182"}.fa-fighter-jet:before{content:"\f0fb"}.fa-figma:before{content:"\f799"}.fa-file:before{content:"\f15b"}.fa-file-alt:before{content:"\f15c"}.fa-file-archive:before{content:"\f1c6"}.fa-file-audio:before{content:"\f1c7"}.fa-file-code:before{content:"\f1c9"}.fa-file-contract:before{content:"\f56c"}.fa-file-csv:before{content:"\f6dd"}.fa-file-download:before{content:"\f56d"}.fa-file-excel:before{content:"\f1c3"}.fa-file-export:before{content:"\f56e"}.fa-file-image:before{content:"\f1c5"}.fa-file-import:before{content:"\f56f"}.fa-file-invoice:before{content:"\f570"}.fa-file-invoice-dollar:before{content:"\f571"}.fa-file-medical:before{content:"\f477"}.fa-file-medical-alt:before{content:"\f478"}.fa-file-pdf:before{content:"\f1c1"}.fa-file-powerpoint:before{content:"\f1c4"}.fa-file-prescription:before{content:"\f572"}.fa-file-signature:before{content:"\f573"}.fa-file-upload:before{content:"\f574"}.fa-file-video:before{content:"\f1c8"}.fa-file-word:before{content:"\f1c2"}.fa-fill:before{content:"\f575"}.fa-fill-drip:before{content:"\f576"}.fa-film:before{content:"\f008"}.fa-filter:before{content:"\f0b0"}.fa-fingerprint:before{content:"\f577"}.fa-fire:before{content:"\f06d"}.fa-fire-alt:before{content:"\f7e4"}.fa-fire-extinguisher:before{content:"\f134"}.fa-firefox:before{content:"\f269"}.fa-firefox-browser:before{content:"\f907"}.fa-first-aid:before{content:"\f479"}.fa-first-order:before{content:"\f2b0"}.fa-first-order-alt:before{content:"\f50a"}.fa-firstdraft:before{content:"\f3a1"}.fa-fish:before{content:"\f578"}.fa-fist-raised:before{content:"\f6de"}.fa-flag:before{content:"\f024"}.fa-flag-checkered:before{content:"\f11e"}.fa-flag-usa:before{content:"\f74d"}.fa-flask:before{content:"\f0c3"}.fa-flickr:before{content:"\f16e"}.fa-flipboard:before{content:"\f44d"}.fa-flushed:before{content:"\f579"}.fa-fly:before{content:"\f417"}.fa-folder:before{content:"\f07b"}.fa-folder-minus:before{content:"\f65d"}.fa-folder-open:before{content:"\f07c"}.fa-folder-plus:before{content:"\f65e"}.fa-font:before{content:"\f031"}.fa-font-awesome:before{content:"\f2b4"}.fa-font-awesome-alt:before{content:"\f35c"}.fa-font-awesome-flag:before{content:"\f425"}.fa-font-awesome-logo-full:before{content:"\f4e6"}.fa-fonticons:before{content:"\f280"}.fa-fonticons-fi:before{content:"\f3a2"}.fa-football-ball:before{content:"\f44e"}.fa-fort-awesome:before{content:"\f286"}.fa-fort-awesome-alt:before{content:"\f3a3"}.fa-forumbee:before{content:"\f211"}.fa-forward:before{content:"\f04e"}.fa-foursquare:before{content:"\f180"}.fa-free-code-camp:before{content:"\f2c5"}.fa-freebsd:before{content:"\f3a4"}.fa-frog:before{content:"\f52e"}.fa-frown:before{content:"\f119"}.fa-frown-open:before{content:"\f57a"}.fa-fulcrum:before{content:"\f50b"}.fa-funnel-dollar:before{content:"\f662"}.fa-futbol:before{content:"\f1e3"}.fa-galactic-republic:before{content:"\f50c"}.fa-galactic-senate:before{content:"\f50d"}.fa-gamepad:before{content:"\f11b"}.fa-gas-pump:before{content:"\f52f"}.fa-gavel:before{content:"\f0e3"}.fa-gem:before{content:"\f3a5"}.fa-genderless:before{content:"\f22d"}.fa-get-pocket:before{content:"\f265"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-ghost:before{content:"\f6e2"}.fa-gift:before{content:"\f06b"}.fa-gifts:before{content:"\f79c"}.fa-git:before{content:"\f1d3"}.fa-git-alt:before{content:"\f841"}.fa-git-square:before{content:"\f1d2"}.fa-github:before{content:"\f09b"}.fa-github-alt:before{content:"\f113"}.fa-github-square:before{content:"\f092"}.fa-gitkraken:before{content:"\f3a6"}.fa-gitlab:before{content:"\f296"}.fa-gitter:before{content:"\f426"}.fa-glass-cheers:before{content:"\f79f"}.fa-glass-martini:before{content:"\f000"}.fa-glass-martini-alt:before{content:"\f57b"}.fa-glass-whiskey:before{content:"\f7a0"}.fa-glasses:before{content:"\f530"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-globe:before{content:"\f0ac"}.fa-globe-africa:before{content:"\f57c"}.fa-globe-americas:before{content:"\f57d"}.fa-globe-asia:before{content:"\f57e"}.fa-globe-europe:before{content:"\f7a2"}.fa-gofore:before{content:"\f3a7"}.fa-golf-ball:before{content:"\f450"}.fa-goodreads:before{content:"\f3a8"}.fa-goodreads-g:before{content:"\f3a9"}.fa-google:before{content:"\f1a0"}.fa-google-drive:before{content:"\f3aa"}.fa-google-play:before{content:"\f3ab"}.fa-google-plus:before{content:"\f2b3"}.fa-google-plus-g:before{content:"\f0d5"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-wallet:before{content:"\f1ee"}.fa-gopuram:before{content:"\f664"}.fa-graduation-cap:before{content:"\f19d"}.fa-gratipay:before{content:"\f184"}.fa-grav:before{content:"\f2d6"}.fa-greater-than:before{content:"\f531"}.fa-greater-than-equal:before{content:"\f532"}.fa-grimace:before{content:"\f57f"}.fa-grin:before{content:"\f580"}.fa-grin-alt:before{content:"\f581"}.fa-grin-beam:before{content:"\f582"}.fa-grin-beam-sweat:before{content:"\f583"}.fa-grin-hearts:before{content:"\f584"}.fa-grin-squint:before{content:"\f585"}.fa-grin-squint-tears:before{content:"\f586"}.fa-grin-stars:before{content:"\f587"}.fa-grin-tears:before{content:"\f588"}.fa-grin-tongue:before{content:"\f589"}.fa-grin-tongue-squint:before{content:"\f58a"}.fa-grin-tongue-wink:before{content:"\f58b"}.fa-grin-wink:before{content:"\f58c"}.fa-grip-horizontal:before{content:"\f58d"}.fa-grip-lines:before{content:"\f7a4"}.fa-grip-lines-vertical:before{content:"\f7a5"}.fa-grip-vertical:before{content:"\f58e"}.fa-gripfire:before{content:"\f3ac"}.fa-grunt:before{content:"\f3ad"}.fa-guitar:before{content:"\f7a6"}.fa-gulp:before{content:"\f3ae"}.fa-h-square:before{content:"\f0fd"}.fa-hacker-news:before{content:"\f1d4"}.fa-hacker-news-square:before{content:"\f3af"}.fa-hackerrank:before{content:"\f5f7"}.fa-hamburger:before{content:"\f805"}.fa-hammer:before{content:"\f6e3"}.fa-hamsa:before{content:"\f665"}.fa-hand-holding:before{content:"\f4bd"}.fa-hand-holding-heart:before{content:"\f4be"}.fa-hand-holding-medical:before{content:"\f95c"}.fa-hand-holding-usd:before{content:"\f4c0"}.fa-hand-holding-water:before{content:"\f4c1"}.fa-hand-lizard:before{content:"\f258"}.fa-hand-middle-finger:before{content:"\f806"}.fa-hand-paper:before{content:"\f256"}.fa-hand-peace:before{content:"\f25b"}.fa-hand-point-down:before{content:"\f0a7"}.fa-hand-point-left:before{content:"\f0a5"}.fa-hand-point-right:before{content:"\f0a4"}.fa-hand-point-up:before{content:"\f0a6"}.fa-hand-pointer:before{content:"\f25a"}.fa-hand-rock:before{content:"\f255"}.fa-hand-scissors:before{content:"\f257"}.fa-hand-sparkles:before{content:"\f95d"}.fa-hand-spock:before{content:"\f259"}.fa-hands:before{content:"\f4c2"}.fa-hands-helping:before{content:"\f4c4"}.fa-hands-wash:before{content:"\f95e"}.fa-handshake:before{content:"\f2b5"}.fa-handshake-alt-slash:before{content:"\f95f"}.fa-handshake-slash:before{content:"\f960"}.fa-hanukiah:before{content:"\f6e6"}.fa-hard-hat:before{content:"\f807"}.fa-hashtag:before{content:"\f292"}.fa-hat-cowboy:before{content:"\f8c0"}.fa-hat-cowboy-side:before{content:"\f8c1"}.fa-hat-wizard:before{content:"\f6e8"}.fa-hdd:before{content:"\f0a0"}.fa-head-side-cough:before{content:"\f961"}.fa-head-side-cough-slash:before{content:"\f962"}.fa-head-side-mask:before{content:"\f963"}.fa-head-side-virus:before{content:"\f964"}.fa-heading:before{content:"\f1dc"}.fa-headphones:before{content:"\f025"}.fa-headphones-alt:before{content:"\f58f"}.fa-headset:before{content:"\f590"}.fa-heart:before{content:"\f004"}.fa-heart-broken:before{content:"\f7a9"}.fa-heartbeat:before{content:"\f21e"}.fa-helicopter:before{content:"\f533"}.fa-highlighter:before{content:"\f591"}.fa-hiking:before{content:"\f6ec"}.fa-hippo:before{content:"\f6ed"}.fa-hips:before{content:"\f452"}.fa-hire-a-helper:before{content:"\f3b0"}.fa-history:before{content:"\f1da"}.fa-hockey-puck:before{content:"\f453"}.fa-holly-berry:before{content:"\f7aa"}.fa-home:before{content:"\f015"}.fa-hooli:before{content:"\f427"}.fa-hornbill:before{content:"\f592"}.fa-horse:before{content:"\f6f0"}.fa-horse-head:before{content:"\f7ab"}.fa-hospital:before{content:"\f0f8"}.fa-hospital-alt:before{content:"\f47d"}.fa-hospital-symbol:before{content:"\f47e"}.fa-hospital-user:before{content:"\f80d"}.fa-hot-tub:before{content:"\f593"}.fa-hotdog:before{content:"\f80f"}.fa-hotel:before{content:"\f594"}.fa-hotjar:before{content:"\f3b1"}.fa-hourglass:before{content:"\f254"}.fa-hourglass-end:before{content:"\f253"}.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-start:before{content:"\f251"}.fa-house-damage:before{content:"\f6f1"}.fa-house-user:before{content:"\f965"}.fa-houzz:before{content:"\f27c"}.fa-hryvnia:before{content:"\f6f2"}.fa-html5:before{content:"\f13b"}.fa-hubspot:before{content:"\f3b2"}.fa-i-cursor:before{content:"\f246"}.fa-ice-cream:before{content:"\f810"}.fa-icicles:before{content:"\f7ad"}.fa-icons:before{content:"\f86d"}.fa-id-badge:before{content:"\f2c1"}.fa-id-card:before{content:"\f2c2"}.fa-id-card-alt:before{content:"\f47f"}.fa-ideal:before{content:"\f913"}.fa-igloo:before{content:"\f7ae"}.fa-image:before{content:"\f03e"}.fa-images:before{content:"\f302"}.fa-imdb:before{content:"\f2d8"}.fa-inbox:before{content:"\f01c"}.fa-indent:before{content:"\f03c"}.fa-industry:before{content:"\f275"}.fa-infinity:before{content:"\f534"}.fa-info:before{content:"\f129"}.fa-info-circle:before{content:"\f05a"}.fa-instagram:before{content:"\f16d"}.fa-instagram-square:before{content:"\f955"}.fa-intercom:before{content:"\f7af"}.fa-internet-explorer:before{content:"\f26b"}.fa-invision:before{content:"\f7b0"}.fa-ioxhost:before{content:"\f208"}.fa-italic:before{content:"\f033"}.fa-itch-io:before{content:"\f83a"}.fa-itunes:before{content:"\f3b4"}.fa-itunes-note:before{content:"\f3b5"}.fa-java:before{content:"\f4e4"}.fa-jedi:before{content:"\f669"}.fa-jedi-order:before{content:"\f50e"}.fa-jenkins:before{content:"\f3b6"}.fa-jira:before{content:"\f7b1"}.fa-joget:before{content:"\f3b7"}.fa-joint:before{content:"\f595"}.fa-joomla:before{content:"\f1aa"}.fa-journal-whills:before{content:"\f66a"}.fa-js:before{content:"\f3b8"}.fa-js-square:before{content:"\f3b9"}.fa-jsfiddle:before{content:"\f1cc"}.fa-kaaba:before{content:"\f66b"}.fa-kaggle:before{content:"\f5fa"}.fa-key:before{content:"\f084"}.fa-keybase:before{content:"\f4f5"}.fa-keyboard:before{content:"\f11c"}.fa-keycdn:before{content:"\f3ba"}.fa-khanda:before{content:"\f66d"}.fa-kickstarter:before{content:"\f3bb"}.fa-kickstarter-k:before{content:"\f3bc"}.fa-kiss:before{content:"\f596"}.fa-kiss-beam:before{content:"\f597"}.fa-kiss-wink-heart:before{content:"\f598"}.fa-kiwi-bird:before{content:"\f535"}.fa-korvue:before{content:"\f42f"}.fa-landmark:before{content:"\f66f"}.fa-language:before{content:"\f1ab"}.fa-laptop:before{content:"\f109"}.fa-laptop-code:before{content:"\f5fc"}.fa-laptop-house:before{content:"\f966"}.fa-laptop-medical:before{content:"\f812"}.fa-laravel:before{content:"\f3bd"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-laugh:before{content:"\f599"}.fa-laugh-beam:before{content:"\f59a"}.fa-laugh-squint:before{content:"\f59b"}.fa-laugh-wink:before{content:"\f59c"}.fa-layer-group:before{content:"\f5fd"}.fa-leaf:before{content:"\f06c"}.fa-leanpub:before{content:"\f212"}.fa-lemon:before{content:"\f094"}.fa-less:before{content:"\f41d"}.fa-less-than:before{content:"\f536"}.fa-less-than-equal:before{content:"\f537"}.fa-level-down-alt:before{content:"\f3be"}.fa-level-up-alt:before{content:"\f3bf"}.fa-life-ring:before{content:"\f1cd"}.fa-lightbulb:before{content:"\f0eb"}.fa-line:before{content:"\f3c0"}.fa-link:before{content:"\f0c1"}.fa-linkedin:before{content:"\f08c"}.fa-linkedin-in:before{content:"\f0e1"}.fa-linode:before{content:"\f2b8"}.fa-linux:before{content:"\f17c"}.fa-lira-sign:before{content:"\f195"}.fa-list:before{content:"\f03a"}.fa-list-alt:before{content:"\f022"}.fa-list-ol:before{content:"\f0cb"}.fa-list-ul:before{content:"\f0ca"}.fa-location-arrow:before{content:"\f124"}.fa-lock:before{content:"\f023"}.fa-lock-open:before{content:"\f3c1"}.fa-long-arrow-alt-down:before{content:"\f309"}.fa-long-arrow-alt-left:before{content:"\f30a"}.fa-long-arrow-alt-right:before{content:"\f30b"}.fa-long-arrow-alt-up:before{content:"\f30c"}.fa-low-vision:before{content:"\f2a8"}.fa-luggage-cart:before{content:"\f59d"}.fa-lungs:before{content:"\f604"}.fa-lungs-virus:before{content:"\f967"}.fa-lyft:before{content:"\f3c3"}.fa-magento:before{content:"\f3c4"}.fa-magic:before{content:"\f0d0"}.fa-magnet:before{content:"\f076"}.fa-mail-bulk:before{content:"\f674"}.fa-mailchimp:before{content:"\f59e"}.fa-male:before{content:"\f183"}.fa-mandalorian:before{content:"\f50f"}.fa-map:before{content:"\f279"}.fa-map-marked:before{content:"\f59f"}.fa-map-marked-alt:before{content:"\f5a0"}.fa-map-marker:before{content:"\f041"}.fa-map-marker-alt:before{content:"\f3c5"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-markdown:before{content:"\f60f"}.fa-marker:before{content:"\f5a1"}.fa-mars:before{content:"\f222"}.fa-mars-double:before{content:"\f227"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mask:before{content:"\f6fa"}.fa-mastodon:before{content:"\f4f6"}.fa-maxcdn:before{content:"\f136"}.fa-mdb:before{content:"\f8ca"}.fa-medal:before{content:"\f5a2"}.fa-medapps:before{content:"\f3c6"}.fa-medium:before{content:"\f23a"}.fa-medium-m:before{content:"\f3c7"}.fa-medkit:before{content:"\f0fa"}.fa-medrt:before{content:"\f3c8"}.fa-meetup:before{content:"\f2e0"}.fa-megaport:before{content:"\f5a3"}.fa-meh:before{content:"\f11a"}.fa-meh-blank:before{content:"\f5a4"}.fa-meh-rolling-eyes:before{content:"\f5a5"}.fa-memory:before{content:"\f538"}.fa-mendeley:before{content:"\f7b3"}.fa-menorah:before{content:"\f676"}.fa-mercury:before{content:"\f223"}.fa-meteor:before{content:"\f753"}.fa-microblog:before{content:"\f91a"}.fa-microchip:before{content:"\f2db"}.fa-microphone:before{content:"\f130"}.fa-microphone-alt:before{content:"\f3c9"}.fa-microphone-alt-slash:before{content:"\f539"}.fa-microphone-slash:before{content:"\f131"}.fa-microscope:before{content:"\f610"}.fa-microsoft:before{content:"\f3ca"}.fa-minus:before{content:"\f068"}.fa-minus-circle:before{content:"\f056"}.fa-minus-square:before{content:"\f146"}.fa-mitten:before{content:"\f7b5"}.fa-mix:before{content:"\f3cb"}.fa-mixcloud:before{content:"\f289"}.fa-mixer:before{content:"\f956"}.fa-mizuni:before{content:"\f3cc"}.fa-mobile:before{content:"\f10b"}.fa-mobile-alt:before{content:"\f3cd"}.fa-modx:before{content:"\f285"}.fa-monero:before{content:"\f3d0"}.fa-money-bill:before{content:"\f0d6"}.fa-money-bill-alt:before{content:"\f3d1"}.fa-money-bill-wave:before{content:"\f53a"}.fa-money-bill-wave-alt:before{content:"\f53b"}.fa-money-check:before{content:"\f53c"}.fa-money-check-alt:before{content:"\f53d"}.fa-monument:before{content:"\f5a6"}.fa-moon:before{content:"\f186"}.fa-mortar-pestle:before{content:"\f5a7"}.fa-mosque:before{content:"\f678"}.fa-motorcycle:before{content:"\f21c"}.fa-mountain:before{content:"\f6fc"}.fa-mouse:before{content:"\f8cc"}.fa-mouse-pointer:before{content:"\f245"}.fa-mug-hot:before{content:"\f7b6"}.fa-music:before{content:"\f001"}.fa-napster:before{content:"\f3d2"}.fa-neos:before{content:"\f612"}.fa-network-wired:before{content:"\f6ff"}.fa-neuter:before{content:"\f22c"}.fa-newspaper:before{content:"\f1ea"}.fa-nimblr:before{content:"\f5a8"}.fa-node:before{content:"\f419"}.fa-node-js:before{content:"\f3d3"}.fa-not-equal:before{content:"\f53e"}.fa-notes-medical:before{content:"\f481"}.fa-npm:before{content:"\f3d4"}.fa-ns8:before{content:"\f3d5"}.fa-nutritionix:before{content:"\f3d6"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-oil-can:before{content:"\f613"}.fa-old-republic:before{content:"\f510"}.fa-om:before{content:"\f679"}.fa-opencart:before{content:"\f23d"}.fa-openid:before{content:"\f19b"}.fa-opera:before{content:"\f26a"}.fa-optin-monster:before{content:"\f23c"}.fa-orcid:before{content:"\f8d2"}.fa-osi:before{content:"\f41a"}.fa-otter:before{content:"\f700"}.fa-outdent:before{content:"\f03b"}.fa-page4:before{content:"\f3d7"}.fa-pagelines:before{content:"\f18c"}.fa-pager:before{content:"\f815"}.fa-paint-brush:before{content:"\f1fc"}.fa-paint-roller:before{content:"\f5aa"}.fa-palette:before{content:"\f53f"}.fa-palfed:before{content:"\f3d8"}.fa-pallet:before{content:"\f482"}.fa-paper-plane:before{content:"\f1d8"}.fa-paperclip:before{content:"\f0c6"}.fa-parachute-box:before{content:"\f4cd"}.fa-paragraph:before{content:"\f1dd"}.fa-parking:before{content:"\f540"}.fa-passport:before{content:"\f5ab"}.fa-pastafarianism:before{content:"\f67b"}.fa-paste:before{content:"\f0ea"}.fa-patreon:before{content:"\f3d9"}.fa-pause:before{content:"\f04c"}.fa-pause-circle:before{content:"\f28b"}.fa-paw:before{content:"\f1b0"}.fa-paypal:before{content:"\f1ed"}.fa-peace:before{content:"\f67c"}.fa-pen:before{content:"\f304"}.fa-pen-alt:before{content:"\f305"}.fa-pen-fancy:before{content:"\f5ac"}.fa-pen-nib:before{content:"\f5ad"}.fa-pen-square:before{content:"\f14b"}.fa-pencil-alt:before{content:"\f303"}.fa-pencil-ruler:before{content:"\f5ae"}.fa-penny-arcade:before{content:"\f704"}.fa-people-arrows:before{content:"\f968"}.fa-people-carry:before{content:"\f4ce"}.fa-pepper-hot:before{content:"\f816"}.fa-percent:before{content:"\f295"}.fa-percentage:before{content:"\f541"}.fa-periscope:before{content:"\f3da"}.fa-person-booth:before{content:"\f756"}.fa-phabricator:before{content:"\f3db"}.fa-phoenix-framework:before{content:"\f3dc"}.fa-phoenix-squadron:before{content:"\f511"}.fa-phone:before{content:"\f095"}.fa-phone-alt:before{content:"\f879"}.fa-phone-slash:before{content:"\f3dd"}.fa-phone-square:before{content:"\f098"}.fa-phone-square-alt:before{content:"\f87b"}.fa-phone-volume:before{content:"\f2a0"}.fa-photo-video:before{content:"\f87c"}.fa-php:before{content:"\f457"}.fa-pied-piper:before{content:"\f2ae"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-pied-piper-hat:before{content:"\f4e5"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-square:before{content:"\f91e"}.fa-piggy-bank:before{content:"\f4d3"}.fa-pills:before{content:"\f484"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-p:before{content:"\f231"}.fa-pinterest-square:before{content:"\f0d3"}.fa-pizza-slice:before{content:"\f818"}.fa-place-of-worship:before{content:"\f67f"}.fa-plane:before{content:"\f072"}.fa-plane-arrival:before{content:"\f5af"}.fa-plane-departure:before{content:"\f5b0"}.fa-plane-slash:before{content:"\f969"}.fa-play:before{content:"\f04b"}.fa-play-circle:before{content:"\f144"}.fa-playstation:before{content:"\f3df"}.fa-plug:before{content:"\f1e6"}.fa-plus:before{content:"\f067"}.fa-plus-circle:before{content:"\f055"}.fa-plus-square:before{content:"\f0fe"}.fa-podcast:before{content:"\f2ce"}.fa-poll:before{content:"\f681"}.fa-poll-h:before{content:"\f682"}.fa-poo:before{content:"\f2fe"}.fa-poo-storm:before{content:"\f75a"}.fa-poop:before{content:"\f619"}.fa-portrait:before{content:"\f3e0"}.fa-pound-sign:before{content:"\f154"}.fa-power-off:before{content:"\f011"}.fa-pray:before{content:"\f683"}.fa-praying-hands:before{content:"\f684"}.fa-prescription:before{content:"\f5b1"}.fa-prescription-bottle:before{content:"\f485"}.fa-prescription-bottle-alt:before{content:"\f486"}.fa-print:before{content:"\f02f"}.fa-procedures:before{content:"\f487"}.fa-product-hunt:before{content:"\f288"}.fa-project-diagram:before{content:"\f542"}.fa-pump-medical:before{content:"\f96a"}.fa-pump-soap:before{content:"\f96b"}.fa-pushed:before{content:"\f3e1"}.fa-puzzle-piece:before{content:"\f12e"}.fa-python:before{content:"\f3e2"}.fa-qq:before{content:"\f1d6"}.fa-qrcode:before{content:"\f029"}.fa-question:before{content:"\f128"}.fa-question-circle:before{content:"\f059"}.fa-quidditch:before{content:"\f458"}.fa-quinscape:before{content:"\f459"}.fa-quora:before{content:"\f2c4"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-quran:before{content:"\f687"}.fa-r-project:before{content:"\f4f7"}.fa-radiation:before{content:"\f7b9"}.fa-radiation-alt:before{content:"\f7ba"}.fa-rainbow:before{content:"\f75b"}.fa-random:before{content:"\f074"}.fa-raspberry-pi:before{content:"\f7bb"}.fa-ravelry:before{content:"\f2d9"}.fa-react:before{content:"\f41b"}.fa-reacteurope:before{content:"\f75d"}.fa-readme:before{content:"\f4d5"}.fa-rebel:before{content:"\f1d0"}.fa-receipt:before{content:"\f543"}.fa-record-vinyl:before{content:"\f8d9"}.fa-recycle:before{content:"\f1b8"}.fa-red-river:before{content:"\f3e3"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-alien:before{content:"\f281"}.fa-reddit-square:before{content:"\f1a2"}.fa-redhat:before{content:"\f7bc"}.fa-redo:before{content:"\f01e"}.fa-redo-alt:before{content:"\f2f9"}.fa-registered:before{content:"\f25d"}.fa-remove-format:before{content:"\f87d"}.fa-renren:before{content:"\f18b"}.fa-reply:before{content:"\f3e5"}.fa-reply-all:before{content:"\f122"}.fa-replyd:before{content:"\f3e6"}.fa-republican:before{content:"\f75e"}.fa-researchgate:before{content:"\f4f8"}.fa-resolving:before{content:"\f3e7"}.fa-restroom:before{content:"\f7bd"}.fa-retweet:before{content:"\f079"}.fa-rev:before{content:"\f5b2"}.fa-ribbon:before{content:"\f4d6"}.fa-ring:before{content:"\f70b"}.fa-road:before{content:"\f018"}.fa-robot:before{content:"\f544"}.fa-rocket:before{content:"\f135"}.fa-rocketchat:before{content:"\f3e8"}.fa-rockrms:before{content:"\f3e9"}.fa-route:before{content:"\f4d7"}.fa-rss:before{content:"\f09e"}.fa-rss-square:before{content:"\f143"}.fa-ruble-sign:before{content:"\f158"}.fa-ruler:before{content:"\f545"}.fa-ruler-combined:before{content:"\f546"}.fa-ruler-horizontal:before{content:"\f547"}.fa-ruler-vertical:before{content:"\f548"}.fa-running:before{content:"\f70c"}.fa-rupee-sign:before{content:"\f156"}.fa-sad-cry:before{content:"\f5b3"}.fa-sad-tear:before{content:"\f5b4"}.fa-safari:before{content:"\f267"}.fa-salesforce:before{content:"\f83b"}.fa-sass:before{content:"\f41e"}.fa-satellite:before{content:"\f7bf"}.fa-satellite-dish:before{content:"\f7c0"}.fa-save:before{content:"\f0c7"}.fa-schlix:before{content:"\f3ea"}.fa-school:before{content:"\f549"}.fa-screwdriver:before{content:"\f54a"}.fa-scribd:before{content:"\f28a"}.fa-scroll:before{content:"\f70e"}.fa-sd-card:before{content:"\f7c2"}.fa-search:before{content:"\f002"}.fa-search-dollar:before{content:"\f688"}.fa-search-location:before{content:"\f689"}.fa-search-minus:before{content:"\f010"}.fa-search-plus:before{content:"\f00e"}.fa-searchengin:before{content:"\f3eb"}.fa-seedling:before{content:"\f4d8"}.fa-sellcast:before{content:"\f2da"}.fa-sellsy:before{content:"\f213"}.fa-server:before{content:"\f233"}.fa-servicestack:before{content:"\f3ec"}.fa-shapes:before{content:"\f61f"}.fa-share:before{content:"\f064"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-share-square:before{content:"\f14d"}.fa-shekel-sign:before{content:"\f20b"}.fa-shield-alt:before{content:"\f3ed"}.fa-shield-virus:before{content:"\f96c"}.fa-ship:before{content:"\f21a"}.fa-shipping-fast:before{content:"\f48b"}.fa-shirtsinbulk:before{content:"\f214"}.fa-shoe-prints:before{content:"\f54b"}.fa-shopify:before{content:"\f957"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-shopping-cart:before{content:"\f07a"}.fa-shopware:before{content:"\f5b5"}.fa-shower:before{content:"\f2cc"}.fa-shuttle-van:before{content:"\f5b6"}.fa-sign:before{content:"\f4d9"}.fa-sign-in-alt:before{content:"\f2f6"}.fa-sign-language:before{content:"\f2a7"}.fa-sign-out-alt:before{content:"\f2f5"}.fa-signal:before{content:"\f012"}.fa-signature:before{content:"\f5b7"}.fa-sim-card:before{content:"\f7c4"}.fa-simplybuilt:before{content:"\f215"}.fa-sistrix:before{content:"\f3ee"}.fa-sitemap:before{content:"\f0e8"}.fa-sith:before{content:"\f512"}.fa-skating:before{content:"\f7c5"}.fa-sketch:before{content:"\f7c6"}.fa-skiing:before{content:"\f7c9"}.fa-skiing-nordic:before{content:"\f7ca"}.fa-skull:before{content:"\f54c"}.fa-skull-crossbones:before{content:"\f714"}.fa-skyatlas:before{content:"\f216"}.fa-skype:before{content:"\f17e"}.fa-slack:before{content:"\f198"}.fa-slack-hash:before{content:"\f3ef"}.fa-slash:before{content:"\f715"}.fa-sleigh:before{content:"\f7cc"}.fa-sliders-h:before{content:"\f1de"}.fa-slideshare:before{content:"\f1e7"}.fa-smile:before{content:"\f118"}.fa-smile-beam:before{content:"\f5b8"}.fa-smile-wink:before{content:"\f4da"}.fa-smog:before{content:"\f75f"}.fa-smoking:before{content:"\f48d"}.fa-smoking-ban:before{content:"\f54d"}.fa-sms:before{content:"\f7cd"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-snowboarding:before{content:"\f7ce"}.fa-snowflake:before{content:"\f2dc"}.fa-snowman:before{content:"\f7d0"}.fa-snowplow:before{content:"\f7d2"}.fa-soap:before{content:"\f96e"}.fa-socks:before{content:"\f696"}.fa-solar-panel:before{content:"\f5ba"}.fa-sort:before{content:"\f0dc"}.fa-sort-alpha-down:before{content:"\f15d"}.fa-sort-alpha-down-alt:before{content:"\f881"}.fa-sort-alpha-up:before{content:"\f15e"}.fa-sort-alpha-up-alt:before{content:"\f882"}.fa-sort-amount-down:before{content:"\f160"}.fa-sort-amount-down-alt:before{content:"\f884"}.fa-sort-amount-up:before{content:"\f161"}.fa-sort-amount-up-alt:before{content:"\f885"}.fa-sort-down:before{content:"\f0dd"}.fa-sort-numeric-down:before{content:"\f162"}.fa-sort-numeric-down-alt:before{content:"\f886"}.fa-sort-numeric-up:before{content:"\f163"}.fa-sort-numeric-up-alt:before{content:"\f887"}.fa-sort-up:before{content:"\f0de"}.fa-soundcloud:before{content:"\f1be"}.fa-sourcetree:before{content:"\f7d3"}.fa-spa:before{content:"\f5bb"}.fa-space-shuttle:before{content:"\f197"}.fa-speakap:before{content:"\f3f3"}.fa-speaker-deck:before{content:"\f83c"}.fa-spell-check:before{content:"\f891"}.fa-spider:before{content:"\f717"}.fa-spinner:before{content:"\f110"}.fa-splotch:before{content:"\f5bc"}.fa-spotify:before{content:"\f1bc"}.fa-spray-can:before{content:"\f5bd"}.fa-square:before{content:"\f0c8"}.fa-square-full:before{content:"\f45c"}.fa-square-root-alt:before{content:"\f698"}.fa-squarespace:before{content:"\f5be"}.fa-stack-exchange:before{content:"\f18d"}.fa-stack-overflow:before{content:"\f16c"}.fa-stackpath:before{content:"\f842"}.fa-stamp:before{content:"\f5bf"}.fa-star:before{content:"\f005"}.fa-star-and-crescent:before{content:"\f699"}.fa-star-half:before{content:"\f089"}.fa-star-half-alt:before{content:"\f5c0"}.fa-star-of-david:before{content:"\f69a"}.fa-star-of-life:before{content:"\f621"}.fa-staylinked:before{content:"\f3f5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-steam-symbol:before{content:"\f3f6"}.fa-step-backward:before{content:"\f048"}.fa-step-forward:before{content:"\f051"}.fa-stethoscope:before{content:"\f0f1"}.fa-sticker-mule:before{content:"\f3f7"}.fa-sticky-note:before{content:"\f249"}.fa-stop:before{content:"\f04d"}.fa-stop-circle:before{content:"\f28d"}.fa-stopwatch:before{content:"\f2f2"}.fa-stopwatch-20:before{content:"\f96f"}.fa-store:before{content:"\f54e"}.fa-store-alt:before{content:"\f54f"}.fa-store-alt-slash:before{content:"\f970"}.fa-store-slash:before{content:"\f971"}.fa-strava:before{content:"\f428"}.fa-stream:before{content:"\f550"}.fa-street-view:before{content:"\f21d"}.fa-strikethrough:before{content:"\f0cc"}.fa-stripe:before{content:"\f429"}.fa-stripe-s:before{content:"\f42a"}.fa-stroopwafel:before{content:"\f551"}.fa-studiovinari:before{content:"\f3f8"}.fa-stumbleupon:before{content:"\f1a4"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-subscript:before{content:"\f12c"}.fa-subway:before{content:"\f239"}.fa-suitcase:before{content:"\f0f2"}.fa-suitcase-rolling:before{content:"\f5c1"}.fa-sun:before{content:"\f185"}.fa-superpowers:before{content:"\f2dd"}.fa-superscript:before{content:"\f12b"}.fa-supple:before{content:"\f3f9"}.fa-surprise:before{content:"\f5c2"}.fa-suse:before{content:"\f7d6"}.fa-swatchbook:before{content:"\f5c3"}.fa-swift:before{content:"\f8e1"}.fa-swimmer:before{content:"\f5c4"}.fa-swimming-pool:before{content:"\f5c5"}.fa-symfony:before{content:"\f83d"}.fa-synagogue:before{content:"\f69b"}.fa-sync:before{content:"\f021"}.fa-sync-alt:before{content:"\f2f1"}.fa-syringe:before{content:"\f48e"}.fa-table:before{content:"\f0ce"}.fa-table-tennis:before{content:"\f45d"}.fa-tablet:before{content:"\f10a"}.fa-tablet-alt:before{content:"\f3fa"}.fa-tablets:before{content:"\f490"}.fa-tachometer-alt:before{content:"\f3fd"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-tape:before{content:"\f4db"}.fa-tasks:before{content:"\f0ae"}.fa-taxi:before{content:"\f1ba"}.fa-teamspeak:before{content:"\f4f9"}.fa-teeth:before{content:"\f62e"}.fa-teeth-open:before{content:"\f62f"}.fa-telegram:before{content:"\f2c6"}.fa-telegram-plane:before{content:"\f3fe"}.fa-temperature-high:before{content:"\f769"}.fa-temperature-low:before{content:"\f76b"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-tenge:before{content:"\f7d7"}.fa-terminal:before{content:"\f120"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-th:before{content:"\f00a"}.fa-th-large:before{content:"\f009"}.fa-th-list:before{content:"\f00b"}.fa-the-red-yeti:before{content:"\f69d"}.fa-theater-masks:before{content:"\f630"}.fa-themeco:before{content:"\f5c6"}.fa-themeisle:before{content:"\f2b2"}.fa-thermometer:before{content:"\f491"}.fa-thermometer-empty:before{content:"\f2cb"}.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-think-peaks:before{content:"\f731"}.fa-thumbs-down:before{content:"\f165"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbtack:before{content:"\f08d"}.fa-ticket-alt:before{content:"\f3ff"}.fa-times:before{content:"\f00d"}.fa-times-circle:before{content:"\f057"}.fa-tint:before{content:"\f043"}.fa-tint-slash:before{content:"\f5c7"}.fa-tired:before{content:"\f5c8"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-toilet:before{content:"\f7d8"}.fa-toilet-paper:before{content:"\f71e"}.fa-toilet-paper-slash:before{content:"\f972"}.fa-toolbox:before{content:"\f552"}.fa-tools:before{content:"\f7d9"}.fa-tooth:before{content:"\f5c9"}.fa-torah:before{content:"\f6a0"}.fa-torii-gate:before{content:"\f6a1"}.fa-tractor:before{content:"\f722"}.fa-trade-federation:before{content:"\f513"}.fa-trademark:before{content:"\f25c"}.fa-traffic-light:before{content:"\f637"}.fa-trailer:before{content:"\f941"}.fa-train:before{content:"\f238"}.fa-tram:before{content:"\f7da"}.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-trash:before{content:"\f1f8"}.fa-trash-alt:before{content:"\f2ed"}.fa-trash-restore:before{content:"\f829"}.fa-trash-restore-alt:before{content:"\f82a"}.fa-tree:before{content:"\f1bb"}.fa-trello:before{content:"\f181"}.fa-tripadvisor:before{content:"\f262"}.fa-trophy:before{content:"\f091"}.fa-truck:before{content:"\f0d1"}.fa-truck-loading:before{content:"\f4de"}.fa-truck-monster:before{content:"\f63b"}.fa-truck-moving:before{content:"\f4df"}.fa-truck-pickup:before{content:"\f63c"}.fa-tshirt:before{content:"\f553"}.fa-tty:before{content:"\f1e4"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-tv:before{content:"\f26c"}.fa-twitch:before{content:"\f1e8"}.fa-twitter:before{content:"\f099"}.fa-twitter-square:before{content:"\f081"}.fa-typo3:before{content:"\f42b"}.fa-uber:before{content:"\f402"}.fa-ubuntu:before{content:"\f7df"}.fa-uikit:before{content:"\f403"}.fa-umbraco:before{content:"\f8e8"}.fa-umbrella:before{content:"\f0e9"}.fa-umbrella-beach:before{content:"\f5ca"}.fa-underline:before{content:"\f0cd"}.fa-undo:before{content:"\f0e2"}.fa-undo-alt:before{content:"\f2ea"}.fa-uniregistry:before{content:"\f404"}.fa-unity:before{content:"\f949"}.fa-universal-access:before{content:"\f29a"}.fa-university:before{content:"\f19c"}.fa-unlink:before{content:"\f127"}.fa-unlock:before{content:"\f09c"}.fa-unlock-alt:before{content:"\f13e"}.fa-untappd:before{content:"\f405"}.fa-upload:before{content:"\f093"}.fa-ups:before{content:"\f7e0"}.fa-usb:before{content:"\f287"}.fa-user:before{content:"\f007"}.fa-user-alt:before{content:"\f406"}.fa-user-alt-slash:before{content:"\f4fa"}.fa-user-astronaut:before{content:"\f4fb"}.fa-user-check:before{content:"\f4fc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-clock:before{content:"\f4fd"}.fa-user-cog:before{content:"\f4fe"}.fa-user-edit:before{content:"\f4ff"}.fa-user-friends:before{content:"\f500"}.fa-user-graduate:before{content:"\f501"}.fa-user-injured:before{content:"\f728"}.fa-user-lock:before{content:"\f502"}.fa-user-md:before{content:"\f0f0"}.fa-user-minus:before{content:"\f503"}.fa-user-ninja:before{content:"\f504"}.fa-user-nurse:before{content:"\f82f"}.fa-user-plus:before{content:"\f234"}.fa-user-secret:before{content:"\f21b"}.fa-user-shield:before{content:"\f505"}.fa-user-slash:before{content:"\f506"}.fa-user-tag:before{content:"\f507"}.fa-user-tie:before{content:"\f508"}.fa-user-times:before{content:"\f235"}.fa-users:before{content:"\f0c0"}.fa-users-cog:before{content:"\f509"}.fa-usps:before{content:"\f7e1"}.fa-ussunnah:before{content:"\f407"}.fa-utensil-spoon:before{content:"\f2e5"}.fa-utensils:before{content:"\f2e7"}.fa-vaadin:before{content:"\f408"}.fa-vector-square:before{content:"\f5cb"}.fa-venus:before{content:"\f221"}.fa-venus-double:before{content:"\f226"}.fa-venus-mars:before{content:"\f228"}.fa-viacoin:before{content:"\f237"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-vial:before{content:"\f492"}.fa-vials:before{content:"\f493"}.fa-viber:before{content:"\f409"}.fa-video:before{content:"\f03d"}.fa-video-slash:before{content:"\f4e2"}.fa-vihara:before{content:"\f6a7"}.fa-vimeo:before{content:"\f40a"}.fa-vimeo-square:before{content:"\f194"}.fa-vimeo-v:before{content:"\f27d"}.fa-vine:before{content:"\f1ca"}.fa-virus:before{content:"\f974"}.fa-virus-slash:before{content:"\f975"}.fa-viruses:before{content:"\f976"}.fa-vk:before{content:"\f189"}.fa-vnv:before{content:"\f40b"}.fa-voicemail:before{content:"\f897"}.fa-volleyball-ball:before{content:"\f45f"}.fa-volume-down:before{content:"\f027"}.fa-volume-mute:before{content:"\f6a9"}.fa-volume-off:before{content:"\f026"}.fa-volume-up:before{content:"\f028"}.fa-vote-yea:before{content:"\f772"}.fa-vr-cardboard:before{content:"\f729"}.fa-vuejs:before{content:"\f41f"}.fa-walking:before{content:"\f554"}.fa-wallet:before{content:"\f555"}.fa-warehouse:before{content:"\f494"}.fa-water:before{content:"\f773"}.fa-wave-square:before{content:"\f83e"}.fa-waze:before{content:"\f83f"}.fa-weebly:before{content:"\f5cc"}.fa-weibo:before{content:"\f18a"}.fa-weight:before{content:"\f496"}.fa-weight-hanging:before{content:"\f5cd"}.fa-weixin:before{content:"\f1d7"}.fa-whatsapp:before{content:"\f232"}.fa-whatsapp-square:before{content:"\f40c"}.fa-wheelchair:before{content:"\f193"}.fa-whmcs:before{content:"\f40d"}.fa-wifi:before{content:"\f1eb"}.fa-wikipedia-w:before{content:"\f266"}.fa-wind:before{content:"\f72e"}.fa-window-close:before{content:"\f410"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-windows:before{content:"\f17a"}.fa-wine-bottle:before{content:"\f72f"}.fa-wine-glass:before{content:"\f4e3"}.fa-wine-glass-alt:before{content:"\f5ce"}.fa-wix:before{content:"\f5cf"}.fa-wizards-of-the-coast:before{content:"\f730"}.fa-wolf-pack-battalion:before{content:"\f514"}.fa-won-sign:before{content:"\f159"}.fa-wordpress:before{content:"\f19a"}.fa-wordpress-simple:before{content:"\f411"}.fa-wpbeginner:before{content:"\f297"}.fa-wpexplorer:before{content:"\f2de"}.fa-wpforms:before{content:"\f298"}.fa-wpressr:before{content:"\f3e4"}.fa-wrench:before{content:"\f0ad"}.fa-x-ray:before{content:"\f497"}.fa-xbox:before{content:"\f412"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-y-combinator:before{content:"\f23b"}.fa-yahoo:before{content:"\f19e"}.fa-yammer:before{content:"\f840"}.fa-yandex:before{content:"\f413"}.fa-yandex-international:before{content:"\f414"}.fa-yarn:before{content:"\f7e3"}.fa-yelp:before{content:"\f1e9"}.fa-yen-sign:before{content:"\f157"}.fa-yin-yang:before{content:"\f6ad"}.fa-yoast:before{content:"\f2b1"}.fa-youtube:before{content:"\f167"}.fa-youtube-square:before{content:"\f431"}.fa-zhihu:before{content:"\f63f"}.sr-only{border:0;clip:rect(0,0,0,0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.sr-only-focusable:active,.sr-only-focusable:focus{clip:auto;height:auto;margin:0;overflow:visible;position:static;width:auto}zeal-0.7.1/src/app/resources/browser/assets/css/highlight.css000066400000000000000000000004111462517734600243040ustar00rootroot00000000000000/* Highlight on navigation to an anchor. */ @-webkit-keyframes targetNavigatedAnimation { from { background: #ffffff; } 50% { background: #ffff00; } to { background: #ffffff; } } *:target { -webkit-animation: targetNavigatedAnimation .5s linear; } zeal-0.7.1/src/app/resources/browser/assets/css/welcome.min.css000066400000000000000000003457061462517734600245750ustar00rootroot00000000000000/*! Style for Zeal's built-in welcome page. Based on Bulma. *//*! bulma.io v0.7.4 | MIT License | github.com/jgthms/bulma */@keyframes spinAround{from{transform:rotate(0)}to{transform:rotate(359deg)}}.button,.is-unselectable{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.navbar-link:not(.is-arrowless)::after{border:3px solid transparent;border-radius:2px;border-right:0;border-top:0;content:" ";display:block;height:.625em;margin-top:-.4375em;pointer-events:none;position:absolute;top:50%;transform:rotate(-45deg);transform-origin:center;width:.625em}.box:not(:last-child),.content:not(:last-child),.subtitle:not(:last-child),.title:not(:last-child){margin-bottom:1.5rem}.button.is-loading::after{animation:spinAround .5s infinite linear;border:2px solid #dbdbdb;border-radius:290486px;border-right-color:transparent;border-top-color:transparent;content:"";display:block;height:1em;position:relative;width:1em}.hero-video,.is-overlay{bottom:0;left:0;position:absolute;right:0;top:0}.button{-moz-appearance:none;-webkit-appearance:none;align-items:center;border:1px solid transparent;border-radius:4px;box-shadow:none;display:inline-flex;font-size:1rem;height:2.25em;justify-content:flex-start;line-height:1.5;padding-bottom:calc(.375em - 1px);padding-left:calc(.625em - 1px);padding-right:calc(.625em - 1px);padding-top:calc(.375em - 1px);position:relative;vertical-align:top}.button:active,.button:focus,.is-active.button,.is-focused.button{outline:0}.button[disabled],fieldset[disabled] .button{cursor:not-allowed}/*! minireset.css v0.0.4 | MIT License | github.com/jgthms/minireset.css */blockquote,body,dd,dl,dt,fieldset,figure,h1,h2,h3,h4,h5,h6,hr,html,iframe,legend,li,ol,p,pre,textarea,ul{margin:0;padding:0}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:400}ul{list-style:none}button,input,select,textarea{margin:0}html{box-sizing:border-box}*,::after,::before{box-sizing:inherit}embed,iframe,img,object,video{height:auto;max-width:100%}audio{max-width:100%}iframe{border:0}table{border-collapse:collapse;border-spacing:0}td,th{padding:0;text-align:left}html{background-color:#fff;font-size:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;min-width:300px;overflow-x:hidden;overflow-y:scroll;text-rendering:optimizeLegibility;text-size-adjust:100%}article,aside,figure,footer,header,hgroup,section{display:block}body,button,input,select,textarea{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif}code,pre{-moz-osx-font-smoothing:auto;-webkit-font-smoothing:auto;font-family:monospace}body{color:#4a4a4a;font-size:1rem;font-weight:400;line-height:1.5}a{color:#469;cursor:pointer;text-decoration:none}a strong{color:currentColor}a:hover{color:#363636}code{background-color:#f5f5f5;color:#ff3860;font-size:.875em;font-weight:400;padding:.25em .5em .25em}hr{background-color:#f5f5f5;border:none;display:block;height:2px;margin:1.5rem 0}img{height:auto;max-width:100%}input[type=checkbox],input[type=radio]{vertical-align:baseline}small{font-size:.875em}span{font-style:inherit;font-weight:inherit}strong{color:#363636;font-weight:700}fieldset{border:none}pre{-webkit-overflow-scrolling:touch;background-color:#f5f5f5;color:#4a4a4a;font-size:.875em;overflow-x:auto;padding:1.25rem 1.5rem;white-space:pre;word-wrap:normal}pre code{background-color:transparent;color:currentColor;font-size:1em;padding:0}table td,table th{text-align:left;vertical-align:top}table th{color:#363636}.is-clearfix::after{clear:both;content:" ";display:table}.is-pulled-left{float:left!important}.is-pulled-right{float:right!important}.is-clipped{overflow:hidden!important}.is-size-1{font-size:3rem!important}.is-size-2{font-size:2.5rem!important}.is-size-3{font-size:2rem!important}.is-size-4{font-size:1.5rem!important}.is-size-5{font-size:1.25rem!important}.is-size-6{font-size:1rem!important}.is-size-7{font-size:.75rem!important}@media screen and (max-width:768px){.is-size-1-mobile{font-size:3rem!important}.is-size-2-mobile{font-size:2.5rem!important}.is-size-3-mobile{font-size:2rem!important}.is-size-4-mobile{font-size:1.5rem!important}.is-size-5-mobile{font-size:1.25rem!important}.is-size-6-mobile{font-size:1rem!important}.is-size-7-mobile{font-size:.75rem!important}}@media screen and (min-width:769px),print{.is-size-1-tablet{font-size:3rem!important}.is-size-2-tablet{font-size:2.5rem!important}.is-size-3-tablet{font-size:2rem!important}.is-size-4-tablet{font-size:1.5rem!important}.is-size-5-tablet{font-size:1.25rem!important}.is-size-6-tablet{font-size:1rem!important}.is-size-7-tablet{font-size:.75rem!important}}@media screen and (max-width:-1px){.is-size-1-touch{font-size:3rem!important}.is-size-2-touch{font-size:2.5rem!important}.is-size-3-touch{font-size:2rem!important}.is-size-4-touch{font-size:1.5rem!important}.is-size-5-touch{font-size:1.25rem!important}.is-size-6-touch{font-size:1rem!important}.is-size-7-touch{font-size:.75rem!important}}@media screen and (min-width:0px){.is-size-1-desktop{font-size:3rem!important}.is-size-2-desktop{font-size:2.5rem!important}.is-size-3-desktop{font-size:2rem!important}.is-size-4-desktop{font-size:1.5rem!important}.is-size-5-desktop{font-size:1.25rem!important}.is-size-6-desktop{font-size:1rem!important}.is-size-7-desktop{font-size:.75rem!important}}@media screen and (min-width:1280px){.is-size-1-widescreen{font-size:3rem!important}.is-size-2-widescreen{font-size:2.5rem!important}.is-size-3-widescreen{font-size:2rem!important}.is-size-4-widescreen{font-size:1.5rem!important}.is-size-5-widescreen{font-size:1.25rem!important}.is-size-6-widescreen{font-size:1rem!important}.is-size-7-widescreen{font-size:.75rem!important}}@media screen and (min-width:1472px){.is-size-1-fullhd{font-size:3rem!important}.is-size-2-fullhd{font-size:2.5rem!important}.is-size-3-fullhd{font-size:2rem!important}.is-size-4-fullhd{font-size:1.5rem!important}.is-size-5-fullhd{font-size:1.25rem!important}.is-size-6-fullhd{font-size:1rem!important}.is-size-7-fullhd{font-size:.75rem!important}}.has-text-centered{text-align:center!important}.has-text-justified{text-align:justify!important}.has-text-left{text-align:left!important}.has-text-right{text-align:right!important}@media screen and (max-width:768px){.has-text-centered-mobile{text-align:center!important}}@media screen and (min-width:769px),print{.has-text-centered-tablet{text-align:center!important}}@media screen and (min-width:769px) and (max-width:-1px){.has-text-centered-tablet-only{text-align:center!important}}@media screen and (max-width:-1px){.has-text-centered-touch{text-align:center!important}}@media screen and (min-width:0px){.has-text-centered-desktop{text-align:center!important}}@media screen and (min-width:0px) and (max-width:1279px){.has-text-centered-desktop-only{text-align:center!important}}@media screen and (min-width:1280px){.has-text-centered-widescreen{text-align:center!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-centered-widescreen-only{text-align:center!important}}@media screen and (min-width:1472px){.has-text-centered-fullhd{text-align:center!important}}@media screen and (max-width:768px){.has-text-justified-mobile{text-align:justify!important}}@media screen and (min-width:769px),print{.has-text-justified-tablet{text-align:justify!important}}@media screen and (min-width:769px) and (max-width:-1px){.has-text-justified-tablet-only{text-align:justify!important}}@media screen and (max-width:-1px){.has-text-justified-touch{text-align:justify!important}}@media screen and (min-width:0px){.has-text-justified-desktop{text-align:justify!important}}@media screen and (min-width:0px) and (max-width:1279px){.has-text-justified-desktop-only{text-align:justify!important}}@media screen and (min-width:1280px){.has-text-justified-widescreen{text-align:justify!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-justified-widescreen-only{text-align:justify!important}}@media screen and (min-width:1472px){.has-text-justified-fullhd{text-align:justify!important}}@media screen and (max-width:768px){.has-text-left-mobile{text-align:left!important}}@media screen and (min-width:769px),print{.has-text-left-tablet{text-align:left!important}}@media screen and (min-width:769px) and (max-width:-1px){.has-text-left-tablet-only{text-align:left!important}}@media screen and (max-width:-1px){.has-text-left-touch{text-align:left!important}}@media screen and (min-width:0px){.has-text-left-desktop{text-align:left!important}}@media screen and (min-width:0px) and (max-width:1279px){.has-text-left-desktop-only{text-align:left!important}}@media screen and (min-width:1280px){.has-text-left-widescreen{text-align:left!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-left-widescreen-only{text-align:left!important}}@media screen and (min-width:1472px){.has-text-left-fullhd{text-align:left!important}}@media screen and (max-width:768px){.has-text-right-mobile{text-align:right!important}}@media screen and (min-width:769px),print{.has-text-right-tablet{text-align:right!important}}@media screen and (min-width:769px) and (max-width:-1px){.has-text-right-tablet-only{text-align:right!important}}@media screen and (max-width:-1px){.has-text-right-touch{text-align:right!important}}@media screen and (min-width:0px){.has-text-right-desktop{text-align:right!important}}@media screen and (min-width:0px) and (max-width:1279px){.has-text-right-desktop-only{text-align:right!important}}@media screen and (min-width:1280px){.has-text-right-widescreen{text-align:right!important}}@media screen and (min-width:1280px) and (max-width:1471px){.has-text-right-widescreen-only{text-align:right!important}}@media screen and (min-width:1472px){.has-text-right-fullhd{text-align:right!important}}.is-capitalized{text-transform:capitalize!important}.is-lowercase{text-transform:lowercase!important}.is-uppercase{text-transform:uppercase!important}.is-italic{font-style:italic!important}.has-text-white{color:#fff!important}a.has-text-white:focus,a.has-text-white:hover{color:#e6e6e6!important}.has-background-white{background-color:#fff!important}.has-text-black{color:#0a0a0a!important}a.has-text-black:focus,a.has-text-black:hover{color:#000!important}.has-background-black{background-color:#0a0a0a!important}.has-text-light{color:#f5f5f5!important}a.has-text-light:focus,a.has-text-light:hover{color:#dbdbdb!important}.has-background-light{background-color:#f5f5f5!important}.has-text-dark{color:#363636!important}a.has-text-dark:focus,a.has-text-dark:hover{color:#1c1c1c!important}.has-background-dark{background-color:#363636!important}.has-text-primary{color:#469!important}a.has-text-primary:focus,a.has-text-primary:hover{color:#344e76!important}.has-background-primary{background-color:#469!important}.has-text-link{color:#469!important}a.has-text-link:focus,a.has-text-link:hover{color:#344e76!important}.has-background-link{background-color:#469!important}.has-text-info{color:#209cee!important}a.has-text-info:focus,a.has-text-info:hover{color:#0f81cc!important}.has-background-info{background-color:#209cee!important}.has-text-success{color:#23d160!important}a.has-text-success:focus,a.has-text-success:hover{color:#1ca64c!important}.has-background-success{background-color:#23d160!important}.has-text-warning{color:#ffdd57!important}a.has-text-warning:focus,a.has-text-warning:hover{color:#ffd324!important}.has-background-warning{background-color:#ffdd57!important}.has-text-danger{color:#ff3860!important}a.has-text-danger:focus,a.has-text-danger:hover{color:#ff0537!important}.has-background-danger{background-color:#ff3860!important}.has-text-black-bis{color:#121212!important}.has-background-black-bis{background-color:#121212!important}.has-text-black-ter{color:#242424!important}.has-background-black-ter{background-color:#242424!important}.has-text-grey-darker{color:#363636!important}.has-background-grey-darker{background-color:#363636!important}.has-text-grey-dark{color:#4a4a4a!important}.has-background-grey-dark{background-color:#4a4a4a!important}.has-text-grey{color:#7a7a7a!important}.has-background-grey{background-color:#7a7a7a!important}.has-text-grey-light{color:#b5b5b5!important}.has-background-grey-light{background-color:#b5b5b5!important}.has-text-grey-lighter{color:#dbdbdb!important}.has-background-grey-lighter{background-color:#dbdbdb!important}.has-text-white-ter{color:#f5f5f5!important}.has-background-white-ter{background-color:#f5f5f5!important}.has-text-white-bis{color:#fafafa!important}.has-background-white-bis{background-color:#fafafa!important}.has-text-weight-light{font-weight:300!important}.has-text-weight-normal{font-weight:400!important}.has-text-weight-semibold{font-weight:600!important}.has-text-weight-bold{font-weight:700!important}.is-family-primary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-secondary{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-sans-serif{font-family:BlinkMacSystemFont,-apple-system,"Segoe UI",Roboto,Oxygen,Ubuntu,Cantarell,"Fira Sans","Droid Sans","Helvetica Neue",Helvetica,Arial,sans-serif!important}.is-family-monospace{font-family:monospace!important}.is-family-code{font-family:monospace!important}.is-block{display:block!important}@media screen and (max-width:768px){.is-block-mobile{display:block!important}}@media screen and (min-width:769px),print{.is-block-tablet{display:block!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-block-tablet-only{display:block!important}}@media screen and (max-width:-1px){.is-block-touch{display:block!important}}@media screen and (min-width:0px){.is-block-desktop{display:block!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-block-desktop-only{display:block!important}}@media screen and (min-width:1280px){.is-block-widescreen{display:block!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-block-widescreen-only{display:block!important}}@media screen and (min-width:1472px){.is-block-fullhd{display:block!important}}.is-flex{display:flex!important}@media screen and (max-width:768px){.is-flex-mobile{display:flex!important}}@media screen and (min-width:769px),print{.is-flex-tablet{display:flex!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-flex-tablet-only{display:flex!important}}@media screen and (max-width:-1px){.is-flex-touch{display:flex!important}}@media screen and (min-width:0px){.is-flex-desktop{display:flex!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-flex-desktop-only{display:flex!important}}@media screen and (min-width:1280px){.is-flex-widescreen{display:flex!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-flex-widescreen-only{display:flex!important}}@media screen and (min-width:1472px){.is-flex-fullhd{display:flex!important}}.is-inline{display:inline!important}@media screen and (max-width:768px){.is-inline-mobile{display:inline!important}}@media screen and (min-width:769px),print{.is-inline-tablet{display:inline!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-inline-tablet-only{display:inline!important}}@media screen and (max-width:-1px){.is-inline-touch{display:inline!important}}@media screen and (min-width:0px){.is-inline-desktop{display:inline!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-inline-desktop-only{display:inline!important}}@media screen and (min-width:1280px){.is-inline-widescreen{display:inline!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-widescreen-only{display:inline!important}}@media screen and (min-width:1472px){.is-inline-fullhd{display:inline!important}}.is-inline-block{display:inline-block!important}@media screen and (max-width:768px){.is-inline-block-mobile{display:inline-block!important}}@media screen and (min-width:769px),print{.is-inline-block-tablet{display:inline-block!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-inline-block-tablet-only{display:inline-block!important}}@media screen and (max-width:-1px){.is-inline-block-touch{display:inline-block!important}}@media screen and (min-width:0px){.is-inline-block-desktop{display:inline-block!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-inline-block-desktop-only{display:inline-block!important}}@media screen and (min-width:1280px){.is-inline-block-widescreen{display:inline-block!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-block-widescreen-only{display:inline-block!important}}@media screen and (min-width:1472px){.is-inline-block-fullhd{display:inline-block!important}}.is-inline-flex{display:inline-flex!important}@media screen and (max-width:768px){.is-inline-flex-mobile{display:inline-flex!important}}@media screen and (min-width:769px),print{.is-inline-flex-tablet{display:inline-flex!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-inline-flex-tablet-only{display:inline-flex!important}}@media screen and (max-width:-1px){.is-inline-flex-touch{display:inline-flex!important}}@media screen and (min-width:0px){.is-inline-flex-desktop{display:inline-flex!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-inline-flex-desktop-only{display:inline-flex!important}}@media screen and (min-width:1280px){.is-inline-flex-widescreen{display:inline-flex!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-inline-flex-widescreen-only{display:inline-flex!important}}@media screen and (min-width:1472px){.is-inline-flex-fullhd{display:inline-flex!important}}.is-hidden{display:none!important}.is-sr-only{border:none!important;clip:rect(0,0,0,0)!important;height:.01em!important;overflow:hidden!important;padding:0!important;position:absolute!important;white-space:nowrap!important;width:.01em!important}@media screen and (max-width:768px){.is-hidden-mobile{display:none!important}}@media screen and (min-width:769px),print{.is-hidden-tablet{display:none!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-hidden-tablet-only{display:none!important}}@media screen and (max-width:-1px){.is-hidden-touch{display:none!important}}@media screen and (min-width:0px){.is-hidden-desktop{display:none!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-hidden-desktop-only{display:none!important}}@media screen and (min-width:1280px){.is-hidden-widescreen{display:none!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-hidden-widescreen-only{display:none!important}}@media screen and (min-width:1472px){.is-hidden-fullhd{display:none!important}}.is-invisible{visibility:hidden!important}@media screen and (max-width:768px){.is-invisible-mobile{visibility:hidden!important}}@media screen and (min-width:769px),print{.is-invisible-tablet{visibility:hidden!important}}@media screen and (min-width:769px) and (max-width:-1px){.is-invisible-tablet-only{visibility:hidden!important}}@media screen and (max-width:-1px){.is-invisible-touch{visibility:hidden!important}}@media screen and (min-width:0px){.is-invisible-desktop{visibility:hidden!important}}@media screen and (min-width:0px) and (max-width:1279px){.is-invisible-desktop-only{visibility:hidden!important}}@media screen and (min-width:1280px){.is-invisible-widescreen{visibility:hidden!important}}@media screen and (min-width:1280px) and (max-width:1471px){.is-invisible-widescreen-only{visibility:hidden!important}}@media screen and (min-width:1472px){.is-invisible-fullhd{visibility:hidden!important}}.is-marginless{margin:0!important}.is-paddingless{padding:0!important}.is-radiusless{border-radius:0!important}.is-shadowless{box-shadow:none!important}.box{background-color:#fff;border-radius:6px;box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);color:#4a4a4a;display:block;padding:1.25rem}a.box:focus,a.box:hover{box-shadow:0 2px 3px rgba(10,10,10,.1),0 0 0 1px #469}a.box:active{box-shadow:inset 0 1px 2px rgba(10,10,10,.2),0 0 0 1px #469}.button{background-color:#fff;border-color:#dbdbdb;border-width:1px;color:#363636;cursor:pointer;justify-content:center;padding-bottom:calc(.375em - 1px);padding-left:.75em;padding-right:.75em;padding-top:calc(.375em - 1px);text-align:center;white-space:nowrap}.button strong{color:inherit}.button .icon,.button .icon.is-large,.button .icon.is-medium,.button .icon.is-small{height:1.5em;width:1.5em}.button .icon:first-child:not(:last-child){margin-left:calc(-.375em - 1px);margin-right:.1875em}.button .icon:last-child:not(:first-child){margin-left:.1875em;margin-right:calc(-.375em - 1px)}.button .icon:first-child:last-child{margin-left:calc(-.375em - 1px);margin-right:calc(-.375em - 1px)}.button.is-hovered,.button:hover{border-color:#b5b5b5;color:#363636}.button.is-focused,.button:focus{border-color:#469;color:#363636}.button.is-focused:not(:active),.button:focus:not(:active){box-shadow:0 0 0 .125em rgba(68,102,153,.25)}.button.is-active,.button:active{border-color:#4a4a4a;color:#363636}.button.is-text{background-color:transparent;border-color:transparent;color:#4a4a4a;text-decoration:underline}.button.is-text.is-focused,.button.is-text.is-hovered,.button.is-text:focus,.button.is-text:hover{background-color:#f5f5f5;color:#363636}.button.is-text.is-active,.button.is-text:active{background-color:#e8e8e8;color:#363636}.button.is-text[disabled],fieldset[disabled] .button.is-text{background-color:transparent;border-color:transparent;box-shadow:none}.button.is-white{background-color:#fff;border-color:transparent;color:#0a0a0a}.button.is-white.is-hovered,.button.is-white:hover{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.button.is-white.is-focused,.button.is-white:focus{border-color:transparent;color:#0a0a0a}.button.is-white.is-focused:not(:active),.button.is-white:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,255,255,.25)}.button.is-white.is-active,.button.is-white:active{background-color:#f2f2f2;border-color:transparent;color:#0a0a0a}.button.is-white[disabled],fieldset[disabled] .button.is-white{background-color:#fff;border-color:transparent;box-shadow:none}.button.is-white.is-inverted{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted:hover{background-color:#000}.button.is-white.is-inverted[disabled],fieldset[disabled] .button.is-white.is-inverted{background-color:#0a0a0a;border-color:transparent;box-shadow:none;color:#fff}.button.is-white.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-white.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-white.is-outlined:focus,.button.is-white.is-outlined:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.button.is-white.is-outlined.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-white.is-outlined[disabled],fieldset[disabled] .button.is-white.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-white.is-inverted.is-outlined:focus,.button.is-white.is-inverted.is-outlined:hover{background-color:#0a0a0a;color:#fff}.button.is-white.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-white.is-inverted.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black{background-color:#0a0a0a;border-color:transparent;color:#fff}.button.is-black.is-hovered,.button.is-black:hover{background-color:#040404;border-color:transparent;color:#fff}.button.is-black.is-focused,.button.is-black:focus{border-color:transparent;color:#fff}.button.is-black.is-focused:not(:active),.button.is-black:focus:not(:active){box-shadow:0 0 0 .125em rgba(10,10,10,.25)}.button.is-black.is-active,.button.is-black:active{background-color:#000;border-color:transparent;color:#fff}.button.is-black[disabled],fieldset[disabled] .button.is-black{background-color:#0a0a0a;border-color:transparent;box-shadow:none}.button.is-black.is-inverted{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted:hover{background-color:#f2f2f2}.button.is-black.is-inverted[disabled],fieldset[disabled] .button.is-black.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#0a0a0a}.button.is-black.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;color:#0a0a0a}.button.is-black.is-outlined:focus,.button.is-black.is-outlined:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.button.is-black.is-outlined.is-loading::after{border-color:transparent transparent #0a0a0a #0a0a0a!important}.button.is-black.is-outlined[disabled],fieldset[disabled] .button.is-black.is-outlined{background-color:transparent;border-color:#0a0a0a;box-shadow:none;color:#0a0a0a}.button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-black.is-inverted.is-outlined:focus,.button.is-black.is-inverted.is-outlined:hover{background-color:#fff;color:#0a0a0a}.button.is-black.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-black.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-light{background-color:#f5f5f5;border-color:transparent;color:#363636}.button.is-light.is-hovered,.button.is-light:hover{background-color:#eee;border-color:transparent;color:#363636}.button.is-light.is-focused,.button.is-light:focus{border-color:transparent;color:#363636}.button.is-light.is-focused:not(:active),.button.is-light:focus:not(:active){box-shadow:0 0 0 .125em rgba(245,245,245,.25)}.button.is-light.is-active,.button.is-light:active{background-color:#e8e8e8;border-color:transparent;color:#363636}.button.is-light[disabled],fieldset[disabled] .button.is-light{background-color:#f5f5f5;border-color:transparent;box-shadow:none}.button.is-light.is-inverted{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted:hover{background-color:#292929}.button.is-light.is-inverted[disabled],fieldset[disabled] .button.is-light.is-inverted{background-color:#363636;border-color:transparent;box-shadow:none;color:#f5f5f5}.button.is-light.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-light.is-outlined:focus,.button.is-light.is-outlined:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.button.is-light.is-outlined.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-light.is-outlined[disabled],fieldset[disabled] .button.is-light.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-light.is-inverted.is-outlined:focus,.button.is-light.is-inverted.is-outlined:hover{background-color:#363636;color:#f5f5f5}.button.is-light.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-light.is-inverted.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark{background-color:#363636;border-color:transparent;color:#f5f5f5}.button.is-dark.is-hovered,.button.is-dark:hover{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused,.button.is-dark:focus{border-color:transparent;color:#f5f5f5}.button.is-dark.is-focused:not(:active),.button.is-dark:focus:not(:active){box-shadow:0 0 0 .125em rgba(54,54,54,.25)}.button.is-dark.is-active,.button.is-dark:active{background-color:#292929;border-color:transparent;color:#f5f5f5}.button.is-dark[disabled],fieldset[disabled] .button.is-dark{background-color:#363636;border-color:transparent;box-shadow:none}.button.is-dark.is-inverted{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted:hover{background-color:#e8e8e8}.button.is-dark.is-inverted[disabled],fieldset[disabled] .button.is-dark.is-inverted{background-color:#f5f5f5;border-color:transparent;box-shadow:none;color:#363636}.button.is-dark.is-loading::after{border-color:transparent transparent #f5f5f5 #f5f5f5!important}.button.is-dark.is-outlined{background-color:transparent;border-color:#363636;color:#363636}.button.is-dark.is-outlined:focus,.button.is-dark.is-outlined:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.button.is-dark.is-outlined.is-loading::after{border-color:transparent transparent #363636 #363636!important}.button.is-dark.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-outlined{background-color:transparent;border-color:#363636;box-shadow:none;color:#363636}.button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#f5f5f5;color:#f5f5f5}.button.is-dark.is-inverted.is-outlined:focus,.button.is-dark.is-inverted.is-outlined:hover{background-color:#f5f5f5;color:#363636}.button.is-dark.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-dark.is-inverted.is-outlined{background-color:transparent;border-color:#f5f5f5;box-shadow:none;color:#f5f5f5}.button.is-primary{background-color:#469;border-color:transparent;color:#fff}.button.is-primary.is-hovered,.button.is-primary:hover{background-color:#406090;border-color:transparent;color:#fff}.button.is-primary.is-focused,.button.is-primary:focus{border-color:transparent;color:#fff}.button.is-primary.is-focused:not(:active),.button.is-primary:focus:not(:active){box-shadow:0 0 0 .125em rgba(68,102,153,.25)}.button.is-primary.is-active,.button.is-primary:active{background-color:#3c5a87;border-color:transparent;color:#fff}.button.is-primary[disabled],fieldset[disabled] .button.is-primary{background-color:#469;border-color:transparent;box-shadow:none}.button.is-primary.is-inverted{background-color:#fff;color:#469}.button.is-primary.is-inverted:hover{background-color:#f2f2f2}.button.is-primary.is-inverted[disabled],fieldset[disabled] .button.is-primary.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#469}.button.is-primary.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-primary.is-outlined{background-color:transparent;border-color:#469;color:#469}.button.is-primary.is-outlined:focus,.button.is-primary.is-outlined:hover{background-color:#469;border-color:#469;color:#fff}.button.is-primary.is-outlined.is-loading::after{border-color:transparent transparent #469 #469!important}.button.is-primary.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-outlined{background-color:transparent;border-color:#469;box-shadow:none;color:#469}.button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-primary.is-inverted.is-outlined:focus,.button.is-primary.is-inverted.is-outlined:hover{background-color:#fff;color:#469}.button.is-primary.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-primary.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-link{background-color:#469;border-color:transparent;color:#fff}.button.is-link.is-hovered,.button.is-link:hover{background-color:#406090;border-color:transparent;color:#fff}.button.is-link.is-focused,.button.is-link:focus{border-color:transparent;color:#fff}.button.is-link.is-focused:not(:active),.button.is-link:focus:not(:active){box-shadow:0 0 0 .125em rgba(68,102,153,.25)}.button.is-link.is-active,.button.is-link:active{background-color:#3c5a87;border-color:transparent;color:#fff}.button.is-link[disabled],fieldset[disabled] .button.is-link{background-color:#469;border-color:transparent;box-shadow:none}.button.is-link.is-inverted{background-color:#fff;color:#469}.button.is-link.is-inverted:hover{background-color:#f2f2f2}.button.is-link.is-inverted[disabled],fieldset[disabled] .button.is-link.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#469}.button.is-link.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-link.is-outlined{background-color:transparent;border-color:#469;color:#469}.button.is-link.is-outlined:focus,.button.is-link.is-outlined:hover{background-color:#469;border-color:#469;color:#fff}.button.is-link.is-outlined.is-loading::after{border-color:transparent transparent #469 #469!important}.button.is-link.is-outlined[disabled],fieldset[disabled] .button.is-link.is-outlined{background-color:transparent;border-color:#469;box-shadow:none;color:#469}.button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-link.is-inverted.is-outlined:focus,.button.is-link.is-inverted.is-outlined:hover{background-color:#fff;color:#469}.button.is-link.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-link.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-info{background-color:#209cee;border-color:transparent;color:#fff}.button.is-info.is-hovered,.button.is-info:hover{background-color:#1496ed;border-color:transparent;color:#fff}.button.is-info.is-focused,.button.is-info:focus{border-color:transparent;color:#fff}.button.is-info.is-focused:not(:active),.button.is-info:focus:not(:active){box-shadow:0 0 0 .125em rgba(32,156,238,.25)}.button.is-info.is-active,.button.is-info:active{background-color:#118fe4;border-color:transparent;color:#fff}.button.is-info[disabled],fieldset[disabled] .button.is-info{background-color:#209cee;border-color:transparent;box-shadow:none}.button.is-info.is-inverted{background-color:#fff;color:#209cee}.button.is-info.is-inverted:hover{background-color:#f2f2f2}.button.is-info.is-inverted[disabled],fieldset[disabled] .button.is-info.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#209cee}.button.is-info.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-info.is-outlined{background-color:transparent;border-color:#209cee;color:#209cee}.button.is-info.is-outlined:focus,.button.is-info.is-outlined:hover{background-color:#209cee;border-color:#209cee;color:#fff}.button.is-info.is-outlined.is-loading::after{border-color:transparent transparent #209cee #209cee!important}.button.is-info.is-outlined[disabled],fieldset[disabled] .button.is-info.is-outlined{background-color:transparent;border-color:#209cee;box-shadow:none;color:#209cee}.button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-info.is-inverted.is-outlined:focus,.button.is-info.is-inverted.is-outlined:hover{background-color:#fff;color:#209cee}.button.is-info.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-info.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-success{background-color:#23d160;border-color:transparent;color:#fff}.button.is-success.is-hovered,.button.is-success:hover{background-color:#22c65b;border-color:transparent;color:#fff}.button.is-success.is-focused,.button.is-success:focus{border-color:transparent;color:#fff}.button.is-success.is-focused:not(:active),.button.is-success:focus:not(:active){box-shadow:0 0 0 .125em rgba(35,209,96,.25)}.button.is-success.is-active,.button.is-success:active{background-color:#20bc56;border-color:transparent;color:#fff}.button.is-success[disabled],fieldset[disabled] .button.is-success{background-color:#23d160;border-color:transparent;box-shadow:none}.button.is-success.is-inverted{background-color:#fff;color:#23d160}.button.is-success.is-inverted:hover{background-color:#f2f2f2}.button.is-success.is-inverted[disabled],fieldset[disabled] .button.is-success.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#23d160}.button.is-success.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-success.is-outlined{background-color:transparent;border-color:#23d160;color:#23d160}.button.is-success.is-outlined:focus,.button.is-success.is-outlined:hover{background-color:#23d160;border-color:#23d160;color:#fff}.button.is-success.is-outlined.is-loading::after{border-color:transparent transparent #23d160 #23d160!important}.button.is-success.is-outlined[disabled],fieldset[disabled] .button.is-success.is-outlined{background-color:transparent;border-color:#23d160;box-shadow:none;color:#23d160}.button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-success.is-inverted.is-outlined:focus,.button.is-success.is-inverted.is-outlined:hover{background-color:#fff;color:#23d160}.button.is-success.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-success.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-warning{background-color:#ffdd57;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-hovered,.button.is-warning:hover{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused,.button.is-warning:focus{border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning.is-focused:not(:active),.button.is-warning:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,221,87,.25)}.button.is-warning.is-active,.button.is-warning:active{background-color:#ffd83d;border-color:transparent;color:rgba(0,0,0,.7)}.button.is-warning[disabled],fieldset[disabled] .button.is-warning{background-color:#ffdd57;border-color:transparent;box-shadow:none}.button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted:hover{background-color:rgba(0,0,0,.7)}.button.is-warning.is-inverted[disabled],fieldset[disabled] .button.is-warning.is-inverted{background-color:rgba(0,0,0,.7);border-color:transparent;box-shadow:none;color:#ffdd57}.button.is-warning.is-loading::after{border-color:transparent transparent rgba(0,0,0,.7) rgba(0,0,0,.7)!important}.button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;color:#ffdd57}.button.is-warning.is-outlined:focus,.button.is-warning.is-outlined:hover{background-color:#ffdd57;border-color:#ffdd57;color:rgba(0,0,0,.7)}.button.is-warning.is-outlined.is-loading::after{border-color:transparent transparent #ffdd57 #ffdd57!important}.button.is-warning.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-outlined{background-color:transparent;border-color:#ffdd57;box-shadow:none;color:#ffdd57}.button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);color:rgba(0,0,0,.7)}.button.is-warning.is-inverted.is-outlined:focus,.button.is-warning.is-inverted.is-outlined:hover{background-color:rgba(0,0,0,.7);color:#ffdd57}.button.is-warning.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-warning.is-inverted.is-outlined{background-color:transparent;border-color:rgba(0,0,0,.7);box-shadow:none;color:rgba(0,0,0,.7)}.button.is-danger{background-color:#ff3860;border-color:transparent;color:#fff}.button.is-danger.is-hovered,.button.is-danger:hover{background-color:#ff2b56;border-color:transparent;color:#fff}.button.is-danger.is-focused,.button.is-danger:focus{border-color:transparent;color:#fff}.button.is-danger.is-focused:not(:active),.button.is-danger:focus:not(:active){box-shadow:0 0 0 .125em rgba(255,56,96,.25)}.button.is-danger.is-active,.button.is-danger:active{background-color:#ff1f4b;border-color:transparent;color:#fff}.button.is-danger[disabled],fieldset[disabled] .button.is-danger{background-color:#ff3860;border-color:transparent;box-shadow:none}.button.is-danger.is-inverted{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted:hover{background-color:#f2f2f2}.button.is-danger.is-inverted[disabled],fieldset[disabled] .button.is-danger.is-inverted{background-color:#fff;border-color:transparent;box-shadow:none;color:#ff3860}.button.is-danger.is-loading::after{border-color:transparent transparent #fff #fff!important}.button.is-danger.is-outlined{background-color:transparent;border-color:#ff3860;color:#ff3860}.button.is-danger.is-outlined:focus,.button.is-danger.is-outlined:hover{background-color:#ff3860;border-color:#ff3860;color:#fff}.button.is-danger.is-outlined.is-loading::after{border-color:transparent transparent #ff3860 #ff3860!important}.button.is-danger.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-outlined{background-color:transparent;border-color:#ff3860;box-shadow:none;color:#ff3860}.button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;color:#fff}.button.is-danger.is-inverted.is-outlined:focus,.button.is-danger.is-inverted.is-outlined:hover{background-color:#fff;color:#ff3860}.button.is-danger.is-inverted.is-outlined[disabled],fieldset[disabled] .button.is-danger.is-inverted.is-outlined{background-color:transparent;border-color:#fff;box-shadow:none;color:#fff}.button.is-small{border-radius:2px;font-size:.75rem}.button.is-normal{font-size:1rem}.button.is-medium{font-size:1.25rem}.button.is-large{font-size:1.5rem}.button[disabled],fieldset[disabled] .button{background-color:#fff;border-color:#dbdbdb;box-shadow:none;opacity:.5}.button.is-fullwidth{display:flex;width:100%}.button.is-loading{color:transparent!important;pointer-events:none}.button.is-loading::after{position:absolute;left:calc(50% - (1em / 2));top:calc(50% - (1em / 2));position:absolute!important}.button.is-static{background-color:#f5f5f5;border-color:#dbdbdb;color:#7a7a7a;box-shadow:none;pointer-events:none}.button.is-rounded{border-radius:290486px;padding-left:1em;padding-right:1em}.buttons{align-items:center;display:flex;flex-wrap:wrap;justify-content:flex-start}.buttons .button{margin-bottom:.5rem}.buttons .button:not(:last-child):not(.is-fullwidth){margin-right:.5rem}.buttons:last-child{margin-bottom:-.5rem}.buttons:not(:last-child){margin-bottom:1rem}.buttons.are-small .button:not(.is-normal):not(.is-medium):not(.is-large){border-radius:2px;font-size:.75rem}.buttons.are-medium .button:not(.is-small):not(.is-normal):not(.is-large){font-size:1.25rem}.buttons.are-large .button:not(.is-small):not(.is-normal):not(.is-medium){font-size:1.5rem}.buttons.has-addons .button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.buttons.has-addons .button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0;margin-right:-1px}.buttons.has-addons .button:last-child{margin-right:0}.buttons.has-addons .button.is-hovered,.buttons.has-addons .button:hover{z-index:2}.buttons.has-addons .button.is-active,.buttons.has-addons .button.is-focused,.buttons.has-addons .button.is-selected,.buttons.has-addons .button:active,.buttons.has-addons .button:focus{z-index:3}.buttons.has-addons .button.is-active:hover,.buttons.has-addons .button.is-focused:hover,.buttons.has-addons .button.is-selected:hover,.buttons.has-addons .button:active:hover,.buttons.has-addons .button:focus:hover{z-index:4}.buttons.has-addons .button.is-expanded{flex-grow:1}.buttons.is-centered{justify-content:center}.buttons.is-right{justify-content:flex-end}.container{margin:0 auto;position:relative}@media screen and (min-width:0px){.container{max-width:-128px;width:-128px}.container.is-fluid{margin-left:64px;margin-right:64px;max-width:none;width:auto}}@media screen and (max-width:1279px){.container.is-widescreen{max-width:1152px;width:auto}}@media screen and (max-width:1471px){.container.is-fullhd{max-width:1344px;width:auto}}@media screen and (min-width:1280px){.container{max-width:1152px;width:1152px}}@media screen and (min-width:1472px){.container{max-width:1344px;width:1344px}}.content li+li{margin-top:.25em}.content blockquote:not(:last-child),.content dl:not(:last-child),.content ol:not(:last-child),.content p:not(:last-child),.content pre:not(:last-child),.content table:not(:last-child),.content ul:not(:last-child){margin-bottom:1em}.content h1,.content h2,.content h3,.content h4,.content h5,.content h6{color:#363636;font-weight:600;line-height:1.125}.content h1{font-size:2em;margin-bottom:.5em}.content h1:not(:first-child){margin-top:1em}.content h2{font-size:1.75em;margin-bottom:.5714em}.content h2:not(:first-child){margin-top:1.1428em}.content h3{font-size:1.5em;margin-bottom:.6666em}.content h3:not(:first-child){margin-top:1.3333em}.content h4{font-size:1.25em;margin-bottom:.8em}.content h5{font-size:1.125em;margin-bottom:.8888em}.content h6{font-size:1em;margin-bottom:1em}.content blockquote{background-color:#f5f5f5;border-left:5px solid #dbdbdb;padding:1.25em 1.5em}.content ol{list-style-position:outside;margin-left:2em;margin-top:1em}.content ol:not([type]){list-style-type:decimal}.content ol:not([type]).is-lower-alpha{list-style-type:lower-alpha}.content ol:not([type]).is-lower-roman{list-style-type:lower-roman}.content ol:not([type]).is-upper-alpha{list-style-type:upper-alpha}.content ol:not([type]).is-upper-roman{list-style-type:upper-roman}.content ul{list-style:disc outside;margin-left:2em;margin-top:1em}.content ul ul{list-style-type:circle;margin-top:.5em}.content ul ul ul{list-style-type:square}.content dd{margin-left:2em}.content figure{margin-left:2em;margin-right:2em;text-align:center}.content figure:not(:first-child){margin-top:2em}.content figure:not(:last-child){margin-bottom:2em}.content figure img{display:inline-block}.content figure figcaption{font-style:italic}.content pre{-webkit-overflow-scrolling:touch;overflow-x:auto;padding:1.25em 1.5em;white-space:pre;word-wrap:normal}.content sub,.content sup{font-size:75%}.content table{width:100%}.content table td,.content table th{border:1px solid #dbdbdb;border-width:0 0 1px;padding:.5em .75em;vertical-align:top}.content table th{color:#363636;text-align:left}.content table thead td,.content table thead th{border-width:0 0 2px;color:#363636}.content table tfoot td,.content table tfoot th{border-width:2px 0 0;color:#363636}.content table tbody tr:last-child td,.content table tbody tr:last-child th{border-bottom-width:0}.content.is-small{font-size:.75rem}.content.is-medium{font-size:1.25rem}.content.is-large{font-size:1.5rem}.icon{align-items:center;display:inline-flex;justify-content:center;height:1.5rem;width:1.5rem}.icon.is-small{height:1rem;width:1rem}.icon.is-medium{height:2rem;width:2rem}.icon.is-large{height:3rem;width:3rem}.subtitle,.title{word-break:break-word}.subtitle em,.subtitle span,.title em,.title span{font-weight:inherit}.subtitle sub,.title sub{font-size:.75em}.subtitle sup,.title sup{font-size:.75em}.subtitle .tag,.title .tag{vertical-align:middle}.title{color:#363636;font-size:2rem;font-weight:600;line-height:1.125}.title strong{color:inherit;font-weight:inherit}.title+.highlight{margin-top:-.75rem}.title:not(.is-spaced)+.subtitle{margin-top:-1.25rem}.title.is-1{font-size:3rem}.title.is-2{font-size:2.5rem}.title.is-3{font-size:2rem}.title.is-4{font-size:1.5rem}.title.is-5{font-size:1.25rem}.title.is-6{font-size:1rem}.title.is-7{font-size:.75rem}.subtitle{color:#4a4a4a;font-size:1.25rem;font-weight:400;line-height:1.25}.subtitle strong{color:#363636;font-weight:600}.subtitle:not(.is-spaced)+.title{margin-top:-1.25rem}.subtitle.is-1{font-size:3rem}.subtitle.is-2{font-size:2.5rem}.subtitle.is-3{font-size:2rem}.subtitle.is-4{font-size:1.5rem}.subtitle.is-5{font-size:1.25rem}.subtitle.is-6{font-size:1rem}.subtitle.is-7{font-size:.75rem}.navbar{background-color:#fff;min-height:3.25rem;position:relative;z-index:30}.navbar.is-white{background-color:#fff;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link,.navbar.is-white .navbar-brand>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link.is-active,.navbar.is-white .navbar-brand .navbar-link:hover,.navbar.is-white .navbar-brand>a.navbar-item.is-active,.navbar.is-white .navbar-brand>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-brand .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-burger{color:#0a0a0a}@media screen and (min-width:0px){.navbar.is-white .navbar-end .navbar-link,.navbar.is-white .navbar-end>.navbar-item,.navbar.is-white .navbar-start .navbar-link,.navbar.is-white .navbar-start>.navbar-item{color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link.is-active,.navbar.is-white .navbar-end .navbar-link:hover,.navbar.is-white .navbar-end>a.navbar-item.is-active,.navbar.is-white .navbar-end>a.navbar-item:hover,.navbar.is-white .navbar-start .navbar-link.is-active,.navbar.is-white .navbar-start .navbar-link:hover,.navbar.is-white .navbar-start>a.navbar-item.is-active,.navbar.is-white .navbar-start>a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-end .navbar-link::after,.navbar.is-white .navbar-start .navbar-link::after{border-color:#0a0a0a}.navbar.is-white .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-white .navbar-item.has-dropdown:hover .navbar-link{background-color:#f2f2f2;color:#0a0a0a}.navbar.is-white .navbar-dropdown a.navbar-item.is-active{background-color:#fff;color:#0a0a0a}}.navbar.is-black{background-color:#0a0a0a;color:#fff}.navbar.is-black .navbar-brand .navbar-link,.navbar.is-black .navbar-brand>.navbar-item{color:#fff}.navbar.is-black .navbar-brand .navbar-link.is-active,.navbar.is-black .navbar-brand .navbar-link:hover,.navbar.is-black .navbar-brand>a.navbar-item.is-active,.navbar.is-black .navbar-brand>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-black .navbar-end .navbar-link,.navbar.is-black .navbar-end>.navbar-item,.navbar.is-black .navbar-start .navbar-link,.navbar.is-black .navbar-start>.navbar-item{color:#fff}.navbar.is-black .navbar-end .navbar-link.is-active,.navbar.is-black .navbar-end .navbar-link:hover,.navbar.is-black .navbar-end>a.navbar-item.is-active,.navbar.is-black .navbar-end>a.navbar-item:hover,.navbar.is-black .navbar-start .navbar-link.is-active,.navbar.is-black .navbar-start .navbar-link:hover,.navbar.is-black .navbar-start>a.navbar-item.is-active,.navbar.is-black .navbar-start>a.navbar-item:hover{background-color:#000;color:#fff}.navbar.is-black .navbar-end .navbar-link::after,.navbar.is-black .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-black .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-black .navbar-item.has-dropdown:hover .navbar-link{background-color:#000;color:#fff}.navbar.is-black .navbar-dropdown a.navbar-item.is-active{background-color:#0a0a0a;color:#fff}}.navbar.is-light{background-color:#f5f5f5;color:#363636}.navbar.is-light .navbar-brand .navbar-link,.navbar.is-light .navbar-brand>.navbar-item{color:#363636}.navbar.is-light .navbar-brand .navbar-link.is-active,.navbar.is-light .navbar-brand .navbar-link:hover,.navbar.is-light .navbar-brand>a.navbar-item.is-active,.navbar.is-light .navbar-brand>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-brand .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-burger{color:#363636}@media screen and (min-width:0px){.navbar.is-light .navbar-end .navbar-link,.navbar.is-light .navbar-end>.navbar-item,.navbar.is-light .navbar-start .navbar-link,.navbar.is-light .navbar-start>.navbar-item{color:#363636}.navbar.is-light .navbar-end .navbar-link.is-active,.navbar.is-light .navbar-end .navbar-link:hover,.navbar.is-light .navbar-end>a.navbar-item.is-active,.navbar.is-light .navbar-end>a.navbar-item:hover,.navbar.is-light .navbar-start .navbar-link.is-active,.navbar.is-light .navbar-start .navbar-link:hover,.navbar.is-light .navbar-start>a.navbar-item.is-active,.navbar.is-light .navbar-start>a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-end .navbar-link::after,.navbar.is-light .navbar-start .navbar-link::after{border-color:#363636}.navbar.is-light .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-light .navbar-item.has-dropdown:hover .navbar-link{background-color:#e8e8e8;color:#363636}.navbar.is-light .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#363636}}.navbar.is-dark{background-color:#363636;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link,.navbar.is-dark .navbar-brand>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link.is-active,.navbar.is-dark .navbar-brand .navbar-link:hover,.navbar.is-dark .navbar-brand>a.navbar-item.is-active,.navbar.is-dark .navbar-brand>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-brand .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-burger{color:#f5f5f5}@media screen and (min-width:0px){.navbar.is-dark .navbar-end .navbar-link,.navbar.is-dark .navbar-end>.navbar-item,.navbar.is-dark .navbar-start .navbar-link,.navbar.is-dark .navbar-start>.navbar-item{color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link.is-active,.navbar.is-dark .navbar-end .navbar-link:hover,.navbar.is-dark .navbar-end>a.navbar-item.is-active,.navbar.is-dark .navbar-end>a.navbar-item:hover,.navbar.is-dark .navbar-start .navbar-link.is-active,.navbar.is-dark .navbar-start .navbar-link:hover,.navbar.is-dark .navbar-start>a.navbar-item.is-active,.navbar.is-dark .navbar-start>a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-end .navbar-link::after,.navbar.is-dark .navbar-start .navbar-link::after{border-color:#f5f5f5}.navbar.is-dark .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-dark .navbar-item.has-dropdown:hover .navbar-link{background-color:#292929;color:#f5f5f5}.navbar.is-dark .navbar-dropdown a.navbar-item.is-active{background-color:#363636;color:#f5f5f5}}.navbar.is-primary{background-color:#469;color:#fff}.navbar.is-primary .navbar-brand .navbar-link,.navbar.is-primary .navbar-brand>.navbar-item{color:#fff}.navbar.is-primary .navbar-brand .navbar-link.is-active,.navbar.is-primary .navbar-brand .navbar-link:hover,.navbar.is-primary .navbar-brand>a.navbar-item.is-active,.navbar.is-primary .navbar-brand>a.navbar-item:hover{background-color:#3c5a87;color:#fff}.navbar.is-primary .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-primary .navbar-end .navbar-link,.navbar.is-primary .navbar-end>.navbar-item,.navbar.is-primary .navbar-start .navbar-link,.navbar.is-primary .navbar-start>.navbar-item{color:#fff}.navbar.is-primary .navbar-end .navbar-link.is-active,.navbar.is-primary .navbar-end .navbar-link:hover,.navbar.is-primary .navbar-end>a.navbar-item.is-active,.navbar.is-primary .navbar-end>a.navbar-item:hover,.navbar.is-primary .navbar-start .navbar-link.is-active,.navbar.is-primary .navbar-start .navbar-link:hover,.navbar.is-primary .navbar-start>a.navbar-item.is-active,.navbar.is-primary .navbar-start>a.navbar-item:hover{background-color:#3c5a87;color:#fff}.navbar.is-primary .navbar-end .navbar-link::after,.navbar.is-primary .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-primary .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-primary .navbar-item.has-dropdown:hover .navbar-link{background-color:#3c5a87;color:#fff}.navbar.is-primary .navbar-dropdown a.navbar-item.is-active{background-color:#469;color:#fff}}.navbar.is-link{background-color:#469;color:#fff}.navbar.is-link .navbar-brand .navbar-link,.navbar.is-link .navbar-brand>.navbar-item{color:#fff}.navbar.is-link .navbar-brand .navbar-link.is-active,.navbar.is-link .navbar-brand .navbar-link:hover,.navbar.is-link .navbar-brand>a.navbar-item.is-active,.navbar.is-link .navbar-brand>a.navbar-item:hover{background-color:#3c5a87;color:#fff}.navbar.is-link .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-link .navbar-end .navbar-link,.navbar.is-link .navbar-end>.navbar-item,.navbar.is-link .navbar-start .navbar-link,.navbar.is-link .navbar-start>.navbar-item{color:#fff}.navbar.is-link .navbar-end .navbar-link.is-active,.navbar.is-link .navbar-end .navbar-link:hover,.navbar.is-link .navbar-end>a.navbar-item.is-active,.navbar.is-link .navbar-end>a.navbar-item:hover,.navbar.is-link .navbar-start .navbar-link.is-active,.navbar.is-link .navbar-start .navbar-link:hover,.navbar.is-link .navbar-start>a.navbar-item.is-active,.navbar.is-link .navbar-start>a.navbar-item:hover{background-color:#3c5a87;color:#fff}.navbar.is-link .navbar-end .navbar-link::after,.navbar.is-link .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-link .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-link .navbar-item.has-dropdown:hover .navbar-link{background-color:#3c5a87;color:#fff}.navbar.is-link .navbar-dropdown a.navbar-item.is-active{background-color:#469;color:#fff}}.navbar.is-info{background-color:#209cee;color:#fff}.navbar.is-info .navbar-brand .navbar-link,.navbar.is-info .navbar-brand>.navbar-item{color:#fff}.navbar.is-info .navbar-brand .navbar-link.is-active,.navbar.is-info .navbar-brand .navbar-link:hover,.navbar.is-info .navbar-brand>a.navbar-item.is-active,.navbar.is-info .navbar-brand>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-info .navbar-end .navbar-link,.navbar.is-info .navbar-end>.navbar-item,.navbar.is-info .navbar-start .navbar-link,.navbar.is-info .navbar-start>.navbar-item{color:#fff}.navbar.is-info .navbar-end .navbar-link.is-active,.navbar.is-info .navbar-end .navbar-link:hover,.navbar.is-info .navbar-end>a.navbar-item.is-active,.navbar.is-info .navbar-end>a.navbar-item:hover,.navbar.is-info .navbar-start .navbar-link.is-active,.navbar.is-info .navbar-start .navbar-link:hover,.navbar.is-info .navbar-start>a.navbar-item.is-active,.navbar.is-info .navbar-start>a.navbar-item:hover{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-end .navbar-link::after,.navbar.is-info .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-info .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-info .navbar-item.has-dropdown:hover .navbar-link{background-color:#118fe4;color:#fff}.navbar.is-info .navbar-dropdown a.navbar-item.is-active{background-color:#209cee;color:#fff}}.navbar.is-success{background-color:#23d160;color:#fff}.navbar.is-success .navbar-brand .navbar-link,.navbar.is-success .navbar-brand>.navbar-item{color:#fff}.navbar.is-success .navbar-brand .navbar-link.is-active,.navbar.is-success .navbar-brand .navbar-link:hover,.navbar.is-success .navbar-brand>a.navbar-item.is-active,.navbar.is-success .navbar-brand>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-success .navbar-end .navbar-link,.navbar.is-success .navbar-end>.navbar-item,.navbar.is-success .navbar-start .navbar-link,.navbar.is-success .navbar-start>.navbar-item{color:#fff}.navbar.is-success .navbar-end .navbar-link.is-active,.navbar.is-success .navbar-end .navbar-link:hover,.navbar.is-success .navbar-end>a.navbar-item.is-active,.navbar.is-success .navbar-end>a.navbar-item:hover,.navbar.is-success .navbar-start .navbar-link.is-active,.navbar.is-success .navbar-start .navbar-link:hover,.navbar.is-success .navbar-start>a.navbar-item.is-active,.navbar.is-success .navbar-start>a.navbar-item:hover{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-end .navbar-link::after,.navbar.is-success .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-success .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-success .navbar-item.has-dropdown:hover .navbar-link{background-color:#20bc56;color:#fff}.navbar.is-success .navbar-dropdown a.navbar-item.is-active{background-color:#23d160;color:#fff}}.navbar.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link,.navbar.is-warning .navbar-brand>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link.is-active,.navbar.is-warning .navbar-brand .navbar-link:hover,.navbar.is-warning .navbar-brand>a.navbar-item.is-active,.navbar.is-warning .navbar-brand>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-brand .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-burger{color:rgba(0,0,0,.7)}@media screen and (min-width:0px){.navbar.is-warning .navbar-end .navbar-link,.navbar.is-warning .navbar-end>.navbar-item,.navbar.is-warning .navbar-start .navbar-link,.navbar.is-warning .navbar-start>.navbar-item{color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link.is-active,.navbar.is-warning .navbar-end .navbar-link:hover,.navbar.is-warning .navbar-end>a.navbar-item.is-active,.navbar.is-warning .navbar-end>a.navbar-item:hover,.navbar.is-warning .navbar-start .navbar-link.is-active,.navbar.is-warning .navbar-start .navbar-link:hover,.navbar.is-warning .navbar-start>a.navbar-item.is-active,.navbar.is-warning .navbar-start>a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-end .navbar-link::after,.navbar.is-warning .navbar-start .navbar-link::after{border-color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-warning .navbar-item.has-dropdown:hover .navbar-link{background-color:#ffd83d;color:rgba(0,0,0,.7)}.navbar.is-warning .navbar-dropdown a.navbar-item.is-active{background-color:#ffdd57;color:rgba(0,0,0,.7)}}.navbar.is-danger{background-color:#ff3860;color:#fff}.navbar.is-danger .navbar-brand .navbar-link,.navbar.is-danger .navbar-brand>.navbar-item{color:#fff}.navbar.is-danger .navbar-brand .navbar-link.is-active,.navbar.is-danger .navbar-brand .navbar-link:hover,.navbar.is-danger .navbar-brand>a.navbar-item.is-active,.navbar.is-danger .navbar-brand>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-brand .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-burger{color:#fff}@media screen and (min-width:0px){.navbar.is-danger .navbar-end .navbar-link,.navbar.is-danger .navbar-end>.navbar-item,.navbar.is-danger .navbar-start .navbar-link,.navbar.is-danger .navbar-start>.navbar-item{color:#fff}.navbar.is-danger .navbar-end .navbar-link.is-active,.navbar.is-danger .navbar-end .navbar-link:hover,.navbar.is-danger .navbar-end>a.navbar-item.is-active,.navbar.is-danger .navbar-end>a.navbar-item:hover,.navbar.is-danger .navbar-start .navbar-link.is-active,.navbar.is-danger .navbar-start .navbar-link:hover,.navbar.is-danger .navbar-start>a.navbar-item.is-active,.navbar.is-danger .navbar-start>a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-end .navbar-link::after,.navbar.is-danger .navbar-start .navbar-link::after{border-color:#fff}.navbar.is-danger .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-danger .navbar-item.has-dropdown:hover .navbar-link{background-color:#ff1f4b;color:#fff}.navbar.is-danger .navbar-dropdown a.navbar-item.is-active{background-color:#ff3860;color:#fff}}.navbar>.container{align-items:stretch;display:flex;min-height:3.25rem;width:100%}.navbar.has-shadow{box-shadow:0 2px 0 0 #f5f5f5}.navbar.is-fixed-bottom,.navbar.is-fixed-top{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom{bottom:0}.navbar.is-fixed-bottom.has-shadow{box-shadow:0 -2px 0 0 #f5f5f5}.navbar.is-fixed-top{top:0}body.has-navbar-fixed-top,html.has-navbar-fixed-top{padding-top:3.25rem}body.has-navbar-fixed-bottom,html.has-navbar-fixed-bottom{padding-bottom:3.25rem}.navbar-brand,.navbar-tabs{align-items:stretch;display:flex;flex-shrink:0;min-height:3.25rem}.navbar-brand a.navbar-item:hover{background-color:transparent}.navbar-tabs{-webkit-overflow-scrolling:touch;max-width:100vw;overflow-x:auto;overflow-y:hidden}.navbar-burger{color:#4a4a4a;cursor:pointer;display:block;height:3.25rem;position:relative;width:3.25rem;margin-left:auto}.navbar-burger span{background-color:currentColor;display:block;height:1px;left:calc(50% - 8px);position:absolute;transform-origin:center;transition-duration:86ms;transition-property:background-color,opacity,transform;transition-timing-function:ease-out;width:16px}.navbar-burger span:nth-child(1){top:calc(50% - 6px)}.navbar-burger span:nth-child(2){top:calc(50% - 1px)}.navbar-burger span:nth-child(3){top:calc(50% + 4px)}.navbar-burger:hover{background-color:rgba(0,0,0,.05)}.navbar-burger.is-active span:nth-child(1){transform:translateY(5px) rotate(45deg)}.navbar-burger.is-active span:nth-child(2){opacity:0}.navbar-burger.is-active span:nth-child(3){transform:translateY(-5px) rotate(-45deg)}.navbar-menu{display:none}.navbar-item,.navbar-link{color:#4a4a4a;display:block;line-height:1.5;padding:.5rem .75rem;position:relative}.navbar-item .icon:only-child,.navbar-link .icon:only-child{margin-left:-.25rem;margin-right:-.25rem}.navbar-link,a.navbar-item{cursor:pointer}.navbar-link.is-active,.navbar-link:hover,a.navbar-item.is-active,a.navbar-item:hover{background-color:#fafafa;color:#469}.navbar-item{display:block;flex-grow:0;flex-shrink:0}.navbar-item img{max-height:1.75rem}.navbar-item.has-dropdown{padding:0}.navbar-item.is-expanded{flex-grow:1;flex-shrink:1}.navbar-item.is-tab{border-bottom:1px solid transparent;min-height:3.25rem;padding-bottom:calc(.5rem - 1px)}.navbar-item.is-tab:hover{background-color:transparent;border-bottom-color:#469}.navbar-item.is-tab.is-active{background-color:transparent;border-bottom-color:#469;border-bottom-style:solid;border-bottom-width:3px;color:#469;padding-bottom:calc(.5rem - 3px)}.navbar-content{flex-grow:1;flex-shrink:1}.navbar-link:not(.is-arrowless){padding-right:2.5em}.navbar-link:not(.is-arrowless)::after{border-color:#469;margin-top:-.375em;right:1.125em}.navbar-dropdown{font-size:.875rem;padding-bottom:.5rem;padding-top:.5rem}.navbar-dropdown .navbar-item{padding-left:1.5rem;padding-right:1.5rem}.navbar-divider{background-color:#f5f5f5;border:none;display:none;height:2px;margin:.5rem 0}@media screen and (max-width:-1px){.navbar>.container{display:block}.navbar-brand .navbar-item,.navbar-tabs .navbar-item{align-items:center;display:flex}.navbar-link::after{display:none}.navbar-menu{background-color:#fff;box-shadow:0 8px 16px rgba(10,10,10,.1);padding:.5rem 0}.navbar-menu.is-active{display:block}.navbar.is-fixed-bottom-touch,.navbar.is-fixed-top-touch{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-touch{bottom:0}.navbar.is-fixed-bottom-touch.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-touch{top:0}.navbar.is-fixed-top .navbar-menu,.navbar.is-fixed-top-touch .navbar-menu{-webkit-overflow-scrolling:touch;max-height:calc(100vh - 3.25rem);overflow:auto}body.has-navbar-fixed-top-touch,html.has-navbar-fixed-top-touch{padding-top:3.25rem}body.has-navbar-fixed-bottom-touch,html.has-navbar-fixed-bottom-touch{padding-bottom:3.25rem}}@media screen and (min-width:0px){.navbar,.navbar-end,.navbar-menu,.navbar-start{align-items:stretch;display:flex}.navbar{min-height:3.25rem}.navbar.is-spaced{padding:1rem 2rem}.navbar.is-spaced .navbar-end,.navbar.is-spaced .navbar-start{align-items:center}.navbar.is-spaced .navbar-link,.navbar.is-spaced a.navbar-item{border-radius:4px}.navbar.is-transparent .navbar-link.is-active,.navbar.is-transparent .navbar-link:hover,.navbar.is-transparent a.navbar-item.is-active,.navbar.is-transparent a.navbar-item:hover{background-color:transparent!important}.navbar.is-transparent .navbar-item.has-dropdown.is-active .navbar-link,.navbar.is-transparent .navbar-item.has-dropdown.is-hoverable:hover .navbar-link{background-color:transparent!important}.navbar.is-transparent .navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar.is-transparent .navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#469}.navbar-burger{display:none}.navbar-item,.navbar-link{align-items:center;display:flex}.navbar-item{display:flex}.navbar-item.has-dropdown{align-items:stretch}.navbar-item.has-dropdown-up .navbar-link::after{transform:rotate(135deg) translate(.25em,-.25em)}.navbar-item.has-dropdown-up .navbar-dropdown{border-bottom:2px solid #dbdbdb;border-radius:6px 6px 0 0;border-top:none;bottom:100%;box-shadow:0 -8px 8px rgba(10,10,10,.1);top:auto}.navbar-item.is-active .navbar-dropdown,.navbar-item.is-hoverable:hover .navbar-dropdown{display:block}.navbar-item.is-active .navbar-dropdown.is-boxed,.navbar-item.is-hoverable:hover .navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-item.is-active .navbar-dropdown,.navbar.is-spaced .navbar-item.is-hoverable:hover .navbar-dropdown{opacity:1;pointer-events:auto;transform:translateY(0)}.navbar-menu{flex-grow:1;flex-shrink:0}.navbar-start{justify-content:flex-start;margin-right:auto}.navbar-end{justify-content:flex-end;margin-left:auto}.navbar-dropdown{background-color:#fff;border-bottom-left-radius:6px;border-bottom-right-radius:6px;border-top:2px solid #dbdbdb;box-shadow:0 8px 8px rgba(10,10,10,.1);display:none;font-size:.875rem;left:0;min-width:100%;position:absolute;top:100%;z-index:20}.navbar-dropdown .navbar-item{padding:.375rem 1rem;white-space:nowrap}.navbar-dropdown a.navbar-item{padding-right:3rem}.navbar-dropdown a.navbar-item:hover{background-color:#f5f5f5;color:#0a0a0a}.navbar-dropdown a.navbar-item.is-active{background-color:#f5f5f5;color:#469}.navbar-dropdown.is-boxed,.navbar.is-spaced .navbar-dropdown{border-radius:6px;border-top:none;box-shadow:0 8px 8px rgba(10,10,10,.1),0 0 0 1px rgba(10,10,10,.1);display:block;opacity:0;pointer-events:none;top:calc(100% + (-4px));transform:translateY(-5px);transition-duration:86ms;transition-property:opacity,transform}.navbar-dropdown.is-right{left:auto;right:0}.navbar-divider{display:block}.container>.navbar .navbar-brand,.navbar>.container .navbar-brand{margin-left:-.75rem}.container>.navbar .navbar-menu,.navbar>.container .navbar-menu{margin-right:-.75rem}.navbar.is-fixed-bottom-desktop,.navbar.is-fixed-top-desktop{left:0;position:fixed;right:0;z-index:30}.navbar.is-fixed-bottom-desktop{bottom:0}.navbar.is-fixed-bottom-desktop.has-shadow{box-shadow:0 -2px 3px rgba(10,10,10,.1)}.navbar.is-fixed-top-desktop{top:0}body.has-navbar-fixed-top-desktop,html.has-navbar-fixed-top-desktop{padding-top:3.25rem}body.has-navbar-fixed-bottom-desktop,html.has-navbar-fixed-bottom-desktop{padding-bottom:3.25rem}body.has-spaced-navbar-fixed-top,html.has-spaced-navbar-fixed-top{padding-top:5.25rem}body.has-spaced-navbar-fixed-bottom,html.has-spaced-navbar-fixed-bottom{padding-bottom:5.25rem}.navbar-link.is-active,a.navbar-item.is-active{color:#0a0a0a}.navbar-link.is-active:not(:hover),a.navbar-item.is-active:not(:hover){background-color:transparent}.navbar-item.has-dropdown.is-active .navbar-link,.navbar-item.has-dropdown:hover .navbar-link{background-color:#fafafa}}.hero.is-fullheight-with-navbar{min-height:calc(100vh - 3.25rem)}.column{display:block;flex-basis:0;flex-grow:1;flex-shrink:1;padding:.75rem}.columns.is-mobile>.column.is-narrow{flex:none}.columns.is-mobile>.column.is-full{flex:none;width:100%}.columns.is-mobile>.column.is-three-quarters{flex:none;width:75%}.columns.is-mobile>.column.is-two-thirds{flex:none;width:66.6666%}.columns.is-mobile>.column.is-half{flex:none;width:50%}.columns.is-mobile>.column.is-one-third{flex:none;width:33.3333%}.columns.is-mobile>.column.is-one-quarter{flex:none;width:25%}.columns.is-mobile>.column.is-one-fifth{flex:none;width:20%}.columns.is-mobile>.column.is-two-fifths{flex:none;width:40%}.columns.is-mobile>.column.is-three-fifths{flex:none;width:60%}.columns.is-mobile>.column.is-four-fifths{flex:none;width:80%}.columns.is-mobile>.column.is-offset-three-quarters{margin-left:75%}.columns.is-mobile>.column.is-offset-two-thirds{margin-left:66.6666%}.columns.is-mobile>.column.is-offset-half{margin-left:50%}.columns.is-mobile>.column.is-offset-one-third{margin-left:33.3333%}.columns.is-mobile>.column.is-offset-one-quarter{margin-left:25%}.columns.is-mobile>.column.is-offset-one-fifth{margin-left:20%}.columns.is-mobile>.column.is-offset-two-fifths{margin-left:40%}.columns.is-mobile>.column.is-offset-three-fifths{margin-left:60%}.columns.is-mobile>.column.is-offset-four-fifths{margin-left:80%}.columns.is-mobile>.column.is-1{flex:none;width:8.33333%}.columns.is-mobile>.column.is-offset-1{margin-left:8.33333%}.columns.is-mobile>.column.is-2{flex:none;width:16.66667%}.columns.is-mobile>.column.is-offset-2{margin-left:16.66667%}.columns.is-mobile>.column.is-3{flex:none;width:25%}.columns.is-mobile>.column.is-offset-3{margin-left:25%}.columns.is-mobile>.column.is-4{flex:none;width:33.33333%}.columns.is-mobile>.column.is-offset-4{margin-left:33.33333%}.columns.is-mobile>.column.is-5{flex:none;width:41.66667%}.columns.is-mobile>.column.is-offset-5{margin-left:41.66667%}.columns.is-mobile>.column.is-6{flex:none;width:50%}.columns.is-mobile>.column.is-offset-6{margin-left:50%}.columns.is-mobile>.column.is-7{flex:none;width:58.33333%}.columns.is-mobile>.column.is-offset-7{margin-left:58.33333%}.columns.is-mobile>.column.is-8{flex:none;width:66.66667%}.columns.is-mobile>.column.is-offset-8{margin-left:66.66667%}.columns.is-mobile>.column.is-9{flex:none;width:75%}.columns.is-mobile>.column.is-offset-9{margin-left:75%}.columns.is-mobile>.column.is-10{flex:none;width:83.33333%}.columns.is-mobile>.column.is-offset-10{margin-left:83.33333%}.columns.is-mobile>.column.is-11{flex:none;width:91.66667%}.columns.is-mobile>.column.is-offset-11{margin-left:91.66667%}.columns.is-mobile>.column.is-12{flex:none;width:100%}.columns.is-mobile>.column.is-offset-12{margin-left:100%}@media screen and (max-width:768px){.column.is-narrow-mobile{flex:none}.column.is-full-mobile{flex:none;width:100%}.column.is-three-quarters-mobile{flex:none;width:75%}.column.is-two-thirds-mobile{flex:none;width:66.6666%}.column.is-half-mobile{flex:none;width:50%}.column.is-one-third-mobile{flex:none;width:33.3333%}.column.is-one-quarter-mobile{flex:none;width:25%}.column.is-one-fifth-mobile{flex:none;width:20%}.column.is-two-fifths-mobile{flex:none;width:40%}.column.is-three-fifths-mobile{flex:none;width:60%}.column.is-four-fifths-mobile{flex:none;width:80%}.column.is-offset-three-quarters-mobile{margin-left:75%}.column.is-offset-two-thirds-mobile{margin-left:66.6666%}.column.is-offset-half-mobile{margin-left:50%}.column.is-offset-one-third-mobile{margin-left:33.3333%}.column.is-offset-one-quarter-mobile{margin-left:25%}.column.is-offset-one-fifth-mobile{margin-left:20%}.column.is-offset-two-fifths-mobile{margin-left:40%}.column.is-offset-three-fifths-mobile{margin-left:60%}.column.is-offset-four-fifths-mobile{margin-left:80%}.column.is-1-mobile{flex:none;width:8.33333%}.column.is-offset-1-mobile{margin-left:8.33333%}.column.is-2-mobile{flex:none;width:16.66667%}.column.is-offset-2-mobile{margin-left:16.66667%}.column.is-3-mobile{flex:none;width:25%}.column.is-offset-3-mobile{margin-left:25%}.column.is-4-mobile{flex:none;width:33.33333%}.column.is-offset-4-mobile{margin-left:33.33333%}.column.is-5-mobile{flex:none;width:41.66667%}.column.is-offset-5-mobile{margin-left:41.66667%}.column.is-6-mobile{flex:none;width:50%}.column.is-offset-6-mobile{margin-left:50%}.column.is-7-mobile{flex:none;width:58.33333%}.column.is-offset-7-mobile{margin-left:58.33333%}.column.is-8-mobile{flex:none;width:66.66667%}.column.is-offset-8-mobile{margin-left:66.66667%}.column.is-9-mobile{flex:none;width:75%}.column.is-offset-9-mobile{margin-left:75%}.column.is-10-mobile{flex:none;width:83.33333%}.column.is-offset-10-mobile{margin-left:83.33333%}.column.is-11-mobile{flex:none;width:91.66667%}.column.is-offset-11-mobile{margin-left:91.66667%}.column.is-12-mobile{flex:none;width:100%}.column.is-offset-12-mobile{margin-left:100%}}@media screen and (min-width:769px),print{.column.is-narrow,.column.is-narrow-tablet{flex:none}.column.is-full,.column.is-full-tablet{flex:none;width:100%}.column.is-three-quarters,.column.is-three-quarters-tablet{flex:none;width:75%}.column.is-two-thirds,.column.is-two-thirds-tablet{flex:none;width:66.6666%}.column.is-half,.column.is-half-tablet{flex:none;width:50%}.column.is-one-third,.column.is-one-third-tablet{flex:none;width:33.3333%}.column.is-one-quarter,.column.is-one-quarter-tablet{flex:none;width:25%}.column.is-one-fifth,.column.is-one-fifth-tablet{flex:none;width:20%}.column.is-two-fifths,.column.is-two-fifths-tablet{flex:none;width:40%}.column.is-three-fifths,.column.is-three-fifths-tablet{flex:none;width:60%}.column.is-four-fifths,.column.is-four-fifths-tablet{flex:none;width:80%}.column.is-offset-three-quarters,.column.is-offset-three-quarters-tablet{margin-left:75%}.column.is-offset-two-thirds,.column.is-offset-two-thirds-tablet{margin-left:66.6666%}.column.is-offset-half,.column.is-offset-half-tablet{margin-left:50%}.column.is-offset-one-third,.column.is-offset-one-third-tablet{margin-left:33.3333%}.column.is-offset-one-quarter,.column.is-offset-one-quarter-tablet{margin-left:25%}.column.is-offset-one-fifth,.column.is-offset-one-fifth-tablet{margin-left:20%}.column.is-offset-two-fifths,.column.is-offset-two-fifths-tablet{margin-left:40%}.column.is-offset-three-fifths,.column.is-offset-three-fifths-tablet{margin-left:60%}.column.is-offset-four-fifths,.column.is-offset-four-fifths-tablet{margin-left:80%}.column.is-1,.column.is-1-tablet{flex:none;width:8.33333%}.column.is-offset-1,.column.is-offset-1-tablet{margin-left:8.33333%}.column.is-2,.column.is-2-tablet{flex:none;width:16.66667%}.column.is-offset-2,.column.is-offset-2-tablet{margin-left:16.66667%}.column.is-3,.column.is-3-tablet{flex:none;width:25%}.column.is-offset-3,.column.is-offset-3-tablet{margin-left:25%}.column.is-4,.column.is-4-tablet{flex:none;width:33.33333%}.column.is-offset-4,.column.is-offset-4-tablet{margin-left:33.33333%}.column.is-5,.column.is-5-tablet{flex:none;width:41.66667%}.column.is-offset-5,.column.is-offset-5-tablet{margin-left:41.66667%}.column.is-6,.column.is-6-tablet{flex:none;width:50%}.column.is-offset-6,.column.is-offset-6-tablet{margin-left:50%}.column.is-7,.column.is-7-tablet{flex:none;width:58.33333%}.column.is-offset-7,.column.is-offset-7-tablet{margin-left:58.33333%}.column.is-8,.column.is-8-tablet{flex:none;width:66.66667%}.column.is-offset-8,.column.is-offset-8-tablet{margin-left:66.66667%}.column.is-9,.column.is-9-tablet{flex:none;width:75%}.column.is-offset-9,.column.is-offset-9-tablet{margin-left:75%}.column.is-10,.column.is-10-tablet{flex:none;width:83.33333%}.column.is-offset-10,.column.is-offset-10-tablet{margin-left:83.33333%}.column.is-11,.column.is-11-tablet{flex:none;width:91.66667%}.column.is-offset-11,.column.is-offset-11-tablet{margin-left:91.66667%}.column.is-12,.column.is-12-tablet{flex:none;width:100%}.column.is-offset-12,.column.is-offset-12-tablet{margin-left:100%}}@media screen and (max-width:-1px){.column.is-narrow-touch{flex:none}.column.is-full-touch{flex:none;width:100%}.column.is-three-quarters-touch{flex:none;width:75%}.column.is-two-thirds-touch{flex:none;width:66.6666%}.column.is-half-touch{flex:none;width:50%}.column.is-one-third-touch{flex:none;width:33.3333%}.column.is-one-quarter-touch{flex:none;width:25%}.column.is-one-fifth-touch{flex:none;width:20%}.column.is-two-fifths-touch{flex:none;width:40%}.column.is-three-fifths-touch{flex:none;width:60%}.column.is-four-fifths-touch{flex:none;width:80%}.column.is-offset-three-quarters-touch{margin-left:75%}.column.is-offset-two-thirds-touch{margin-left:66.6666%}.column.is-offset-half-touch{margin-left:50%}.column.is-offset-one-third-touch{margin-left:33.3333%}.column.is-offset-one-quarter-touch{margin-left:25%}.column.is-offset-one-fifth-touch{margin-left:20%}.column.is-offset-two-fifths-touch{margin-left:40%}.column.is-offset-three-fifths-touch{margin-left:60%}.column.is-offset-four-fifths-touch{margin-left:80%}.column.is-1-touch{flex:none;width:8.33333%}.column.is-offset-1-touch{margin-left:8.33333%}.column.is-2-touch{flex:none;width:16.66667%}.column.is-offset-2-touch{margin-left:16.66667%}.column.is-3-touch{flex:none;width:25%}.column.is-offset-3-touch{margin-left:25%}.column.is-4-touch{flex:none;width:33.33333%}.column.is-offset-4-touch{margin-left:33.33333%}.column.is-5-touch{flex:none;width:41.66667%}.column.is-offset-5-touch{margin-left:41.66667%}.column.is-6-touch{flex:none;width:50%}.column.is-offset-6-touch{margin-left:50%}.column.is-7-touch{flex:none;width:58.33333%}.column.is-offset-7-touch{margin-left:58.33333%}.column.is-8-touch{flex:none;width:66.66667%}.column.is-offset-8-touch{margin-left:66.66667%}.column.is-9-touch{flex:none;width:75%}.column.is-offset-9-touch{margin-left:75%}.column.is-10-touch{flex:none;width:83.33333%}.column.is-offset-10-touch{margin-left:83.33333%}.column.is-11-touch{flex:none;width:91.66667%}.column.is-offset-11-touch{margin-left:91.66667%}.column.is-12-touch{flex:none;width:100%}.column.is-offset-12-touch{margin-left:100%}}@media screen and (min-width:0px){.column.is-narrow-desktop{flex:none}.column.is-full-desktop{flex:none;width:100%}.column.is-three-quarters-desktop{flex:none;width:75%}.column.is-two-thirds-desktop{flex:none;width:66.6666%}.column.is-half-desktop{flex:none;width:50%}.column.is-one-third-desktop{flex:none;width:33.3333%}.column.is-one-quarter-desktop{flex:none;width:25%}.column.is-one-fifth-desktop{flex:none;width:20%}.column.is-two-fifths-desktop{flex:none;width:40%}.column.is-three-fifths-desktop{flex:none;width:60%}.column.is-four-fifths-desktop{flex:none;width:80%}.column.is-offset-three-quarters-desktop{margin-left:75%}.column.is-offset-two-thirds-desktop{margin-left:66.6666%}.column.is-offset-half-desktop{margin-left:50%}.column.is-offset-one-third-desktop{margin-left:33.3333%}.column.is-offset-one-quarter-desktop{margin-left:25%}.column.is-offset-one-fifth-desktop{margin-left:20%}.column.is-offset-two-fifths-desktop{margin-left:40%}.column.is-offset-three-fifths-desktop{margin-left:60%}.column.is-offset-four-fifths-desktop{margin-left:80%}.column.is-1-desktop{flex:none;width:8.33333%}.column.is-offset-1-desktop{margin-left:8.33333%}.column.is-2-desktop{flex:none;width:16.66667%}.column.is-offset-2-desktop{margin-left:16.66667%}.column.is-3-desktop{flex:none;width:25%}.column.is-offset-3-desktop{margin-left:25%}.column.is-4-desktop{flex:none;width:33.33333%}.column.is-offset-4-desktop{margin-left:33.33333%}.column.is-5-desktop{flex:none;width:41.66667%}.column.is-offset-5-desktop{margin-left:41.66667%}.column.is-6-desktop{flex:none;width:50%}.column.is-offset-6-desktop{margin-left:50%}.column.is-7-desktop{flex:none;width:58.33333%}.column.is-offset-7-desktop{margin-left:58.33333%}.column.is-8-desktop{flex:none;width:66.66667%}.column.is-offset-8-desktop{margin-left:66.66667%}.column.is-9-desktop{flex:none;width:75%}.column.is-offset-9-desktop{margin-left:75%}.column.is-10-desktop{flex:none;width:83.33333%}.column.is-offset-10-desktop{margin-left:83.33333%}.column.is-11-desktop{flex:none;width:91.66667%}.column.is-offset-11-desktop{margin-left:91.66667%}.column.is-12-desktop{flex:none;width:100%}.column.is-offset-12-desktop{margin-left:100%}}@media screen and (min-width:1280px){.column.is-narrow-widescreen{flex:none}.column.is-full-widescreen{flex:none;width:100%}.column.is-three-quarters-widescreen{flex:none;width:75%}.column.is-two-thirds-widescreen{flex:none;width:66.6666%}.column.is-half-widescreen{flex:none;width:50%}.column.is-one-third-widescreen{flex:none;width:33.3333%}.column.is-one-quarter-widescreen{flex:none;width:25%}.column.is-one-fifth-widescreen{flex:none;width:20%}.column.is-two-fifths-widescreen{flex:none;width:40%}.column.is-three-fifths-widescreen{flex:none;width:60%}.column.is-four-fifths-widescreen{flex:none;width:80%}.column.is-offset-three-quarters-widescreen{margin-left:75%}.column.is-offset-two-thirds-widescreen{margin-left:66.6666%}.column.is-offset-half-widescreen{margin-left:50%}.column.is-offset-one-third-widescreen{margin-left:33.3333%}.column.is-offset-one-quarter-widescreen{margin-left:25%}.column.is-offset-one-fifth-widescreen{margin-left:20%}.column.is-offset-two-fifths-widescreen{margin-left:40%}.column.is-offset-three-fifths-widescreen{margin-left:60%}.column.is-offset-four-fifths-widescreen{margin-left:80%}.column.is-1-widescreen{flex:none;width:8.33333%}.column.is-offset-1-widescreen{margin-left:8.33333%}.column.is-2-widescreen{flex:none;width:16.66667%}.column.is-offset-2-widescreen{margin-left:16.66667%}.column.is-3-widescreen{flex:none;width:25%}.column.is-offset-3-widescreen{margin-left:25%}.column.is-4-widescreen{flex:none;width:33.33333%}.column.is-offset-4-widescreen{margin-left:33.33333%}.column.is-5-widescreen{flex:none;width:41.66667%}.column.is-offset-5-widescreen{margin-left:41.66667%}.column.is-6-widescreen{flex:none;width:50%}.column.is-offset-6-widescreen{margin-left:50%}.column.is-7-widescreen{flex:none;width:58.33333%}.column.is-offset-7-widescreen{margin-left:58.33333%}.column.is-8-widescreen{flex:none;width:66.66667%}.column.is-offset-8-widescreen{margin-left:66.66667%}.column.is-9-widescreen{flex:none;width:75%}.column.is-offset-9-widescreen{margin-left:75%}.column.is-10-widescreen{flex:none;width:83.33333%}.column.is-offset-10-widescreen{margin-left:83.33333%}.column.is-11-widescreen{flex:none;width:91.66667%}.column.is-offset-11-widescreen{margin-left:91.66667%}.column.is-12-widescreen{flex:none;width:100%}.column.is-offset-12-widescreen{margin-left:100%}}@media screen and (min-width:1472px){.column.is-narrow-fullhd{flex:none}.column.is-full-fullhd{flex:none;width:100%}.column.is-three-quarters-fullhd{flex:none;width:75%}.column.is-two-thirds-fullhd{flex:none;width:66.6666%}.column.is-half-fullhd{flex:none;width:50%}.column.is-one-third-fullhd{flex:none;width:33.3333%}.column.is-one-quarter-fullhd{flex:none;width:25%}.column.is-one-fifth-fullhd{flex:none;width:20%}.column.is-two-fifths-fullhd{flex:none;width:40%}.column.is-three-fifths-fullhd{flex:none;width:60%}.column.is-four-fifths-fullhd{flex:none;width:80%}.column.is-offset-three-quarters-fullhd{margin-left:75%}.column.is-offset-two-thirds-fullhd{margin-left:66.6666%}.column.is-offset-half-fullhd{margin-left:50%}.column.is-offset-one-third-fullhd{margin-left:33.3333%}.column.is-offset-one-quarter-fullhd{margin-left:25%}.column.is-offset-one-fifth-fullhd{margin-left:20%}.column.is-offset-two-fifths-fullhd{margin-left:40%}.column.is-offset-three-fifths-fullhd{margin-left:60%}.column.is-offset-four-fifths-fullhd{margin-left:80%}.column.is-1-fullhd{flex:none;width:8.33333%}.column.is-offset-1-fullhd{margin-left:8.33333%}.column.is-2-fullhd{flex:none;width:16.66667%}.column.is-offset-2-fullhd{margin-left:16.66667%}.column.is-3-fullhd{flex:none;width:25%}.column.is-offset-3-fullhd{margin-left:25%}.column.is-4-fullhd{flex:none;width:33.33333%}.column.is-offset-4-fullhd{margin-left:33.33333%}.column.is-5-fullhd{flex:none;width:41.66667%}.column.is-offset-5-fullhd{margin-left:41.66667%}.column.is-6-fullhd{flex:none;width:50%}.column.is-offset-6-fullhd{margin-left:50%}.column.is-7-fullhd{flex:none;width:58.33333%}.column.is-offset-7-fullhd{margin-left:58.33333%}.column.is-8-fullhd{flex:none;width:66.66667%}.column.is-offset-8-fullhd{margin-left:66.66667%}.column.is-9-fullhd{flex:none;width:75%}.column.is-offset-9-fullhd{margin-left:75%}.column.is-10-fullhd{flex:none;width:83.33333%}.column.is-offset-10-fullhd{margin-left:83.33333%}.column.is-11-fullhd{flex:none;width:91.66667%}.column.is-offset-11-fullhd{margin-left:91.66667%}.column.is-12-fullhd{flex:none;width:100%}.column.is-offset-12-fullhd{margin-left:100%}}.columns{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.columns:last-child{margin-bottom:-.75rem}.columns:not(:last-child){margin-bottom:calc(1.5rem - .75rem)}.columns.is-centered{justify-content:center}.columns.is-gapless{margin-left:0;margin-right:0;margin-top:0}.columns.is-gapless>.column{margin:0;padding:0!important}.columns.is-gapless:not(:last-child){margin-bottom:1.5rem}.columns.is-gapless:last-child{margin-bottom:0}.columns.is-mobile{display:flex}.columns.is-multiline{flex-wrap:wrap}.columns.is-vcentered{align-items:center}@media screen and (min-width:769px),print{.columns:not(.is-desktop){display:flex}}@media screen and (min-width:0px){.columns.is-desktop{display:flex}}.columns.is-variable{--columnGap:0.75rem;margin-left:calc(-1 * var(--columnGap));margin-right:calc(-1 * var(--columnGap))}.columns.is-variable .column{padding-left:var(--columnGap);padding-right:var(--columnGap)}.columns.is-variable.is-0{--columnGap:0rem}@media screen and (max-width:768px){.columns.is-variable.is-0-mobile{--columnGap:0rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-0-tablet{--columnGap:0rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-0-tablet-only{--columnGap:0rem}}@media screen and (max-width:-1px){.columns.is-variable.is-0-touch{--columnGap:0rem}}@media screen and (min-width:0px){.columns.is-variable.is-0-desktop{--columnGap:0rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-0-desktop-only{--columnGap:0rem}}@media screen and (min-width:1280px){.columns.is-variable.is-0-widescreen{--columnGap:0rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-0-widescreen-only{--columnGap:0rem}}@media screen and (min-width:1472px){.columns.is-variable.is-0-fullhd{--columnGap:0rem}}.columns.is-variable.is-1{--columnGap:0.25rem}@media screen and (max-width:768px){.columns.is-variable.is-1-mobile{--columnGap:0.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-1-tablet{--columnGap:0.25rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-1-tablet-only{--columnGap:0.25rem}}@media screen and (max-width:-1px){.columns.is-variable.is-1-touch{--columnGap:0.25rem}}@media screen and (min-width:0px){.columns.is-variable.is-1-desktop{--columnGap:0.25rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-1-desktop-only{--columnGap:0.25rem}}@media screen and (min-width:1280px){.columns.is-variable.is-1-widescreen{--columnGap:0.25rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-1-widescreen-only{--columnGap:0.25rem}}@media screen and (min-width:1472px){.columns.is-variable.is-1-fullhd{--columnGap:0.25rem}}.columns.is-variable.is-2{--columnGap:0.5rem}@media screen and (max-width:768px){.columns.is-variable.is-2-mobile{--columnGap:0.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-2-tablet{--columnGap:0.5rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-2-tablet-only{--columnGap:0.5rem}}@media screen and (max-width:-1px){.columns.is-variable.is-2-touch{--columnGap:0.5rem}}@media screen and (min-width:0px){.columns.is-variable.is-2-desktop{--columnGap:0.5rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-2-desktop-only{--columnGap:0.5rem}}@media screen and (min-width:1280px){.columns.is-variable.is-2-widescreen{--columnGap:0.5rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-2-widescreen-only{--columnGap:0.5rem}}@media screen and (min-width:1472px){.columns.is-variable.is-2-fullhd{--columnGap:0.5rem}}.columns.is-variable.is-3{--columnGap:0.75rem}@media screen and (max-width:768px){.columns.is-variable.is-3-mobile{--columnGap:0.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-3-tablet{--columnGap:0.75rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-3-tablet-only{--columnGap:0.75rem}}@media screen and (max-width:-1px){.columns.is-variable.is-3-touch{--columnGap:0.75rem}}@media screen and (min-width:0px){.columns.is-variable.is-3-desktop{--columnGap:0.75rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-3-desktop-only{--columnGap:0.75rem}}@media screen and (min-width:1280px){.columns.is-variable.is-3-widescreen{--columnGap:0.75rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-3-widescreen-only{--columnGap:0.75rem}}@media screen and (min-width:1472px){.columns.is-variable.is-3-fullhd{--columnGap:0.75rem}}.columns.is-variable.is-4{--columnGap:1rem}@media screen and (max-width:768px){.columns.is-variable.is-4-mobile{--columnGap:1rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-4-tablet{--columnGap:1rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-4-tablet-only{--columnGap:1rem}}@media screen and (max-width:-1px){.columns.is-variable.is-4-touch{--columnGap:1rem}}@media screen and (min-width:0px){.columns.is-variable.is-4-desktop{--columnGap:1rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-4-desktop-only{--columnGap:1rem}}@media screen and (min-width:1280px){.columns.is-variable.is-4-widescreen{--columnGap:1rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-4-widescreen-only{--columnGap:1rem}}@media screen and (min-width:1472px){.columns.is-variable.is-4-fullhd{--columnGap:1rem}}.columns.is-variable.is-5{--columnGap:1.25rem}@media screen and (max-width:768px){.columns.is-variable.is-5-mobile{--columnGap:1.25rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-5-tablet{--columnGap:1.25rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-5-tablet-only{--columnGap:1.25rem}}@media screen and (max-width:-1px){.columns.is-variable.is-5-touch{--columnGap:1.25rem}}@media screen and (min-width:0px){.columns.is-variable.is-5-desktop{--columnGap:1.25rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-5-desktop-only{--columnGap:1.25rem}}@media screen and (min-width:1280px){.columns.is-variable.is-5-widescreen{--columnGap:1.25rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-5-widescreen-only{--columnGap:1.25rem}}@media screen and (min-width:1472px){.columns.is-variable.is-5-fullhd{--columnGap:1.25rem}}.columns.is-variable.is-6{--columnGap:1.5rem}@media screen and (max-width:768px){.columns.is-variable.is-6-mobile{--columnGap:1.5rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-6-tablet{--columnGap:1.5rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-6-tablet-only{--columnGap:1.5rem}}@media screen and (max-width:-1px){.columns.is-variable.is-6-touch{--columnGap:1.5rem}}@media screen and (min-width:0px){.columns.is-variable.is-6-desktop{--columnGap:1.5rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-6-desktop-only{--columnGap:1.5rem}}@media screen and (min-width:1280px){.columns.is-variable.is-6-widescreen{--columnGap:1.5rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-6-widescreen-only{--columnGap:1.5rem}}@media screen and (min-width:1472px){.columns.is-variable.is-6-fullhd{--columnGap:1.5rem}}.columns.is-variable.is-7{--columnGap:1.75rem}@media screen and (max-width:768px){.columns.is-variable.is-7-mobile{--columnGap:1.75rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-7-tablet{--columnGap:1.75rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-7-tablet-only{--columnGap:1.75rem}}@media screen and (max-width:-1px){.columns.is-variable.is-7-touch{--columnGap:1.75rem}}@media screen and (min-width:0px){.columns.is-variable.is-7-desktop{--columnGap:1.75rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-7-desktop-only{--columnGap:1.75rem}}@media screen and (min-width:1280px){.columns.is-variable.is-7-widescreen{--columnGap:1.75rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-7-widescreen-only{--columnGap:1.75rem}}@media screen and (min-width:1472px){.columns.is-variable.is-7-fullhd{--columnGap:1.75rem}}.columns.is-variable.is-8{--columnGap:2rem}@media screen and (max-width:768px){.columns.is-variable.is-8-mobile{--columnGap:2rem}}@media screen and (min-width:769px),print{.columns.is-variable.is-8-tablet{--columnGap:2rem}}@media screen and (min-width:769px) and (max-width:-1px){.columns.is-variable.is-8-tablet-only{--columnGap:2rem}}@media screen and (max-width:-1px){.columns.is-variable.is-8-touch{--columnGap:2rem}}@media screen and (min-width:0px){.columns.is-variable.is-8-desktop{--columnGap:2rem}}@media screen and (min-width:0px) and (max-width:1279px){.columns.is-variable.is-8-desktop-only{--columnGap:2rem}}@media screen and (min-width:1280px){.columns.is-variable.is-8-widescreen{--columnGap:2rem}}@media screen and (min-width:1280px) and (max-width:1471px){.columns.is-variable.is-8-widescreen-only{--columnGap:2rem}}@media screen and (min-width:1472px){.columns.is-variable.is-8-fullhd{--columnGap:2rem}}.tile{align-items:stretch;display:block;flex-basis:0;flex-grow:1;flex-shrink:1;min-height:min-content}.tile.is-ancestor{margin-left:-.75rem;margin-right:-.75rem;margin-top:-.75rem}.tile.is-ancestor:last-child{margin-bottom:-.75rem}.tile.is-ancestor:not(:last-child){margin-bottom:.75rem}.tile.is-child{margin:0!important}.tile.is-parent{padding:.75rem}.tile.is-vertical{flex-direction:column}.tile.is-vertical>.tile.is-child:not(:last-child){margin-bottom:1.5rem!important}@media screen and (min-width:769px),print{.tile:not(.is-child){display:flex}.tile.is-1{flex:none;width:8.33333%}.tile.is-2{flex:none;width:16.66667%}.tile.is-3{flex:none;width:25%}.tile.is-4{flex:none;width:33.33333%}.tile.is-5{flex:none;width:41.66667%}.tile.is-6{flex:none;width:50%}.tile.is-7{flex:none;width:58.33333%}.tile.is-8{flex:none;width:66.66667%}.tile.is-9{flex:none;width:75%}.tile.is-10{flex:none;width:83.33333%}.tile.is-11{flex:none;width:91.66667%}.tile.is-12{flex:none;width:100%}}.hero{align-items:stretch;display:flex;flex-direction:column;justify-content:space-between}.hero .navbar{background:0 0}.hero .tabs ul{border-bottom:none}.hero.is-white{background-color:#fff;color:#0a0a0a}.hero.is-white a:not(.button):not(.dropdown-item):not(.tag),.hero.is-white strong{color:inherit}.hero.is-white .title{color:#0a0a0a}.hero.is-white .subtitle{color:rgba(10,10,10,.9)}.hero.is-white .subtitle a:not(.button),.hero.is-white .subtitle strong{color:#0a0a0a}@media screen and (max-width:-1px){.hero.is-white .navbar-menu{background-color:#fff}}.hero.is-white .navbar-item,.hero.is-white .navbar-link{color:rgba(10,10,10,.7)}.hero.is-white .navbar-link.is-active,.hero.is-white .navbar-link:hover,.hero.is-white a.navbar-item.is-active,.hero.is-white a.navbar-item:hover{background-color:#f2f2f2;color:#0a0a0a}.hero.is-white .tabs a{color:#0a0a0a;opacity:.9}.hero.is-white .tabs a:hover{opacity:1}.hero.is-white .tabs li.is-active a{opacity:1}.hero.is-white .tabs.is-boxed a,.hero.is-white .tabs.is-toggle a{color:#0a0a0a}.hero.is-white .tabs.is-boxed a:hover,.hero.is-white .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-white .tabs.is-boxed li.is-active a,.hero.is-white .tabs.is-boxed li.is-active a:hover,.hero.is-white .tabs.is-toggle li.is-active a,.hero.is-white .tabs.is-toggle li.is-active a:hover{background-color:#0a0a0a;border-color:#0a0a0a;color:#fff}.hero.is-white.is-bold{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-white.is-bold .navbar-menu{background-image:linear-gradient(141deg,#e6e6e6 0,#fff 71%,#fff 100%)}}.hero.is-black{background-color:#0a0a0a;color:#fff}.hero.is-black a:not(.button):not(.dropdown-item):not(.tag),.hero.is-black strong{color:inherit}.hero.is-black .title{color:#fff}.hero.is-black .subtitle{color:rgba(255,255,255,.9)}.hero.is-black .subtitle a:not(.button),.hero.is-black .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-black .navbar-menu{background-color:#0a0a0a}}.hero.is-black .navbar-item,.hero.is-black .navbar-link{color:rgba(255,255,255,.7)}.hero.is-black .navbar-link.is-active,.hero.is-black .navbar-link:hover,.hero.is-black a.navbar-item.is-active,.hero.is-black a.navbar-item:hover{background-color:#000;color:#fff}.hero.is-black .tabs a{color:#fff;opacity:.9}.hero.is-black .tabs a:hover{opacity:1}.hero.is-black .tabs li.is-active a{opacity:1}.hero.is-black .tabs.is-boxed a,.hero.is-black .tabs.is-toggle a{color:#fff}.hero.is-black .tabs.is-boxed a:hover,.hero.is-black .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-black .tabs.is-boxed li.is-active a,.hero.is-black .tabs.is-boxed li.is-active a:hover,.hero.is-black .tabs.is-toggle li.is-active a,.hero.is-black .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#0a0a0a}.hero.is-black.is-bold{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}@media screen and (max-width:768px){.hero.is-black.is-bold .navbar-menu{background-image:linear-gradient(141deg,#000 0,#0a0a0a 71%,#181616 100%)}}.hero.is-light{background-color:#f5f5f5;color:#363636}.hero.is-light a:not(.button):not(.dropdown-item):not(.tag),.hero.is-light strong{color:inherit}.hero.is-light .title{color:#363636}.hero.is-light .subtitle{color:rgba(54,54,54,.9)}.hero.is-light .subtitle a:not(.button),.hero.is-light .subtitle strong{color:#363636}@media screen and (max-width:-1px){.hero.is-light .navbar-menu{background-color:#f5f5f5}}.hero.is-light .navbar-item,.hero.is-light .navbar-link{color:rgba(54,54,54,.7)}.hero.is-light .navbar-link.is-active,.hero.is-light .navbar-link:hover,.hero.is-light a.navbar-item.is-active,.hero.is-light a.navbar-item:hover{background-color:#e8e8e8;color:#363636}.hero.is-light .tabs a{color:#363636;opacity:.9}.hero.is-light .tabs a:hover{opacity:1}.hero.is-light .tabs li.is-active a{opacity:1}.hero.is-light .tabs.is-boxed a,.hero.is-light .tabs.is-toggle a{color:#363636}.hero.is-light .tabs.is-boxed a:hover,.hero.is-light .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-light .tabs.is-boxed li.is-active a,.hero.is-light .tabs.is-boxed li.is-active a:hover,.hero.is-light .tabs.is-toggle li.is-active a,.hero.is-light .tabs.is-toggle li.is-active a:hover{background-color:#363636;border-color:#363636;color:#f5f5f5}.hero.is-light.is-bold{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}@media screen and (max-width:768px){.hero.is-light.is-bold .navbar-menu{background-image:linear-gradient(141deg,#dfd8d9 0,#f5f5f5 71%,#fff 100%)}}.hero.is-dark{background-color:#363636;color:#f5f5f5}.hero.is-dark a:not(.button):not(.dropdown-item):not(.tag),.hero.is-dark strong{color:inherit}.hero.is-dark .title{color:#f5f5f5}.hero.is-dark .subtitle{color:rgba(245,245,245,.9)}.hero.is-dark .subtitle a:not(.button),.hero.is-dark .subtitle strong{color:#f5f5f5}@media screen and (max-width:-1px){.hero.is-dark .navbar-menu{background-color:#363636}}.hero.is-dark .navbar-item,.hero.is-dark .navbar-link{color:rgba(245,245,245,.7)}.hero.is-dark .navbar-link.is-active,.hero.is-dark .navbar-link:hover,.hero.is-dark a.navbar-item.is-active,.hero.is-dark a.navbar-item:hover{background-color:#292929;color:#f5f5f5}.hero.is-dark .tabs a{color:#f5f5f5;opacity:.9}.hero.is-dark .tabs a:hover{opacity:1}.hero.is-dark .tabs li.is-active a{opacity:1}.hero.is-dark .tabs.is-boxed a,.hero.is-dark .tabs.is-toggle a{color:#f5f5f5}.hero.is-dark .tabs.is-boxed a:hover,.hero.is-dark .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-dark .tabs.is-boxed li.is-active a,.hero.is-dark .tabs.is-boxed li.is-active a:hover,.hero.is-dark .tabs.is-toggle li.is-active a,.hero.is-dark .tabs.is-toggle li.is-active a:hover{background-color:#f5f5f5;border-color:#f5f5f5;color:#363636}.hero.is-dark.is-bold{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}@media screen and (max-width:768px){.hero.is-dark.is-bold .navbar-menu{background-image:linear-gradient(141deg,#1f191a 0,#363636 71%,#46403f 100%)}}.hero.is-primary{background-color:#469;color:#fff}.hero.is-primary a:not(.button):not(.dropdown-item):not(.tag),.hero.is-primary strong{color:inherit}.hero.is-primary .title{color:#fff}.hero.is-primary .subtitle{color:rgba(255,255,255,.9)}.hero.is-primary .subtitle a:not(.button),.hero.is-primary .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-primary .navbar-menu{background-color:#469}}.hero.is-primary .navbar-item,.hero.is-primary .navbar-link{color:rgba(255,255,255,.7)}.hero.is-primary .navbar-link.is-active,.hero.is-primary .navbar-link:hover,.hero.is-primary a.navbar-item.is-active,.hero.is-primary a.navbar-item:hover{background-color:#3c5a87;color:#fff}.hero.is-primary .tabs a{color:#fff;opacity:.9}.hero.is-primary .tabs a:hover{opacity:1}.hero.is-primary .tabs li.is-active a{opacity:1}.hero.is-primary .tabs.is-boxed a,.hero.is-primary .tabs.is-toggle a{color:#fff}.hero.is-primary .tabs.is-boxed a:hover,.hero.is-primary .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-primary .tabs.is-boxed li.is-active a,.hero.is-primary .tabs.is-boxed li.is-active a:hover,.hero.is-primary .tabs.is-toggle li.is-active a,.hero.is-primary .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#469}.hero.is-primary.is-bold{background-image:linear-gradient(141deg,#2c5a7e 0,#469 71%,#465fb1 100%)}@media screen and (max-width:768px){.hero.is-primary.is-bold .navbar-menu{background-image:linear-gradient(141deg,#2c5a7e 0,#469 71%,#465fb1 100%)}}.hero.is-link{background-color:#469;color:#fff}.hero.is-link a:not(.button):not(.dropdown-item):not(.tag),.hero.is-link strong{color:inherit}.hero.is-link .title{color:#fff}.hero.is-link .subtitle{color:rgba(255,255,255,.9)}.hero.is-link .subtitle a:not(.button),.hero.is-link .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-link .navbar-menu{background-color:#469}}.hero.is-link .navbar-item,.hero.is-link .navbar-link{color:rgba(255,255,255,.7)}.hero.is-link .navbar-link.is-active,.hero.is-link .navbar-link:hover,.hero.is-link a.navbar-item.is-active,.hero.is-link a.navbar-item:hover{background-color:#3c5a87;color:#fff}.hero.is-link .tabs a{color:#fff;opacity:.9}.hero.is-link .tabs a:hover{opacity:1}.hero.is-link .tabs li.is-active a{opacity:1}.hero.is-link .tabs.is-boxed a,.hero.is-link .tabs.is-toggle a{color:#fff}.hero.is-link .tabs.is-boxed a:hover,.hero.is-link .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-link .tabs.is-boxed li.is-active a,.hero.is-link .tabs.is-boxed li.is-active a:hover,.hero.is-link .tabs.is-toggle li.is-active a,.hero.is-link .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#469}.hero.is-link.is-bold{background-image:linear-gradient(141deg,#2c5a7e 0,#469 71%,#465fb1 100%)}@media screen and (max-width:768px){.hero.is-link.is-bold .navbar-menu{background-image:linear-gradient(141deg,#2c5a7e 0,#469 71%,#465fb1 100%)}}.hero.is-info{background-color:#209cee;color:#fff}.hero.is-info a:not(.button):not(.dropdown-item):not(.tag),.hero.is-info strong{color:inherit}.hero.is-info .title{color:#fff}.hero.is-info .subtitle{color:rgba(255,255,255,.9)}.hero.is-info .subtitle a:not(.button),.hero.is-info .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-info .navbar-menu{background-color:#209cee}}.hero.is-info .navbar-item,.hero.is-info .navbar-link{color:rgba(255,255,255,.7)}.hero.is-info .navbar-link.is-active,.hero.is-info .navbar-link:hover,.hero.is-info a.navbar-item.is-active,.hero.is-info a.navbar-item:hover{background-color:#118fe4;color:#fff}.hero.is-info .tabs a{color:#fff;opacity:.9}.hero.is-info .tabs a:hover{opacity:1}.hero.is-info .tabs li.is-active a{opacity:1}.hero.is-info .tabs.is-boxed a,.hero.is-info .tabs.is-toggle a{color:#fff}.hero.is-info .tabs.is-boxed a:hover,.hero.is-info .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-info .tabs.is-boxed li.is-active a,.hero.is-info .tabs.is-boxed li.is-active a:hover,.hero.is-info .tabs.is-toggle li.is-active a,.hero.is-info .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#209cee}.hero.is-info.is-bold{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}@media screen and (max-width:768px){.hero.is-info.is-bold .navbar-menu{background-image:linear-gradient(141deg,#04a6d7 0,#209cee 71%,#3287f5 100%)}}.hero.is-success{background-color:#23d160;color:#fff}.hero.is-success a:not(.button):not(.dropdown-item):not(.tag),.hero.is-success strong{color:inherit}.hero.is-success .title{color:#fff}.hero.is-success .subtitle{color:rgba(255,255,255,.9)}.hero.is-success .subtitle a:not(.button),.hero.is-success .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-success .navbar-menu{background-color:#23d160}}.hero.is-success .navbar-item,.hero.is-success .navbar-link{color:rgba(255,255,255,.7)}.hero.is-success .navbar-link.is-active,.hero.is-success .navbar-link:hover,.hero.is-success a.navbar-item.is-active,.hero.is-success a.navbar-item:hover{background-color:#20bc56;color:#fff}.hero.is-success .tabs a{color:#fff;opacity:.9}.hero.is-success .tabs a:hover{opacity:1}.hero.is-success .tabs li.is-active a{opacity:1}.hero.is-success .tabs.is-boxed a,.hero.is-success .tabs.is-toggle a{color:#fff}.hero.is-success .tabs.is-boxed a:hover,.hero.is-success .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-success .tabs.is-boxed li.is-active a,.hero.is-success .tabs.is-boxed li.is-active a:hover,.hero.is-success .tabs.is-toggle li.is-active a,.hero.is-success .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#23d160}.hero.is-success.is-bold{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}@media screen and (max-width:768px){.hero.is-success.is-bold .navbar-menu{background-image:linear-gradient(141deg,#12af2f 0,#23d160 71%,#2ce28a 100%)}}.hero.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.hero.is-warning a:not(.button):not(.dropdown-item):not(.tag),.hero.is-warning strong{color:inherit}.hero.is-warning .title{color:rgba(0,0,0,.7)}.hero.is-warning .subtitle{color:rgba(0,0,0,.9)}.hero.is-warning .subtitle a:not(.button),.hero.is-warning .subtitle strong{color:rgba(0,0,0,.7)}@media screen and (max-width:-1px){.hero.is-warning .navbar-menu{background-color:#ffdd57}}.hero.is-warning .navbar-item,.hero.is-warning .navbar-link{color:rgba(0,0,0,.7)}.hero.is-warning .navbar-link.is-active,.hero.is-warning .navbar-link:hover,.hero.is-warning a.navbar-item.is-active,.hero.is-warning a.navbar-item:hover{background-color:#ffd83d;color:rgba(0,0,0,.7)}.hero.is-warning .tabs a{color:rgba(0,0,0,.7);opacity:.9}.hero.is-warning .tabs a:hover{opacity:1}.hero.is-warning .tabs li.is-active a{opacity:1}.hero.is-warning .tabs.is-boxed a,.hero.is-warning .tabs.is-toggle a{color:rgba(0,0,0,.7)}.hero.is-warning .tabs.is-boxed a:hover,.hero.is-warning .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-warning .tabs.is-boxed li.is-active a,.hero.is-warning .tabs.is-boxed li.is-active a:hover,.hero.is-warning .tabs.is-toggle li.is-active a,.hero.is-warning .tabs.is-toggle li.is-active a:hover{background-color:rgba(0,0,0,.7);border-color:rgba(0,0,0,.7);color:#ffdd57}.hero.is-warning.is-bold{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}@media screen and (max-width:768px){.hero.is-warning.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ffaf24 0,#ffdd57 71%,#fffa70 100%)}}.hero.is-danger{background-color:#ff3860;color:#fff}.hero.is-danger a:not(.button):not(.dropdown-item):not(.tag),.hero.is-danger strong{color:inherit}.hero.is-danger .title{color:#fff}.hero.is-danger .subtitle{color:rgba(255,255,255,.9)}.hero.is-danger .subtitle a:not(.button),.hero.is-danger .subtitle strong{color:#fff}@media screen and (max-width:-1px){.hero.is-danger .navbar-menu{background-color:#ff3860}}.hero.is-danger .navbar-item,.hero.is-danger .navbar-link{color:rgba(255,255,255,.7)}.hero.is-danger .navbar-link.is-active,.hero.is-danger .navbar-link:hover,.hero.is-danger a.navbar-item.is-active,.hero.is-danger a.navbar-item:hover{background-color:#ff1f4b;color:#fff}.hero.is-danger .tabs a{color:#fff;opacity:.9}.hero.is-danger .tabs a:hover{opacity:1}.hero.is-danger .tabs li.is-active a{opacity:1}.hero.is-danger .tabs.is-boxed a,.hero.is-danger .tabs.is-toggle a{color:#fff}.hero.is-danger .tabs.is-boxed a:hover,.hero.is-danger .tabs.is-toggle a:hover{background-color:rgba(10,10,10,.1)}.hero.is-danger .tabs.is-boxed li.is-active a,.hero.is-danger .tabs.is-boxed li.is-active a:hover,.hero.is-danger .tabs.is-toggle li.is-active a,.hero.is-danger .tabs.is-toggle li.is-active a:hover{background-color:#fff;border-color:#fff;color:#ff3860}.hero.is-danger.is-bold{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}@media screen and (max-width:768px){.hero.is-danger.is-bold .navbar-menu{background-image:linear-gradient(141deg,#ff0561 0,#ff3860 71%,#ff5257 100%)}}.hero.is-small .hero-body{padding-bottom:1.5rem;padding-top:1.5rem}@media screen and (min-width:769px),print{.hero.is-medium .hero-body{padding-bottom:9rem;padding-top:9rem}}@media screen and (min-width:769px),print{.hero.is-large .hero-body{padding-bottom:18rem;padding-top:18rem}}.hero.is-fullheight .hero-body,.hero.is-fullheight-with-navbar .hero-body,.hero.is-halfheight .hero-body{align-items:center;display:flex}.hero.is-fullheight .hero-body>.container,.hero.is-fullheight-with-navbar .hero-body>.container,.hero.is-halfheight .hero-body>.container{flex-grow:1;flex-shrink:1}.hero.is-halfheight{min-height:50vh}.hero.is-fullheight{min-height:100vh}.hero-video{overflow:hidden}.hero-video video{left:50%;min-height:100%;min-width:100%;position:absolute;top:50%;transform:translate3d(-50%,-50%,0)}.hero-video.is-transparent{opacity:.3}@media screen and (max-width:768px){.hero-video{display:none}}.hero-buttons{margin-top:1.5rem}@media screen and (max-width:768px){.hero-buttons .button{display:flex}.hero-buttons .button:not(:last-child){margin-bottom:.75rem}}@media screen and (min-width:769px),print{.hero-buttons{display:flex;justify-content:center}.hero-buttons .button:not(:last-child){margin-right:1.5rem}}.hero-foot,.hero-head{flex-grow:0;flex-shrink:0}.hero-body{flex-grow:1;flex-shrink:0;padding:3rem 1.5rem}.section{padding:3rem 1.5rem}@media screen and (min-width:0px){.section.is-medium{padding:9rem 1.5rem}.section.is-large{padding:18rem 1.5rem}}.footer{background-color:#fafafa;padding:3rem 1.5rem 6rem}.command-block{background-color:#f5f5f5;border-radius:4px;cursor:pointer;padding:1.25rem 2.5rem 1.25rem 1.5rem;position:relative}.command-block:not(:last-child){margin-bottom:1.5rem}.command-block a:not(.block){color:currentColor;text-decoration:underline}.command-block strong{color:currentColor}.command-block code,.command-block pre{background:#fff}.command-block pre code{background:0 0}.command-block>.delete{position:absolute;right:.5rem;top:.5rem}.command-block .content,.command-block .subtitle,.command-block .title{color:currentColor}.command-block.is-hovered,.command-block:hover{background-color:#eee}.command-block.is-white{background-color:#fff;color:#0a0a0a}.command-block.is-white.is-hovered,.command-block.is-white:hover{background-color:#f9f9f9;border-color:transparent;color:#0a0a0a}.command-block.is-black{background-color:#0a0a0a;color:#fff}.command-block.is-black.is-hovered,.command-block.is-black:hover{background-color:#040404;border-color:transparent;color:#fff}.command-block.is-light{background-color:#f5f5f5;color:#363636}.command-block.is-light.is-hovered,.command-block.is-light:hover{background-color:#eee;border-color:transparent;color:#363636}.command-block.is-dark{background-color:#363636;color:#f5f5f5}.command-block.is-dark.is-hovered,.command-block.is-dark:hover{background-color:#2f2f2f;border-color:transparent;color:#f5f5f5}.command-block.is-primary{background-color:#469;color:#fff}.command-block.is-primary.is-hovered,.command-block.is-primary:hover{background-color:#406090;border-color:transparent;color:#fff}.command-block.is-link{background-color:#469;color:#fff}.command-block.is-link.is-hovered,.command-block.is-link:hover{background-color:#406090;border-color:transparent;color:#fff}.command-block.is-info{background-color:#209cee;color:#fff}.command-block.is-info.is-hovered,.command-block.is-info:hover{background-color:#1496ed;border-color:transparent;color:#fff}.command-block.is-success{background-color:#23d160;color:#fff}.command-block.is-success.is-hovered,.command-block.is-success:hover{background-color:#22c65b;border-color:transparent;color:#fff}.command-block.is-warning{background-color:#ffdd57;color:rgba(0,0,0,.7)}.command-block.is-warning.is-hovered,.command-block.is-warning:hover{background-color:#ffdb4a;border-color:transparent;color:rgba(0,0,0,.7)}.command-block.is-danger{background-color:#ff3860;color:#fff}.command-block.is-danger.is-hovered,.command-block.is-danger:hover{background-color:#ff2b56;border-color:transparent;color:#fff}zeal-0.7.1/src/app/resources/browser/assets/webfonts/000077500000000000000000000000001462517734600226665ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/browser/assets/webfonts/fa-brands-400.woff2000066400000000000000000002255041462517734600260010ustar00rootroot00000000000000wOF2+D x*K ?FFTM`j \|6$t | <[qE6 ^tı\^wg3B/bM8flG/5!Ɍ"R! oLA.rVM[J*Okl[+X}z>܌#hz !yf]Y^`+)@w, 6B-Sve1^fnJ+3J}߅5=Ԅ}|_ڇK>eXw}c)?@9⟿-gٻ-1&plױ)ýD6t,i{$)- œS14El贈OTa̍[/`IFŒY%@FR`Db`UwFW W5*}sLL;<6QW5yqpw-G eFOŨMћ ki/urkߵ՝d'9IvuW^x :;F 5BrԬ?͐3CD_టg!P@14N|tzOvr` p\^ qO_\]g_otfy _tvk4b+ NE4}7w yAŽ4櫥ջ,8qm76&}hN+fX ׉cs{gitnЀI!r<3^ly սA_qOGuW-PH jSk?4,i<; +:~U Eٛk}US %sL  3'{BAKm(hDq,1K}UbY @6BZf} 1{ʛJjC*ؙ@\Ve|3Y=%Ri{wus~=>W&H*À0v0 *8R ̾W@="6Pw(]; pШF@ lw$祶|-~/*l#j+KV*2Lu2gIGPE }9+B=RrZS^I)(;'L +\SJK)m+}X2`!tesJV{= qDO!jjrZUj)O/zR\e_ѥܢ(!HRJ#G([d/Ig/rIiF$hOXMr]GZ)Hy&->%6 ݋oSmiJ}3,, LXkEĆt4Ώ_`sl0J&2@$~,f/1e6k$KN`~:KYvxL'֊]U->d% q @`k@k<EV [(=/jYܟ0s=yD`[nǢWJSZuVuhDKB_G5K5*V5MAZ-VJȠft3k&9Y=7G&<̘]R^LԼﺪZQkCuT]Z^~O{F3ty#>3[~#dNJnrg5yvFMLҠFڵ@HeR˚FǵGu^WBo!C+uFLwNT( ktP,1v+qbh?FXJֱ' z'^][vx/ +g{㰼r=.J:Ꚗn[m&묵ژ,4dPN34*V@ 3=_Yi|#oʥV }طe"Obo87וx)ѡM#m[[_WTVI!؃M^:*ʢeM3s9L9ePI]ޥRT/.MHU_~&?w X}%Ƅ(` x5V4gNK4]v|#[dGR+( HGhbf ] u&v$̨1x8ܣ+VEa(JpWt*ŨԸ=2TDy6Mn-qj0~ؚ (vlcakV8yCeDa{?bxe QVmNK)@mQ!dDa 1h-;T&XfR jjI_0feN76OE'4%3'=jBUU$gP1 7P:+3a6_fٝTD|}b_x^D^]23lN$fB3Xy}H^ |E{)FZjg.'&rbeMْHѹxn5@q X6lh$y~>enLKG$DZ2dI؜:`NJy SG瓅h{'M75уe.hߒ-ŻF}FhVY0J$޿d!`u}$4~Hǝ=wM\Vlcb2/E!„J d}1N,!|:d*T,ihy7iY:3ERGoŰTaV.Qj;E j~GmR*Gk,CY$eG0h6;=Z%sœ|"Φ@]n?/rn-2|.3%].]6p]I.d}9kDꌛU"OH\,7Gs`n < t=DK&*ɈM/D k]4t~H&3Ukj,g/DUgƦ?ƎS4H~?Q ҋR>Nzg3wÜAbY߶#ٮ^%mHO9+}Q : .:.7|n0HWc*TIj]Y?'* z6 miƒGM :bV %ab Vz&1 ;ˁoQHlϝR\ O'D83P/̝wJCY{ ZV(c2'/Lë1em@6]8>2ߪZE"hU"l@nQS.ذerմeX, G^v<DjƳX["N mpdv2tk&>Jې50o)d|Kr[ef;%OSQt9/% S"ŶoK ¾: h; adW.LU2]g1n\I֟+˴W;ݓΤIA_:ZtmeoO&OIXfyum͖~9thT=gtt8JiT$@nKDB^FM'h4kX0sL%$\-)OҚE_8˸㏧>QҷmL}'y:,i=쌩 )g-1эCr,B}?~ 1ъQ%ԤH0K_U13Z+M߃ W泅qAd93jX~BdkѭAGB!>@I3,x̢MTF J2BeaQ$qƤKpmy% eIn;n(M!z ,e4yZ!PdN:b)w`#a]\ljgl\JMwTLJƙj͉!1?b9{B*R:V ~ \>^n^8utZn緫>Qw^*YX_xX [L.g˦҆=#z8,zxk<('"(1|{}嬔7]>W[e&~OΝƄw7=C|qMfgRs_wi@L׀TLE>{jX1a ]U*mm'oU7űsXzK.M &O)lԦ-$Nj`WZ߾B:;Wd?XjkT?3]5eZ5gBZk WhOzw=՚z B.dG^hͼSanFh7ޕZ u(?r!N2GBa0 M7u_4_F Ƚұkf" w6:*^3JJ ddHs4h  wKc[~ 52Ï1a|/]y"qOo]&Q7LAŭ?rGUvY2I" "(#(5y;M7^I߳Cz1+R|O{ 0:şt#F#9>L:+ tH@kRHk.DiN\oDa#DߞC$y;U[@,LΗxw8 FN$;F47" d"%%]*3E_R4y-0,4JĢ 1D%(Ԫ2 c,6?Z3=V[UKËCHA6B`JUTiqTZZ1D,Si @)G$ɾ hJ8*⭧8 (ݝ3EEa i 3 &,ZJsQmpQLQ]4ĵDZ,O w,"#<_sy#|"&Wb1LHg3^<$#6v,#˨/X1 KRs%Jjm7֓ojl"} " ѽO/tuQt^钻 ~| S4i`ua7= @0u/4=<2aтBozJmcɭC)] : []+UZyRz7eٶQE-dcV ͺ=z.Jx`DDoaFK} 2SK k^jƷj`S po\,_9D#=DjZq81}vִIf:?QdJ|N^Btrv<`\_"i$1裉:OqBd>}JSsʻ/g&0I)(D;!sd {69hUŶ=8$p`u)GswÒ5%=tlj?B@dtT+H8ڱmwOz"]{l@Y{MAFs{Nka/k8PmGZ 8AĚZҢF$,^i~$s١rIsӒ.B޴Y>w1Ix0VԴa7y"YB>?T,1Ýsv(d# lV^iQ8T4k}QZ/:b66m}Ğ[gUXwQCG]ANV0חtv[ ZR2XJX@*{NyXsl޶U>עTA ֎)G^ۗ\ 958,`<4Os wG,k?\n-zs%C4<1̳[rT pAPvmك"ᥤgTPV^?xc]!qGET9p?ѝ^|l {bcr0**٦?dg.ǵEZn^Q7j2-(RHEg @#9]}[Df;qzT˔&c}fB7۟{\t9ό\/j:֒ Fc=?樵ieTI YY3ĩ4qg+Xp>ƙhK9oi "Ov @dpSK L@ C&Jha 8Al"X ?fCo=["IR)sΗ)%N] ̎l:VTr[=~A48boL/"p`9 d*McRfԒikⴞ&h:[y|]e$M;~ktyh*mM@:-@%փ&J32 QABV @) `C&: kã_h:hV*-ܓZ B%֩\+h^K3j# Ue 53G$99<,&b~`s)ygPIW0!0G0N~iEa*SȊB%(nC hٷL.(z*{U 2ޡt O+Zf2lxZ_ !NUQuZ)Əxĥ܈R\`՜sX2ߧSWЋ gQ^N-Evڏδ^~o5pe9b# 4R sl+&Q34Υvc:1fٚhda1dc3ey(U]’jvɽҝ[ud*XrNK83c|ZZ[/vV| nk?ӄ U]﵌tjay{[\%uCMNlU{"IUgW t%LFAʏw" vv76[ >s Ȳ+EaPHRm‡+uSʼ I$CPE$ #ɬy v,AlQ;AsAmgHHq,E_*T ,%%cTzsXHGH+p"ՊVNj&I2ap*='J; ,Kٌ·R"pz*ׁDBN1UӻO?nd )@0΍@/LotڏWb :X}4ށ(1] JU9( 2<1YjBoki5Iz 'M3HPƘ*3I %1$t'ZS49a S碩''@TZ?ܤ#[K"}4.ӼĤWo:Eɰ_G aChΗCxvf~OBty *t.q\ʸ@1FƺḋqW4;Q3v#F6\+zބtHiqi+ MCV2K- s7m/1pՅ j?<0tZΠ?}4hvCu[uۂHࢫ hObEKr7/ ̝ٵo[kKӣbBTǒNǷbF~K6ZDQv's?ee`s&c *-u RV<܁iS iV⣘9HM/eT/?b߿Eq;g=Pqt+eO m-E\4Z}GE۹5yTzdf˼KJq=Uɱ+yQ60l ϝ؋FIB%WS(KĻ!Pi)ZK[^v? L˂r"Vfrz C0O'N;rWA,>٩uGvPsK&T/T\֓P)X|/ Ha QpR9硫Se6,oY']/wu0{14L d=@aďwh`gAXU$8)8jM%Kj&(b&AXyr>j)244 Hyި>^#H{LncœCee6 WjnTsk ׇ|PGs ^7޺cɾ;`5[f?f=Txan_\O+MgyT~[,f >L=ug Wk)CFRLS LaIѸM+F$ |'[p$fpK^6켊ipFJw}4gVk&A$=i|`RvoObESSxO N~NlsO5yS>E,1_DRHw:??~t"V'JN}&Rhꤷ碝$|.y VdT[}_$,sVD}{a${].(&mMFRᬖx"Y] Y6&萝ٞ炴*_ %ۼ|1fYb]l&R|¯KNe4A=XXL&O@ld~i̟чeq?ײy)?M#^AZq WlF_4AN}9x59n:+>C}>* RυiWKwD?@hX7_=ť+ 4@;{zqhcC@.|M-;fEʡBو/6m8}8RRu]ֈ@Ϯ'T/sƚ>ցPٌIY.ԏU~? 0ue;!8.1wՈg<4o#4֍8O9aKe꛽=<c{*99`g^2!a}Zk.~K ϛ>6b'+KQϏN!ۤw7̐c{\7R#;@MczE‡q{e$1jOwvmUUY5併Eĵkm$OgOҶw9j!ѿߛ#ש1oQ zs&޽ d|=@spw+Ans?ԘaaFt]VCoVw^τnAO MCipx!'9ƻ^2okzU+m4;C=<ʲY.JnnI+$7ZFOּ$14_Iμ&`4Y%aqǼe2[:&4AMyĜ^aXxdW0SQMIGb:Iz+HrIJ3j±s  dZ ; .FJXFYLZL,m,`bή%h!P9[,V D.t ,Ib,ǍB}l)-?@BGpK'Iz$A-ǟwz)>J9?j7URJskt0[R1xkZ?K8B."2ٰSzVxmލlfG +(an x%/O`'WPAa17s%' ҋ=ZɵiIxG?Ye3E号!ewD!mǴn}8R!! Q[.FTl)Sr{N׶D}Sڴuu8}g=af'_(<ڢq *O?;6Q,a q(!U [ ǫqװH RLÚ:ϣ,wڮLW}΄ebHBm}#Rb!g_@DN6Sda'v:%&{IE%]wuƕ>rqp@*c=ZߙIN$Uj,}m0SQ|_|+I֋d&~ SÄLۭaNֆhMFlq7-Bkʿ{~I USZb0ZT0@>r Ϥ\#JJ!,ST"IrcY&gI,Z3@ '{h>б番}TFAlM7DÒqS Vb#Jw|Oq(RuƢOy=aT|S*t1NU Խt?W.]DƸxr{d" ]Ήߐ&S,P!- jdLr˦; `+I!fFKSuN,g~qF ~^?|q:ڔI0c)?Ky3Y:V7 oLg~ERI7Ӝl#Pqc!F eych54$j(KvE{:O8( 9K.H[)Þ]w<OfmCFve)Ixػ+KJ<2aؗ*5j|cj~ R#r3)x|Gy!C6˶mYc=d,X)ȭ-oDFp.grC1Bٳ#3\*.'٥IGՓx@UB&u9tz_8 ?|ńQw3"o&!K_$g^>@t!+-> *sW|enjF{.;yhs =kECca_fShR*:a_3\hC7 [^8$z>Ch,W/sђ-saCfE2cjG|ک[IU;ECқňG#ߙ\p 9|wpli+mVypnHSAh1DH2Up.Q6Jʄ64_< J & EփSX1s h ܶ8R&5S#HW4ysFN[l(mM_O J&]qKR1-ށ2S-ݫ4BeD9q5,vr4VS8ˉC ZʋXF cYݕoJcB v08<*0O7VV|BVWaX}mW]-}+woŘgŃ6&wb&[l4DwxXJ郺G3!WT>֎T%70nW/Q⑊r(CTs 98v8Rʂ>w V:Doh%0=,EֆF,{{T[,yhת͝t(!Èv&I 0%Л};V 1LJ:?Ֆ EJɚF%aޯ5 ޏaWYHF7Yra~37Oi^DBQhcYt[ mYB`a;ɠB){]2HP%/Ϧ2Pћs "*x/0Lƍ(Ȍ4[+-`a%RڵE DPQ 9j pV 5t9 %P'NECD0J2u6oa{@ciރ($ox}rI`7ٮX 8 6JnymS/lT<5ʯ _,y59о{;~bqn004ku CbW IKU~*&!* OySeIOmA;:jNziGW>sDN-s<<?OO,U}bD:f- 9F^!dā@,'7giб;X-|?O[$DTس ̻Z~xOg\xg]%&^]&1cGкtb+pbœ;] p`fDO!8 lki4@&-uItp257 sl? :X5qƈ ,}j^KicAIDlW6X][~a@z$+L2ŜBqnӨ?lo"@ m[\kt!JA,%v_~lP0%`eA+ tK ) D'#"ت Ln,># /02LUVtQI$&"$WԳq@asHv6, (ʇp_-vRJƞwnWd_g#i&ȠrJQM2I'7ߺdiTcr."KrS~yn`[ v۵\`}(u>AT˳=q9lj2x31hL R!T2G)X&Б yf UC+UZ\iVҴer_ozlYBA*(9sGH%gTOZR>zQdkZM+IYTj$cD> 0Mw-ZAbZ[}]M:FK $FlGd"d-Hk~3i@Aɲ'-!Y#Hye; @ ')À)Է$x}RriQY@en\SÔҮRI,8xuV ÁfxLbAE. n*H@D8LjT'@qmЛ0j!tZUf7VL2/q zwV;?эD\d=ĨA.!eͅe|0ܬѪgN%^& NN:]&|ҐM~!@x$hTZD-1CIɜIDt\ ˸ʪLs:ux)X=isƴrOy۪H9نmQiw9s2TdϹ􍸹'-Dk  ƊU2oP݅rEc| rGU6뫩857JJr-*zIo M$|U~x%#zJڍ"f^^  -)}>hw,;-?>pm2 ޝ.,aWAĿJ GO=::8#՗$:E_ ̙?K*z3tEC6g:?(~/yCb&"e6TcZo6FhiNSoH POQi-e"! +~]F)0JhFLD"mT@Kp@#.03 СnjE{A{6c3CӢRa4Za} {Poݷ[`}Щos]`1t'h3SHC7iZuV v}qLwoi!2q@c޺"YӞ̕&5!kz|$!.^3 ˝tYJFF% D7ũ0:+&pp%EJ9ͷ3{KEnZ"lY9LNzgN J\yy6I!737︧Shhk}%82ynLǃ^9~odd|U Rbz.Or-$qcJb^$x[:w)BŶ. U [,6-)'J%\2/sA V8Es.i{F93ic4%e+#h`R+H-z%ur 0"7!xJ5pqҦHֱ$͕SrٍƦˑ Tr[PQPi] 7Utu}ٮ_brq/ѭLn9t` [%$!Q{:3[bG]\:T[MC8.S7=5 <C7Pr/ FLyHiJ<{;[,PħY_fBeV Zx=[뮓&Q2lff܈v%,jA,mZ|o| EzΣǮRW$q)V@>nΝkg7\cˤO.5skkH1ɯP8M:|;Z8Hi-'3V5lpg)Lu\s>~_գ}]S:ca&?L:-MzO[?c͙1wqھ:Hߐ>1BWܺ7P}n:wC{/,.VaɅ5flb:?αynA9ac,ayx$~zQi<(Sl*2 ^tIq 8Tru=ԪEF4Ĉ\}x^?ȘF"?/wF Z!ەYtҌufZ9%u鱍H"gv6vG5dN$72c@bCaZO0. *GP,D 4tL2ą.Ώ5iIꕳwEa<8tjƧTs {5fFq=wn}6μ;,Uw?N]:&AMi[d':&VZ>iJiQ)I0!0&JRA$ZZ1(;Hp;eH%N^a=m>X#H?*F%M̀ݾl098ҡlQ,d4繦4Q:^vb(b:MhNq:I˜:wt ;]ڻ!OqeXaSʫS̓/?y~l} bQ"wzKT4hJ!BքikZ0k.Vܛ0Z'nNЁPbiPBM:;ˆI~Q&uz_،jlyF$0#V9z1:%Ӗ RTZ^>^K0>Cx͟ T"IJ>eFrps5y*,/yjڮ L߅cWȩ3GкM\:q1s`2(^Sf ř me|-=E,ggBaߝ&5~#UH|+ $Ǯqx.z޴exWň5Ȉ}-Q^.5k*o%BkMwd"ZzB`U{wE8h`]۹ii!*DqdCbxUjvhCCV]mW_1ezn@(#g meXY?.<{ȑfk֏F93 TpjQS[rJ))%yj{HM(ֺͱm`!EtI j x;%|@H%zfg+gO '5q]3?N9nǭ#6SlFqu%wȽs[_Jm"27cVxDŽ'`} " yP&AImnP?|S`JG'^9|5I^p$hKx£c3x~^*96eP /퇰8쁤:d]So7#l"r^R'⇽n(y\F~4ˉOgBxd*&d<$-lbnݏ0'6x*hް2n.v?kɈX*Mr4s慓YRbYyp&2UwdQr xy!E7If!NuK?Vif 'S0@w, if[yv٫w+WX#򢍏ɐ"aC ~4f_j.MlϐGZZ|n!D*NBZ1Z'ƬOp]EH߮X/7یXwTitPq./>\@[P6/eXb2?DDz!cTBq "| Gђު n`@2x$Gᬐ4MVg8MXLYC@-t@MX"V^ʋd^_6c,&wyݎt$rɼV6C-Є_}ϊIk\̤57Ys/&?(s{UPW\7NHۇ [tƋGzblbV|r|Vv<OU$,|k?*ǩS*RAg.5Gst$zKh?q ,˄GtVOV2X^5+%$60+Mupت\v|5BWr/Q~AuʒRQ V?{kd찻;4]Nw:W?G2h=v资Czel8y-dL },Y ]ta[XbI 'Ь+֍U9F ˝Vg7zXq<W)r#O OqSJHG*\R뭗h̴ڗ EuHee$= Qi7# Sgxy2ˏya tއ^Zlɗ9dS2  %*{}wYݿnwع *YB{';\Ob)݄` 8վ32[~`ky^?mДYlb7C|FTsXF+Fӆ#|6}@rM^1L"[Y?뵽r-U0{LxvKwhϷFNߓ B,Q+WQq ]X;$d OTxdy.X4)hr| EbC2ӭ=뜨bAV,$\V W,_p#F0mw4/0kM 脉Whަ¦i`pսg˫0 Ig@l֨yy=1fĿY<">l#-Ңi{֋WsI*m|#/^|hbrpn.!ެ|=~/19E-9~  ]swNg73g2a ImH%A_+f!'AKVVǶ}93ki1i8.]2ؐż <<ɰ[>, nfY }M%=P.ӃNӊ(6v~'@6^ |[L#sNLO/aöx%1sx}']ruS'/_cn;O=xYO>z݉;( lgܭƚ2Bg$v ݊A9R>)C)cN% ] 5 0YDr7Pq0Qh36TXn)Gt8 lPd Դ E$4 (e.)w8P ('vё,|%4gwx;|Rrx B< И*H=|ˈ""/;7P>d-2C,KFi\:\kUBD+`i im.zSja4 uK<<#T )MS@L`"WYEH,b-1P6̆=J%xәḁPtŹuI| WvG85d{s{Ctfʇڝ^OȃD.gI+-ZslgRp+MmB@^!Fٱ{Uo7's~stH>:6kF.8j{g$tty) yAB:LY1Wh6ҹT6ge gQg,mFzK? WFOl~ fSU!eM[ #Eurex*1FO艠1gCA8CZz;okS2/SaNl2w6z;âvs(;CTІYf2 (7Jc'c !S3p{DڔAzJPNrr+y:d!D4[ڷy՟BݧWuUD/szAsd0 ջ׮!1XVELzFBֿ\aϩ6t Fa%qPSMbcu h>R mapNӗ3{j_D|+<8v cW;w=FNncit EI]72`pdрq J־ӂj\}qyͻYVb6hť}uKDhK4739 |`_9&|KIΤ(؂(p~νLc^^ Z=] ztopϵ"/|8ý7/L|SDVИܞt{w 2jj9pSPdW/C;{(6zeffj3(͍T uߵB&L:kF+{Jz_M7 DeT6[#?^Ynh{(oU jzT̴MP"h@CtfSAӡ۷dpU~H !(4Euz%*XJ3 7 ȉ( p;u]]s+J^k.ԧ2MLG^ye}m >άNk> ipK! % O,lO@?7gYJ'U =npAR>ʦ6%Y,]g%U^+x!cJqun}s J?HX'!]$h҉h#bw/ HJT1Yp$I菌0UM%t!aѯNOQn@ڞUCc%"y$3܇9 5> #⊺\⦖@7=˖dYNkL8~$ (u+U8- XΏDwY}TEm)Ό%O;qD`R9YOCܧ*et Qf4ظ,3-CjK\X qX9:- f&G"S%DO& ,z0Xd5npCr},w7TAT@K>蕀q NX΄ S#HϞ@ہX*TNaGsqfiٕHk WQX- ζb5.@dC` 3BYV%9 ?s q*bS (]9AdB D"ݫ2rRy5RćV‰8R2X6ȳX M;ט(Rj}%·1bș; rd{$ڗ :J`ЂHϑuǟ-nl/O3JD./3' W]9z[aeC+ %!X92%#.սHX+@HټcJDS <+ ?F9J*X2 '$X#!U҅QLMU{O|lQ 1lrXi).<JfL`**iRw0'r ä鑛vsf܋Z֣2Z%* @Bk#>!2e S|fYZL+;=N7&B*>@hD M t_B"L9 vW)/o 27aVb~_Ep%Z/eW1ikQwk;VZKz+QYf@P.K=37sʙ8SI3Yb՘"F.rAOtgAA+[Q|*5`EvL!$|0>3b:FrO1 !eijqhi!5ƣޡUhb8!Ehդ|%J;En˳젉9Q*>AaFrhblOCf̡Lº\ jDk{$sv2^2DgnZ$=q&a$MT:yWKK~m3//D$8+=9JG*F\ww:7Vozv@,t /Y93;]q~=炙8RM>m%<9_&Tq.?WP/ +p⓼,85b`HZ|+{H"Mb+~TsCiU;66fG:78t4.Ke*oL @ &飬5'eYs5Q5ݮnK+[(\]#U)G4|T3<̓z<_k¿.o"[ }%) l5.,8 VK=#M{4|ǘ%G(_惿Фk;ۛɲAvauΊ#Kh"&ˡrXDdT;mC5ܺIPbPRq yH2M7hD\ʡ{,\$Jb@+&c{.z v6ºL]k|cmAkQS4ߩ3zYRBR{<8xدE]1T-ؽX頓֊;&dU%ϞM疽!NIkT:CM*vzpa3nw*9mefe51nKzRO"lr8 <BAPv$ nt}8]e&{Ж҈XE\]=H>xކl63D8D啭@UYxNς|@l[6"p:gQF#CDO^UhΐJxT5Qz25F=Q3iTK^ ?.S,W \i0)Xau+pR=ٓnkJ :~J:{Yf~X[Zz!AzǠuHa-.WQ 4{`lչy_F(,jn@.v$h D]֮g?GnS`sՋ3TfHג3M޴ˡa2G4ICheQC` a:Z {MJ7"*i( 쒷+y qE&#S>ND:1fCXX!) bri+爀9S 7G-( 6Q 3d GIq*RkTEPuu$Jz7@^QfꉁaȤzm a8IRkV򏖼)&95\;5P ݞ+w>"n0k!?9͘E;O#Ʃqݴk׻] 0̗}fz& Ui+ **n.71zY[lJbc"9X ?6j UxX!]že8agxX%cf۸<^}Pً:&QMNjd*Z C{[=e1)n#KZb\{GDiAؘeBOF^k`]dvSێyo;Pv6b?8jt=vBFx؊ 5ĨZyMeDY<9i,C~^FC;Y3DbI tYѐ{~@lELrNgi N/{PTz{!d$n͗JŅdQ`#[̏9Cᗦ vj-OUCH<qGYsr$R5^$DY7$pgv!\%`1- ؆ oגcDd.Zoֿ )oh0ܰeMnH%$]AL[Ȱ%/.sxsOBEdmS#)vK3?g- NdtZ$NQzc`SO!׻=o+}k]aq$YM$B&{L5K;)$s!VBbM)FRSTj} 0B*8# őI>)㤔 X᭺i\XI{˜ƀO6]>ø~/|?8'BlM!v,(D X+Gn`WQ{;?N90u֔ͩF1R!1%DvOAo^xķ=;Z,f :TdC\s~Aa c8m⾹.)TqĽTB4f M*JDH>A"{Ɣѣ"ST!UlRr@j&"V.v V*ܗ4Lzww-N2CP@PlDƅ1UDCLI u!IzULRR(Z ?cD.n]-Ջӳl)A0f*QuQ ɂj\tE8}GAFu]X]$ׅ{c*4h.wX?Ӕ9HëJ$Ù.iN=T28k/ZD'I6-W瓳[ηoS5r$vG6"A"&Q{s$\ٞe&}#P؃)D'W37iid\BcU34LES5Fb°z&6,sr ןFQc7Kׄ70= )<:Ot/wl9j o:!J{֯t\\C}|e¯ @O|i1`H\^30yK֥?>G M:[0pR6K'FθF=r t!ppnF̖ DC¹?'rA{Z1{p47:DVY;ɼ=zJ$s/PڞAͱ&w zP"3<`#1 791 /uBn!;wsXدu`ˢ;v}yoWJ;@/} y)U$Ҡ6#2Qn}{3I4#ݰqd[k֬FQt693ݝf  U$|~ʕga.d`'bQF$2HS7{reoWIT Ji Ǝ/`/QdZ6ͫVsfYnѢRI3+>E#t( U=q l *YP h,A`,JJrhAz qx0,;&D{=1P$_RUto?7E1&TQVmd1FDJV+f;?x<0(miKl7YFC.@Sq||!Tu@]LʞŒuZNe,۷$|;?8FD~.55:"=RHĹw-%W_\HWX$zRj$[ĥCy&Z'ۜZН)2^b1q"ܛsO1rU!bfC&雕J tw9chCN#%|Ntba;!:a0SUP33!Rf _%+eD/5d`a2|}mH:|o0*Z B1|zp丣WRT[{?6_*O 0{4ۏd?&TO3oE"l&g6)9XcNP`1'u:胐-e =h[YK^ u;dW>Q=*97g$St b'/ ~RzxdÓtՄYntu4@z{k7*IuAw7 k(r t8ݰcrr=VovCg]fDgN'#hpmrl:G\`Jh"*:מ{pr턺`Mf0hO7ڢLn. P؁"C|Yxݲ .._afby*:ù!2EtF>nD30Jruo;slǎa[F&VHШ]DE4k:“BYVWa&j+`0T+!X ?a<@ z>BRGoUo jv'!A9&*4GOhEWy7׿֭&%(("7)yZL֪zgw"%+n"=__xFqͩa@>Wa#D/h~_~yߚh/ 1}v{~eW\1p( is/Dcۗ$[G7ٍ$o/)HEgKŹGw ļ%~א !ë uI  T' WpGvn6Qw #VWnOw:9-xMfz-rMRFi(b.V+@iT55*Mb{3>>"'%U5:8:@yU yg"bӟ/\{-1ޢp]q6r5x̘NPlcT%BQnc[~ߓ*"^ilF8to{^oe I"3F/ #Z s [+ljfbc:s=ylUn]yz233v'Yq6i rYxzq*7,,f +K높Ҥ 3t"G 'ѹDK 1Ϙɻ 6杤TILQM 7,ӇoDIBE39鱤JREt)Y5PG@1򫝿VTjǶաhۺM(3owwF#|bg6 #l*0哦Qd*?_+)\߫$8{.8@r ac߶.<'-Kl4DɒY?v]G~<|* i"~I~q$kiGÖS9ӌ2OLjWofT+Ms%bG-Y/DݞJViވτ?N!2 M~)IIJ!4%7/[X{,&0$@89~ 'G%mf2:s*E%Ev ƒtHFdlѶ]5]n.~FÒ,M@BB:([N/Ǻ=6CGH:jsfJ_LI*~XSߩS&+UƦ.nBPN1#2Um;Um{ɽg xfKC4J'[z? ҳZD<tKkW>4BpiSfZ*7 4ai_ʸ,J?M2p#,,"S > gE?Ӕ/!`7iҢ'GWpIzڽg"A<+C`bCw.+ LO$CxO`3:xl4$C:& mx6J`{oǝ偀 6kO51ߛqEyNKEK &7oe7g>gVS)c)$4+Tr=i:ĠJXQ3w{I?sORʤ„F/Q٭Pk"xyf<);GIZI~K GDRA$Q7U kXRl0,ޝCmd]I5UA2<! 6MAExG(F1T-N+QQ_] vF̯ɏ97iOGD{},LF q"4 ;:"21،tm! VD˓CEIAMo$N'(hnXPL~B<56TI-s7Qz" Rgq#AA10eO"K%6sJ#߭%S^22Uv}DۜP~ 帵w$~/c;:0.6RT<5.^wD& 4BK*exY^r"bPBi|\z$'L=ahˉqB_Ha+*y ;!P,-d Dk26Q[TW*NPz6=ا$̣f 3)OY$ZC* 4һ$W?YIrPF86g6u35_K bQnlS#r*&;0E%Vgqb.g1i6_lH3-B Ȇ8T"< 9PMA.O 4^z}/SjD4)/C-ZE2a+:z< .!_L:$dV1;g\ʧVmDեN)* 'IB,D5oJ R43b9wz?6]IB_aR0&'ꉹTuFV\C,J);|Dm86`&1ĔKr%  c7Rh1W3>'9O'8?Zn-E73̢3džQWGQF's[6B7M*il=} ͢k6 ,U&r`S5K %^q$G9cn4 e,1o<xf=-TӄkbQ˕/qv~ʸ98ҐCeRv]8C e+?y*.5=\jמ|*lU{@)z}J0Hl|P2t\?l1˻5>`+_&2lgs3E_EvDً_;hllN3Un7uYc .$_ƽDO/I J iO>]_^)Vv(< ;#焨J -qn> )vb)#h3ūFEA-xrCQ͕nG`{q#4^toiN@ZPf9uExj{r@92 A%"RW IZ4@z TWঈtnsTH+VvH D'E!D%#B7Rذ:̡qxb_B19fkE[g-cpX%8mvɆ|bdA9csդaV84? {` t  aP¢gD(o[_,hȣd t>KYpausp+M \qw8騦h$oB`4y?Ϸ.btoOO]܊-۠L4gw>(- Kr|vԥHPհ}[HK tUG"oGX"|8Cİ8L,| (oR2%^@?~̃^iԃ٘aνLyY=ȅk-#`RaaZ^%{iz@Cq%fw%R撢ޞK9jagd'tOvn[ŤsZ|$>nndYV"zx)ZxauIz2#_060i{5 W&/>5f Q}6lE0c@Kf9a[f*eK3pMlo9-V%8|*GS&GIΘ殠ͮʯ>ߦchO~GrPyFWJULtm1zH"dz烙YjuZF{wF |+rZI ;oMC}GbZ{aH_e">WSUKOSw[$F479ca&SCEZ)lM49a}K}f!sb4C'>pӡZn{\ʴnT ξCw׿4n&g Nӥ-+r*y\N͑5ozx"~@X&>OB7I,]>ǚ Hkrs1yոƔ.<ըV@ϪBNأ2T2nAq>,aWTXfy3ep:2NIf OtړxWnQIMԀb5: ]o<) ?sZhw[kVn_"|5-f??M&L M1Dd}ѢD 0'TX=T!gW?|ytq3f[Y?6yZ +JLnH|Y&36s]5Ȃ%b5u&pFQ!$˪!<xB,0[0[Lk*~\>z(Wz3a7;w2OxC)r,㰾K? K{x;XOQȟcCduπ<܆v7SkjjjXMፕ;tIG%ه~o,>w,*PaHIPWW&/M"bu<mvgEC? 8>bh#%dd=) $f9H{ ?n^;d4؍Toc<ͥOE¹I .L#p-K|vXVXšR;_n/!CFoY{ts>+mxqPIFlni[Q!je]^A?x HCWMZ Pbč:#!F#uT4f@B0lEHvS)aքd_pOԾP+GߓpΘol‘')w;"}^4E\Q?z;KN{J2c%'akYkhnySʑ697F=ܞG($p&$CqEG JP 6GM×(UKmkyU2]J ˃^?"/] ,us lP:*Yqj"NRqE}"u1 >'<XdOxGEJ ,p '!7 %%$GH[QӐ(T@rXՙC?W7tJOݡ^>/,pRL(簛l=<3?I8;+$(ZPwPfIijV)M遀^Wpt+li aHcGXN:.o|bO0>\XKźan)6@Wf#| N%vnO<F JFnXG +: ̡39_ߥ֢673Ic8i% nٰ~m 0JP6` ~ ) L WC{lUTmLtBo3U[\;R8 ÉҼ]_ՉA =a7cÌ.rkZj ~eسKBP,RŚ^$K.2$^x޹pW3S/G yBF_m  ?8D{&-Se~)#*#ʃ##BB!*ҪwVtu_ӑ/F|aڭ4vdΚw[Nubf^!sx|g>/g=cj] !ka|B^98-J_ r:%SWWSEM?R× P]-Ue'?Ys~pP-9=-{vz@$hб0?RzÆ9)xÙA%{,KTfBp<~txDĺ:5p wp΢Hm~8 Yz[3X o#\P볹J&Jy^}\M [>:3E~s[w5__\a؎\Y];"7fdfovn''r-CðˉyxaҥeԂ*(^ tm'U ˾߰p WoM"皣VLؖ@JoJ|$V"rQS,ya"gsZ^.p8VWJAtf)?^9,AL%EFL :)(S6b20aJpW"Y<]8j[v[M, zM ͆X3!`ѸN(y(a0Y&5j3 0-w]cAe翺ɒz@^GSfVtAPh_a?'t@*2\}a(u=&)Y"L75w 8JTUwOzxV SO$*SBҟJ tæO2eatTBUmb،@I{d&ϥ JL_ss|VOOT;:`QnN:(\wI/KI'ܪ4mAϳGz~"J~/wNǹ} :¹(fŗӫ oBG[4o}2 :)65Ptœ Fn6cH07+*B"m8'DIdz!U!4g0joq04'/aVtoRݷR[ /]v\PNl#2|@cy'kR8sۊ5YqZ['gjl@@%>pO$7CPpۆLӴ(Rʣzqf$(J-;i-b$rb~/:Lx#<%յvrse<@W (USSj5>{'\/f4YE7: 6\b e>=WG{%*c;<]8H=[Api8Ή?Vv^1&<><7 K%G7<5(v'YJڎS}BXN^ YW1l }%J,* R>ꜿqFI@c_;r rD/̰|E^fEc@U2Ԫ [asOͫ sĀkGݿϵd  ΍u# ZmNJ2/G{F!ƹH{.ʺ-л`e{7 spxD}ŢKCzΕ%J %^Ahy1>,襩q6aoG&4h P_~w`;kxaC+H-|kˆ@+lE< Gw HsI,S!|%(FYڻ욹"}'eO.jVBqP +84Lje*wx̕ ѮǗ{ATf<8M *7 pVuG5F#AjdY4`ڡлظL<~NtfCKd3wl[]- ^`IWQE09rݵjۋj}ٿ9UmKMeΞE]68Zip(80¹>,s[b&7t ނFe݇`AaMOIRw !؉Ӂig8wbWNJ "^㗟&XNo[Y˝,[S3N7VoCmmЇ~~"sR>(S/QN~$X-ցф3f5lUA[ߋ6G%%zRdt:f!쫥 [m0|[G㹻WrOr>΄DZ;N>T{Ul}9ưr~ Akvpf-/I~H 莓y- aތ_KpTib&)H%٦ ̉,*s82 D!R;PZO;N4*W6)8"n\2 @T;6ߜnDȂ> "-oCț]ߎ9(,dڋs#ھL755d[Mn)a̭J%bV17͇3ҏ%REImkosFu{񏌕h2 uɮ+W+;7ϱm/3!pjoV$eAD%mXԷǍ>)os-fuurO-f75NUťeO:ɣVD}oEۚInNq0ֿvVX&O Q&x`Ř-C+yt:nTbTQJ>ǧVeco(]d{9o`]Ϝ!f bc 9oFF 6$;ԼtP?mٟ: *>ob)$ZO0U(l.ׁľXod<[ }?e{.zd\jv#*2 (HP?paW $fI%D998s.OVߡi_΋̈{TLFɠ \G[)hMKj RJ^5UXM̲ЯN8ơ][\ۈ9"3 M"IDREU U 㝲~gmp Q$'Y"Sy¥%G2 V{fO7[8sfkV΋T/5>b1=]DK0E.yi_ZG K,$O0Ck?PCy2e#,Ay yyW^|:cjˇl/:x%u%)$_+CE$t"T5xkwLK[oyǖrl*Bd8nٷcb`Ֆ9޴^ zo_~3A (\]ՏԷܓGAܬp$7Zu-l.:i2/2q= N㹆J~E%22$k$=#LjԊSh>4s,)]m%dsJ8#&J ΜIzeɇ9iT'2*MSXn˷~xgݵh]J k:zTG]mkIDIJ+NUɩ֓X>B.=>^=jMP^Ox2EqGa@KR0~CFt5@障W㎨GY8Ƌa $cbYOCrL)fdy;7?pg MWj~xv (^*undM0O 멖ɀJ_t 1wk0^&{WϊA8ˇe@QPa9+`ѤB뤧I ,(/ t @@L^{UQߛ00ǨYހW]5iō+j6 74*L?]^b0!l8]]5ݸsO]!Xˡ*k;֥Vr$)6 ˆj5d Ȧ"[b`fJm 3psKY^9@KT>)5HZ8[0on hmg].+JQOa07`1\?R_?x3~_e[ox7I-! tB#ɛO{-O Z1?B\i*|KgÞ|. bI8A#1>4-h!*>ρfZ[N,%}Fyd9|mOp$ezwWhH?H@;{NM^S|6ڂ2 8<“1DL"yzqyaC +Bk#I?.zmq)?/4hhx)cs=wo Ќ¢.܇{-[;2ߧKeyfwA.X,prsRVN2?P -֏VϩdՁЙ2b/U&!ȕ)2n!?W](jJ.fYaG+4Z+o!~$n EŻ%' pb/8s֢%,"AiW`d&ᑅkG߈]qZQLV [–L-B4%IUD;Afa{/ӈ4pq* MhBAИ:n2@8e'x/ (c".A 0,t#NםbYu(ڪjѹC5imf'ZnǶ,V\C1  )'(FaSfJP"1`6.՘lǏ͍z'a^[X^dh9A1sMQxLٻ?5(" AUHa#Z7AGv ..M6@0Wr8ݤNH^=:e8>f+cڝ )7(Qy1AZC&8Eƕo|7U2&~ET9v!unqݡ-E79=1Cϸ\$q$tXɋY ٻ|gVQfK{x܌b]Ҕ4Ϛ2?4k{\l <-`U/cCmߵ4 )zJ 8Kl_ RPO+,؇xPd%(#E8M_mbXA#F4[LL "e=.Mq7ڐFGc cjIn0'=Q ؛ Lu}mL%,_N;qҙ{;_#q߱Q(*=eT'ЋtD㺓/O`*"*ZTF;3AHH j24xYo;PAH"_* X J>/T-EpG|.ǝ.*CC…i5W+IU%Xr g Q#2ˁVqQRm>9I, cYWu<7&߱r/H^bϳ Kaf&'%gWХY.?o6W.j\Z |eĬ .$=$&}d;pԓ]yS޴P"gW>Up gyT_KԷdP)Ni*+Ifቃn@[5}DM}0>O'4W\Ǖ‚!!9I5*H(Jc#{GP?i(QLNOdZqMIܧv\FP$@LCg ]|8]'Rozn9`+j\VC &YȢvUZ$pd97_53`DLWY5_k|MR4\A4<l" Ȧ&J.]wK)Oi5^01.XݍUc, Urc-ڳEgV_ܩD gg8xc#2o6䡟QV0G,buA3b1hͪ#p@+@}sצ;~QJ覯ɜ JA3wW'[ilZD;iCE n*$q5D{9o} %y@'I3\?`/tu)PP6Pd-\)"u .BHs"^萡 ku7Au M}>ȫC3OWA p5OE㏘"7 l)4vk"Gߝ<ׅ`?N]6./{EY(fHn_C@w/w:lCjE 'sGZLmI ="meKIGȵӭ_ s<xE$XRE1S/dqGg>%!wE/]?WwT L,Zʎד|C1SffAʆZ򔤂rf:,bOpZW=՘ t. :½,@#Ck3GqreノB}+|}xN Mmn 5mno_䀤&L?|낛"(01(To"\K #dYy-7oE[rwՉ1Vۋش&>w߹vvb#2<J%m.1NN,ZTǼ%[f>^#aGT˕wBHo{ViF\UJTCWQ3891BS9Ͳߖo+uD74Cp%+طp,kW@HuEQDY0BifknA!eꅔr~5G L&AGX34/Wpͳ3Tk@ \sD?C#fnpAܲģי}|yPddnN~p" $~$vuLʆkQ%/{po&1Rh6)ZcvV2G ZJa93|s ؖyyVY{K,>i[wQ0w} hBTPia|X'QBQ譄_On@ZŽ(nQڅ;{A~šݙTH:eq ĕ;DX$vb.ee_RV_kAud8t&I XnNќd2%] ѻ:kczPٶ*}aKm#U4>%(=POy X~Z5' 0Z-Sq͍l/+F6R RSgnzߓԛv~i^dVGSiӼoxN€ 9ӕuS\'Li}jǝ;TϬrHބGi3D@+ڭ>!+>5x::A)[!GǶT/p[)!b} .a5&;U==ħCQ0!7u:dhcN}dw+ b1IzÃxkb[G5Yhj@q 8:EEb:5+Zhafp0nqHN} kk½JpF ~L,4NіAO&SLPd cpuJlZ,8ONX\QHhRj4n`+o^/Kx`w0tƬpt-'S4ϛwm leEޥPME?JQL pʾ_=FR@ 2‚ƿ\pô(~WX&^6%͜gʋaq.ULQf}/GH'ێ_SUw +J`iѶ7 v89GD޿9pT1u2 MWR^j{Z $+_űioCX??u9sjǥӊWWL9E"tVdg. yZW>ܼP#=d[t5[|#;~g_V̴I~_^6|H T; U^ig[C~fؾ{#QK76 ?v{ {a's@>CM#z7>xd-EμNd~c~{;xDyK֟^-R)WN&[=01+=ĮJp5/srX!Q$xBFxmQY}*pߌ;_>z{{k _gLYTwN pGj`~̈kG~rt ùͩ]sI19+T-[F 9ZĠ=Nhh !6o fZ!?E?/~S8mCq ȂA[&3]-0l $y2]Mȱ_Iy^CaQMT st}dY138qQL9cfrx2R C3YEӛ~>3e~ \: 9ʈ>N_؟}hDk;LjSSJ=77vsHdCt/Ŀ}bnȍˋ'ř'~bIT8": ]bcUCU26IH>"IɂCp_&%nO֝l1.CH"FQǧ5T^OOuhh2d0m<9#;zqA@7#;=cqZZ\K4MZDlA5ZX-Rjjɮ&Wh |4%I^pI%C/A\x{iZJFX+@ZTJalDS0D1w/g- ({h9Bʼn:*tl@[#py%0IE^~m,[toRJם/@em"ԛZg3?';kO8UL3m[+6,8?}<u: X}#9SXܓJTO[DLEDmv>B*b֣V槢ru>%͕@\_/1I9!pps&?:pX!B9{ţ3B ]d6{J*43>b3-#E#Bקw8܄i dUMD/֮B}@lI31eW_7J@ĦE﹁xovr|hRI ϥXe?Q612 {P@P`#G)Eݚ`>]8WM[ !4B 2F;Mkd)ݯL[ WJ&fw  Ɔ 0 Mѱ$i=U7Wj+,>q~j0{F1@^(Jy+`FY4 _P.I.^>M+T[Rgfwdl'hS ٞQ>[8FUK4րe?kM3KX%eE )4ț)=)o}|kjuO'F_t2Y4qj" w;)4i ؖG}grUHʓ`dbs,*mR<'_'cE!>F*%%ۇsB1A+Եǀ(rȬ~qr * X5<|)5![#*<ńO#_'%-٬y09-v%nGXp/eTL@qbZO*M.[lq1{I³hA~%6pMW,Q<$G/Fj)<_O:mc!N/"W њ+ 4覙`%i?]vѠfJ| \HК LpmbX,66hPW[b& FPhl98@|.}bľ3}9 {Չ{6X:fl+5Rr>\rZ{ܾ|.BY9o)y<1t*1A)~x 頛W%RFhJdmOkBN0R L~(!W?S"kvlFcv/n#񫞱6Y85_KU440aRfY8GK:_=1]1 x]&b;>k^1^,GZSa[p;AH6`eȃռi?ž+wNL'z//95QCzNȖp_lK}T?x *aHw5= lա5z˾C`druǕ\GH$,!x3<ǿp4/.BR5,Cn/6X4:QcLf ~%y`DQC1&:Z84|p[<K ?OɭZʀ@bkH%ݠ%ao$,%.ߦ=!Yz>ϥW&׭C>kVzrz<$N@='%:Jk WWS{MBm=ߚ{Ҷ17˛ή搮|g?_SL ?Mʕ /( aVR9B!QQA?#dq5Ixj"̑6"`VVFuTG"bR!s& hVvlP!֤`#G5ܢd~ "đA^ȱ{j˔)p(o3ftk..y8.V _܄Sp о*%t"spoL@³ WR5B+89>¥D-MԔ9&\<\w$M\^9_Da)Q.v(7 Fp?)EYw9r.a~X7Mቈ|9]5&$ێ||x18UH]˓<^DzBa8T5Q(f#*menwl/_dPokT>zNkda{a#[y['VbT}w"β{@H yBMI8~CC=$.J3Ù$_+ ))2'.xV%/xE xv$uTܝS .Bc>ZX͘)ӢY~'`<:yUX!:wR&6GvN2ʘeibFZI󖿖t>P'4^c*V``xlK1߈/5T-\:1>>ChC e1 o E~ib!p\Q*vGJ.BM6*ݨr#s4FFA}2&i< @ )ks Jm,[U8)4dj; C;ws8>ZQ~- b݌,wF",!~R N.zasV_ 7rŪq2i!UN{jiU 5`?W krlg@GT';ܧ?8yGt2nfCᨏw󱕩q(~kUJ\+O_(- -:*$Mrl FB^29M[,t[}F"jvn|:;}cvÐAx8?݁^WRӻTq$uX(BM#O* NLπޱm +{Kx1k٬k D\ЊAW~`GpgzsªMjx K5k4 Qݟ>r2\=E0xg(&vU9DHAgSFD\ ο V(Bhsʱbu@5Hg2b@`|lZԵ355Eȏi3¦~Myј6mYzXW<3/ӄg)<2Չ{T2~ж3< Qɀ *|T\pnX-8p)'@R~QSgY$,#2<VNF"9݀ꃜׯBMWA~{CT=i5M_ixgGL.oD~dd.L/_6]*Y^ZԨP/I:Wʆ ^7p`Kwn J̟_߀՜g`⾢"$)e{'sTKdd9z{:({>:Tiݎ 7?/9!I[w.?&s neciAؼILP;((!zIiqjxbtH8ܖv4E-4dJ|c6gb7K-1 eyT|* kmm;K:oU(B3Hg,w4U9RѕyIM kjCo0 OL\ sX(wi֌GT@=wa H+氼*.?V#H{ujBtaHbR2qb f+j[4ow1@m#$q'CxM+uR Pvwv!< Z4AP\@J߶8il,W!c89eȁʪXR^3\Mj߰iO6|qh1WߗWmw1s~{9܊ҝP.贉Cdg{-De꿸(JI1 VZ6SiV7;AEu$:u%&w_!eOzQ;@JpA/ת)>&ZjEU^AC\-#Z=ӝƭd&>=+Uȷw?^/<+]GX[B _s -;Ѓ؀ -& ,M->޴umV}N"LЏ-Hi3⊃- D0Dƣ(1o) ueh !Kuݶjz߽`/(j=ܱنN +1 j0j+PϠ人DH}!NƯ\g^*){KRyrBu}.B!@@&9H&GD;sAע9G i#cτJuo7YnzI7|[4@I!BvB*r(o/Z,%.gE 4wÍ3+ѣi.CQ&-f1 |hK)Qbv0VFsՃ훆O,gNmBɔP\#H5f,nr4Շ/E4 `Zӝ;+6+:浖a_/'VXl`˅!ԉ\s1MjrVJ&  9g!C4RzuK02ǼmX9Dޔ, !(a$dyy*NɀO9uR>6!.Gn%%%z"VD06HN"Gk\4o^Ό CHCʷn3bQB (L&G#6vԁroPE㎉<otqq|O\ջf? P1+~'C)S^Vj\)Za/%RCdxo'|it.|<F=< CiiGiNװY5op|aw("Ö1iBx$п*eDohcA|tCRbgo3-)b[ 3,]6TjXax(rz`L%4 *7Vן>e.>P.7 USLRA9|JyX0_W D_ϞLl 5,g ѐ/YL>j?<#o9S2X(4rF 9 3 `%,#_v;23/{閲 x*xyL00fSXAapd1c*{:v//}p^~ ڊvpScnjeYn=;Ai2;9LJ}W-6Q#Gp &yD$ =pҶWr9xty'~~Y}igGlB'< 201dHXo AL~f\ОnZFE%b]"5~]#eg^=BFF:B8}GJsY]hN4WI7&40 >Qyqc^?0ۉ( B#)#k᠖q~t3GlK\>TuD`?.|#n@,G[ Fev97 :~>ܿN[fr+ ~|b F2C:R<hf`mrg\8qڐINR^yJ$ۄ^i;͒6 &3bAX)u>>{K&5^~Si1  k,?DGu>g|e(2TdNp})'\b+Wh6I9k׌dVomhǽgH݀3[{9 'ڈLKT5M-4Md=c%rndd0-#;;)˖10>7cX/?ljQ,[Eo*/>@ܣe1'[~с]@mY:QS5oNZ~UoqV|S[օ8i.NHADf xUt}_r9CwhT \r4Ez 3F#2rf]k@.!yCcc^N-f|*۶;j:hh 8 Z-C#˴oD5Gv$84Q!r<$m9j~[0lb WY"s5ܖ=C{Yĭ ۘ8S&^I]@.\Hs;lA@pR2Gה lwZ6~WytrrKЎ7MmIR(\܈aQ "D$L{@Q =:…hw&;쓪%!FJO}kt砤>t(/1%yO'ޔZ=RE;o63zlH'L'ݣ_'d{f|vL'j~h<< FIrhHZ[5?O}*˷-HYP_hw&K0BnlRS-Q?!)dcG؀wIj]d $]M=|ZC)>9c=ޡ`*,l1:E2ŷ;V~)$=x(ZS! @,-ThS}#87urZI֎<?"X A8'aFmSlԯo•Azi8 ;XGʏ巛Lg]|> O|vf)}u')|A I,0UM6@3.Xt+U+8ۜ?KlTnb{ MzШq謒q~!ui|۴&jT]M5W >CCSA;PG<~"qsQ-ɐ:eehZN!yrR2lU/\=-ָ^=`F35e4 b.g{JȝJōPQ%瘃^4=Ǩs\Q!x{Px쩃5uw 3Lk@ n; 0PC5xq ΚFcyUöyɹN=xp~5%L$Mby,j"L|{L Z\akO.*;C5oVP>|y uUxbAA RdX\vSQUO, "/ I@s*M}ҌtXբPuk%읔Z' 2@T1x`X MIZ82̩ kqIPT*B}\zV{ǓОkq@k Eu9x|t'>k64SǢ%Zx7 B'~%,,658| oᠱy9@;/v*j$t f;Mm1hj=PV DF'؈twߖD'l }OA$QU:er٧zP3$FDCw֡!Q."ibaUOcO'BlO*nؑ]S\ ;SuWiA ɿ n! ]~z}Sݍ5sAc5S*N% wsn&]pYٿOkmfܟrM$\OԔ])2[_'ԦU.M ȏO\1SQ ^5[j:p ^,$Ap,]PW")p7dkBЪr pwQ꽘qn Pq$ )9rmEzf<3^WlID:{_OOg{޷H-^}ct[ӍUp(ΐmzutzaxz帴Q!5fೈP#T\WGuX `Yef F%8'DPw}(~=z0׬ "<*P 2Ϛ#yDO- ۿql `p2mDĴD7)}M@\+|}Nd:o2 7l]Y)&,F(9&x&3S]ⲕbzs:.i>2ĬJd3]m\;>_i`$ѹHhHQMUqU!d eЫ{ OJiHّ7۫)eTDIu=Yňӝ'ud ^( t|Mf`S@(0@)sH&~}|0ΎȰ4Whmys]p[x3U^6dbj64Y;R|o.ߡ"jpbׯ]̍,Y0,:veB9<,[ e(;[X%G1F9D 4Xq㌔0~D n^C=Z%XBݶa2&A FK!F)wfF:jB,=pLwo"DJd-KHr3*?Uلa)AɾE/A'Y) iL $ |I;c\od-~K?N^E6P;vg:gЬ9]Vw42^^;|ʰUzMcW( ^rk3 Xs چH =ǓBعqY`tuV6%h ~Gv.\f{D-v@.βe3It&`(#_/{'~ʭ3\>ᒲ"x?̝hqe1S>P5jS`Y@=ho0hsP,͇O)0'CfYrR pB;0W$-->F bh7-d//jי~~Y=}76>*6b&q1'd^Y oA0BMSχe2VGcZKwNEDk5G *3ǮAQ滘z۟nF7ƍV5襩ش_nvFÏQ[8;^3-ٸZ ˏs5>fN9-r~f" ʠ]ۯq%!K,Mӝ~\TP]q;+z$c̫_4^.SHZR%p.4@XEj)噆 %l6x;HM1nj3%] ,֡F IG}ӡS?Ja1'e֥P}?'f;3- @yq Y`W"ؑWPD2m!q:B`,:CYr8TVe/0yp qx0Y^r}Vt՟'^p|Wqe]cM 7Bd+AVW0쯪n&o7MkBt  h`-Dl "wvD$?ճd-Eyo+Ƚ9CkΔyhM8n(Z-PmD9 ukV#֪i2 Y# 5\816`0͆RĖ7C b S5'"0ʴuvР㓂[UGrcS 元EcF4ήj-Tte쟲+=!!=Dڅ BeKޮ69DW_(߼afMƍgU3>LZz6(srZ /dND(ovypԳ1w',Y8AQ+CN Ho0@)z}ښ )wg MRF!SS0;FFۑLL@lPVɮ܈*4? w/&׍-:z伈N1P,YHEg\g@fx{~Ya>kYOjΤ7UkV+i> 9Faf-8 %0RCۖxKhIݬR^#f ,ks1In/{Y K7%6PHl/W@D9U$B;#h2DŚʽϲGUE-.taշ){3jSf,fHeǒ;qߝpH8Н&UPvRZF|;[!PN~QF! YkKp..vƑT!0Τ t6>Լg,6o$XOۑ\l:pPB)?[ mDbGhǯҋ~C#S&/OhusV)5|wPCVTyf2F ;זGyhxbqb7ﺗ@g#O8y2 6((z~-% ]NzַX[KJ[]%ZJ}ud]uCIo3xKLے^^յQqA~!C5fR-w 9B"Fxd6lazUگދĊ':0K\HYzwzB2OyK%Un}U!@(EM4 9XtSj{s[O$^^ اB1uNUQ-g4c,WA \Hs:m"H`"~QZ< ,(F?|"Gt4CPr&fǫ/}%"Z5 !Cܾ(m1Gށ⿍V>NdJtauKcGx+6N ? &*a?s&Ku686:&:Lj앱T|Z5bok++`IF.^}, &oYYr411,oiuvg ;hB, @~$lnY}*G̲?j XyO;rPnE.-/Ti%M zhpBVhYVApTAR p{j=.J=h B*h _@Rgp S PfCtlf$p;x҅3=tɮ֪kMDuzbYN),݅ĔD#LLSRΌؕvX87GaQМ ) [3l ^VrDB60׍R=5DNq0Ƿu_o# hy U!E:GzED,7%fп-X C8==x{̲q8DjKyP4<(Xuy-(!:8cG|Z ` o!?OL78 |=76{pDx"3!\s{g{*23~fv-OԗF/-rdZ..Uj)X(AX_ !#6 KVQ5dYu BSE2 2_<>WOeA= A-vOAfX Xe|vzĉnvɓ s:iQ}gGH93n|'\\BͿ+i4/,3u~פv/޲`P*B.{-Xv.W5;xsDtJSFgGoTܭWь3r sȥET&ՕpzmY=Lh !t2 9e\g@!b%' 9PqLW:ht@'4}rgvbӔ`a'7E^IB5M l$f%^V@'L8[-ֱXbVsl҄e6Zhmܯ^ {-MEcDO eAGpnl2-~YT8A$AΠʭgh!MawmBi_PXCevb I)?jRQU.|xSA :tTqlkR"EnӝcݡtYc#lR*R13NAQn/?c3mR:f/]Gh|! QІ"'{ 7exlljj[61-Wg,/T[֭4xlbnɾͶ; aXuG+\v[0&̭WT* %⦒v=Xbx‘Ab\:] }r5VJH`&ӾēCD]qO"M[J>p̸ls~=[M\6)DǬs*Qu6bv*oG\Mre]RtPP%#p*u{6^YV՞m650v1s|R^ r窇ikC%A&c :?P[rzjN|iX8 qph ;EjcYmj YG;sjC@atdUV.2IE,XM|~A4>2'r>YQVurw3Lzz`rO!z sNTÛTL(n!KJNb-ڳZRDA[UXp>Lr+9+_9n|q..E¤Wҕ׊z)"J/m5թopJ/L|k4q !2t"k+y&X8?k/INHH[HY /BӜ ;`6$HW|tA'H3=|@dbMKmb^%XO:.ajeAi;&pKjtӛC=8(5cm *`jv翧O2|Ԟ]]jWK^nG=:` ;y ~\^&Ӱb[%ca[hF8>n&6?k~yx|8$*r<79o*}?`yZR~Ohs*uyLxwZTrlWY-Hq`ݥe^_Gtg9*iTU?-)$~7ZF <4^|py#"4MDZ)#2R( ObϦ2E{ȼFdʎdl.{4߮vlBb"C>n]?s8{7F~<_%'@R32o'oNm9@FP 'Hf "D*+*F&j;. #($E3, $:Ge;a'ieU7m4/u?c'HfXDIVTM7Lv\`IEYM8yY8y?x?ʸJ_ސxa}.E)Y>r*|}4k₭W1M9 ׷l7,a& +bδxxGoU+ [5WE yԍM$laP[q[G4 <$*Hks4[8*p@p·`$'~F7K8-טE9+rB9lXI%GbmSEzƎ #:Nѡr|C>jP҆RлE'`@%'xLw|rǰ)ǰ. ޢ2c/,R1cU}FcUuJF<99\eT/`I5] j.$A(ӱQ\QJګ!'Pӳb&lV?u0X#* bf2$zt A~ 'g3'*AȱG:]@1 (XUv2+.rj}AƓZDQCTvegp['u pDqvC(PZ +a#{VVdݖwQi΃.%c`}qی+1vۆr@E miNK2É5moa2:=AN(!~@Q1チ}VaxB5(o e*/8} +-]YʹG׾+=;<ޠ.mC] I@ˌW+1|CC7X𬨎c$v$9FGdU;vߞR+DKwr/m]1 j+%gUy$^ɡDMq)MPvaHcx>Ҁټ.y !Pd Z4[QQ?f+cxб;M}lb@-DEk\!cIuXˠb[ޔUO <ܑʊbꚭM+]Il+Çpҕ2O'9>Z<9McƦ݋w٥PW҆A8,~>DUj^v]3nJNt,;+ FJdf.6i1+)60~-0}lq{JT40+Mʋx t7[srMJ|w%c❳9j.+ֹ!<>{ݴ($/bP j-ƈ ހY]$V'u]~̰ ۋbpv׻̫.'бX;` rMBo\`41mv5]3ԩX.Uqq֡$S\.-ɌKڹ*O:j[U  /})UWGo]__ j2ⱇSu@ wd}ӝDr: hn*WYc[a/P ~ 1\o9 |]}DL?w{-_*8}y__QWw5a/0L]LTIMh9կؿ(RXPz/l8KNPD -7/~0 en} cƌ[NJUg*ePIZzEo^W8aX*$D$ p.mP+2~Zԛ>|rI}ȮY1F#rh%)&^?x1%o^v\h<FW&M_٢.bj0f%~ǣDREG glP[j]lK9R-}b* gT;i=EF g,x-( xp>>5}BWƗaX`?nˬY~I:<>K.;T\u {Pe6<YFGA[0ޱJ}/ 7+G.=+.á{l+$ͭDMwKf$<1| ]5Tǿɯ)ʵǟӅOΫTZ.zeal-0.7.1/src/app/resources/browser/assets/webfonts/fa-solid-900.woff2000066400000000000000000002331241462517734600256440ustar00rootroot00000000000000wOF26T t5K ?FFTM`V T+6$  +l[tATme;ٸpp+4\1e {[ܝ\fs{oEP"KU321zRXTդĈltj2or{l[{Jjr&р)Sݷʋ^cZy7TIPPPNk9.s(Lky"|e9ogI[u+C?MQ Ph9qA! !#f o뜐IK8x;xʇ_  {?zR<_.&L$k[t p?⠽kHrw9sDpGO]=:@"Z؃.k>$h& S7npLMeeC[Z[N[VZic]vum㺮[Y׸ӦJmvJt͸IC(GWB'yBzO)[LEQmf{5R`2U yTE[s54 {JhF `pZ՚=9f4$ao=L2/H6Z$2%,;fC{j^oE8H# ؘX ښJC@0lh!V7?oTj&W`=d;E2?g6EcPRHi 6unh oq pi#!I}_x$C4>d~fw !iK;ѕA"cB<$6lޠ('OpֵK1,hdX9A> yA[8g-hot# CH~4OR6g;ER=X#l"geeU`ؓ^>9zb<#yDZo+2AWt)#u"Il8N@|soI/锆$,"JMLf"94-A0%˴38Sd'@ )Y)YhEY{JNyJdz?m9p1@h\Ho .@K^.Gz; za]vְ6p^CGt9B!P q2K (DZHr-4OG#kAj|,ЁOv6#mQ`+# k0fxC(39L, 9o\tu8)%B%wBכRYu/YڡOF+sp2OoV ,Ùn2([eˆ QfeieU4<<!&ͤ TIx@~^N"t]1yn '.؏Un# Ш<;8ݺT\8kNǤW/R%vW:S& 8hl|]1_ lTVB3]qg_&O3foVw1xCWU-Ev+%1oob ѭebׯmhp~ 5:z_m_!tuR*Ykmf%~McK ^Z^&Ɉ qՙ&Ʃ&CmٴVTQmv_?y9Â2uuUz*hM=07W"Zޅ&f17 Y}C ߍYVR/My`6f)UcRq9B xA<Ñ /SSQ;ck0Yc< gʝ0(WvYt{:< Yff7ȍz=TsY`TT,9Z =>`rM"EVq#hPn6Ze??4rRMKx0:V`К*F6LmtX 1}~f2sL]HӢR힔xϹc:Ȳ][O7䱅 nQ -o_lE[cbn\#sЪqe/UV*5Y"2a88*:JDUiSxY{ӧ}5HR"%mtfV].cE zrŋ ଌlϏ7Ch4TM8MZ:-۶n%k`+ W0]|O4ă>*Z:ě_Z.Y`o}0ϩZB?Ȣ4h=aw7q5lY=^>aa6ԙp>ŷib'nE[ d'LᚖE<7z3ńlsdkmz=#~Li,vM$@!B8r5b |ɧ !_N"vUƯh4ݏM;);OqKz1+73P=SeM I.:,v(#uBEV>Wt#)wYGd:q"NQ?l>fc;T@kE™t[w{)97Qq*[[k *sG{eU8p{|ɫa:~ [s7yZM]3,)÷3Հ/ &!w؅F7xkJSo$ Sì'rqӎ&ׂطF|s9s1=pۣn@Jll1yl\6^62i4yo7KK\n*Yn$C2۫h.LNƇ @0X‰tiZ9JDPp(XNt!{ oIU79%`Px=S[hj3}4K{ДN1`̓)!Gﴔdt4to}zU9|߃༬~m\Z:^XIQx č!+Ƞ2ÝyY>I0q7@,jE8SXn!eaa%#-_9ߡ/+YӾۉ,ԭ"gd[lbl"׵էctӚd-zI gCS) ]>払х'{4`y's%qr 'Tu:ibZUU$rH eHMsw"HYEf R)f yK:9MjJEqkd&J̑?e}yF!R .|i am4kZҧzKg%/W/7=I:<8Llm,2N^v"}5hE,9q@ɐϨG"ѐe3W'Sjy 9|(W:zx6dC@o~Il> >NU+ybΝy$ ڥ};?}TyiLzyR[EClBHG|eQxح>6vlѹkllϐxs|'l=:01LAٍ40% bQ ~댍ww#PǸvXHexfR ϶iȹ>u)+2 .`wR{M'TWw!ch-M*n wVOR{g{XCȞ V.Mlam6U}d袀ɱYtOrЂ󝒘c^Cx<;/EdF.{ҁFP}OK(tڱ\I'S_1>PbgzhLIAtv9e*Iu0Igrlr5evT g3&b_: Y˴ .!-fqtYQlh6E }8Lf;Cq֬F fKrј^5s(P}C$ luu1c ɦ*36K>T}+ 0[Gx9~++I4=@lҗ?Y53yEIo~13 (NEP ++lPi :͸gUW+3̓=./ {b+SR$>S;ro~ b6<& x!kG )Jl%UŊk3Iَzto i*t9igZޛ[ޜa~edם)W;n}f85 ϊ e{+ix-ԅo{Aھ=. wbjopvv <]:"AvfB)-' g?w.4g7,pZ\8>/_^Iug&JX|1?HbN֍r8xrЫ vH'v 4GдM Lq,+*5 *q Ltj .\u 0lAx;I -}ϰ5x!8K 9f"B2a5K`@=< -C=>tW7x*ޠ ԥG;X'a.cB1MUfk9bTy n%ߪ^H8=`FYU*,zN7TqS"5]0K.HSK3wj,1 k?K*/k% hxA9BQ,+.Xb.ߤYVUlWQxP4[ #2Oހ ݎu`yguZ%įI|#ǽ:z^50Kj!$ …Yxr%2cd>-<_N] #8a0cA)*k&oF d0C:%f R"+\Y0dr 0c_!Dv j3B}10 N R cEXXQN,j; NF=`@UWwvvܟDzf(ۋ3dSFL5{S mj xO#,|9= U!!!hdÿKeXmG>/5ZS;K_X]3%o2Ю%Troz3uͦvb9A_4!ڝ Gn{X^#aowBfW.!4" @F#}ͷ "UH6mMo;ma8 w(޸{fKn\v 46ΜwY ۉI~iyT}ۥ<1+ [T13,l*=ɛ-^9@Zwg.$.1&xϙ{DkQI+!lǧ*_ pH'sn)@qTn 8- :\2bKJ @ϾxR1lչy1L4L$L@z/`EJ2 #K2CXTjWNs4g}"Sg@Al|vQע!﹤-蔈Q@).>r'K zozi&-[){%L&6[zCyL#{X;<}ds.}}'~TcyAõ;w`l ~Tht:wP\Qye.~~3t%D6O`B%l"ޞ7@bc_vam JM'PoJUkf8T]x{'*E9lb'{Y| RΘ;b1c9qCpF3sD`@pUE4#LK |L^'9`^oZlN9^b FWsJ\IzlؚuZ :[F-8V 2\ Y <.;߄Gu9NY3߷9-Y7Vc*oN_ɛ'In@8/xo3YHH!&h { U$򶼖jTXet`=)}J&&3 ~}M }<Ϯ:rxŠ|غ{=.6зY4sJꎓX[k^fCVqD37\u,l-A, ; ? :YOJ0e񌎫3LJ@  !6]ޚ6FQ#6gר /pq ԶOFmv7b411֚Gˁ'/Q,kök fy+GM~g,xꧼUrr* sq% +%~ȭDKmE*!ӡ졦~s"M(߷~tzE H h \>(M3{- v8/|c1Ϣ`9M9Z;viA2GqpGi Sʿ$ϲ{#ݍ>Wą̴B;|hm:&$}je:}:\M5Ӿݡc~[I`ew[,Փ{4|+G:8(&|WwxfL+@G^!^fc;'q.ulPm:X"D(ADWƱGDpm+5~iF継P{0akdK/؁)Rڃ݇j{J JZ}%p 3p9lL{qKcO cz bҽHa]$sDƤ? y][9A1wYpv?S[~b5IA(;"QhQDbbB@FM ` A-B. $%QmT@MͰ wz< ii7`k/_EOHLfW\j} ~]R".Ȼ6/}(nG 7.z9l)-%|7fۊkZ{Rh&wwkF%teTۥƝP3K00Ҷs0'^)zкΤ9':*{8{7.m R.\[XAlIA'S#ʬX/ sVbgvBS{d8^xt_ Zh$kqQ!FpaXOW(.M^o[cҶˊs YAc5yӅW۞ƊΦ!N!H^@=XG{$n,֏\&>Ftl[f=.'+~dJҕ4h:^l5`5ש?5JV:omg&q^dwH{$R,,ާ@ #h^.[ih[WKj[4l e@CF)R|v}6&6sP΍Nj=t#wbjo] KR=9D[-Q :e{5g% |܅ļ1e`F |8ޯeiUmw64fGAǓ Ap/xlTYg~*Mf~clLRDUP$g䃬A( k<Ī ucf7,P=sm=},\]4 ERބ 1GJ۬!v!dBmx.o1:;Kt ƝS-j2С= G" 3~Pk]{<*I1r,i]|"ly sn5~ GsV+t UL(^Bͬ&t3\5WD2Pt6Ny/|0/`'OBº<,] MVFjKDTyR!dU!U3'n#q.U9(< Vp]XUDS|35S}d8@5Y &/:*6~t&KsUC.ʼ*@J`b}8V@6^'EuC6W^I86 mķZR8T֊@LtP!li =S|g E$7" wg] ~39# @ }q 0s5 *smgs!nym5# #$D:_q'sі)غm .[M]AA[j`I$}zͫ q`2SOP~풖FSu,}\i@<%\ Vu4|#| }2o Ne~L2T'fv'ѩ/`zxcԦ8$4E 47@3>VЌ'^;m=6n(ŭ!Al+;ERũTceReRdo\SM ⏄B35yWY @L+WȤEgit@kGa@/J%7e!!E#ݔoy&VĆTR&yD Og &`2J⏍FɯlKCWj\qꕡ>s0cn;1ı]*N܈G'ojnWsA/~p[puA-Ȗޘ|e> ɭ5 $<(.5]ڭuVzMV2xGDGYq %j9_kb* XńÁB'ᓛ{savl^n-|*Ew8.5՟ᏼc};5;C.BR4փz2@-'mn֦AYD8bbE)Ym=aEۑ2j݀0m @ױ=xraq : RqaU\QgwNz7~s]27&D]WI^ r/.J<3 RyL*+ T, ˩Ml=kDScȈ j.I<+ rbE*=Zknlj^K9j'њ1J4ʪ0liC8#j+P;ݷql̼aff kroRrlNQQeԻ#>R.O\7zk[D[ϨJb<2Kg~0+>)R@J1a@i9EUdF”i^2IE|W>&ntOڵ!FU2v fJY>|#Pn,$}}#cc8H)FJqʉoT\ڷ[+|KAʎnVOSmL)FCrD˲bUK=&n-3k.Lm˱E= ng㢊U}Tz;DZm< T f& *&8 \+y4U=1LŒ2jE aǀGǡ>S*FQ5-^sd}? T C3giG@o(K0Y Q8ntgnF|,R.sx4΂݄F%6E0:# yӨR$H09~|(eqf lCzzUtc!P2ӓfTI&hI cgnA3L1'VOhFI-WV͕2=^ofdnqJ;#$KyQBM'rNp= ]ScVr*;>@H E2U\1ev k#tT!Z;%{kj&{ ,:Yꉎ';򇁜ߵRlj>1ѱXZCSM9`Ze1^ޔѪi˯y^j"ο1~X3hZ9M>j,go[]P`ZN3..xwZlFu~_IE*j1J`hE -O$RPKǒ1;AJ@V%S L]@6e ,4mF|OweWߦGԄ`ۉP\wkVGe7AvBW1*Cs"m,fP"-};eSgۀ0KVMm:qvu-=xɥy:m/lՇ|NyIaX%sO08}xdD,5j-{n-3l-)I*bŨ\(imp+Ygnnxa}^E%u%08|O`'?يl1,ȱ_w"Va*Y BPzࡓ꧹wFc,ޚh͡ғ: z+  xGD[>IJR3G =piyGDq$7{ . TȒMꅅ~Ue{O‘7ׅN9_36%V4筁E"P =mjP5K&^TZKuFob"Qk5=R^1CJߒ"Jn)az\RʹźwyZRj7Ggub y@)㱕9.\6s[ǭKV4;fу,ؤj傱\zt#H>"!CxmTyz ѷLzqT5D<96։z|ixVxTؑir4zĮըËbM'!:ΪQ̄%dYz}*vճѤLqr44/x@!DO7ˠSyL~!в8 c'2-@Л8($FN)k)+h%E/aȞf/YH&!g=FM,6@Ҋ¥/1Bjtчa6y1])ыzGrv^:PtT$a"qFdtGRЎ.^mf<$n?%i);LVv<[C8|PIbL +>6%M.%#_?KS6G9*ԓ~jb?YdI9rDq[u2DUfV)TAVTL*q.7χDєғk+Q!6}irdGhh؍҇<&b;}+Q%qi~j.ArYyܴ;ڥzB49\ `B-l+*1_!QL6cfb֭BDOY!ٺ>roDDYKV_=>6P*gVד"@论^cD;l…d<-rsrOJs <$ӮS66 M&AIG:X7Nj4 ";bP%YRTEv0H5*܊@&Np3@0YȚHNt lJE |~ ̢@` %Ըc `O˩Y!{ZZ9Jv{)m͖SD+C6gq]YYi 8Sg]N&; @gk)s{̦*z.Xe&I^Z+n P bbh ѭW,p%~yzMLGD|'}U[I2UQl%}-[)ZXOyeŜ9UfL*}N)ۿC{R3V ޭmiͧD[qL>'(;OE9kSl3G<S?Jͅp4Ԣ܂t[1ҥ_wX^N3\ W}D bᩛ/]>sĪ56mݑL%R"C( eeLYLFyVv!)`1v`]N͝D0<%EDnkKX^:N@}Pp1k; t&~ȷI4y i}Yz n}Z %Jƿ邆׵`V5d0@^~)svRlbߒ6Of-VO3|3/a6]NvkoD҃䃹pĎk.x^Ju{| ѽ܊Y=v5hQ+ OB h7Ka);,$~$bi%*M Hi|bex|̑ |A-Qz\/)_v 'XryIxlukAX9u #0xߥv%y񦇸3zGP ]]#ӖSqvWw_]d$;vz -V4%3hA]\Gp?0_I,4^W#1 M SH31,nFDpqO65ddJZMAcmsm]:|YC3)ͅ TCozu [ZʏI]f;򀽿tMo~,E+ P(,Do#gû]uJRD^aeT5t):Sc10{ζ`O Sb@Onڮ5XW-K2)A8%QsP UǝavX1տKmh*%\Mt՟W7ᙯ heI> Ɂs+L!ِx]ҜL4Yy49eBp LY ON-窪=񽃍 Ih<\8 z@lS,(g?KjCeʔ6}Evße([{]7en)//RvRR8[UʹXȣΧJ__;d#z4o'tE g @qh_h_#89q+&o@+gtA7YXB3HacE57xRt@B$ 3GOXA*18 ]%CyXP; EܯyܳAvP"Uۆ^ aZu#Zjcځ2c}'ֽV>bf4@m!m:̌2;l%m yC@p'Q\& >DH^(ZY;6p§$/?v9瓬x]R_h9ߗ^$l^bdIV9n@ܾl4Lnz;*!/%~h* VN]VK+tw. gqJEK|VҢuʑ[PKYt\G,:>gx1mad{ٹ8=l3?)҈'ߪ[Z٭#_8,.o,_&ǯ4l 38A,YrsCiM5qtͅKc0W$}@Dɦ\G qr>.L^?2:PR(G[!XO0<!o|~ 5-s:Zf`̈́mN 7*htdŘ&m+p_zfuTwب^II V4jX/~N_lg1boGt[ aca b׃t%lCx59n$JS;A]^m"FVWgتծv>X /v.vA;%ī2glsv nP2vH~<>-;&{(k-O6ƹ5.4ӽ#EMD+zJs?Qi]Xg!87f7ISs=+nE+A=\]645UTًty1W֌zLB?)\Nh1I ?-&A`ۊf fD=~OhqS_*r٩!|Krc]EѬ]Uଥ}%GU0kn)'r,)&cϟa}mdIVAP4xS#I,Sqg|֛ۭn Odi./Nz G^Db/Hg9El9Ԝllʇ,Y3/cnx}Oh,yg.1>O9kiS-%. J[FEЋjLWC_j`/+RvGX2fUɦ- o< Tt!Kbg3Jb@vc A Gv.چ.䍸 c:)fvuߊIE64]K: 35xCG!n$AJT@v:<у =&mGB1.MR!Yd'q 촽a!rɚuT;f+}I3ҖwKKdosdk )_ ]7F矩3D~ܠۧgu`ow?r<+}]zL{Э6ɴI,xJ4t]q6C 4U˛VoYFxzu.w`h^f-s誙,s`w8YOu,Q׾s 5OBs;CQr٥_ O yc'okBEQ¯ um@<efe})fքXJx' }!ޠVzSuBFhȉhߟ@ޛCǝ$x};U|iޒPN`&'4= ־u$(rRHI)j,Rr c'  ᚑnޟ44AWGZ_lUȼT{ohq>tʊQvW[684q]EaEB;ȱJtp\yd;ϾkᜟĻi1 ,6zN]$wb!eʭEN|IW\MS[\x=$U@rby5̦{LG&vtCF$ c (8<2Tu_"R!|HkG4[I2F/.:/N,xy] E&^e"!~j*+̨ur!EWcԋ抵dhmF곐oR8 {փM%h@ce(rr QC9y A?߸9.R R ObkZN_^iQmN/$+=--- 4"^b]JL]=rkD_ͭSɦo=}&/%΅ _{<o֮uOަG쬒׹& ~[PN=cp0MV6z2tL5"dtvbi*`9 JvGvZ#“GwΏL͖=j`[lNQrHb;?TS0wSָԤ]`2/Y3%8bsy]Zſ'-ٚg<"0j(l<%ZG ^Jr&)B]P>Mw]ɪpp;NItDD@^Nܝ6.pcuݎ~dS4@|7/̨ﳶߋR*:P7oIgp|JQ!(xKXk@;Bs )gVk>"1-RBcc#ʱCtB9g,)hp5@+wȭZCuYANJ)͖VzEA Ū׽z|-pC@ uQ8,lnPeD&pyU3C88#WaOuAiY೻×3$١p'M]zPŽAQ :TۈGⶄIvR`]zFIE4-d-FmȞ`D.6ݺYRmt@Z86$;F+g6xYd8Y1}Rb}\ I;y}( A l[EwМmI:{ 9؉'(iDO Gw.sj2 Qž]d;VUfotsOg"TFI5"KXӁZx(b2,u*Gsϒx~vRAƏeeM'?FW>RCKUS?sr͟A L-_U $!;\JE%-{{mtmlʿW?yZuoai _0\4@08&QPq9]a&@խǍҚ0t}hax< H?|4eZ*ˡ<ʘۂ(:aiʠ >xZ>ٲ dT~-w NgwځT@iD6yIꈛ,eJY ЄCMz 2rH9AN; mȩҮOnkkf\2s{Xo;>{@9/Ge7w<}dͨœunc@-o`s\wZX#>f,QՎRe _xuVzO;q_.xR |SI438D_.huKu) "?~''|uuZXգeYtX_wTɩ6j!1);+W337bBX  ΗfL&νnkXջjT^h^cW#Ƙe5>hYj;ľ;|f^W+bM-֛5xMD:F"-\rSخ х*Z/K~MT^_k^k!:I_bLq[_Il߮iBgHaRd*FͅEΎ (bv#O]9{gS3 CmG$ޚR7 6^%a0'@cE4=SHY#ȅz1S׷5n521ݤL,gFEcԈM*-rڏ"$a;M!)A?./?ѯ8b{&M.| 4z<$XLΦS#-bɬiY ~?TZHkM{9VdXy.^iE[HC6H~pv};}Lo\^*eöY[ _ן`'4yUJ[e9D,x_dDZLZcXߑw1Ge/6;HZk{ZmtY|U.4T @m>;;8LAw(hCJ#2|%cݾ3FQ夠,IS;]TZdX`gը\udJYIax9%:~uHcz;&pN} ),[Q/4ü,y_n^x}iAnŇ,>8$u=.'?]7c#-28b [ =-hGR}N>F[ԽR0͕X>g @7zAERU7j4.C1SU4$#" MJ٥W= R#:=A=֛=A!sGT]/9wW6* [ƭc͟Oq+k,o{yHZ eΫu˓a.tH|S:sr/oPIZZ+_8#"I$Т r`OYoG7?r;2z&^.#9loq_fl- )!seط6Y+ {kpR/7X!V[eщR_vEđw-_]r¤5cO r#/hNYL\劏l'ֶBph`ڊ;uyDA&o1PC28Q>D΢Үj ekqu?In+Ξ Xk>/U ݺUu2H1ZU7=cV8*M,qP>{CQ2y{Y.vwέc[96VYpY}CE+^FY["tvHL(xbWr[~Pʜ: 09;uImwmZ(&ڥm;d3!:>wP}6yc\7|x#f*3{ϯ||ڍNoƈ5Q2Aok0oyɛBhL?~N)M葊x, ֤iwQfiWXaLBYf‡+@,ih1}`W4b.RW_Lmh ֲ4bCu[]?#zc%Mص00Z*]V $PDIvik3W6x143:R}clļu &o8US\Os3M?u1b8qjF!>~qi-cMj=bj+: GL%@"7*gp(AG47hAcٍKX`/WJn\݊Ԧu:߳IrY s 5f)1z-PX`BnWXb&>nB ŨG-Yٗ>u ,w fڃPe1szӠVS떖.~fS|Qڴ0 9UQ/8u f3F5PkL an9&Hy!. x,`Uنol˯/HItQׅİBk1oc/tTipG$F x\Ҡ,4`@*!>PY+קZ wpEBϿte=C\;$蹛Ю ([j͐CJ Y: %)UH\2ObIE^#*CYhCPcL&0;56J)M,=Hgh(l0mv7ZE$*SEK`\YSKQӜEB2ф[? jNb vMe;9]tg1"OW ]E ?ȠBz d2l lzGGvXӿ3c'[kB7>;rXZkw  i@0$ZK(fvRbe7+2 XKt  [sd] ohH.]w. N^+k& 5^%7-)$77 tsӒOӷ ߄]`j)׌yP$*h~D! gY\ݽp;,Z[.P6&'/knO!Y?EYĀWi}[Efc \:r-$VI!1*qz/G>!*zpd:Mg,V1ׂ WOK/S%wNߧ?5 liT3ݱ$Zm|wi"cm|Rx 7gSu ;2q'3k=X9Y]Vy khX3i+g"R?Q|0ӆյ.)\V`u SI/2e޲:bڲgrU=I_O퉑1p_HTg:=ʌ~A_3  Kp:3yMjʣ=:ک&0^ H˜֩>WCKY4얏a=( P!7ڕJ`âLW,;wGr崴+iY=P.غlbr5#, wZ 7ikkn'ŀ5Vסs6S|g|e혻_P{ ;,Ě-&[Ys=,uGMw8P%%\񳀇3zk-#ƧHF|uN ԊfwV ]A9?oe˧|Zs\ 3t(#_/_̖|žۜ1 $pwI4M&2RF!"պ*ǭ{Q49bW]g'燗"5Wn8 g]=uuM۷cot!ffn_y(n޸+H.H-Bb~ sD%\VL>CKjYuwO3dGzE4gv{c사,5:h Y3?ͬ?Ś{lj緱-r&j#xފ+-MP:*"4Ґ+G=7>Ԑѐh E͋"d8Ήr>׌:K&Rm0hG+ڼmҴ侹[JElF0g,$<dzc5'Y: =7蓺* ʈ1iF\Y10F;HJN}- b9>e -9WUS4𑨦jgG_p:,4)Z0vAB{SY=Ʌ`&}{iߧZFg5Qh؜QRLWT/j? Ms t;.ozJ; "P jx}L-*"JW\bbm[V qš",6y7_ޥL9unii/zJS?ڼ=3TF@}Iqs(l1fnu}5rM#o 'PmӬC}#-|SS M .{ M $]ٗbBΒJ*Y#b2 2W(fkdX"!Dv%sz={!_{ DRr*@?3dQS fU8ߊ EjT#+m`w%Y&$֞Kլق q\H* b~I&N~#Pz$XRƵ1ZJ}Wx3- Yy"X+(Okӟo6~0NC~ KN$57|@*`}ݳcn@8Hig($nRM܍\E? l9L۰9U#2׋δ'>0k0۹W*_;pr}*Zxjt?-{w_.T%Ɠ'0z"h:fر* {/bkcDeX:&p& 22 3"FдPPq8R'PR#%"| `&Pw^>q$n<&:@ҹ*3 @(o G?̬hD]R׏AU?SL-U֜24"vѨ0eA婓Eiejk߾1|V#eeNN,+=TF2 =;bc:BXp䩷~Q3kZrE-ueQa_W%uiIsyX$UD3堈G80`6&A9,Z| E/_⏨n<[`.k1_wet3/̜ RL &=h=hDX=,qCHz1EGyܟkI'xowhN3)¡XT閷rX`M~ ϑJ"sKRL2Ĥe|9NsD5xqvYP*dd60fḣ|i&с#c1S5X0)(cM޾=)vEǒQ:V1 =xv]3}oْR2%AІ2]oAL ̳(`^l!6{ZATsD*,BnB*n% fhA ~ ^Bd'Dsh 6  tJPuW:\hrnM m ڻ쌤bqkF}Q_9#QՑ?l^Ie60z+%~S^4ke2;̤ 8~M; Cɱl摘N7t2QuqQEg2: P_ nY̙]+[(8!?7b]_J'^}fYKLyf`Ӭ<ѣd>Sn9@> [,Kґ6+̑/0eF~A82!g8?L؀￁8pn[*#,%@m~ 6nz9_OpzAӍY^OQ:w54^@/6>`y ܘrpS38,8y@^xa1Xz'ٳ lzڶ8@-X@Hq FyrkZlmϫ-!K;ǒǏG7nTˏXI)W!ΑxaC7rx}ixS M=)BQzv{-Q zVxzz=ȟQqjѓS[S'hgp oЈ} 9\`XaBL.Y1ERg-5X5Uli* 6,kf̓O"ׂhke]\~QhvPb :7?X79LIHj[urpGDuc?D?$C4T$"(wOΑJKWwhSh; 卡y#q/f@5KTO;UQ^$LݻyYF/<,TRvCdӉ&̊{RSWH]ɡ\י,ԙ| :]hFHK"sCмʨxL5pn6}+/m7/_\?;ȐqTeT|VV<-BqoE6mG9Aa2(+ >.,lΕaMΧ}qL6VkUjڥu{)bU/cHzyXa!ze=ﳒ/@=ӳx"6쓋}jKfEGTp*p! |N8)Zv۷@nr3c!&A!EmFuJr_TY5eO.AP5c-haEփ,:Z¤h3w!Ch@@2s_m#sc-1`z<=Xީrq5QZ-/> 0-sOa;B'OS>&qZ^iAmbҀەJ8{ F"%MV!QS~H溪V0@dyNW"ܟ)DQ.]òĠ?l͆._#xK)?pړ(վ;/?'JA3nX%rGd8=ժ 4C̡>9ߟ`{K__ ᒅL B))hQ{,$c -˕Qr=<=xZ8zrDv DڮlB(CDvĝ€fq9 D~FÇxU†z6]>2E5v׾ 7`M*?qP[\J.SuKc)[v|:ь =t( t4@R?ƛH4v$G֫y, z{r,]b`K]ĵ .G1bt;zgd*Jl^xV r0uf0md& z:l麥F*n헬tmoZ/.;\zq\GO% ].`(4y_CAc$Իv+vdy%VԱU_4X-!悟Tl>8?&j$amUO"\se;R$e݀%ٽ{haa@ kxx7cyWW|lb6'7DQ d$+5|uhPhhz7/ZuZY6Cy?؜X$"yctjLOwhCCn{U*tDL/~}z5Xyh=nӍ,s7<@QR寑·9QP&OUCЦ/QlaXN V4 xJ U^ Kj% l5ŀ@O3{{ի>0IiR49=M*zzk J8"u=aETKjP ŲgDĥ^Êi XxAѕ3˝ahTs+gn UcAD%dr2k<R>9 @PTT"e`D\ Yz"j"sr+T{?G"R^N;7Y?&t@A%X/&`۩;!۱N~:r߷/7o^K_sѨV'jbqȏzt>7A-IʲJ{Z}{}ֶ q] x415)$kۑ^ m˭ tSsyfFkr IO!Zmh1BïԎ\hS*mVc4OgBn<YWgㅝDg:I/!۷)&ҰnWcbxJi좩k.Xф@|34?O"EGDVa*:Pg{S&<+;nR[ fЂl@v7xK6saf9X=4AZ{m >/,Gl v&r,--onHJ f2qOԢX, _q_df&c(;vfj^I]؄!ZyO@Qm" p9.ww; $Ǥ(eyl@¯b 6RʞW^]yG(\RJ1&+Y.v*W|؂0n1kq딸 C б-&̈qyy[\,rf2$+~PTPֿC jڔ-yP k52UqYXep\+D@4U 8%0n`cgW5p3Q>T;pu=滷}Dt޴"  V汦V_ %nq<;iӊ Q\MֈI 01v&5SSn\bT^bZb2_7rfX1L:V9|~CaX|AX|>[RΟf*&V(@m%'s$+=ӣ6C0faxɟ/2;3.m(W@Ld+Hv o:=j݉GZ8+2TES:whvXܐ SLJ:𐁷m r|Si`<xoƒ /& M2tCygԞ Կnn/רdBQ@ȬLWY_faBXax |blj 'K Om@g{ukCQmg?(RT?_Vb|^ )J+=F#4zJդef_#"׵R-lwo= ~Jh:EL,42ZI>rYYپoʝ샍y qm_TI0ӈ<įa~X`%-P:N(b>>^z ;!qal:p(9s%؝;Κ*cϦj^z 1L3fӦ,O6'z@VQ)4݂]!>֣J5v/R9/:oZ͟I.,5$v7.Z֔L&Ek_k[?Zm_=IwT)y2w,>,.1LvddflSz616VzFfzFZ2?bLV؅%;:ưjAzϒ (";u$r8-bq^xuU[S(v=̮Sx+;2K Yb?zDCeܣlϸn'?s@#ܚA~bYM=B[ Q$YE3,!l2yZ5plDX, 7<8}9GޙlN|8qsoSr *x c]Rxd}Z٨EO[sd66 B]qb7\'ax(>ӵ8p?\(ۜgL磂*J洸¸"p%ˆc sbČ[{64k%Op(&UpY0)a/C-zz*6kwMsfkXm|HUh^27Ou|s!Bvprk91|gbƑ+9'qfgޟr c*َ zV=~&+ŗxhZrZ w/#[;) FF L@Uo*BL `Qd-صp|-ZaηC9@ΘCokTovKJy}Bi]\c &hԒZdYcz4Pv3qaDOsUw=.fkYs͈γ~+lK#~';yj ;Ȼɷc=H8;}q@gd!C; tВ6 ƙo @IV! PԋnP,(X ߎY+E,\YM$"-1B53ǵ!@ȝA ^^[$ixJVjG-6v( Hegݓ&±xd*n=MG>OoUȕ_oTdgb~}k (H2܁YU9,6IJwP%(M$c`ǫ|8(A$/٭Sn*_%Qxu`%0d، I_Ig^2#BbCfnBWE_~dJAJTB$?M@RNbHNKNAT`X& n~ =$VYHjh6el{u:Uɓ1I{ط!۬X`i>dHX|Zh*97;;j[K~@"{xZ'Ε%+ĉa ~׆raKLrSdPsyy' xM-kgdw& i>vD'q^?[md@TW.KJ#OW< S 1L] B/X`GN7>ݜoF&Nn҉Ν[n"-m] ,L,('< e)bX_YG;ܥ3ȰҌ׏MnВ\ˆՎ+ؽJsX pp.-aG7IL74fn8> { ܺnU*IhTX`Z=lt uq'1ټR@j7I8PW`iz+C=  4ZR SR^VTM}SFJSSQpT̓.5T-YMs% Tez҃yAIXB4ΛmxMyvHe^<74Q$-P.)CƱ0Lh5S OB[~%@xF߳. gEb }8h8_+#<\>4Y]Yr;2{=ԞQSus>x(hMdRisfNͯ7}s`{|y፛dj}^DͥⱣQ&+AN36t0Bչ)f<8Vj@+X3?J_;Xlsr\x ozO+c=3ԄN߻wzCvΛjg<>?Ƴ%;yWMws1:&IV}9a YZ.]:sE)iHб"SzL;!XVg'jo._,J]*Oՠ ʺ\ E?-eIQwX#r/}折yh WwiSF-e-X1Ԅ/|^0I@9=F3LpyM⒘EIoL^9,pƄtɧ &j7;]ҔVxALU YPvVbcm }P LRIK Hn8^M뗹%^ϼvQ+$E&hDZ-޸w6۩6@: rW_.fH=rS_"μ~L*Xf -`^pz7ܫҋ9 ҺݲnR<AqHd5V~NyG%OTx^2Tp+\\[T\am#'DO3&dՖ`\cO1N<1hLz 쀸u} .tnY.D_w&D6 lDo\YfZ@oW[L7ImG?eS֬pVt7MB#" ҩ0id/_a~ʸkjw|)=쬊gV\R-TJ IGR#0<`Q]1Bx W|[$pܠC#1-XG`Of F#(96fV?P*{F~jӨ;'}d5i'~I9]!CV:D sOsILlՏLGz3$T?%7Î.ŦbLX+fjD~8^G#y<.\|c6;!VW{"!<0$jVCOd7 @!C0 K(sEYmJ1ƯMΗ 9F[yѶPզNԺG[ڪFm6ۚNPjRZ?C_\p*6[r!%t܇94$h BHh122$GpnRF_L9dzd >H(r'4<Č%;d3V|]*ɇ.]e8Iz=[%V j+nYN4")$(f>o 7YU8XuȰaιBx׶b6s;M1s5uns;74ɘ*U0\ܞB;9\cCUn* *JmhEC^7,(sN6n>yoTԤW-` N|ȝж?+=Uo:4̠}%4}As3Eg^Sڜ$}).d͂=+Rs9>ݓc}.>"Z( gɏsOذCdB.7xwnF%!՜L7n,';,b< *~ v4 xV@@BYs/ydaL++D 4=v܄C`.A+TyDet.7t* HJdԥ][ld]6[ZSMeܕlQX ]ӡXEJɤEqiPtN0kxo@oISFI[.[xk=tV*v.H˟!D'{Ceɐ\ǩX zPZ*lf5|!~7)3ew Y+J+nd6ϱvz97%iWl(ON@Xd9 t_pQ<~{:۽?qՎZ@vkKh>kZl9l4mBZhwZG[J Z }oհl^Cj'P¯*m +> *;7/#]kTZ^^iyieq2d;bY E՜jq1/ѻjSxh$Cu~q_&m0$8DCNK,F8CaD{Nn摭*< *uV`AQwE@X_uWʗ'yC$*R]eM#ңc^X\(8txM 1πTT֨ꚇ:p(G4o8 ǣD^Kܑ?xCc^^!9t;&Rd {,!y N?i߄3C  yV>H#3G0^WH{Ąe@) (4Pף^63AE&:FUtMKNFeM 4vһT'= |6-STqJnO@oWg~tEd՗!)OʪKFxG~vRbxO&{$@}*QB2rgsv|5<_3YdLuo|4q1MtͿD7ᾙ)uw^yTmg*K]r)riJ SԴ#]>lf~&^v/͖80L.%Fll?+g=;5so;Ir{-m(ޘPc$`'/Tj 4Y?`W^`A50E `{o,w^Z|[ǞvnlPZ9yi5-ӘK@ 7T")}oK4*@oǯ@4G(E"D|}6. C{dP%$|_"nBVJalL/oJ t״=Rr8p+B4bzd#:pϚ!=CYQ}ʥ)-?mZoW, h,/לT_ <11f͹L|uk~\n:pq:/zS $gTx_Dije"فDLMˇRonh!>=cj-Jt(ìj2ehIl/~$ۤe6hrtR4Fb+e\fkO̓b鯱@>ng2 !;a.$vB2{=q~[b mM6gx=֡3gs/L ~7w*%3`IlQE>n5P=JSXEҤ@PGxRF1009b(g@QT4r>)K!9G b&b`k=:9[wevEaPw&A|TtC|IE_pyD냄ߍD*aA_ Szr/==dL ȫ3T8M۷'`{'0wgX{-)O$_H^y.Y|Gb1k8&WQw/:'/\,m\7[GAaJlrXgKh(MӾnM#kěULXWV> +D,ZO 옌=?QJLcZ X!jS #L̮w)Ck\>עzZ1r7xqѯHqE[C/ew1=uq6ت=_V':GjI4&} 0Nr-r3UCsuHZ儮j::|GzT8:S| 1NL =?5ێfѐѶ8|DfPm5*(AIjk?!Z*+373XcFCtBHyt,kZ}r*;`5NK UjWY5Q1j0(f WT{gkDѓk¨b"7hr$m_9$,M6В↉M;to!_A̼{RZo;^ӭ~x|E=ev'N7`Y3$>te+':'۷mS\: 4$#ՠ2P+, #o! C)ZcWťkH0䇉4 $+dej:G?a_ARXw)J∲ϡK,f.qT$\C.b-ƦF쀑ο玾dmg5zA$7S&!ʦ =TdPžίCb9kf6x<x?Bg,>593?GMLf2;;B(NOy<)I u`HQnn$}m~ڕ&\_ {3:o"*!?ŖwgE{2o(2pg=~e)q҉^m<Y1ScT2(-]5ws!]aFهeB{*q;t0ITXi4#zr֮SӃZ=!Ɵՙmj>%oA\.5Ѿ _-6$e0BC:o/kO_XS jk;vt /"[5R+[lj\7AcF' TdgaC d$::ݥ 1 xΡ]u!շgombL2,iNSmpdF^iHPa^`VЧO/;VS߫U v'D^}'>R}qdH-c%G>XS( ]5c9ow% 8 qIjl+^Ps1ԣGF\Һ)Z3Р 2 8f 1"NDjxN 8Aa/?܏Xգ#ɪȢQG 6zʊe(.\/ 6)jI8+U_DEdw;~MJ}\՘x!F~wX "cWom$hEVߧK<1zb_]p9:ᏇI a'Q= q^,l*FIm_HNqw@#i Y=РamkS9(ҡ̈́&$)}&04rƦhfj~N:"kfK9IŃUqf;&b;mlRz`pli:1"]Oft>.D(Q&QX,<<5A!@Ix` ߝ֋F?zDY: ;0%iS4ۨ 9̛A57ssVs*W6~~Z;M#vMHiy37A wSvE~^mGwnܶ@m/wW z/f=:b a3]mA˜ )WPָ7X0w.S7nu1l?d(cms"ohKZrD= @ih3Yޚw/-[,T\ZɵovmFLK>>~R9Qm? B L9.񀜾"4GyzpQC/')ցl:ze\=tu 29`E9ϸJ2QLB0:T9Ƃ5a͇wvz3~ʴGi ^Pyf]W$5EvQQ C+жl نKQZG'{=v)^c1nNuVŭKbCB}d.E Y|1ŋO aitv3vaɗ@_t?d_[;HET;NJ Y#]1]FGvyvHW(G7v(tB\دBliVjoNRd2nҫ4&2s@7܊9q2|K+CLPf)o_^y&-ї7Y7.<0r%.##@!Y8YAV*fO&^JXE6aZpb#.~sn#׺όRq'}Vp͜l|k۝R]?Z=1Qf.E_"-q9.jO ј`}3)iΊKOIŹu:X" m[&coc99mH ~Z9엷;%;Z<!j'c>ԁ4"k/֫Y `\y.1b\y9Y+X<*(trT(&G TSMĦ(fR9;9;@RfFt=UfS#N|gxm͟&̸ǡ1&zɋIiN4ZyFWq"yJE\0S1`\wا>LuPTv,ZCXPb@M{un/]~`֕,s/ ~fWՔ Xc6R]XﻝyL?]@BIpU:44xjcscoWr_!IօNΆǛ0{rU6HF6ЏJGT>T B`nDI$? 3 ~/QUD6s Rm&j]CSrQBBkdkAS45,9)j V<0+n'•">Y̻fX(F?*DVxTI~rA8!I1j̡hgLrNNbU'^¤?tԅj)wȬ-IqP($v( ш4lP-Zzbb~W?V%1P@6CCwP-FRޚQM5lQc_hz=Ҕ-eFfzEO}}8{Up{=S_#0Hv攊qEMKnHr\eEY4u"XoNzᰍUHn205*ׄ D| mK|xrZ,%_wɉyN`^Аr/Ş1s YAF"*S ug!n>W|2kkv|X§ 2i)8; Ys8G45|UgoVlYºo d\9eQ{{T`_1 0AAȂ Q$l;tjBŝ8DW:%#GW{v1ˆ2G4M̪W\87&('<%o^5puvط[~ՊOքƓ0F:g}&\dP bLأ>|HtsTb5򙖢o'4nL9)*pQ+Mu ]nLi?9:\!)G?eC&Ѱ488b Cڍ7wn{7h|#.EU,<uՌ;(5Kd^IbE(]0*a0=cf͏ < 6C5 5(,x5/-6;|>X@ /(, fh[} 9v%YtTUUw#h(kj_*FPWٲOnLuY6dߠ#냦j'2X>T8Cz: e1 4v Dq5.X2 ` u4<;R|9O|so;V= QTgeܠKk q߷I2h8hMU]y?:xgv|ehȿx6c `Mi]K2T)~صݔ͕i.NҔU~qd7tR:@8,ܱCf()vt]b&k)_]Ц u~Tk^n$QJX=0_B|b;4C=iXϥ {X\{<_HێMQ\t;V |N& ^^^6|[  줷`0N0l-`X, WZu7>WeH._W_Y3">xOi6mlKor#~rK*[:ѥU#!t]:==O|)[%RI-Փ~RZzK*r!Jyx6T;3ſhh`%\M+\*-FeQ.Ξ޺[%ɿlqDiR3XU"EL&eu ߅0YD1,ߝ 0 pwx)S(_3?)H-Cm'o v"0,~~PBڀ,p֎L6MR퍑7$͍Og`Fkm"d眼(-Y @o=ɵj$Ռ셾A&:=*_oUow4<|Y˵|*4 zZ͈p&d땯\Ͻx9y#@59) )Ld~І JP | O <Yx[bJa>)=ɳvlm:c[bJSA1ڪ\>LR$~*KOeHq^(W [6?PY3cVTzq'/NȗG"̨p5PoX։oF-:NcaLl2eַ^\($,ѼSo2#z[60T$yv#GG{UgqạFٶpl0u[zx<ȱA/O.-@:weobYCJ3`` /v&;ӽZ[<'}zPzM*v|!V lOulGAѽXwg5ѠDO/ul_>fT7aov*;w_Lˈ)via6TLQC wrѦ&(.6Rm$Yq,cN.韨XbX&A)]W2&qPk,9|BQ kM@$cјKxfgL߾T  @9sD9nk6agL*5$ldn9Q}X/j'\L@q[ e=g;FĿ1qIiDDRɫ_|"A"o$7$ _(ShjƼ^;;ì#kJY5vS8A& " _ba>O,[5 T>Bٳc#$hʪ..ON) q9  ,n<U e55eM-Dp%8f0,sޢ(#,R$uT~EbFh. W1p^*D)Jj IĢϴfm9Vki=fGZekޘs#,yH ȟ\fҪ2Pw|まE9T]pGvu&ˣ2(naě\_woMyPOy ϣB#;]<3PS]܏]KnoDǴE%u?%ծ^-(,mzjElUk{*h|ؤ 5!ڸ;H- UOt_Yx?-AڡkDZʚ?y,c#{Jؔ·ٍH/|#ls5zڔ5d^Е o{o8[58c|W+vS1| *K"UteI؉iY삾&Uyٝa)%PhK9uᰓ56wA"g #dK[y $@kbܿ BH\q&̆( @IrHઙ{T݅ﱟ羹TD1=eM:D2]+/j#BrnT>ֲܚ)`(oykpCоj1hM'ϹjPL\|~x;1e}뼰sU\1H,Q`c)J8pu Y|@C` p\'beZx`̢S8y3iv)UKOې|g. Qtcr,~xJocHL޼;I /BFPff Uƞp"J xګDkPŋ_zX)o4d8u͈B"HE[Fh~sQ~X}``SbO[fTDꣁYnBe(Ɖ3n|/' ֡mCX~b㞗8[%06>) k׏jjOGݒcY,G=D歟Hf3giww{P<_ y'5ޫc P)7^)SIVٴnS!p=,zw}|_&! ډߗQOs"(eY}\'[J#JKb5.^{@ u| `E䃆 N!x{O| :([M[XRm Ne+{W*\e'$;r1W{]ϺE^7\jA_7Hl?Jf(m fJO9 W ?g/鹷Yŋ-[bVӓT@Lh@cnt'2w|^E/nɉϓ̄}%0L5&o2>۬6%@t< Tf&/|J)YLL'{f3!-,ۀ#[I "ֱFbʄ&c>e*-He/wY_їԂy6t7C<-F.+&*[<=]+H)=\Gs_B0.梈 iw-lzhu-:5!uv$Jg*ZS-@.qELVOg. z&iNt#Q ,Ki|.vZz_#fmߋIY/XTsp`ȡ -Y0sYO·?HsF6 MO"V79׶/ڹYnDʝh8n+9MWe1DVDIZ>hm ޷sڟ=t$.D#b$pC'[ 0 }'F*| ~+aKJfJm?2PW#DH6K7|u VJN6ܳ13$28כt}tgRL椶{ Gyfzcx/jVTv/$ l6^BoU.J59Z'ϡzrݗyoi|K?5LZMQ%Zo;K9d(LFyUtq6B ߹Njя nr,´mcCc2H9c!n74/a("5H7fÅÑH-5`ZfE|>l~f6e#ٕQ_H~ 8doAdZnGy:̞9id&Z$@0[a?7=@yH<@${ggJu&m&R}e]L}AQs=f&;Yi,s|":zN6 kŀ ne| wx,pL4!\%TX-춃o0F& q,7 !"y0<~/D|#\.+I1 _b|NetnϜ*RJTt(Dfb5E MKCla y~Sa7r' ;W{nrloXgHAxP&R*FPdjc ?m zp6r̼#tGR7$-ڮԖ;OH#ˆ Ms˜{;Z-oz{RJ<9`:O (K9ބpweϛ_y,q{m9M|3+;@=Ohf0$iX֎ӅLR-:=.x*I$>5ƐXY)GiAD$aymjJ;u %WW^ |PE2^.LE{ǚ!RSq}3aXsYJa݀/i U.PIYP/͸g[:Sl$tR2߇ANC2Nv@v23(ͱ ?P P+ [E?`6|u %{DSƒҍz]0Ƅ1wȔzuksHbw n~`bA.cK]]ggf;LX`Bv1W EDѧn,Də- ߢò͏|ՏQˁLkk)ſg }?#ֱ }s^ˠeP" ;1fjk R(rfArTTmDòdAX^I\AHdovz}\yK?;@okV9۫BEB5rN%nǏ<[kysSNd -30jŲZ8}v 5(\: ŗqꅶn?˳m1 OXHiv$?#jQx!@w6uf Vfj:U==Iâ.N|cKZRW⟘?iAa܆4bު;qʸΪyiJ>/] J% UeXĝ[yKi|WJw03`n1R >?v ]5"XpM;t9>Bn[Ԅ~{od8" ZF ;T_֐7T6] U~A Ћm_B1jQʹNh±$4e` v$5>j"aP#čry̴ w?SmHOh,Ĺɩ-sո`֎.v滜ĴP7#T17'4ئMgpWn7?S!yS~K;LK[TwlӲ*XCz%.DGQA(I!Ɠ04[T虾r$NSHg˰p] uXll蜞!%EKϣ|H#P uh0W/ڃI>Zs2C>MH=mx\41kMl%CZ'm1C=2H jmKllWίiC%KKàAChٰP fSɞAд2~<~Wyt|t,f7d@ QI&cvw\n\lUT,gNۑ:t C/{.nVEsj<|__CI ;Kj1Tba=2sqT4yՕWm-La* yә?`cbQ24Vo|W!˧I凪͊t!vj]/6(GUr.:gk0|nU_mYR&\r6!5Z*H9v-s'8Z*M55oU1ύo 0 koZ&N-Ne/dhMhbq(3(PgO\KK.woÊM˗/3gDds\*cH %-ً @ԧhS)U?O I?`Ie/8r։bOy1=e[Ji.%U$$$ě|x^O%b&«<TfY,!LNw5R(_(S6vjrdٗF". IYBuhH*%AOmt>"u2s_fxV F͙C0 5HRg}sږO󽐹9gO_Gة,wfYՖ!'E&r q&ŅQq-ݹP"ң7h9RWJA|xKkTIUDF=~LO< ߟ CP} 6Z-_Sɲ?<$yXU b-e kDk)JĖgٖwQxұljhGF^=eEwyGz^ҩ%#hN~?]Fvt1-LPUs" ̍- OXgp|\('7$}ǟLn }3FDp{u?cwʮ935'w<;VG몮ޜLQD.ex~TK`3Oa% ס51Pòtāj0SrNVvjC;E«_l݁K )ja&Ffj^:s}7Aӊ"]reX?ރt˞:48?4#DW/ `̲US2vSHGf6{ӓ[iA ѩ,7|*t OFcP đāE֜ ~yƟΝ\yǀRG4-Q@'"eTYew.yq>鶮`Jf\cyHXy)>sʢ_.OA-fy$sSV8{t+MaOw\ަDcW_`Uhbh9[vt=)d/CCW=|e$c;5CY)ծrʪ>ڟs>,u7jSA0;P\g#0Bk'D0qյWaD!\"yU +|Yz DYf'$Cש+PTh$8yK=2ЌS4Oh 5`gί2;+pvge,*DE-BnGm5,ZˉWܜ-24 y̗_EU4b}`i{K&Ѐ_J$Zb7kKoBe`8|sz"v`Gz>R`@ީz CȪp}]yvEn˨\_" 3vη寎OG(.z/16q=^M{/s䟖~kvA$S*GW:=\p+ePfl"_ .E.) ryآ UxhpB܃C +AWq!COhHݣ}3.y}ʁ?KK w)J8tY"M7Vx.Ee<, ҡ~adT*BP^r!QRhcABYTLX2]Zab `?7ܞ.htJBW悂C])AL@Il 'v`<t%l;Ugnj\ADRaET^?R  F,݂)_VN!{h(.NG111^Jxsmkkjuzp1?(F^+AMةz&B~rGyA%ZDOsHk,vr{#.2lAߞ)ge8'у'ͼxTNl說{u6DGKBP.|ٝ'X[2I{:kGd4b2Km(򰜊eٝMTWP`31۠h~;)I/z?޻H'?N*GCLX4աlq&7!qagΦAf^pfc(Bɝjy߼u>q"d:ĆK3q #mi<#~}/M?O>xW3ݥ)컈 P j|N~, bw_q!;,/I 2e,xq!wyѽyIK?1XrԨJpJ@β@i/8& 3!RSqh]4^[)˩ f)/"p˔~MV+o?gб,LoauC'CY=cFՒb:ښm"t(Z<^Q/=@0.ud]ץhxcsc͋+56PYwitu?Q:6=ӊΝ& Wm: >\iͼ6Uեիܳ Γ:L9:'^RL!1f0Mn'eN씝ܠI Ԇ"P"sc>?# R_g8a9n%}H$5kLu$u[IXtfZjWc$p/guLq08ީ-ʲ SE\ZuN.F~VO#W=g990#,An'r SuNRD}:A~ ,x/,WaE"J(vRYW}#Fif1CAsAö 7bkPMKiB2ۢߥ߯~q~W_v»kVy4^%a΁% VF62c)l+n0PI^y5 /_ #JDuރ&TY.F'){o\PiɈdm<{m'^hoJ6n ~*Dtk[5|Tj%(@jB쎅f <;PKgƆ*VCDqKef|7'<]Fp5W·SDUW?&ISoy$&OyQRNIh|-#_LUoݏ#w@KFgE*#{s?E]ӗ6Eݽs< 3UI<*Us:qMj[͆Yoh5ԣ)1JwGd8T]$e7|~y#,7w ,֘ݗKdw%5ͳ&]޴[n\<vX 84%,53wYeO;Fʶgl'գR?*0L&Vɋb^MtU*kSio6R- }ٽ_>p{> 5R)d-{֪Ro8'ZA{CD## xp®T}Zs8 ՟ 5|b\UGx(+oˮzؓ 30&С"RTa@k rd ^|Pfb$(Gg0Ea99k` JeDƲn9B"+#GtKe -Az!Wy4BL?aPꒀN|A}' h{6a14(e%#c%+Ѡi:nd=uj'ۚR%%nwR]6ZI ?M#̺9ʠ8I&G^+`m bI!"z A '"H:,1͎%tKVԳq`t & 12T!Cdf&5Y6 \~J 5"%1xuMJ1ǁL`&*yn.W9?o1j$-d_[HbE*%ŒQ-]l'WH,Mo*_'TϻoKֳ9Q+6/Dy!{uֱge%% oг &k.1݇=CI<{m[mfI)~^K|-{Ebv !U/۵o$==:ˤѢq}zn:NҦl}E/v,+Ax"n{K\?Y.aFJ>̎7 ]oVSvvmS l.Ey3S3GPak tI_eˈ8H6*#-a4DQ"Vņ3a~-kAy4]F!OA=[eN4vV+=$VD(TEU= Ef뢞XJʼ*c8hJc*=<c1/#>[Uj оIjSOCZ ⟰,}o齭Kh8bhm>^SLi֩~v*gI%4B0թ=6o':k4Y^%LPA˸¡lLd|S̙gv<4"K?ׄN:Y@#|4E$)vb3chEAu+=:EJuy+ʁ8M?Q98>k{o^jҒs}'g%437~2ieuuw<&L v.a>LgV Qw-'*\ V*3UhܔBƟ* VJЄ)G"lľh5Cs5mO6fy)6lZV"ڮ%6HI&دYlYM[/ƄjP6iZ-mQ[J/oD5`RZŨd*l̏V*`A;=/7Qu#gF/eYgxާx}\¹4 ٍ4s6 ?A!Zr0 Q$4jv#zZn&z_̘ȀHY,5 !UebkyI&/N"'>.]],C &MnRђ6D]÷*d1E-’0ǎM#n(-!-x]Dh=lՋ<fMb|"w,Ruw LWJFV7s]&qP.r2R"E)T8HfpaIXxbm!+Wu9`ebRU||{*JsAȜS6᳨3dYƼnwAF*d`S?γ/֧GG%#夳I7d:ٝ]|Tm$&gEԕMWHQ@"ۄ <k_z8Utlp շTDT:HR}Wl['ciY6;>&ni5A`vT~D11gf?& ֹe#[FIMe@ "ǵhR׳r(,THxfϒ؟'ѿg2"!Sxuܨ'vؼg; ٌƱ!~¥(a~x;`ƮAّzVnd *eBtQ׏$Qq/Ee{$] ۅ $'3/± CXﮨ~UDSm[9qf !<ctGݎ4lz$7mu%`ZܱeXNJrics ?;"ǭs lN VYF{=`T0`vhQ6~|ͨuN2`!6IY|mBO$+BgDf"C4YK;LJ@h6NYTejs?wgru2'v Dkd׍5=2G@}.ܿE:߿F}a2a1g<KdUIEw˾c5euϽr2Fbs:[I0oJ\L(zpnS:jPusĭ2.i{gە2q 3sBt!d;i@)b|AÆk}}me>=ax4io.ǩ\loZw/BפG©W1[Vwœh+fmg['q˹bt; _ࣵ 30v Wv^m9Uãz$Q n ň36O΋:|ҎXDTwQyS"6 `西% f\}b70/PBJ韲)`_B7B\շYAu\2fjwh0'ktWS: CMtn! ;][PIEZ%rw* g6zx? =%(0*Q{PģXC|f6y *L g (7G>f$>[w6eK$U9g̠lH Wa`H俞w0jy{8P6LY05L EZJ`w8?? j RiHG 2GAϙzH`#A5E\f5cȬ0?}vL9&EܟN?}EQX`~<[D'9xBELf]h u{ Ú53 nNRDLt$KJĞQ*7ȮZ"H~ZJ>"TI]c8S?D/uEOus=(^ST PøS t`rꃂ%9S-S⠕gg9k>5+hHr[qPTE zZQZRlt”kPkcwf1"pY-E~y;xu#Ҡ{v ]נJ3AhLJ_%tKfv%O:~Pk"rkQsň+@E%?Q R+HU]*F2zKVl-r!C[2Ր>3 ~QN$wo$Dzՙ:{TG%c9FFB5`р+"6"jBqRާPJjQ @ TTypg]>Hhe%X dv'PʤF5۹#璹ܩEx݁ R,TՄYLO%WϽx\E)I##n% hkhy~xa~OE߰KϿ#!ny^=RwVӰҐ)U#i9#_9$}b)mgf+1 KQ⊙~6W* f~0WZZ.[=,XyP]!2ZrZ,w$Ӂ*')5>CjcAfގfIZy.CN&ޯvDSv /&Ɋi,ԸjA%\!ߒ).N%1^$u鷗"HY+x 2I&H_ //YU5@X.9wB7TKuj~7m{ki#𯙛 Sc]zOGVS[{^y"I0}N:D S;i*bS D Pb;3v[ry+OMdO뇠NU{uivT2?z%{$ɳX7\3rK<ε@ 7oĴ"c.ID;G2$J':}kU'-2ൺgǤANp=5tLڞGoCμJen8Ả7GO{l/&ڍ!cBlqe)$"6Oqv!,OHd19f\BٶJkgU]8f_ U&˛ CH22,kZ*QǹFs?3#7{.Rt8܂Qo")rr'[a yY o "$j@D{n@-F D2Q喸 "0HAyIB׍TCF ǮfH<~a!bq4_t;G޾`>si=F~†u%;gYGٖuFS[H* xh=!i\NRY10j)Any T/BLR}o']CGVϒȴ?Lg bˁ tBDPitp „2.:(N,/ʪnڮi^m?~@a}_$ "L`./LPNo0p=< NͰ/nQY^UݴD0Nnq:_N@qN?J37./LPNo0sm;~#Ql $+e;NͰ/L>SJl9j;.( @,O *`./LPNo0̖Qp=<}| 0 #(4$2J3,6 "D*+*F&j;]\@ e\HEtôl $aec̵D%Ri8._Ǐ_/ʸJca'dLyY=윷"< 3y4pptb|AmޮY6>CŎUr`5klDϲ~7Ot֔dJ4*P['6CH fnֈ5$*ϩRƋZ45 L+?F\$d3n$'&WX3@LU~LrB'oQrcFMy#Ȟ`ؓM$5L y2 soGHc#ac)uRہr2.03IkP#yq+Sg߂[#T ZP#81 (Gb̍80Y>]^C#U}#p!nxo,ꄮ$%Ukm+T9ׯL"OcśP2{}4 AYx&Hi}{g_y ]]H (aF.[T5)xt4O5uZ'h![hb|Lr9##eT7HDc u N(Fす?747ҋnܑ?Qmk /jÐ)ނqez F!p]^ε;p<1sG'pKۛn_>M M'^E+ WTqrڱ<7ɲB#3a!mCO)F76MK5~ΥVx" H.NPGhªcmm-VY\Q{N5@uPdj¼uۆ?U5#[wΡa1mTg 9PT/M#t\rЩI-hkk=(_r.&@.6J|.a4yN <T]DY o_;0B·oT4 #޾3>Y  }?.-Im]:xT{q(o#爏Lo̧kkʀY aگ Pzg`F[iMS¹Fd-]ecI)dx{#ܑ+Bo}(?B_ܐ`krrI k^߫K "Od$xl1a7_*  l&u$59ȋ7=c]%zc^Mo{9AyD$!++/e?p~7"34gq Ci3,EѢ3)ܜΫҭ)4i0ʷA倍X͇Oolb.%׆||StA߇Of:Ȝ3]CimW%T cn{A3>3%{чF.JzӁN%JAc}TMVsTa~%O/יFoMLR9wz,_^%x .Q"VGw:Ȇf [@h+2q,%Q.!*[f4j/b[ slnF]VEpVm$v&PR3@S1^ཝZn4A * Gݭ*-HTcd/,'>v>&^OxڈDz]=|WJB`jA@DPERBwQZN=CK"Czn)= 2z(I%0#'+7V#lX@*o07*OC ^C|JtgĤ|ۻeRP 4tm{>1kgDHv3܋P +DCΜ|F2N0 PCoD<\r>8U; UMXw}mKd2#=֘OlUTnS"<3 >>ܒ~6qM1B VJ $' Q 2ux:N[ Z,!<&XJo|R%姭sKbAHR`&s4\x9Z% z3W"i)i[eפX M0廘}"⚲'~2*B@K)6r`ݜׯC b44Z,,\iSl>99-Vg_Ǒƫ$OwAs+v DeAvJվđ4Lԍˆ2Zj(~ A{I߶=vҁ~i&fЦ(2Cק|> Q[ pMuI_kc=mkߗK?ֹ*PZd+nmٹ W'U)em밤G>TDθ+ӘZSQ1V{  ǭ;eQa>b[b(T46!Ij-bCnnA.|PV;%U;rVؾ9`?e)w޻  O9 5|$䤇2Ǣ[M:vөT8 /3ع`o܌s,6F)R?_Ja1xZ:)AQ׈6b"J T*8 nsh1ۓCCV֟ ul0kw^͑ #BãI)EuIsO>RG窵8(̾80Ru8aueo.gQv? ][&m1S )a"Rk .eDn~\37$A0]+cpH<*x: >Ŷ!]ѥi-qkNT`?RWÇ|NGkh,Q !aU675%NKP\Kh!C,S1^-1|c-PFdi:V "%~"N(ZJo1Ĥ߁G n efWى'29_SP9pCwmgRw=RڐqmDH#Z||crNFE+tzj:f)GGová3Fyw:Dx n5+r+~;*7;/<vPy" LZD}'6lNJDZIP@(=sp؂|+IҺ-8\qgpa.Id:MƦ,Hcbt(M"G}ȶ`6|WĺevVXcB V}UQ^f9оMwia+wS-|<& d /af[xi0)gUSE^z(Hg'}[|N Bn '0e2zI:b1C7QL:p۷B{Y{;@ >C<~}@-9W\@R掄[rhZ%JwIŜ'!4c^h4[aS}_|nJVtpgzeal-0.7.1/src/app/resources/browser/welcome.html000066400000000000000000000062001462517734600220540ustar00rootroot00000000000000 Welcome

Zeal

Docs for everyone

Customize

Docsets

Install and update docsets

Preferences

Adjust application settings

Get in touch

Gitter

Chat with developers and other users

GitHub

Contribute to the project

zeal-0.7.1/src/app/resources/icons/000077500000000000000000000000001462517734600171655ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/icons/README.md000066400000000000000000000005731462517734600204510ustar00rootroot00000000000000# Resources ## Dash Type Icons (`type`) Upstream repository: https://github.com/Kapeli/Dash-X-Platform-Resources Optimized with: ```shell find . -type f -iname '*.png' -exec pngcrush -ow -rem allb -reduce {} \; find . -type f -name "*.png" -exec convert {} -strip {} \; optipng *.png ``` The following icons are renamed: * `Enum` to `Enumeration` * `Struct` to `Structure` zeal-0.7.1/src/app/resources/icons/logo/000077500000000000000000000000001462517734600201255ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/icons/logo/128x128.png000066400000000000000000000551501462517734600215760ustar00rootroot00000000000000PNG  IHDR>aZ/IDATxweU ~i} UՕi !  ,<=~01c<A&8`1&0I \9pUH}k]]pqqqqqqqqqq_|MN*Gy-ow^~(59'Ҥ01Q(֒R$ҦXr)BjPZc"Z@Nr 1kJˇɉyrf?mq؊v!@kvѣ 1̰MgP* ń,q{2 bF,e hr_Ob>2f]u7 CGN* {C?0Ήŷ΁g/~˯.͓2/]差<'O[s|S~#|wwŗ]?GswU|)?̣unco22GW,_}W_u ?znYq/7Ox[̃r9ǫnMx{?7ξڠF+*fu;_ٖ~_Ox窲59C&S9KL3h]]9 [AUccO3} ^F ?'1(Wa8 yv;Q֡%n^}!&w8Ɵ|m] Go;nf '>|YA9.t=E\8\?z&&Uغx[&]U7/}xӛ~4?&Y6,Zi2:B$2YG!bya,ޓIM視x1;!@N++17FrNʁRD(k)cFW׳#֠8[,FU[ܣLGSFU&n/|C BTe Lc- 2EH9 I.s/ HYE4I>`T¬N awyC{˗[.eoI5M?RU*8gH9c=1%B y흷&o D۵5AR&k֊$,%eJu q$9BA1%'>3(I(IZ"} :%9(9\b{jrl@r(9JV΃ jȠc*M$ő(&S[I`;?H?_x&5zw_)|_xBrk gX5PN4 RJ>=$o>|w޲u"nw, hdrjb>Mm'3>''eQ&O>` 9C# ,f:A)HGJGybxhVc ?QAe!;Rr /e**Q&$RʐC!C@䘆({0#!pa}!zg9?w yo:w]_~am}O~tDΙ埻ʴ9K@դ'N3-?w`%EO }-nu"k (NBΠ!uշ=!f0 ]T9DUUAA` Xn:$ǡG.ʠFöG7*DrE)MΑ`V&瀱NsT =iNENYm ˟$Almp% )sÞü偧xNP;% 7$y>!tnmRRʤ$7hB)¬'Djt]*#H0%bBD][RΘ9+BjLDŽEkR1$5`5 C{0lI\OG3%?$M1"Jr ne,7O|sɘ۱#?kO=c('. FK hYKk4$C$(!E[{n6"R"L\$NLY2$fT*-wwhe PZ?f-~sUW" E.;vPIzO*%RTUO;QhcH>RZoVOyL96$ݮ$yƠDh{1>A>ܻm)"ԣ=?'?  C9xTE]K^,2C7xR4́3/'gI>+3tm%8);,m`Eѵ1L&$uME e5*f|ۓX! ;'¨".:XܾD$amVsm2<*ghnQ1;/5$LmI>Ƒ|Ϫv!n`Y_`'Vr/޵ߚ*_yOB3XS SaĘhFW)uXYmbAЩ 7H$)2ZHa%De" Uq%9hI Rr%*rӖwRFI(C6D$_$m3rޭ62$3+;ta։ۏ-aq$ͯ0Qp' [dWk ۘƱ<'ԇolb!b/'L(p}`-HyE[ p<~43̀2K[$Z\ B\ (QVAuk@kHXtRY#'vd1N\,Cns!ĹG7{tnjRL1fd n*p8DV@.<2ئ"+P4佀dF!T .4  rAQ;.z9}ĭMZBJT ihqcr ;Q4(yO-.?>DUXkh5ʈ&J̐ijK c46?ɰoyivcF g%-v ux){)3O,=eK#9oI>IL4Z.?k]ϊ`t)f |I@ԅB+X9ژZ&0]a W$9-h!ggx(EĮ>FJ( y>ͳ(i*`J1$49gB4}FKCH&ƌ/5"EzR@`JU/ƬAUcƷR- g٢Umw)VG>S7i0ƀQʉwP&e¬[if[b KʎJ mǫ< $ d%DL kY=јfTQ|ҧcMdy|ss|Z+cN:iC@)]эcXl)PڢMK =L>/q૿ >pqf|@<5O?_O~$%Њz"ׯE;߿~g oAʑ3Y|/1*q[%l-@k|~m a[yfx$޶~Wr Ukþ{uAuKǙ6'Hs 'N¯筏ȉ##nR ĮRq$z\gW1#'uɥ~fʡDO?c"^p KITDn! =.RzM^o sRgqxʖNxCBh#b mq€VƢNͤZ#3B $EF):Wˋؓ;Ο׎Ynτ H8YxqFKu%7*#y|>ƂRW?'oϤUҞ@ąK1lԻW8q0rNYq=YF7>0|L!Q,6Zm6@eP1NVovNBc=v:HTNP[8Rt?9G0l,*JZiS[g=d<$TƏEMaE"lrLkcVOu9xBawYvESFZi|~`*WeGJbbEpao[%*N+PcK 8[&!L Vrl4Aǔ3\]Y$GZ>t-_*] eQP9iVwϻq1㩟y)V9%wSerPH"l4qH%އ2l jb b?@cO}x/8@+X 13OCX_[X0D>P׆n>fC)ucc䋧=r^m(J-q;`kG&TZNʼ,n -ʔ~cFi> )1o#xH1aWFp~2 :S 7!$ؒؑŸMTm0VSϗH N*P(R&pn[ #通0\3X.9O%\MC)3O3CH4I{^.L7_ϗV6.#SI#0DZ;b'J2%07n[=L{vS()g9Af:";b)vYLrLmr5Ac|Wyw_9NϣFk=}$lva`L7aōݒf3j% /l)AJ @#§l5zI}>u,kSZaNksR(:S5' ݚ/܌[KPL-$X )4a /,ʐB)c֤Y$[ QAxir X@}fn?B'ɟG<TR(9iLeȹT U`Gj, kiZ) 0IhP4&Hp*tGq>f ~ F]y;XiF1_,P$W'( TUR*0R]xэ!)P"($h!s"^Zd 4q1zBYg',b& TL6UB8H/AՆu9ri AX ‹HPr`Y 03 *BϘ f{vR9Bu:&QX%qDGh أ|-ד7NXW21Apb,nn,X Ppɘ~{H9 1?g4rB줦XV|UeaH] -/y~)1]l'#Oرܸ"U [8@JCB(%h{ 4:'7R2#ٌFWn)QUrZg[]g45 V kBP &TPb7HɅdҠKfj)҇T.l j hJMlxncYn) CQ Ɖשh+y2Z/? u}!9cƇFh~gJxLJ&[me ]nxmϝU:vvIg)C `NJʒi ,TFl%}(AhޅD3e*FYUngϫ?Y`#<3'. A(WXWB}ҙj@UNnؕ{Q$! `F>Ҋ)m+($~\O۶2'ɠj@!T)SC{o}D`me7J5R-B- hWFd!|+ QE a`j0"cj?_Вt21{_\PM(]W tXk UFƔQ^ 78$pQJe[f'~kA31hkWsYl.Y=qe:؇T2{y?%=}o(zĈYmU0kq#;]]B֝GV8VJ:ʌ\T$FSNAYU/ᆿ-mlJŸX5*ZJfYk03Z5[$k!羴9$|_}Oe&xULc22JPkI g5ua(զx+4T GFVIZS9C=ꈮ+ԦJhUoӄn'aUF6Pz15d:(E% }7Oߌ?? )wT"!6ܡ9K" UZKd8q24mV}B9VF[NC$ӄ.6=$^ya膫hΡB nz 6ʒ24Rs/M,=% _E2K}D9_xl 3 8d'Ͳq34wq7}?{>M_l.><>bGV}jsI!㯦 CWR0iP@WF# 1$*:O Cn{)^A3ŅtBFΐ| Bl]%B kE2Pc+zz(~8#>J2n2֯/}G<~Ӵ;o2z{ۑ-)co1N-O׆+g>yh%1*&T!,pk#r?X¬I+OX,G>uUaCu5U^ƽw4^FvZE;C;!QU & :y5\Gx wu/%:_NLeWk28!2 ML ^Bjn.,y'!]Ѝ4~%B5]+6FJ(Zy/ 8"=4%hNpF KVZT<$: q@lhK2ZgHK *r ~:+D ɤfFӌ*L!l~@2~lNym>H:}oQ|! #%}Uk3›_ w4zN>ɤ2 7O_5\'m'Iw7A0 Ƣ+Q7e\cD+ɉ؄ :v; J'Y$ZZa Gh5X|XJR L7#k y/6 Hu$r!@p !den@kVk"3 =mڍ ꧴%9DHNI>aW9ᵄ?Q0.n`r4f yU^ƭU%jѦNCee4Đ=u0f%=n\;6DLѵ(b [(ԉs(6Za ]ĭB0j9Sf>@nivI'/q5!]EB0ҧFdbLԍ %_J!DvC|OAHcRҽ¦4T#-^5zZBD/5xuĘŅ _,m3}dv[H>Qnn&l" )y R2QNHy\I@I*t"SŌJv '{ͅTO&l=# PXDkp5WY B%%ɟ[uҝ"qʙTeks!@QZ]QXf_Φ"S2a4 GZnɬ2n o$NWxJc*LP`j!2)3$&) ru-Զ!Qk슓"b0CtI1Di=L#. %ڂU/x_ k$%TZd3;I8poIޱA) .> 4ϖj`%ZK NIzb嫹G/*}W3!), %A'>@H.'Q6ZԺLr-R"A.w}!+vwcE2exJD38XL-"0\1#aRJ JkV_vnb_pr ("f>P WƹBLĘqEgvbO=rϝFbMV_%: >쟂R[~S,}6hm<~yG?ߒlkm1cʹ!}ig'$ =v4DP`% ' 3rR94;Z[:mG7>~+kwaTq:̊=ì[w@&ܓ`4WSfeX̱iʅ} WW^yqw}9q%'xXoYmHA$UX̩mlmeKFT" ;E'!IY$6F]!c#*aVI@GI"ݤ^At'>8?-}#~#鑤>;7g5qf3CYRqr(8f"4JY1)TN ĺqy&vᬦ/wIЊڄ~",D_x(+K6#>`_kO?S?rg+Z!,"-JbV\:iBe8T%0jUJ8D)mL, No4+5GrV 3iӊ8DXlmcԆEt d\x." %(2VG҈g5 ZK(0RH2U3"tN3HH,RJݻw3OhXfErp*%eWfe i{q90 =_MakER[tI]ؙcVOQ\DA(S)R7PXQ;1 󡨉I{X+7jGȡ j8j!ߺXʈ)SM\|jb$wQ; %"ʴTqDڴ?1Lcx`S.dPJ3jZŞ.Lj{6ӥhtҜ!!7~ŀ[#opǮHZ uiJ+ГUƷM=͡{j{sW~ gO0{AO>ą?y7 3'}{\\_xnދ!S7!gyͯc8߸/0`Jܬ&,r牬2˭)NP0ʅt!3PAL#w2.&79iy3ηĽϷ?%Ra!LV|H`OOq~8Ss? 2T+c峣@3 `ކW"M׉[H*Z XkEw.naD,qUeƜH^H| `f8Eh<`$dfvD d'+~@?,L )ԴO䤤'$O@- DXr݌DsV&zߡuw}JDj#/? MK4մ19`"9kMEb!ROkY/ X/?jM<{*'\ky n a3UeF)USڳ=$ q!qڶ2Ʒ^<ŠuWE\#dUSJYd"T#I6A(}GHcl7iz(ܣֆ;1YnŒW@yYMϷixb C.Bl\L9& 9 U-C][1z~wc=݅JOb6rB-:vaD2D< jK? e1*" R2Ӟ_P5' rAIwrh5hΥ;2f~!2,"n=H(YǸK+bb'- gGD/RR&ؑ+Tk51F]dЖ~ՀxuT!{H LsJ #EY%(niEΙ~&b V_wl_Zx, M^e|81l ATmy뢓c'KPTb"1ۆQ*@h4dR&PzCPݕ$̹:I%#>ʤ ide]_s2&RV"g q)_JzEe; aLdX"D-zj(4t,(EWT/a]]̘vW tt}d/7J9e\"dT}ۧ~*HP&,]/S+M m̱%j!`BjH*9YBDLWb ['b- Tq3մߒjd.v+m-Ŗfb}*K-D!qw5)ʙk ~RIu0E v[%6]RkQ%FDE/hR [%qM cbWџx m4+7nY-ĎrhXciƕpK(c❟<ϟoF6Z&F<Te5zi' nLc4}eR[ODxT5~{Jfst: 4ui^.Nhcm* ]٧dD﨎-v'6bFznfLiE{M-ZMhŨJ⷏{{|wW6N5M]4j9;3|bz|\_$wv2[Nr!D^!{ӚѸ yX1^Ü||>>gu99Jfqڔ7Q5!-K t˲3Af9R(˜(z{>]ZC>d:~^2A Ҁ$.G֔3)QE 9{~J08DpRF q$̽h£L!a' ]=̠Va1Uc!SBgYt 5 BL z APb'B4+E-Jqϼ,|h|ʅ"/bd"(+.QHjÌ$P~"# i+֒|F-l4ɩڲ ٰ ;7t[mvT'(f0E(ńdL];1dEE2DDqWx-QDp9.,e9SUOa }UW[əc6IO杭j TFA Zڍ^j l(+֪<wOD P1d*[ 70=i\]Dd*^\l6z(Ű$h2 e&բ HHB525{ ٬%áh4+6ʐVQ" XY@Naj"V.<;P5F>9ԍG]m0jp˙~]&,܊%,ĕ02VMddEԗ4n$z@ pQ-bV;'xJ8;E]y=QEkHhk*MRi% S5(p dQԖj;ZC+IgZ.+UՉ^T<`u52>H_-졹vM]1_,D ;V( )ƐJ.|$V2}s֑o\m>Ra^󝯺ۯgX h_ %nA:kpVA{ѡ,e܂O?F_Y6r::_(qyYFO&C;P5Ea{z-{ kR"OF2psV8NJFZw|ivDLm/1zCZjTSYo2$)*/1/[O2h+Ҏ<Yf1+e? V+ ]c}?"MqWf(qec"TW"XUGQSrb*VF8~a|Q~ ||g͎QPđh 0mQk7:I#Z|Vgn$E. -%KO}Z.6e~hE#1e\cٖAвJ[# -TjW#PwF1:PeܡV&*M;TaXFQZګ^EUz!;(:W6E4ɪRT1Il~>v`Z[TU"~:RJ g:c)D2iS!Z泎E SX,J,<{rJY1#i֤XB_BN-=nbD9 ;=bfaj*a In6VAWiEvC4miA/I XE?:{Nb!s;i. K9mCXb Oͷ{7C#F]I S|ѳڞ#Jx9:֕X36H 4XB Kv$+iT%KsaaC̲o"`=n$hF20,DU>6mcdAN Ȕ[)e'Ҋz0#MQ IH^md*ɍ, a% U99 M,)e\sP}Đ aX\>B\YJ*#SL.$BrN/jݙ,zYhVZUVaFf>eh÷fjmPe=bÖZ7'( K)=.8@ c峖?-mVX{㷾E|!r]e"rI"Ϧ^4X4] 2Z1 7uz6dX7yñT-Aw^WtѰ7N:W<Ú-o9o8jvfaξmΞ7N{x`vނtp0ߠ{:%eV ޸2ǃM$~C}sau]ۖ;J+hzx/06q%`B/+W{>jDyE^xW`yMEo=}o;(15>nV05Tc!DOkNwqa&ApitPD"ݱǨei=Ns~ةMΞo)\n&|{ѣ)wi5 ]㾓o"9%)Ⱥjvdejmo=ۦ]Rؚj7-DzQ-'sŬ8>YOA `aҷ֔=.Q/g<:RjB^vvv"e)[,oeD1l} nz] Iȥۋl.^jf-oww8qhÕ4ƪsVZͮ{Y۵'NdscD#//X'uBXSR9KeYK{*YVKŘ34AiOsS'O|7i /ï2el/+H!cxɯqVӶ7x##.x<[ػw/uSSU5a<S7 .@Kj.ϳ#P.R@ \;{ pE_biI/dz.4r@*w>)ˮ?c"X, COӌ軁hێ~nXz%LBHM[_}yEӌL&L5u^= SNX 2焈 {.rP"e+<ع/`;! 4wC& 8ZcA e7#(cǎ1 (~t++1NǸ9c.](Wzy%c<+gt/u)ΪXڹY1O伌-v~>/R߹sHtmO-hùJΧWL_q''eʼoo:d2eeedJ4"a)Bp<'>DP9Ky~n^ CiS\JI$FpIK){>Eʽ+2PH9'C &TvX]'Sٳ嬯30iF#r#.qF)W%42\aA/S/(/˄1+@v~𬿿郞g.sESbѶ8k\E۶4MmE]W]'e_R8&HxPy)v`?U{1}=Fcf* '"J.y^:l;/pe >` s e \ 21c 1Fs /.PK5]{ܹzСCr篬0*gsכr_k39@NHeSL0t=uSc IB^|1Kt/ 8r9dn W2Uq"x?ksBU@]עӬ4M0ϤeRݔfݬvŖr|ُҳ]1q}]dzwIZZe#XJEL =US~ǝ]rw]GF Ѳϸ*7OgOei5ɴbg馛v#C/7bU0,3< 92 $h? !X]^ |. WҶ;\2z<)qwivM*OG_8t0Ǐ[~?mw>}#rchJຨƺѲsӄ?=nVZ(Vsqsn8|8?~L_ =yɘkH FD1`dnbBOsO=Ι_1YwՌcA `ᄑo6779rs!3ĈI 9q;5ajFC<3,>|[{EGyロ7|2yO AVgO7"rUXޛ(x껞{penV6%ϧgD.%~ʏq ,z$Dvؓ#pJMQLfd֬}゚ xK6n~^U8x=++<#9} |RkSiDvV)VTJQK_3^guuh91zo~zD|wo/h3N9[[p:<`DFM#F+q1q9~vwͯ%zW( bSUӕV+TUp(\WlPM] @+:6.lqng6{~zkq/wƣM5p՞@򃭭<|ߥ=wlCW>W[Ak|ew};څqp\n9_g?' sqq)(IENDB`zeal-0.7.1/src/app/resources/icons/logo/64x64.png000066400000000000000000000135661462517734600214410ustar00rootroot00000000000000PNG  IHDR@@iq cHRMz&u0`:pQ< pHYs  ~bKGDIDATx{eWuks=33 Hh$"$$^i vpى1ʳrIJSNI9 eJ8ı)0{EzZo!1oLwϻ_q+}ν3D* N߹}Z{o}\?d5o{]z'ZΜ?̞m uh9nnrz'3Ӂ{k 53[8S4oʼn @#1@b^DA E`_I!spԜz Bc1B "ޡ (瓳i~qE6?,<9Id}0%1&FEE>m" nv/8DTCBD"(R`+8!Z$Y==J]rًYJbs""u3(hZK`Ƨϫ*x!ĀFE@ ;ĥtqr$lA@]:)M5/E@P=y [;XDJU\ׂM[v@ H-JrdFED 3{4(ͭG ,)Nϒ:aNJIqJPb煯8ge[zI٢bC@y/m%gh(F8Nulz/оZj&O͐9 ݣ #)A{/}5r˟"?}P(Āy$jI?k*×x[./uFkٱ28H0Jx_孆HԊBPZ_aهm rg)OX1/ d]@#ۻv\k ?Yza Xa2O!`zaB 8DG?uȲ5xv{/.yw-8nf+W bP4#h 1aS"|O}bq^#``拟H9-9B(Eė'0:iĢ5]B?-33+@3SfUd\hg RT#CN-őޘYIcUZ;/g>Bk߼'LJRzaIO*RMW)IRDА%oOe8['E`<1bN-Y97&Gƨ8.Vv@%../ٛl?Șj]e/+>4G;IP/P+꼔M>;wNw6׏\Uc:N"ZYI{GwE޿Vr0ZK(i %,/r~[P#$T|=qFIo@sE2BcvY\bt.7ӄ\)G%ְ_c{X|A6bg 4QҪ)aÓr6-/7'IR~!V퀲*+_b!'W@EԻpFw1}7=J#C*-!^8pbgB(#hNo[HX5P2n@,~&ƘgeyCχZbVzr5*-)MJ&=}{1(踥ˎ&h/U2u*AIr3?|Ĩl1F韽i`y3~/Lq.a2eWlzǻ9? *mb<U!눀s : tbw5 Q&N L66/r񩝭5Nr1V4Fsxb5WFHYo1zf?TOcZʕ=/޶&Vn>zukֱ5t;Y;Bˢc4Q#"ç͉]WGh^r^uyDOiDn@+ɉ%<2c 78»ɼC;/;4o}"zDܡ:vkF'x,mnngvJI,xq0JZ~6;`3 Л=FyC2"5wѫs~K J%^8!WNྕ13l0N{I{@gݽm4l,MԹMeg'֦3WkPߴ׼ўo (cW` wp/U漅p39{3WmP j=!y<(U04tZ1'طH*өAsT%[Qk&P6NOl1"VDNLZZCe+MT:I vewl G"%ciTP*GENZg{ϛۑȼljK@4沷/h131̴ # 䡢ض]F%oi.1KBmxO-Oq]>)1Qߟ zjmo%I'r$ef$ [r[KԚ( ʾ.e$QmYLU+^2Q#nju) L0r4pl Z,U|ϗ$Z]#W˰MLMcǠ6N |r5&QP9c\9 ǞS/f}$ Q2k.T᭭b IK-1#e řdvi|b|Ct,Av[y! .)u[]<үo3uk(9?g8xﵻilJw(MϞď#>;{mtgҘF,r<:ڲi\I;{Ʀ-NR_WoН9BcVshlފ9qylzќ\ox,Ι.HtBS| #1MBTyAף:8b"Pr>#eZ6;vd͚5l6jdYFexoK("r:UйHt#9" .RjBee,el5vfڵGFn\u|byst^\jR4Zٖ!XcgR'ᅲ[~={7,U'c9 H(R;< " 5rQ!}Ucot[|]h4젴O`f.Xi. UU]Plӈ:RGe5z@R"v>/-9r|w~?F {̛r P*}mXϯYQ33'ȋV|hRyxQj:##0K!|Qxxq11D}?ѹ#Lm^Oq_9:s_1?FVq!:eߺm!#dx׃^{W})iwx-XvX?Mvٗ,[[^2+~Tɫpq@r@~z+ /W߽ce_0q5+5a0ځ7F;IENDB`zeal-0.7.1/src/app/resources/icons/logo/icon.png000066400000000000000000000015451462517734600215700ustar00rootroot00000000000000PNG  IHDR(-SPLTE&&&uSAiLjHjIjHjIlIxWrQ94+hFwfIrbFvsp fa\?.ʬq//////--./2!̳1-&c=f9h:gd9b6a4b4)$";52UA_6ܐvǼஞఢ⸬㵥_7[1[/[/!2.&V2[2`9eCkMӈsؕb?\5X/X,W+png1,&2.'T1X0^9dEޮsWcC]9[5Y1T+R(zws4/'Q>W4sXޮhNcG`A]?{alNP*M#sqkA84O/jLĻ⽲ڣՒ҈u҈tՑ~⼱V4K%Hvrn"+'!I)M(O/R3W9eK҅pԎ|זbFM,G I!pnhG2@AC!E%I*M.N0I)B >Q*)% <93L1EE F H#I%I$H%G"N'O@kh`?:3=70MFQJUNWQZRYRSLQLID=HC<~FA;WSL454STUķttRNS@fIDATcd``3a-a#.T bdbDh|ƌ QеLV!72g xEH|g |fcdIHq UIENDB`zeal-0.7.1/src/app/resources/icons/logo/icon@2x.png000066400000000000000000000034311462517734600221360ustar00rootroot00000000000000PNG  IHDR szz pHYs  tIME (YbKGDIDATXÝM\Gofvgwvm L8 (>$DHȑS!8Y EB. !@B/l뵽ޝ7m9yZ}?5O 2UHBG@@C+=fא݃1`/9N`޽a/ӤHC;Qf<Z@u1biDwK³gwϮfܺɆU>P+U揝Ȕmvvb 3>Fp'Y{Y+y0ENROfwiDt`-KT`D#Tk畷&/Գז9{(Q$D63*?XS&"} s}O>/~/pWU`e+KVb$ )! c],4RdB瘚ՕUvNxej/Z"Zx,,EX(e}6j!^PMU 9m;%ejI^'q sk bƸ1Fiʠ?dߣv%B.̰ݻm2jl67$\s.""t@$42Ժu4(ggGĩj{=VkfD$Ɍ&.%1Y >Z$ EU !>j8 KSa0TLJ1Ḽ0MvP)cN2:Z!/=OڭU С79l"ZuLdJrew#?]v!p}W/xppvt ?=pYV'z x`/0^@#z?Z9t)0q=_?Qr2|e(g}1InIENDB`zeal-0.7.1/src/app/resources/icons/type/000077500000000000000000000000001462517734600201465ustar00rootroot00000000000000zeal-0.7.1/src/app/resources/icons/type/Abbreviation.png000066400000000000000000000004771462517734600232710ustar00rootroot00000000000000PNG  IHDR(rPLTE}_}_}_}_xŨҝxǎklz{͑o~`}ψmtpgcuȾԑmp}ϣ|tRNS䨤xIDATJAzXiJF:H̵:X-MQnd~|ʥcۋJg?*P IENDB`zeal-0.7.1/src/app/resources/icons/type/Abbreviation@2x.png000066400000000000000000000007551462517734600236420ustar00rootroot00000000000000PNG  IHDRE/PLTE}_}_}_}_}_}_}_}_}_h}ͨҞyȇf~ЃcasqxňmzjȾԊi~`ovázʏm|^pteh|m tRNS%'(~^!IDAT(}r0@h1"P*׾Bwo2&XG&㑊'ܙI4s*"hIbxP vp`eDK.ņ(D-;-T<<%pNH|zHpNjl}#eۖCWNOrLyOrKrhԣO걠%%A SdMƷ{rb/ԟ3ZM/TXB4-,;IENDB`zeal-0.7.1/src/app/resources/icons/type/Alias.png000066400000000000000000000004771462517734600217150ustar00rootroot00000000000000PNG  IHDR(rPLTEQ]Q]Q]Q]gvm|gw\i\joyix_mR`kzak}cr`mˣXdUbdqӹ]k`nk|jyKetRNS䨤xIDATJAzXiJF:H̵:X-MQnd~|ʥcۋJg?*P IENDB`zeal-0.7.1/src/app/resources/icons/type/Alias@2x.png000066400000000000000000000007551462517734600222660ustar00rootroot00000000000000PNG  IHDRE/PLTEm|Q]Q]Q]Q]Q]Q]Q]Q]Q]Yfkzm|hxWdl}UbS^bpϋangvakoy[hӹZgR`_merhw]kģP^`n}cpVcYejy q tRNS%'(~^!IDAT(}r0@h1"P*׾Bwo2&XG&㑊'ܙI4s*"hIbxP vp`eDK.ņ(D-;-T<<%pNH|zHpNjl}#eۖCWNOrLyOrKrhԣO걠%%A SdMƷ{rb/ԟ3ZM/TXB4-,;IENDB`zeal-0.7.1/src/app/resources/icons/type/Annotation.png000066400000000000000000000004771462517734600227760ustar00rootroot00000000000000PNG  IHDR(rPLTEQ]Q]Q]Q]gvm|gw\i\joyix_mR`kzak}cr`mˣXdUbdqӹ]k`nk|jyKetRNS䨤xIDATJAzXiJF:H̵:X-MQnd~|ʥcۋJg?*P IENDB`zeal-0.7.1/src/app/resources/icons/type/Annotation@2x.png000066400000000000000000000007551462517734600233470ustar00rootroot00000000000000PNG  IHDRE/PLTEm|Q]Q]Q]Q]Q]Q]Q]Q]Q]Yfkzm|hxWdl}UbS^bpϋangvakoy[hӹZgR`_merhw]kģP^`n}cpVcYejy q tRNS%'(~^!IDAT(}r0@h1"P*׾Bwo2&XG&㑊'ܙI4s*"hIbxP vp`eDK.ņ(D-;-T<<%pNH|zHpNjl}#eۖCWNOrLyOrKrhԣO걠%%A SdMƷ{rb/ԟ3ZM/TXB4-,;IENDB`zeal-0.7.1/src/app/resources/icons/type/Attribute.png000066400000000000000000000004771462517734600226270ustar00rootroot00000000000000PNG  IHDR(rPLTEQQQQgmg\\oiӮ_ڮRkaË}ȗc`ΣXUdٹ]`kjDtRNS䨤xIDATJAzXiJF:H̵:X-MQnd~|ʥcۋJg?*P IENDB`zeal-0.7.1/src/app/resources/icons/type/Attribute@2x.png000066400000000000000000000007551462517734600232000ustar00rootroot00000000000000PNG  IHDRE/PLTEmQQQQQQQQQYkmhWlUSbËȗagao[ӮٹʻZR_eh]ΣP`}cVYj] tRNS%'(~^!IDAT(}r0@h1"P*׾Bwo2&XG&㑊'ܙI4s*"hIbxP vp`eDK.ņ(D-;-T<<%pNH|zHpNjl}#eۖCWNOrLyOrKrhԣO걠%%A SdMƷ{rb/ԟ3ZM/TXB4-,;IENDB`zeal-0.7.1/src/app/resources/icons/type/Axiom.png000066400000000000000000000004771462517734600217410ustar00rootroot00000000000000PNG  IHDR(rPLTEQ]Q]Q]Q]gvm|gw\i\joyix_mR`kzak}cr`mˣXdUbdqӹ]k`nk|jyKetRNS䨤xIDATJAzXiJF:H̵:X-MQnd~|ʥcۋJg?*P IENDB`zeal-0.7.1/src/app/resources/icons/type/Axiom@2x.png000066400000000000000000000007551462517734600223120ustar00rootroot00000000000000PNG  IHDRE/PLTEm|Q]Q]Q]Q]Q]Q]Q]Q]Q]Yfkzm|hxWdl}UbS^bpϋangvakoy[hӹZgR`_merhw]kģP^`n}cpVcYejy q tRNS%'(~^!IDAT(}r0@h1"P*׾Bwo2&XG&㑊'ܙI4s*"hIbxP vp`eDK.ņ(D-;-T<<%pNH|zHpNjl}#eۖCWNOrLyOrKrhԣO걠%%A SdMƷ{rb/ԟ3ZM/TXB4-,;IENDB`zeal-0.7.1/src/app/resources/icons/type/Binding.png000066400000000000000000000005111462517734600222230ustar00rootroot00000000000000PNG  IHDR(PLTEzpyƲnpx{|ͽł՛ŴŲp­ɸ鉵rss܁r㌷vűvtRNS@faIDATU@@ ЎN7q@qvák 9%m2З)Q"$6eB8̮J!+eO3Ȯr 9Dji/7|IENDB`zeal-0.7.1/src/app/resources/icons/type/Binding@2x.png000066400000000000000000000007001462517734600225750ustar00rootroot00000000000000PNG  IHDRE/PLTEf|g{g{oh~kuxtw֑lzŏȱʶ«}Į䍸űнՃpроmrt'gtRNSPVVIDAT(} EQ:T)RZE}+.iHr>8،DZq9pM17٥+*n{Fymj|J2N ~43BIENDB`zeal-0.7.1/src/app/resources/icons/type/Callback@2x.png000066400000000000000000000007311462517734600227230ustar00rootroot00000000000000PNG  IHDRE/PLTEíѱӤ̖čœǎϓ·ҦЧ͞ǩϿ٨ΕëϢꡲ☬ŐŖím@ tRNS%'(~^!IDAT(ϕ0"-j1:tw:u,0l1Ab,3i+x!W|s6`J>=Kl0ƪ"dAIQvW 栽C5rKH(Lw~TeVڝcsʢ'PNQ8?(.F`(t[Ҳ5x5%E7×݄.9h/1Mhi,g"IENDB`zeal-0.7.1/src/app/resources/icons/type/Category.png000066400000000000000000000005701462517734600224330ustar00rootroot00000000000000PNG  IHDR(PLTEګؤ٫侠ƪŨക٦ע|ף~۩ݭบɲⷘް㼡⼟⻝෗ӿ㽢߱⺛ݮ⹚ް忡ᵖ忢伞ᶗ߳۬ũڨĬ೒ްӝxϔnӜxЖqДo٩>tRNS@fuIDATJavUI ҭuIh96]U+۾pi6oap~>^' y8zDŽe9v0T:`'Ͻ0-'~Jh=*rIENDB`zeal-0.7.1/src/app/resources/icons/type/Category@2x.png000066400000000000000000000010371462517734600230040ustar00rootroot00000000000000PNG  IHDRE/PLTEɖpʖnʕnΜu٭߹ثϝwʔmΛuҢ}޶۲͙rگ˖pΚtթР|߽ʴ߼˗pПzզٯë޺۶֨Ӥ޹ڳǮӣθԦ˖q‡aʖrРٳ͚vϞxȑkċeʔoРz䠗tRNSPVVIDAT(ύr0@D U 6bχ.Ba7I˦-B,z0PPk"#y#+ |rBNlT̮iCEuxi`4 7ނ[&9dZNq^ݩR.NDM8La[S\<& ٴ/7KuT+R,/o[|nLTeϯoğ BNsf ۍѡ>;c6*3IENDB`zeal-0.7.1/src/app/resources/icons/type/Class.png000066400000000000000000000006501462517734600217220ustar00rootroot00000000000000PNG  IHDR(PLTEʿѽϻ͹˺ͼ;аĥǺŹջͩȯĶݵǼΨé÷ڭ¶ɹ˶ݼݿѿẫˮ³ǥʳdzƮµƧ1%tRNS@fIDAT=NA@{ߎ3j; k>„o쵓L(c Q25n?^O}o(\|P7H\;@@BHso 0a*e֚_A_+ߎ& IENDB`zeal-0.7.1/src/app/resources/icons/type/Class@2x.png000066400000000000000000000010421462517734600222700ustar00rootroot00000000000000PNG  IHDRE/PLTEvut|ɫƣ­ȩĪŮǼὰїݳʟ䚇ϧĸ־⪙ԬȦtRNSPVV)IDAT(ϭv0EQ"LP bE u.Sc !IgeOĤ5傚Ҟ!m\ʳnȿ B.7 {+ 40CP}Qk[ ٓsUɯdP^s d C!rMo Ѓ 5;p&< Q)TB53olK|Vcw§%p%Eb|qaX8n*uP߼(3zQIENDB`zeal-0.7.1/src/app/resources/icons/type/Collection.png000066400000000000000000000005701462517734600227510ustar00rootroot00000000000000PNG  IHDR(PLTENJLj֠ܪۨΕă|~ƆɋКܲјˑӡӟӝЗԢ̑қʍҚːסϖ֢ԞЗ͔Ȋ۩ŅجΒˏӽx϶nӼxзqеoƆ%ItRNS@fuIDATJavUI ҭuIh96]U+۾pi6oap~>^' y8zDŽe9v0T:`'Ͻ0-'~Jh=*rIENDB`zeal-0.7.1/src/app/resources/icons/type/Collection@2x.png000066400000000000000000000010371462517734600233220ustar00rootroot00000000000000PNG  IHDRE/PLTEɵpʶnʵnλuȊҘLJϻwʴmκuҿ}ϕ̐͸rʍ˵pιtԥćн|Ң۴ҟ˶pнzÂȐ֫ѝ͙ąЛ̓ڮ߸Á˵q©aʴrмʗ͸vϼxȱkĬeʴoоz3ktRNSPVVIDAT(ύr0@D U 6bχ.Ba7I˦-B,z0PPk"#y#+ |rBNlT̮iCEuxi`4 7ނ[&9dZNq^ݩR.NDM8La[S\<& ٴ/7KuT+R,/o[|nLTeϯoğ BNsf ۍѡ>;c6*3IENDB`zeal-0.7.1/src/app/resources/icons/type/Column.png000066400000000000000000000005701462517734600221130ustar00rootroot00000000000000PNG  IHDR(PLTEڊو䠽ك|~ۆ݋☹ޑ㡿⟼❻㢿ߑ⛺ݍ⚹ސ塾ᖸ墿䞽ᗸߔۊڅޏxnxqoن9IitRNS@fuIDATJavUI ҭuIh96]U+۾pi6oap~>^' y8zDŽe9v0T:`'Ͻ0-'~Jh=*rIENDB`zeal-0.7.1/src/app/resources/icons/type/Column@2x.png000066400000000000000000000010371462517734600224640ustar00rootroot00000000000000PNG  IHDRE/PLTEpnnuيߘ؇wmu}ޕېrڍptॽՇ|ߢߟpzՂِޝۙօӀޛړԁqarٗvxkeoz,tRNSPVVIDAT(ύr0@D U 6bχ.Ba7I˦-B,z0PPk"#y#+ |rBNlT̮iCEuxi`4 7ނ[&9dZNq^ݩR.NDM8La[S\<& ٴ/7KuT+R,/o[|nLTeϯoğ BNsf ۍѡ>;c6*3IENDB`zeal-0.7.1/src/app/resources/icons/type/Command.png000066400000000000000000000004041462517734600222300ustar00rootroot00000000000000PNG  IHDR(QPLTEαӯҟɐț敺Ϣʿٔ®ҠPD;tRNSIJee]IDAT]Y C[.MLT(d@ وT>r>8،DZq9pM17٥+*n{Fymj|J2N ~43BIENDB`zeal-0.7.1/src/app/resources/icons/type/Command@2x.png000066400000000000000000000007311462517734600226050ustar00rootroot00000000000000PNG  IHDRE/PLTEíѱӤ̖čœǎϓ·ҦЧ͞ǩϿ٨ΕëϢ☽ŐŖíѡq tRNS%'(~^!IDAT(ϕ0"-j1:tw:u,0l1Ab,3i+x!W|s6`J>=Kl0ƪ"dAIQvW 栽C5rKH(Lw~TeVڝcsʢ'PNQ8?(.F`(t[Ҳ5x5%E7×݄.9h/1Mhi,g"IENDB`zeal-0.7.1/src/app/resources/icons/type/Component.png000066400000000000000000000005701462517734600226200ustar00rootroot00000000000000PNG  IHDR(PLTEڊو䠽ك|~ۆ݋☹ޑ㡿⟼❻㢿ߑ⛺ݍ⚹ސ塾ᖸ墿䞽ᗸߔۊڅޏxnxqoن9IitRNS@fuIDATJavUI ҭuIh96]U+۾pi6oap~>^' y8zDŽe9v0T:`'Ͻ0-'~Jh=*rIENDB`zeal-0.7.1/src/app/resources/icons/type/Component@2x.png000066400000000000000000000010371462517734600231710ustar00rootroot00000000000000PNG  IHDRE/PLTEpnnuيߘ؇wmu}ޕېrڍptॽՇ|ߢߟpzՂِޝۙօӀޛړԁqarٗvxkeoz,tRNSPVVIDAT(ύr0@D U 6bχ.Ba7I˦-B,z0PPk"#y#+ |rBNlT̮iCEuxi`4 7ނ[&9dZNq^ݩR.NDM8La[S\<& ٴ/7KuT+R,/o[|nLTeϯoğ BNsf ۍѡ>;c6*3IENDB`zeal-0.7.1/src/app/resources/icons/type/Constant.png000066400000000000000000000006541462517734600224520ustar00rootroot00000000000000PNG  IHDR(PLTEb}Tqa||Khc~jNkvVqf~Jh^yʹkʻο\xv˻ptNllzMke~ǢĴa|}ZvQngxTpӘmePm\v[wPnRo}tRNS@fIDATnQ@ѽ}I E "* t-Q3%+XQ3s! V?;-FU޻`4hp5LP @kK{W) 123OEZfОIENDB`zeal-0.7.1/src/app/resources/icons/type/Constant@2x.png000066400000000000000000000010131462517734600230120ustar00rootroot00000000000000PNG  IHDRE/PLTEPfPgOgXnoi~bylte|FdLiZt|{l}IgvxVqzPlSoʵмDž^vazooì^tRNSPVV0IDAT(}b0 JpP.2)0'9m,ñ-Oöqp7±p7fB0!LDn&I3 Ke=%2PJNP'4?R ѡh3"aO,RԴ Dg(qT.Juڱ C5хS3Ut C?(+dWjƫ6Njd_=pW\B@tGlXNY$ ++レ,QVUd =374~ Nˡ~; ÄIENDB`zeal-0.7.1/src/app/resources/icons/type/Constructor.png000066400000000000000000000006061462517734600232030ustar00rootroot00000000000000PNG  IHDR(PLTEk~|}zɥxy·Ⱦ·u૙y{\D,tRNSIDAT AٴHʅ+@ 8GZ UT@@hکrf}+5=n1Ԗ0 i8uxp[r>8،DZq9pM17٥+*n{Fymj|J2N ~43BIENDB`zeal-0.7.1/src/app/resources/icons/type/Conversion@2x.png000066400000000000000000000007311462517734600233540ustar00rootroot00000000000000PNG  IHDRE/PLTEӱÖѭӱ̤Ė”ǜϪ“ԷҰ˦ЮͧȞǝϩٿΨÕϫʢݧʡΠŘřÖѭ\ tRNS%'(~^!IDAT(ϕ0"-j1:tw:u,0l1Ab,3i+x!W|s6`J>=Kl0ƪ"dAIQvW 栽C5rKH(Lw~TeVڝcsʢ'PNQ8?(.F`(t[Ҳ5x5%E7×݄.9h/1Mhi,g"IENDB`zeal-0.7.1/src/app/resources/icons/type/Data Source.png000066400000000000000000000004171462517734600227500ustar00rootroot00000000000000PNG  IHDR(ZPLTEZYcm`f:auJqclvKq㣁j?fĮZl@goCjV|nBifrFmjvmAh^r'tRNS^[FcIDAT[uI DфHqT@6ۤ:RIoeEKLc%r1rreMlG~)fmΫ L3<nBcrmIENDB`zeal-0.7.1/src/app/resources/icons/type/Data Source@2x.png000066400000000000000000000006741462517734600233270ustar00rootroot00000000000000PNG  IHDRE/PLTEYYY]gm`f:ai=erFmV|fl~SzvKqֱZoCjjj?fuJqxrGmjxLsl@g󿧼{OueqYnBic{!otRNSPVVIDAT(ϕ0 2X*}BKT˖lKKHBJ /=m 'м.Xu:˲( C~^G/ .tT4v{^c6d+;JN1p.4I݁iOc mL+۷UD@z9k9XhyIENDB`zeal-0.7.1/src/app/resources/icons/type/Database.png000066400000000000000000000004161462517734600223610ustar00rootroot00000000000000PNG  IHDR(TPLTEffffffthwƾɷûvºtƿm{StRNSIJeedIDAT[uK0ՖQA0%iJHĩSՖ#6w8?͊WC*qR-+/m Ϟ>KIENDB`zeal-0.7.1/src/app/resources/icons/type/Database@2x.png000066400000000000000000000007341462517734600227360ustar00rootroot00000000000000PNG  IHDRE/PLTEfffffffffpļnhqǿkzyɷ˞tfuts栙mŽ}º|iĽxpûMz.A tRNS%'(~^!IDAT(ϝ0bA*b*>xd cn+s00F#l3)Ee0[Oj'؁Ϥ $T}]2*c)'ȿp^6Cs =/gXJ.b[ !\܄-$B7(05>]axq,<9ך{㸒ztK2:EuXwP.lQ 7IENDB`zeal-0.7.1/src/app/resources/icons/type/Decorator.png000066400000000000000000000004171462517734600226000ustar00rootroot00000000000000PNG  IHDR(ZPLTEZYcm`f:auJqclvKq㣁j?fĮZl@goCjV|nBifrFmjvmAh^r'tRNS^[FcIDAT[uI DфHqT@6ۤ:RIoeEKLc%r1rreMlG~)fmΫ L3<nBcrmIENDB`zeal-0.7.1/src/app/resources/icons/type/Decorator@2x.png000066400000000000000000000006741462517734600231570ustar00rootroot00000000000000PNG  IHDRE/PLTEYYY]gm`f:ai=erFmV|fl~SzvKqֱZoCjjj?fuJqxrGmjxLsl@g󿧼{OueqYnBic{!otRNSPVVIDAT(ϕ0 2X*}BKT˖lKKHBJ /=m 'м.Xu:˲( C~^G/ .tT4v{^c6d+;JN1p.4I݁iOc mL+۷UD@z9k9XhyIENDB`zeal-0.7.1/src/app/resources/icons/type/Define.png000066400000000000000000000003461462517734600220510ustar00rootroot00000000000000PNG  IHDR(?PLTEvw~ؘޣˁݠܟ͆ԔᲲⳳՔtRNS@fUIDAT[m 0 a%^k*Z*8́A=AEA|4> -M鶓$,U\/0s9IENDB`zeal-0.7.1/src/app/resources/icons/type/Define@2x.png000066400000000000000000000004341462517734600224210ustar00rootroot00000000000000PNG  IHDR#§0PLTEiihjhjoqƄΒsvɈ̏ʊƄ֦ʻ #tRNSPVVIDATc`T6#ac$`Ƞ!F an11*//oL07\ ܬ峀xdp[^^wr@vnyisr 9c㦕/d+x, f Rf-Q#BP9/IENDB`zeal-0.7.1/src/app/resources/icons/type/Delegate.png000066400000000000000000000004171462517734600223700ustar00rootroot00000000000000PNG  IHDR(ZPLTEfZfYocymk`F:fUJuncxlWKv䋁J?jeZL@lOCoaVNBnsfRFrvjvMAmPtRNS^[FcIDAT[uI DфHqT@6ۤ:RIoeEKLc%r1rreMlG~)fmΫ L3<nBcrmIENDB`zeal-0.7.1/src/app/resources/icons/type/Delegate@2x.png000066400000000000000000000006741462517734600227470ustar00rootroot00000000000000PNG  IHDRE/PLTEfYgYfYi]sgymk`F:fH=iRFraVsfxl^S~WKvٝeZOCovjJ?jUJuxSGrujXLxL@l\O{pe{qeYNBnnc{QtRNSPVVIDAT(ϕ0 2X*}BKT˖lKKHBJ /=m 'м.Xu:˲( C~^G/ .tT4v{^c6d+;JN1p.4I݁iOc mL+۷UD@z9k9XhyIENDB`zeal-0.7.1/src/app/resources/icons/type/Device.png000066400000000000000000000004171462517734600220550ustar00rootroot00000000000000PNG  IHDR(ZPLTEZYcm`f:auJqclvKq㣁j?fĮZl@goCjV|nBifrFmjvmAh^r'tRNS^[FcIDAT[uI DфHqT@6ۤ:RIoeEKLc%r1rreMlG~)fmΫ L3<nBcrmIENDB`zeal-0.7.1/src/app/resources/icons/type/Device@2x.png000066400000000000000000000006741462517734600224340ustar00rootroot00000000000000PNG  IHDRE/PLTEYYY]gm`f:ai=erFmV|fl~SzvKqֱZoCjjj?fuJqxrGmjxLsl@g󿧼{OueqYnBic{!otRNSPVVIDAT(ϕ0 2X*}BKT˖lKKHBJ /=m 'м.Xu:˲( C~^G/ .tT4v{^c6d+;JN1p.4I݁iOc mL+۷UD@z9k9XhyIENDB`zeal-0.7.1/src/app/resources/icons/type/Diagram.png000066400000000000000000000004171462517734600222220ustar00rootroot00000000000000PNG  IHDR(ZPLTEZYcm`f:auJqclvKq㣁j?fĮZl@goCjV|nBifrFmjvmAh^r'tRNS^[FcIDAT[uI DфHqT@6ۤ:RIoeEKLc%r1rreMlG~)fmΫ L3<nBcrmIENDB`zeal-0.7.1/src/app/resources/icons/type/Diagram@2x.png000066400000000000000000000006741462517734600226010ustar00rootroot00000000000000PNG  IHDRE/PLTEYYY]gm`f:ai=erFmV|fl~SzvKqֱZoCjjj?fuJqxrGmjxLsl@g󿧼{OueqYnBic{!otRNSPVVIDAT(ϕ0 2X*}BKT˖lKKHBJ /=m 'м.Xu:˲( C~^G/ .tT4v{^c6d+;JN1p.4I݁iOc mL+۷UD@z9k9XhyIENDB`zeal-0.7.1/src/app/resources/icons/type/Directive.png000066400000000000000000000004161462517734600225730ustar00rootroot00000000000000PNG  IHDR(TPLTEffffffthwƾɷûvºtƿm{StRNSIJeedIDAT[uK0ՖQA0%iJHĩSՖ#6w8?͊WC*qR-+/m Ϟ>KIENDB`zeal-0.7.1/src/app/resources/icons/type/Directive@2x.png000066400000000000000000000007341462517734600231500ustar00rootroot00000000000000PNG  IHDRE/PLTEfffffffffpļnhqǿkzyɷ˞tfuts栙mŽ}º|iĽxpûMz.A tRNS%'(~^!IDAT(ϝ0bA*b*>xd cn+s00F#l3)Ee0[Oj'؁Ϥ $T}]2*c)'ȿp^6Cs =/gXJ.b[ !\܄-$B7(05>]axq,<9ך{㸒ztK2:EuXwP.lQ 7IENDB`zeal-0.7.1/src/app/resources/icons/type/Element.png000066400000000000000000000003461462517734600222500ustar00rootroot00000000000000PNG  IHDR(3PLTEnփߘڍmwޖ{؈ޞtҰ_StRNS#CQIDAT @ 07V$|vSIHwd a= F\O;<^p @/D[IENDB`zeal-0.7.1/src/app/resources/icons/type/Element@2x.png000066400000000000000000000004231462517734600226160ustar00rootroot00000000000000PNG  IHDRE/?PLTEpnnuيߘڍmՂޕ|ޝtw؇ݒW2:tRNSPVVIDAT(ϕ @436Lj:ďc(lZ(@cc,iP"kh5O{П8Ģt&ᘛf>qb)93ЗWã_x V.8sk2 kIENDB`zeal-0.7.1/src/app/resources/icons/type/Entry.png000066400000000000000000000003461462517734600217600ustar00rootroot00000000000000PNG  IHDR(3PLTEqnˇ֝ߒpmzwϚ~{vt; y>tRNS#CQIDAT @ 07V$|vSIHwd a= F\O;<^p @/D[IENDB`zeal-0.7.1/src/app/resources/icons/type/Entry@2x.png000066400000000000000000000004231462517734600223260ustar00rootroot00000000000000PNG  IHDRE/?PLTEspqnpnyu΍ٝߐomʅ՘~|vtzwϊؖ)ŕtRNSPVVIDAT(ϕ @436Lj:ďc(lZ(@cc,iP"kh5O{П8Ģt&ᘛf>qb)93ЗWã_x V.8sk2 kIENDB`zeal-0.7.1/src/app/resources/icons/type/Enumeration.png000066400000000000000000000003461462517734600231450ustar00rootroot00000000000000PNG  IHDR(3PLTE˅n֙߬ڢʄmύwުё{؜ޮΉtk/tRNS#CQIDAT @ 07V$|vSIHwd a= F\O;<^p @/D[IENDB`zeal-0.7.1/src/app/resources/icons/type/Enumeration@2x.png000066400000000000000000000004231462517734600235130ustar00rootroot00000000000000PNG  IHDRE/?PLTEɆpʅnʄnΌuٞ߬ڡʃm՗ިА|ޮΉtύw؜ݦRtRNSPVVIDAT(ϕ @436Lj:ďc(lZ(@cc,iP"kh5O{П8Ģt&ᘛf>qb)93ЗWã_x V.8sk2 kIENDB`zeal-0.7.1/src/app/resources/icons/type/Environment.png000066400000000000000000000003461462517734600231630ustar00rootroot00000000000000PNG  IHDR(3PLTEqnˇ֝ߒpmzwϚ~{vt; y>tRNS#CQIDAT @ 07V$|vSIHwd a= F\O;<^p @/D[IENDB`zeal-0.7.1/src/app/resources/icons/type/Environment@2x.png000066400000000000000000000004231462517734600235310ustar00rootroot00000000000000PNG  IHDRE/?PLTEspqnpnyu΍ٝߐomʅ՘~|vtzwϊؖ)ŕtRNSPVVIDAT(ϕ @436Lj:ďc(lZ(@cc,iP"kh5O{П8Ģt&ᘛf>qb)93ЗWã_x V.8sk2 kIENDB`zeal-0.7.1/src/app/resources/icons/type/Error.png000066400000000000000000000004211462517734600217420ustar00rootroot00000000000000PNG  IHDR(KPLTEE D [ b\ O K X һȠP H T a7|[ãT ^ `E E <)W)tRNS\IDAT[U P@BeQ һ;sa,[!hפ 3P[8/Fɜb%ַaHRc !ݑ/ر C8IENDB`zeal-0.7.1/src/app/resources/icons/type/Error@2x.png000066400000000000000000000006151462517734600223210ustar00rootroot00000000000000PNG  IHDRE/rPLTEv W W W W W W W W W ` s v u b [ ^ q nn ҫuGβƤg.ǹg _ r o g ` s 7 tRNS%'(~^!IDAT(ϕ0D B+JT%a{;0{,P24 e)8 @2Mc@0¢GV,)Da3lZumnA)gsiAYUx~&<Te^[Ƶ7WL -OG h8e: *|Ղq$}1c!O((iIENDB`zeal-0.7.1/src/app/resources/icons/type/Event.png000066400000000000000000000003131462517734600217320ustar00rootroot00000000000000PNG  IHDRf0-PLTEkkkk‘πpzڈ]tRNS䨤IIDATc`t4dpKoq\i z/ץ= "# wZQ?ݖ6ć l (HqIENDB`zeal-0.7.1/src/app/resources/icons/type/Event@2x.png000066400000000000000000000005111462517734600223040ustar00rootroot00000000000000PNG  IHDRE/WPLTEkkkkkkkkkvʑϐyptkƂvJC tRNS%'(~^!IDAT(ϕ0 C N(}=4I E7D":rZWdxQB;Hr?H"msG\_p@&q0gk[av%)l~g&5Y LK݊x,ԩ^ 'mYAIENDB`zeal-0.7.1/src/app/resources/icons/type/Exception.png000066400000000000000000000004541462517734600226150ustar00rootroot00000000000000PNG  IHDR(TPLTE.>->9P=V3H6Lnx.@3F5I?M™И1D->FjAtRNSkIDAT[U DH RheJlۙ$bY3 gw~T4 1Ħ<ǘ })OM_SǞ:/\jK"ޑ v#!)IENDB`zeal-0.7.1/src/app/resources/icons/type/Exception@2x.png000066400000000000000000000007461462517734600231730ustar00rootroot00000000000000PNG  IHDRE/PLTE@O@O@O@O@O@O@O@O@O@OFVRfTiQeEUQeeoΪAQFWNaT`ԶI[Ǟ@OٲIZOcShCSGYK^K]ϼM`DTRgRf tRNS%'(~^!IDAT(ύn0D &,'q\O ?PDYGs=cvDFv80ɘcI3ס,Ś̹#0rc%#|";vy-<~{ z%9|VU˿}^YKaEZ9H:rsF#U9! C'gjҁ1ۂBH uAZs yε]Kyb_%LK=5&.4*uGIENDB`zeal-0.7.1/src/app/resources/icons/type/Expression.png000066400000000000000000000004541462517734600230160ustar00rootroot00000000000000PNG  IHDR(TPLTE.y-x9=©36n.z35?י»1<089}6-w-x ktRNSkIDAT[U DH RheJlۙ$bY3 gw~T4 1Ħ<ǘ })OM_SǞ:/\jK"ޑ v#!)IENDB`zeal-0.7.1/src/app/resources/icons/type/Expression@2x.png000066400000000000000000000007461462517734600233740ustar00rootroot00000000000000PNG  IHDRE/PLTE@@@@@@@@@@FRʵTϹQƱEQȴeAFNTI@IOïS͸CؑGKKMDRʴRɵ tRNS%'(~^!IDAT(ύn0D &,'q\O ?PDYGs=cvDFv80ɘcI3ס,Ś̹#0rc%#|";vy-<~{ z%9|VU˿}^YKaEZ9H:rsF#U9! C'gjҁ1ۂBH uAZs yε]Kyb_%LK=5&.4*uGIENDB`zeal-0.7.1/src/app/resources/icons/type/Extension.png000066400000000000000000000004541462517734600226330ustar00rootroot00000000000000PNG  IHDR(TPLTE.3-29@=D3:69?}6<-2-2$t(tRNSkIDAT[U DH RheJlۙ$bY3 gw~T4 1Ħ<ǘ })OM_SǞ:/\jK"ޑ v#!)IENDB`zeal-0.7.1/src/app/resources/icons/type/Extension@2x.png000066400000000000000000000007461462517734600232110ustar00rootroot00000000000000PNG  IHDRE/PLTE@C@C@C@C@C@C@C@C@C@CFIRVTYQVEHQVefAEFJNRTUIM@CILOTSXCGۑGKKOKOMRDHRWRW] tRNS%'(~^!IDAT(ύn0D &,'q\O ?PDYGs=cvDFv80ɘcI3ס,Ś̹#0rc%#|";vy-<~{ z%9|VU˿}^YKaEZ9H:rsF#U9! C'gjҁ1ۂBH uAZs yε]Kyb_%LK=5&.4*uGIENDB`zeal-0.7.1/src/app/resources/icons/type/Field.png000066400000000000000000000003211462517734600216730ustar00rootroot00000000000000PNG  IHDRf0*PLTELc~Wzn`jٕrM~tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/Field@2x.png000066400000000000000000000003671462517734600222570ustar00rootroot00000000000000PNG  IHDRE/3PLTEKLLUm}Wzmsbׂ0tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/File.png000066400000000000000000000003211462517734600215270ustar00rootroot00000000000000PNG  IHDRf0*PLTE˖n֨߹Ѡy޷جԦجĪٮMm.tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/File@2x.png000066400000000000000000000003671462517734600221130ustar00rootroot00000000000000PNG  IHDRE/3PLTEɖpʖnʕnΜu٭߹Пy޶ج۳Ԧ˷]tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/Filter.png000066400000000000000000000003211462517734600220750ustar00rootroot00000000000000PNG  IHDRf0*PLTELc~Wzn`jٕrM~tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/Filter@2x.png000066400000000000000000000003671462517734600224610ustar00rootroot00000000000000PNG  IHDRE/3PLTEKLLUm}Wzmsbׂ0tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/Flag.png000066400000000000000000000003211462517734600215210ustar00rootroot00000000000000PNG  IHDRf0*PLTELc~Wzn`jٕrM~tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/Flag@2x.png000066400000000000000000000003671462517734600221050ustar00rootroot00000000000000PNG  IHDRE/3PLTEKLLUm}Wzmsbׂ0tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/Foreign Key.png000066400000000000000000000003211462517734600227520ustar00rootroot00000000000000PNG  IHDRf0*PLTELc~Wzn`jٕrM~tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/Foreign Key@2x.png000066400000000000000000000003671462517734600233360ustar00rootroot00000000000000PNG  IHDRE/3PLTEKLLUm}Wzmsbׂ0tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/Framework.png000066400000000000000000000003211462517734600226050ustar00rootroot00000000000000PNG  IHDRf0*PLTE˖n֨߹Ѡy޷جԦجĪٮMm.tRNScHIDAT[c`!cPd6qqqqiiiΆ@:B{\ Ϝii9B;#.`l"9IK wIENDB`zeal-0.7.1/src/app/resources/icons/type/Framework@2x.png000066400000000000000000000003671462517734600231710ustar00rootroot00000000000000PNG  IHDRE/3PLTEɖpʖnʕnΜu٭߹Пy޶ج۳Ԧ˷]tRNSPVVpIDAT(SI @O+S"!-)H,FX'd 09bzM]q5t MΈkMҖF#!b7XCNTZ1*8AIENDB`zeal-0.7.1/src/app/resources/icons/type/Function.png000066400000000000000000000006071462517734600224440ustar00rootroot00000000000000PNG  IHDR(PLTEzpyƲ۬®~˻ǵ|}İ~{ޞűɝ¯ڛ{ï瀰˻̉Ԡò~­omrtRNS@f{IDAT.CCc1H3xNbMCuN @k킪\U{p۪a;\Whm+k]霭e~_uĺ}3sL`2˶{ؚmmxz k!6jIENDB`zeal-0.7.1/src/app/resources/icons/type/Function@2x.png000066400000000000000000000010141462517734600230070ustar00rootroot00000000000000PNG  IHDRE/PLTEf|g{g{o{rwy{wڸÈƯλ˷숴ղоu|~ȳey]tfz)<tRNSPVV IDAT(ϕR0@v Eڀ6 .de3_|\@&431'5&o{cqNQM8bpD,Ik!/JC̐]z>hhS{a4>|zZ w48W$EsIy$\5hP.OghǨr0l?Xғ%"<`k}z[ +_^j0Òg aܼa,˲T鴵дfUϯ&R;n!d+vIENDB`zeal-0.7.1/src/app/resources/icons/type/Global.png000066400000000000000000000004601462517734600220540ustar00rootroot00000000000000PNG  IHDR(ZPLTE`c_cȅϑɇrusvy|kowzˊ͎Ҥy{ׯǏvyor|~ΎȄȅ_c_ccKtRNS _ ngIDAT[MY CCqCqM) Λi JY W›eF5i;NFKf\7etcGB2D/6ygyY~/x /IENDB`zeal-0.7.1/src/app/resources/icons/type/Global@2x.png000066400000000000000000000007521462517734600224320ustar00rootroot00000000000000PNG  IHDRE/PLTEuvuvuvuvuvuvuvuvuvuvǂמ٣ѓɆ{|ƀ͍Әآˉyz՛|}߽֫Łҡ۴ΗВɌȃΏ՛xy֝ʇvwءן֞& tRNS%'(~^!IDAT(uٖ0Dt" CDQPqCz)Ɔ܃C?BF>C yp23 `^bujĄ/M$€I](RiP_Ii#NRӴJB|P-Z +"ל7E]; k Kk^ޕ 7l:rzN؁\mށ[ 6W4)υyOݸ$ހ(`͗U4'0 M?0 ,IENDB`zeal-0.7.1/src/app/resources/icons/type/Glossary.png000066400000000000000000000004601462517734600224570ustar00rootroot00000000000000PNG  IHDR(ZPLTE`_Ȼϳɥrsykw˺äҪyʯ׶Ǩvo|úβȲȖ__Kp)tRNS _ ngIDAT[MY CCqCqM) Λi JY W›eF5i;NFKf\7etcGB2D/6ygyY~/x /IENDB`zeal-0.7.1/src/app/resources/icons/type/Glossary@2x.png000066400000000000000000000007471462517734600230410ustar00rootroot00000000000000PNG  IHDRE/PLTEuuuuuuuuuuƞʣپѵɭ{ñƺɢطˬyěծ|ʫԽġϴ۾νзɳ经άxƝֶʪvȡǟƞP tRNS%'(~^!IDAT(uv0#$m]7 s^93/08VaPg(>M`^"NFpM^Dflr_(Iy0`Xt r*U'YJ ^p%ѐқ#mk·B.Eώl>pCqEuJuIe3e4/j1+tRNSG4@b~h*Pn\IDATcd`ddsY|F&` pc`τ& ,EUM nF dwPA(,12^B EIENDB`zeal-0.7.1/src/app/resources/icons/type/Guide@2x.png000066400000000000000000000034661462517734600222740ustar00rootroot00000000000000PNG  IHDR szzIDATXåoW?zm$Nb旒6% T@ e '1s4(<?˿X6fQ!"y,C$`1oM Z3CD~")4:nij*fKa1`@+-V,tqr㕊5ǚ4cIM%iu\>Gdcdg5]#i쓽֟F@HPhwvx!pN]lw(N5t]a,-6U+qƂ܎)P7fP -!odYF3$d E1S{TU,R`,!ը! BV-6]@b'7nꃍpA#Ep_Lp%z؏wc]B<% x!8"_o|;a?IENDB`zeal-0.7.1/src/app/resources/icons/type/Handler.png000066400000000000000000000003331462517734600222300ustar00rootroot00000000000000PNG  IHDR([CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Indirection.png000066400000000000000000000002371462517734600231250ustar00rootroot00000000000000PNG  IHDRf0PLTEΨӱʢjtRNS䨤,IDAT[c`t4dpKK(KK3(!@ b`cD%S.PIENDB`zeal-0.7.1/src/app/resources/icons/type/Indirection@2x.png000066400000000000000000000004011462517734600234700ustar00rootroot00000000000000PNG  IHDRE/?PLTEӱÖѭӱˣ”ƚÖѭ/ѩ tRNS%'(~^!gIDAT(;0 PCZhʀRf!C bȺnd@j)E Y]TVX>[CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Inductive.png000066400000000000000000000002371462517734600226100ustar00rootroot00000000000000PNG  IHDRf0PLTEΨӱʢjtRNS䨤,IDAT[c`t4dpKK(KK3(!@ b`cD%S.PIENDB`zeal-0.7.1/src/app/resources/icons/type/Inductive@2x.png000066400000000000000000000004011462517734600231530ustar00rootroot00000000000000PNG  IHDRE/?PLTEӱÖѭӱˣ”ƚÖѭ/ѩ tRNS%'(~^!gIDAT(;0 PCZhʀRf!C bȺnd@j)E Y]TVX>[CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Instance.png000066400000000000000000000002601462517734600224160ustar00rootroot00000000000000PNG  IHDRf0$PLTEoYoYvģi}{fvpZoYZq tRNS./IDAT[c`Ta0dPqqU qqe`O, 1w؞ +mIENDB`zeal-0.7.1/src/app/resources/icons/type/Instance@2x.png000066400000000000000000000004201462517734600227660ustar00rootroot00000000000000PNG  IHDRE/EPLTEϱkkkkkkkkkvʭϱƩtƩkƻvɬz] tRNS%'(~^!pIDAT(9@ Dw|G%C ɦ2IvJ 8 V7.e'p-D-8~?G8L3˺.qnW<\7L})ZQk8&e*IENDB`zeal-0.7.1/src/app/resources/icons/type/Instruction.png000066400000000000000000000002371462517734600231770ustar00rootroot00000000000000PNG  IHDRf0PLTEΨӱʢ!rtRNS䨤,IDAT[c`t4dpKK(KK3(!@ b`cD%S.PIENDB`zeal-0.7.1/src/app/resources/icons/type/Instruction@2x.png000066400000000000000000000004011462517734600235420ustar00rootroot00000000000000PNG  IHDRE/?PLTEӱÖѭӱˣ”ƚÖѭ~+[ tRNS%'(~^!gIDAT(;0 PCZhʀRf!C bȺnd@j)E Y]TVX>[CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Interface.png000066400000000000000000000002371462517734600225560ustar00rootroot00000000000000PNG  IHDRf0PLTEξӱʤKPEtRNS䨤,IDAT[c`t4dpKK(KK3(!@ b`cD%S.PIENDB`zeal-0.7.1/src/app/resources/icons/type/Interface@2x.png000066400000000000000000000004011462517734600231210ustar00rootroot00000000000000PNG  IHDRE/?PLTEûѾӲ˥ªú.e/ tRNS%'(~^!gIDAT(;0 PCZhʀRf!C bȺnd@j)E Y]TVX>[CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Iterator.png000066400000000000000000000002371462517734600224470ustar00rootroot00000000000000PNG  IHDRf0PLTEΨӱʢjtRNS䨤,IDAT[c`t4dpKK(KK3(!@ b`cD%S.PIENDB`zeal-0.7.1/src/app/resources/icons/type/Iterator@2x.png000066400000000000000000000004011462517734600230120ustar00rootroot00000000000000PNG  IHDRE/?PLTEӱÖѭӱˣ”ƚÖѭ/ѩ tRNS%'(~^!gIDAT(;0 PCZhʀRf!C bȺnd@j)E Y]TVX>[CcPv%u+qIENDB`zeal-0.7.1/src/app/resources/icons/type/Keyword.png000066400000000000000000000004411462517734600222770ustar00rootroot00000000000000PNG  IHDR(lPLTEbbvnuǣqtީzǶ񬔀zij쵟vڵ|rpsa3tRNS^[FcIDATU70C]&HÆM''"VbSL, xFq,(LYeݴXFcpiŘruD=o:qߑb_-"L~IENDB`zeal-0.7.1/src/app/resources/icons/type/Keyword@2x.png000066400000000000000000000007021462517734600226510ustar00rootroot00000000000000PNG  IHDRE/PLTEcbbi~n|sxuIJr񵟍qijpǶ~ڬީzͿ쵝ʽǯvƹzatRNSPVVIDAT(}r0 P@.aOSu_t =iǶ|\Yq\$XTJiQUť2-k{$6ΰFhv#0#U MZ (NJZxxIe; F?15-Dӧ^?k}[tIENDB`zeal-0.7.1/src/app/resources/icons/type/Kind.png000066400000000000000000000004411462517734600215400ustar00rootroot00000000000000PNG  IHDR(lPLTEbbvnuǣqtީz񬣀zľ쵬vڵ|rpsa@+tRNS^[FcIDATU70C]&HÆM''"VbSL, xFq,(LYeݴXFcpiŘruD=o:qߑb_-"L~IENDB`zeal-0.7.1/src/app/resources/icons/type/Kind@2x.png000066400000000000000000000007021462517734600221120ustar00rootroot00000000000000PNG  IHDRE/PLTEcbbi~n|sxuĽr񵬍qľp~ڬީzɯ쵬ҽǯvϹzaJJGtRNSPVVIDAT(}r0 P@.aOSu_t =iǶ|\Yq\$XTJiQUť2-k{$6ΰFhv#0#U MZ (NJZxxIe; F?15-Dӧ^?k}[tIENDB`zeal-0.7.1/src/app/resources/icons/type/Lemma.png000066400000000000000000000003031462517734600217030ustar00rootroot00000000000000PNG  IHDRf0$PLTEXn҆xϮT`тЂֱW" tRNS{p !BIDAT[c`!gccEaӴ`cB C\tZёCCCC \jȄ}J{IENDB`zeal-0.7.1/src/app/resources/icons/type/Lemma@2x.png000066400000000000000000000003621462517734600222620ustar00rootroot00000000000000PNG  IHDRE/3PLTEYXX`v҅lɮTЂˇȁxgĺ}|tRNSPVVkIDAT(@0 4=Z(S# c~˜˂(bSPɦT+ I ͬ? +9?~:[ '++< f"~wQIENDB`zeal-0.7.1/src/app/resources/icons/type/Library.png000066400000000000000000000003031462517734600222540ustar00rootroot00000000000000PNG  IHDRf0$PLTEX̘n׬ϡx~TŠ`֪֩W[ tRNS{p !BIDAT[c`!gccEaӴ`cB C\tZёCCCC \jȄ}J{IENDB`zeal-0.7.1/src/app/resources/icons/type/Library@2x.png000066400000000000000000000003621462517734600226330ustar00rootroot00000000000000PNG  IHDRE/3PLTEYXXċ`ϟv׬ɕl~TժӨУϡxďgtRNSPVVkIDAT(@0 4=Z(S# c~˜˂(bSPɦT+ I ͬ? +9?~:[ '++< f"~wQIENDB`zeal-0.7.1/src/app/resources/icons/type/Literal.png000066400000000000000000000003031462517734600222440ustar00rootroot00000000000000PNG  IHDRf0$PLTEXrn̈מxϒTm`y֛֜Wqӧ tRNS{p !BIDAT[c`!gccEaӴ`cB C\tZёCCCC \jȄ}J{IENDB`zeal-0.7.1/src/app/resources/icons/type/Literal@2x.png000066400000000000000000000003621462517734600226230ustar00rootroot00000000000000PNG  IHDRE/3PLTEYrXrXs`zvϐמlɅTm՜ӛЖxϒgĀ=\tRNSPVVkIDAT(@0 4=Z(S# c~˜˂(bSPɦT+ I ͬ? +9?~:[ '++< f"~wQIENDB`zeal-0.7.1/src/app/resources/icons/type/Macro.png000066400000000000000000000003461462517734600217200ustar00rootroot00000000000000PNG  IHDR(?PLTEvw~ؘޣˁݠܟ͆ԔᲲⳳՔtRNS@fUIDAT[m 0 a%^k*Z*8́A=AEA|4> -M鶓$,U\/0s9IENDB`zeal-0.7.1/src/app/resources/icons/type/Macro@2x.png000066400000000000000000000004341462517734600222700ustar00rootroot00000000000000PNG  IHDR#§0PLTEiihjhjoqƄΒsvɈ̏ʊƄ֦ʻ #tRNSPVVIDATc`T6#ac$`Ƞ!F an11*//oL07\ ܬ峀xdp[^^wr@vnyisr 9c㦕/d+x, f Rf-Q#BP9/IENDB`zeal-0.7.1/src/app/resources/icons/type/Member.png000066400000000000000000000005071462517734600220650ustar00rootroot00000000000000PNG  IHDR(PLTEXKYzΉȮTUʄij^ȆsȀa`mЖ\YЕʊ¹iצ~Ӟګաјsբзf͏|jqj9tRNS@feIDAT] 0 D+h牨X*DD\c?MxJ9GPcժ(*Nst*wKu#c`Y lDrH~_(,3 IENDB`zeal-0.7.1/src/app/resources/icons/type/Member@2x.png000066400000000000000000000006721462517734600224420ustar00rootroot00000000000000PNG  IHDRE/PLTE@@@JfyFrXtbIӝɧLhSKnāf[ueʌܰqДΒܮӱ\eOWu٨ߴ׫Rk6tRNSPVVIDAT(}0.qQ\P.SDpKLJQRBFdD ֬jMh TiҷV6?v?ha:A1 `Π-{j}\Z >n2-29`#380wEכn]ˢ=щy^,$H~Qk,0r4k92{a-IENDB`zeal-0.7.1/src/app/resources/icons/type/Message.png000066400000000000000000000005071462517734600222420ustar00rootroot00000000000000PNG  IHDR(PLTEYXLKYZz~ΏT`Ubʊ^hȍs|ȇaj`imuН\fYeڕМʒit׬~ćȞӤڰեўs}էfp͗|…jsqz+tRNS@feIDAT] 0 D+h牨X*DD\c?MxJ9GPcժ(*Nst*wKu#c`Y lDrH~_(,3 IENDB`zeal-0.7.1/src/app/resources/icons/type/Message@2x.png000066400000000000000000000006721462517734600226170ustar00rootroot00000000000000PNG  IHDRE/PLTE@J@J@JJTfqyŅFZrXitbqI]ӧL_hwSeK_nzĎfv[kuÁeťʘӰܸqПΝܷ\nevObWiuٲ߼RckyVQ otRNSPVVIDAT(}0.qQ\P.SDpKLJQRBFdD ֬jMh TiҷV6?v?ha:A1 `Π-{j}\Z >n2-29`#380wEכn]ˢ=щy^,$H~Qk,0r4k92{a-IENDB`zeal-0.7.1/src/app/resources/icons/type/Method.png000066400000000000000000000005071462517734600220760ustar00rootroot00000000000000PNG  IHDR(PLTEXKuY~zljTkUk^usax`xm\sYoЊi}~垭ӫڡ՘sf{|jqA؊tRNS@feIDAT] 0 D+h牨X*DD\c?MxJ9GPcժ(*Nst*wKu#c`Y lDrH~_(,3 IENDB`zeal-0.7.1/src/app/resources/icons/type/Method@2x.png000066400000000000000000000006721462517734600224530ustar00rootroot00000000000000PNG  IHDRE/PLTE@`@`@`Jhf}yFVrXitbsIYL\hyScKZnfu[luev茘밹q~Вή\ketO^WguRck}CtRNSPVVIDAT(}0.qQ\P.SDpKLJQRBFdD ֬jMh TiҷV6?v?ha:A1 `Π-{j}\Z >n2-29`#380wEכn]ˢ=щy^,$H~Qk,0r4k92{a-IENDB`zeal-0.7.1/src/app/resources/icons/type/Mixin.png000066400000000000000000000005071462517734600217420ustar00rootroot00000000000000PNG  IHDR(PLTEXKYzljTU^sa`m\YЊi~ӫڡ՘sf|jq;=tRNS@feIDAT] 0 D+h牨X*DD\c?MxJ9GPcժ(*Nst*wKu#c`Y lDrH~_(,3 IENDB`zeal-0.7.1/src/app/resources/icons/type/Mixin@2x.png000066400000000000000000000006721462517734600223170ustar00rootroot00000000000000PNG  IHDRE/PLTE@@@JfyFrXtbILhSKnf[ue茿qВή\eOWuRkxsntRNSPVVIDAT(}0.qQ\P.SDpKLJQRBFdD ֬jMh TiҷV6?v?ha:A1 `Π-{j}\Z >n2-29`#380wEכn]ˢ=щy^,$H~Qk,0r4k92{a-IENDB`zeal-0.7.1/src/app/resources/icons/type/Modifier.png000066400000000000000000000004721462517734600224150ustar00rootroot00000000000000PNG  IHDR(xPLTEitit|Ϗ]b|flƀҡmrˉfk޹otǃbfpuƈ~եڰڮ͘ˑ͖ko˒jov|΍tRNS^[FpIDATU0Лް-3Ӫv$W&YAnO!ƅxag3ͻ{еcwM i)"yH6(G~ րZ*+\IENDB`zeal-0.7.1/src/app/resources/icons/type/Modifier@2x.png000066400000000000000000000007031462517734600227640ustar00rootroot00000000000000PNG  IHDRE/PLTEitititpzɃϏjo]b|v|΍flpuܵˉ`e˒txyko͘ڰmr͖bfɆ~otզfkƈӞҡ׫3!tRNSPVVIDAT(}0 Bw<޷әzBdef0UoR))lhUMZA*x*j`SF6us=e|㐙#i'2OSayJAJhb$ǩRpŎ .v7R([C8QE9O|>($:J'}/DWM7|r|ܝ_,X?xfR];+!XMIENDB`zeal-0.7.1/src/app/resources/icons/type/Module.png000066400000000000000000000005101462517734600220750ustar00rootroot00000000000000PNG  IHDR(oPLTEҠs٬߹Ѡyݴҡ{޸ūשڱٰ֨ͷ֩Īݶң}ݸԦجӣ}:tM%tRNS8قcIDAT1@0fOI= O@ #XR$eK24ZSt__}@xc 4l{. -P`{I{% @+fIENDB`zeal-0.7.1/src/app/resources/icons/type/Module@2x.png000066400000000000000000000006671462517734600224640ustar00rootroot00000000000000PNG  IHDRE/PLTEҟsџsҟsԤy۱߹Пy޵֨޸٭Ҡ|ӣ~ڰըң~ݴīݵ֫گʳ߻ѼͶٯݶԦج࿣Ԧ۳W:tRNSPVVIDAT(} `d,teobLx~1~Ña!<_=!<'| '}!%kR*MmЬU!HC1-aAKX)]SqMo[ٸt '/ 5^ 1-;ƢMh7Ne^S\~7ߏc-K?4+IC{Ƒ9/\Ű]NIENDB`zeal-0.7.1/src/app/resources/icons/type/Namespace@2x.png000066400000000000000000000006031462517734600231210ustar00rootroot00000000000000PNG  IHDRE/rPLTEҟsџsҟsԤy۱߹͗pΚr۳֨޸޶ӤНw߼Ҡ|ԤΛt͵جӢ}ح֩ٯӾզҟyڰѼtRNSPVVIDAT(} Pv#qIܳ'Ab.M1!&BH #b*JfT)D,u([!iA"4?ɋ n΋,N֗aٺ<67WEԕ#0zw-"`+HoWǼpWhcx g; Y̮R}/8miIENDB`zeal-0.7.1/src/app/resources/icons/type/NewSnippet.png000066400000000000000000000007551462517734600227570ustar00rootroot00000000000000PNG  IHDR(-SPLTE{{zz]]ee[[~~||zzwwuu}}qqyymmllltRNS+IDATM Pge<D*Ҩv-{rIB%D canHݠA‍sGNʱ1m &"&I|i>[PO=^pVh8Nh 0R XwY`(^j\='-@[;"X,|Z>̷X &444`m&3\60x c`p"_&o56F :6?CBpbJGVtL RۣIENDB`zeal-0.7.1/src/app/resources/icons/type/Node.png000066400000000000000000000004171462517734600215430ustar00rootroot00000000000000PNG  IHDR(HPLTE҈sٗߧ́pцwޥҊyަٝזԎ~ҋ{΃rȿ΅tφubUtRNS7^IDATU90 CA;8;aM)xtD4-(H6-dVQKgމ>S\~7ߏc-K?4+IC{Ƒ9/\Ű]NIENDB`zeal-0.7.1/src/app/resources/icons/type/Node@2x.png000066400000000000000000000006031462517734600221120ustar00rootroot00000000000000PNG  IHDRE/rPLTE҇sчs҇sԍy۝ߧ̀p΃r۠֓ަޤӏЇw߬Ҋ|ԏ΅tؘⰤӌ}ؙٜ֕ȾՑ҉yڝZ@tRNSPVVIDAT(} Pv#qIܳ'Ab.M1!&BH #b*JfT)D,u([!iA"4?ɋ n΋,N֗aٺ<67WEԕ#0zw-"`+HoWǼpWhcx g; Y̮R}/8miIENDB`zeal-0.7.1/src/app/resources/icons/type/Notation.png000066400000000000000000000004171462517734600224510ustar00rootroot00000000000000PNG  IHDR(HPLTE҈sٗߧ́pцwޥҊyަٝזԎ~ҋ{΃rȿ΅tφubUtRNS7^IDATU90 CA;8;aM)xtD4-(H6-dVQKgމ>S\~7ߏc-K?4+IC{Ƒ9/\Ű]NIENDB`zeal-0.7.1/src/app/resources/icons/type/Notation@2x.png000066400000000000000000000006031462517734600230200ustar00rootroot00000000000000PNG  IHDRE/rPLTE҇sчs҇sԍy۝ߧ̀p΃r۠֓ަޤӏЇw߬Ҋ|ԏ΅tؘⰤӌ}ؙٜ֕ȾՑ҉yڝZ@tRNSPVVIDAT(} Pv#qIܳ'Ab.M1!&BH #b*JfT)D,u([!iA"4?ɋ n΋,N֗aٺ<67WEԕ#0zw-"`+HoWǼpWhcx g; Y̮R}/8miIENDB`zeal-0.7.1/src/app/resources/icons/type/Object.png000066400000000000000000000005331462517734600220630ustar00rootroot00000000000000PNG  IHDR(iPLTEg|xejf˨nĞc_}fb~qгkm3[#tRNSͻX*~IDATEAQỏKA!P0DAk 6B$l^MyP0BAtdxAv?SUtPA6!z>[{f/ܝWEfUc"$ğ?. IENDB`zeal-0.7.1/src/app/resources/icons/type/Object@2x.png000066400000000000000000000011231462517734600224310ustar00rootroot00000000000000PNG  IHDRE/PLTEfggoyolf`~rg˪ֻɞd|вcʨÞmԹǡiίƠթuѵؿ̸{kؾwr6 tRNSPVVQIDAT(}]W@`du@`"ъ4Ǻ0hs[{f/ܝWEfUc"$ğ?. IENDB`zeal-0.7.1/src/app/resources/icons/type/Operator@2x.png000066400000000000000000000011231462517734600230160ustar00rootroot00000000000000PNG  IHDRE/PLTEfggoyolf`rgªλd|Dzޞcùm̹iįƻʩuɵп{k;w맘rݫ(tRNSPVVQIDAT(}]W@`du@`"ъ4Ǻ0hs[{f/ܝWEfUc"$ğ?. IENDB`zeal-0.7.1/src/app/resources/icons/type/Option@2x.png000066400000000000000000000011231462517734600224730ustar00rootroot00000000000000PNG  IHDRE/PLTEfggoyolf`rg˻d|cmiΠuѿ،{kᕼwrytRNSPVVQIDAT(}]W@`du@`"ъ4Ǻ0hs1]jsxmFIENDB`zeal-0.7.1/src/app/resources/icons/type/Provider.png000066400000000000000000000006021462517734600224440ustar00rootroot00000000000000PNG  IHDR(PLTEŨŦ£äĥѺĤ˳Ȯ֬¢齿9tRNS@f|IDATEKA@{^WWbd%X}2oq_Z̨zo袒E+=xͮM4P!t.T7yyZy\Iwr;`! dTqK&TG4Wd=7$oIIENDB`zeal-0.7.1/src/app/resources/icons/type/Provider@2x.png000066400000000000000000000014131462517734600230170ustar00rootroot00000000000000PNG  IHDRE/qPLTEŨŧŦĥäĦĥ̿¤ťĨĤڸҾֵ¤֮ڲϦ¢ϸҴ޷æίtRNSPPzCBIDATMjSw^GKDA"t:p)\rLARK$H`םnkHt3 V $bf }4aV`@OCAcAAt͇q@IkΓCnYAV+hY g؎0ڽ1wIENDB`zeal-0.7.1/src/app/resources/icons/type/Query@2x.png000066400000000000000000000020451462517734600223340ustar00rootroot00000000000000PNG  IHDRE/PLTErswssvrrv|~xz|wy{y{}{}~Ν{}}z|vxz}~}~vwzⳲ򈊍|}uvy͘wx{tvxz|~y{~suwuwy|~rtvxz}rsvtuxwy|z{~qruprtuvx~Ւoqs␑}{}qsunpr룣Z[]degnor|}†pqtjlnstwmnqrsu{|rtwlnpopsfgjabe^htRNSPPzCIDAT}[KaݙIkF,RdtĠ>@P}ꦫ,!P0 /Vpವξځ y䱜R)G fu`,Q!h1-R#/- aIJrRf>Jb>A=&ŬX]XpB)q R d0 u`"jbrD_;x&(Ry|)G$vhNŒ1:`|Wy'8b涽/qm4U]u9!H}{]c;Q%;C".OoXNBiEa;22~z&L?DW}?wӶȃc{ /~ͯrNwKg1RH6Э)i[ 5\c3lwTrFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Reference.png000066400000000000000000000004411462517734600225510ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Reference@2x.png000066400000000000000000000007151462517734600231270ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Register.png000066400000000000000000000004411462517734600224370ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Register@2x.png000066400000000000000000000007151462517734600230150ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Relationship.png000066400000000000000000000004411462517734600233140ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Relationship@2x.png000066400000000000000000000007151462517734600236720ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Report.png000066400000000000000000000004411462517734600221260ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Report@2x.png000066400000000000000000000007151462517734600225040ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Request.png000066400000000000000000000004411462517734600223030ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Request@2x.png000066400000000000000000000007151462517734600226610ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Resource.png000066400000000000000000000004411462517734600224420ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Resource@2x.png000066400000000000000000000007151462517734600230200ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Role.png000066400000000000000000000004411462517734600215540ustar00rootroot00000000000000PNG  IHDR(cPLTEW W W W W W o v d [ h t g.޲\] Y ǧԽe ƭa r uGj l f Ҫ_ ytRNSIJeehIDAT[U  xXJd2d x `B\:%](F|¡I8DY5nY˺'Jx[i`# (BU{' IENDB`zeal-0.7.1/src/app/resources/icons/type/Role@2x.png000066400000000000000000000007151462517734600221320ustar00rootroot00000000000000PNG  IHDRE/PLTEv W W W W W W W W W ` s v q ^ g q W m ǧԽnޤg.t uGn j \p Y β` Y d b ǹ^ k u b [ s 3! tRNS%'(~^!IDAT(}0rbEg63\ ѓ6ٲg DžA#$ d4E_A@#6T?'-P Q @3_hOd!VXspD;>rFb̴(Lݦ93QȦͫv|@t\Hh׆*q\ ĥ̕*q=>ǣ\ڹ+L`Ч90@Gzcm+P0GIENDB`zeal-0.7.1/src/app/resources/icons/type/Sample.png000066400000000000000000000013401462517734600220730ustar00rootroot00000000000000PNG  IHDR(-SPLTELPNK|GwCp@kh{nJrY`fxy]]ZWSMzKvd_^\XSjbaa^Zoihdb`tmmc_]WP|wrqmcrYb_Yzdd`^`\|ww~sgrutqijoz||{no{tvy~}}tq}~}|{zyQq$tRNS:SO (QY]bcĈ*OGIDATu1ADߛQhK"tNwP@"C_uWW@3_A&½gD dZ!IAz (@h5"#blBg>\~6(!"AVL yIENDB`zeal-0.7.1/src/app/resources/icons/type/Sample@2x.png000066400000000000000000000034651462517734600224570ustar00rootroot00000000000000PNG  IHDR szzIDATXõ]\U{oiiK C!PP jbB" ITׂFAAFc Hc!*RJ-sht̜SS9p%g=Y~7;篹s}!Y46D; #PzQ-oW{Kr>>VAʪU1AqʒjΠFУ?|wOѭ`C#eV\C˴.seߣU.0te[|1[嚻xСU^IBpVUI8UtJ7tO;t~~̀py.dzW 2jXP HTqR;[vрٝdiR RLq}OXF2ӳiE$aT&/Efٖ1 /8UCJJf\XI)Ef7L8));ɝr\ *t5s(A!RdJgCޜ-h^uqQEР RuZVES蕁PPQ ճ⬭4 odisd<\I +o+ap=?V=)Kٖoڇa%DqQ˜ɳͣ&",]>Ѵ;37"^UܲOrxo̸j{DThgpz'Wd[HBB16sƠsƱ زq`o`iW, J+SjB7a6IP{?&ײRb{Ӊ_*63Gn^2N䥷"gօm&XfdG@D0ZAieWnS%u%su ;ERpC9.6Inņ D{$퉔/cN3dh {fk#\J=XBgT h \O TeB6Qe B#z)yqG԰ XQaK6SLZRMb6 R_A~Mqn# umiL% C̙|Y W8 Fpˌ "Ɩ\`2 UU8)9 MMMa0Q%|F[Հ8*ϴ>oGҤ8 w er,("Ri Q͓PDf; % 2*qV 8: 43Ł؉^ׁ" ̷/JRZ,qw½7}X@3snxeꟌ" ʺ*z! \`)HZ=3gtXs~~PMz"S5H-'w ^Z{E$g;m @-e3Ͳ S*`4-ن .| =aivZ vmLDe!5`hPJ͠plTh N;D| ԎvLL,l}6- W7H=UѓX?ާO5$x0m4/{G~Φ fBIENDB`zeal-0.7.1/src/app/resources/icons/type/Schema.png000066400000000000000000000005661462517734600220630ustar00rootroot00000000000000PNG  IHDR(~PLTEyŝ™Ć{~뙥}㮶˜əd*tRNSH_}IDATQJ1lvU|K=?G(,ig"XIBd$$f!mB}  ڎQXou!)S,6HZnK3IV VP'X#AIENDB`zeal-0.7.1/src/app/resources/icons/type/Schema@2x.png000066400000000000000000000010071462517734600224240ustar00rootroot00000000000000PNG  IHDRE/PLTE{zyŘĊ|~Ȏ꒝딠胏ԭ؅ŘWtRNSPVVIDAT(}v0QR+H[hp#BB?8d ce[Y_Y櫵یwos{xԗ; vEѓ<4@HLjCCN( zP`e^^uHWƒ4Xx[aLdJubAYCX;F+2ОUѝC'lKyN:QCj7UtJuЯ'J&(MP]tZ``15ԏ~?$$NIENDB`zeal-0.7.1/src/app/resources/icons/type/Script.png000066400000000000000000000005661462517734600221270ustar00rootroot00000000000000PNG  IHDR(~PLTE{RّlᧆߝݝzᣃՋcъaՏhυUؖsީؗp҈XזoߩӉYՎe⭏ۙ{чWݞ}ؘsⳗޜ~ٚyⱑݚzݜuٔn֏f֋aٕqՑ}*tRNSH_}IDATQJ1lvU|K=?G(,ig"XIBd$$f!mB}  ڎQXou!)S,6HZnK3IV VP'X#AIENDB`zeal-0.7.1/src/app/resources/icons/type/Script@2x.png000066400000000000000000000010071462517734600224700ustar00rootroot00000000000000PNG  IHDRE/PLTEzUzS{RԂZݙuᧆޞyٗnܚwេՏhυWш\ݞ}φZӌcܤఒؖmޠؘsߧЇ[ᯎ˹בjӍfژuؙxҊ`㰕ŰԌb帟̺ީޜy0%!##^L4'nϙ>P#,92o=Pp"%@м bb| MM5hSEeŒ>ύ^юV3>Y"!:yb Z-3A4o5|&Er0ulVuĜDB&;V&K!`kZ ;r '`G4bYë*>i7IؓcXI[670LwS3BBf,5q=Px|x ?zz_*pIENDB`zeal-0.7.1/src/app/resources/icons/type/Sender.png000066400000000000000000000005201462517734600220710ustar00rootroot00000000000000PNG  IHDR(PLTEQE}QE}aUqel`g[i]nbRFuRGt]S}XMxGcԱOYz/7pkv>,=fr^z'=bIENDB`zeal-0.7.1/src/app/resources/icons/type/Unknown.png000066400000000000000000000002661462517734600223170ustar00rootroot00000000000000PNG  IHDRAZ}IDATc??P#h fry%y1%!:?_SU /96I6jbsIKt @X; yIENDB`zeal-0.7.1/src/app/resources/icons/type/Unknown@2x.png000066400000000000000000000005241462517734600226660ustar00rootroot00000000000000PNG  IHDRIDAT8˵MJPềo(T rC  "wMt/$+H d]@Jj篃 ܂Q/ѫQsu0s;5bΚ`5s^lBN(BL9) FqCibOGGEH-0AttĽ5h< mJ-P# ^G+\ Q h=?v %k,qX(1)d_*={\^.8jt4OFNYr IENDB`zeal-0.7.1/src/app/resources/icons/type/Value.png000066400000000000000000000005671462517734600217400ustar00rootroot00000000000000PNG  IHDR(~PLTEbvn|q}v̾աp|ǵrijxzʺЭzĶvɾs즌u ۜ*tRNSH_~IDAT[M0 npQC/hN9?~ɒM_81&ROz5Tm!OuQY+\4Z}EM7a0o`VKҗޞHETIENDB`zeal-0.7.1/src/app/resources/icons/type/Value@2x.png000066400000000000000000000007631462517734600223100ustar00rootroot00000000000000PNG  IHDRE/PLTEcbbi~xnuvs̽zСp~Ǻzqƹvʺɽrij| }tRNSPVVIDAT(}v0` :^esږuWp3 zjpyrOM49J R4]sS\?ֈHdb,yD,R`1"U;Z =(gQK\ZHW9&Bw~?OD v49>MH'D7g[l:HTu$A+gIN.JSޛFuůu[WWw-ӴI!#f43C=;A?\GIENDB`zeal-0.7.1/src/app/resources/icons/type/Variable.png000066400000000000000000000006541462517734600224060ustar00rootroot00000000000000PNG  IHDR(PLTEb~Tta}|KkcjNnvVufJj^|ʫkʫΰ\zv˭ptNnlzMle~ԹĢa~}ZwύQpgxTtԽmePo\{[yPoRq}ptRNS@fIDATnQ@ѽ}I E "* t-Q3%+XQ3s! V?;-FU޻`4hp5LP @kK{W) 123OEZfОIENDB`zeal-0.7.1/src/app/resources/icons/type/Variable@2x.png000066400000000000000000000010131462517734600227460ustar00rootroot00000000000000PNG  IHDRE/PLTEPvPvOtX}oiblteFnLtZ|{l}IqvxV|zPw󺍨Syѳʨּ˴^aooÜJ'tRNSPVV0IDAT(}b0 JpP.2)0'9m,ñ-Oöqp7±p7fB0!LDn&I3 Ke=%2PJNP'4?R ѡh3"aO,RԴ Dg(qT.Juڱ C5хS3Ut C?(+dWjƫ6Njd_=pW\B@tGlXNY$ ++レ,QVUd =374~ Nˡ~; ÄIENDB`zeal-0.7.1/src/app/resources/icons/type/Variant.png000066400000000000000000000005671462517734600222700ustar00rootroot00000000000000PNG  IHDR(~PLTEbvn|q}v̾աp|ǵrijxzʺЭzĶvɾs즌u ۜ*tRNSH_~IDAT[M0 npQC/hN9?~ɒM_81&ROz5Tm!OuQY+\4Z}EM7a0o`VKҗޞHETIENDB`zeal-0.7.1/src/app/resources/icons/type/Variant@2x.png000066400000000000000000000007631462517734600226400ustar00rootroot00000000000000PNG  IHDRE/PLTEcbbi~xnuvs̽zСp~Ǻzqƹvʺɽrij| }tRNSPVVIDAT(}v0` :^esږuWp3 zjpyrOM49J R4]sS\?ֈHdb,yD,R`1"U;Z =(gQK\ZHW9&Bw~?OD v49>MH'D7g[l:HTu$A+gIN.JSޛFuůu[WWw-ӴI!#f43C=;A?\GIENDB`zeal-0.7.1/src/app/resources/icons/type/View.png000066400000000000000000000006541462517734600215730ustar00rootroot00000000000000PNG  IHDR(PLTEbqTfap|K]csjzN`vVgfwJ[^nk{ʰ\lvˇp~tN_l{zM^ev~Ԣaq}ZjQbgwxTfޘm}euPa\n[kPaRc}NtRNS@fIDATnQ@ѽ}I E "* t-Q3%+XQ3s! V?;-FU޻`4hp5LP @kK{W) 123OEZfОIENDB`zeal-0.7.1/src/app/resources/icons/type/View@2x.png000066400000000000000000000010131462517734600221330ustar00rootroot00000000000000PNG  IHDRE/PLTEPhPhOfXooibyltezF^LdZq|{l}IavxVmzPgSi޼օ꘧^uavooKM_tRNSPVV0IDAT(}b0 JpP.2)0'9m,ñ-Oöqp7±p7fB0!LDn&I3 Ke=%2PJNP'4?R ѡh3"aO,RԴ Dg(qT.Juڱ C5хS3Ut C?(+dWjƫ6Njd_=pW\B@tGlXNY$ ++レ,QVUd =374~ Nˡ~; ÄIENDB`zeal-0.7.1/src/app/resources/icons/type/Widget.png000066400000000000000000000005601462517734600221000ustar00rootroot00000000000000PNG  IHDR(PLTEH|H{^{}Gye~ud~a{pb|Ct_yj{Dvh܏ᅙkrt䄞əoxfǀy|tRNS^[FIDATE]NAvU!!D`5Q3tsxJC$(Is\H7=Tq:Qn5߄p@\c-*^.,]W LrP@\~~YZ !l6qIENDB`zeal-0.7.1/src/app/resources/icons/type/Widget@2x.png000066400000000000000000000011341462517734600224500ustar00rootroot00000000000000PNG  IHDRE/PLTEI|H{H{Sq„i{ƂGy_yrmx\wCtjuphfe~{a{ɷFxd~ktv}НЊǴoȽBQtRNSPVV`IDAT(}v@ EQDPXj/jz\>yIȆL&x盇{i4;l3GuH:]{}SO$l@)!e蓅#fӄ(G0G#s\R"9C0>Ωb.)/H_hn.ifˉVU-W]J{m@+8?2oDZQB V2PRYͤ|QlQ{F'(~Tr`H!E2h09sf i)9y˵adM)^N-kzr:|w %z tAa |q[n~VOWR?es-=7IENDB`zeal-0.7.1/src/app/resources/icons/type/Word.png000066400000000000000000000005601462517734600215700ustar00rootroot00000000000000PNG  IHDR(PLTEH|H{^{}Gye~ud~a{pb|Ct_yj{Dvh܏ᅙkrt䄞əoxfǀy|tRNS^[FIDATE]NAvU!!D`5Q3tsxJC$(Is\H7=Tq:Qn5߄p@\c-*^.,]W LrP@\~~YZ !l6qIENDB`zeal-0.7.1/src/app/resources/icons/type/Word@2x.png000066400000000000000000000011341462517734600221400ustar00rootroot00000000000000PNG  IHDRE/PLTEI|H{H{Sq„i{ƂGy_yrmx\wCtjuphfe~{a{ɷFxd~ktv}НЊǴoȽBQtRNSPVV`IDAT(}v@ EQDPXj/jz\>yIȆL&x盇{i4;l3GuH:]{}SO$l@)!e蓅#fӄ(G0G#s\R"9C0>Ωb.)/H_hn.ifˉVU-W]J{m@+8?2oDZQB V2PRYͤ|QlQ{F'(~Tr`H!E2h09sf i)9y˵adM)^N-kzr:|w %z tAa |q[n~VOWR?es-=7IENDB`zeal-0.7.1/src/app/resources/zeal.icns000066400000000000000000025047631462517734600177040ustar00rootroot00000000000000icns TOC `ic08C A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  @IDATxYnu߷c$EHHY)9$JXSESy0n$A ~p# )N")HElN=߾{MUs[iuUZjժUî n ID;ߛ)7%:JuAUzPuJ^VVWN ^@z"ݴl't/]Aܛ SW!S#\Y^.;Z-GMxU=)zrߞvrW_ rk|54a5>Z+pV4\-{5n=}ݴߟn͵n⾋ Lf1LG.I??VntGsrYixI)ϻ)y޼ȿ/|z=IF幛<#~>'~8zq20"4hR%ߌK,Q?Q.Ί<'?/86ZIX*gs&]C -˧!|VLՃ?C-wW9_p]ʙ4CW>xM!KE 5OAF:ɦf R Dz \GU N`?rǤ;=)c )VϕY>W47)`lֳ_¨,~o?::zpp0x?nݺ=coGx?y`VL t鸛>F4v;"}{o=DMA+@m(wA31zRs 7+Ms-xD 9W(u |KW X(8Kv0R.. jRn)rY:Avu+톳Ui njCOd%1t%ۡĂ\1,޷WU |R4'eirS巕Ȥ6Z-q6vuֻãWW_ٿأ;э6&囗/\~g>Hy`gg~l0zvf3Onx ^}:Afa}ڳ*>nizko^薗 -=V(â1@WUbz9t;`u Aq0g,UEkF5Q.8x* D,*| 0PqD(6Au84t )Nc%CCzGDwBw#`m@[.G]IrXPYwAISV$ ع(tg׌zWaMЈq>ֿ~ȧ/|j:79)#oA aAUZr% |3daG#3.v4Roؿ2Ϗo⟿gox# @{wvv~~}f)z.̺EdN1<3DH*FoLAw̺;4nwThm,nJpLC%uJeĺF 6eiYǝ(zN- e^CϹCйh8…{u"E5(p p5*4%e%iDRڮ;˖ 4: 4|A_'tWY!?n!y,t(ֈ^oVu&ݳԳ1/M"|1ti_rC~+(1p(E#ϭmz퍖F[/bFg~^^;338! 3 0 &w2Ο{ދO_ۤqjNsJk@\D  ;⏊@/&a}A;w/S;m.95/`F =m{N-u=?pe ﺏ>~nVVJw |D~w{ 1p7g-:_)hlHȕ(گ=|x W=>>xq}n82 )܇[ ^I9Д†peu@9݇_ G7g~-<|:BV曆|Sr~r[V8E˫G Ct]^C  p0UଷO*g9uP~WVG>zm7z:{x?Vl #gmU|"{61^q?z'],ܾ?`}r}/x=1;I:F&RV+ܿVKjU;`/u䙢!Te;`tci=z<6F!S~ F+}ڛCmxY&o;BK8*(Өx{|+xuS;E6V>t{iIn8{>p_ GYhH/ ><Xإ[Lxܫ!&4l-܇awĸ`K;<66 [<8ι;p#uRS;b1&wP+]|M_uPC/șb nzCNnli;B/٢C2tV=>س۽tzKg6/o<?h'x]~_W~?M <䦂 nߙ1%J5:&a qpoퟣ6Ahox] //:ߵkOxp}&; THzGn]Jv8Hw2T9+{( ƌiLNcnmb6ֺ yyx|OW[}ݥ $-NW_i,|z|}m}3[=8-6 :F/fKOgc|_AwuCTgc6倏*-E&6ЁqT8ˁoD/5@3'kvOjыG9V:Yg$>2ϖrh1)r)t!LuisHEt_\ܒtqSS;ėg{ZT|zaoBw(ψ Cuzc!%' t,vׁn]2D(:+[x CKLH 0aFoǷowp{G>_^Cf[KO/=Hg۷G~}m{{9L+!VId|0~:Ro:?c{Is{\}ZFovgݥA߼\wcx-Gr"ܹQsyq.􁛆hE0y+^S7;KOnc >Ɓ7M9vo쭬OoN][>w??,Eo`wqcvww6vzlXS@SpG qPLl-IqgNa%vR7.g,CTg7b1$3VA KXYȆ۳aD/Zjd^[XX 0wvk#F4I /Fs-MH0Ȅ 2.} ']3E)/y!U> JW#qfGFG('Cxpe {lGy]KFr[o3 k>+u&܁e;-GSq<˭W=V{nʃs7C^:pˆ)ҾCI`ڪs8`iWZVhN7_<;f1ag__3:PQpo]_goyogX|tV ?Y’& :(3L e =ι1W=!J>VE|t{TJ\ۅU!B#z=<0ã0kÐF}^Gh1E ==C WGM+)hXjA4&*g ]TNANL'2:4Da t<] \SVE1{#QB㧮_IFuo@Ӑe E{ȣ֝=х_S>gyu%1* {_qX~d.⭉&Crև ;(GDIJku)~Xej3p]v8?ҳ7'ٷcw?9V1fc?bԵsPl`?֭ٹ}awggsD$5v}ֶn"'L* ԕo*1i`unLCꁎw}}*A!v=ȐlN֋;o\{?zː+/%`<_Z 1ۿs#sU_pxtm4zؗD72 мmh iyJwim[+{=(lҋwoOeJ)@ H:3g0)<@3r;p KL/S( FBZw})( TSAP*6t0_m `Qw0c\|*84Xtac0)jOAc~0[/; %UZ뱉JhrAÑ7,Dąrr"q>4:o,tfxK1D1TQ/=yy{i i zgK zѿ`t|i࿃ڍTifL]_N]|)fc,{YaI%E^IGUdMxZp9eag_k\ ⃄Ћ@ڠ!qʅ'o:Lੌ-5k M !S?Άvi[56s&W ݆LipEO =x ZmWabp}ed!#-yBMh ZxL%*hMN j¢a/MrqjӶ7C*P hҭj~~9PH}vȲLڃkLȢ/LK;p6=ixgV~kKw]3kßWq X(R푏We"D93jeWtlBb3L^ٍ!2ƕu;ֺehnƦ.>rwݛg`/@a(L$X)7w2NV x]EY2E|4cFì=2FoSb(`6Z ==(_:ܕ+| 7\xxL % }T\UEJq60Vꃷ{$_c^zTNoU@;cԋjd&435<';$dYX {4Z')JVpu 22rKr9Ky{&K" J3J:/W0^Y(*5x)a/=nnfj,/>E<>|pv3^D5 ŋ[K*`E[r܄S'Te LcDok|o20u:[u93 >@bfp衅.vBah^w[W,yVGFX2D̦W- cYBQG"$Kr+ /%] T5D 2eL ^{:q#<«tΆx" Z"_ . To dGE Z/?SgZm W2$6AD# QAZy…^dшdʯt[c,>7)x"-ad/ &fa)^d2iKtlZ󗎆[ǸB6|^O;hd5t&B&Al=zx[V>wm!VX/1˪5C)xpdD =. s&cC͵D@(# yxыxP t(\͐jI]6LK$"œ`V3o%0;E)< O)o(@'s(w XVdexTC|!xG nY]!k#}>tmR!pP<8뿱ޯ|~jb(Qx3p8`܂Ӫ( t*@CD Ή`8qYe=.pi0(Y:r@|4LbkË@>e9M({"K^EŴO)(`hWg,VQqVIT4Ð--6ɹ#&DX+H@v$圚l&th <!!mkzAB r+@5 #`k? iI#YK3QY{MeX"V3,X RI-=7dVۿ $L#K._sn]w"w2 bް ޹#Ok ll .tŇo>w4~ 1iTƇM=('֣pWKzMfWz"BbaU)ncֻn_>ql7>c;7RXpޡvy`yKx0u?'[&$ ›@r XN߷O##Nv_{^gS&;9tDA7уҏQ\9f5%N^mG"/. .?op|Ύ7NXC< Q cֲ Mw9*?DgM9r~ ; IzΪ[nݏ"*|Rs_9ŃR&cqGz8dz#^6P=0 FQ0Y˜@HYd9~}`ĖIѢђ^¹u7otLjjd4ZW;xS_{Ud{9)`~”N ,+!^=G`Zhě=7,q@qM#7/z;c0+j|f>O*pZ'po#5%Las4RZmKw"A0hT~4$ߝE <+oo/PtFw<^2 {! PL*j|jowUܣn2s: ar6ԥc[rIjhEƞ Qzfk1(YlȪ C@ 46x F9Jǡ̕v"*_IW3D3 tҀWl)̏(Sܢ֗ 4ܨ{rɬaw \?qXLxW]\rF^\@ӎ~|uu#KW\q SsG0)J݂vRm-<oUՊz+@FOlۀYgvThRVUSE[%By =CP*EeE1M3fH2l0LAtA+|h"Qua"-&o9j "_-עr;[hmNqHW{b l}Q?]h=w-4ƱgM”6oS@\o(WЋP#r0 ,)塳LOCH>sn_yaΎ?1{Sܸqm0.ܺy[6R§TTnXPJM `T)CoL4IK>ω>d 8*cE`#\Xq>/4TDBY'+ȏC-ħŒP^ZV1cݷlh֣7vKJ/4Rj9Q q"GB1kETNW8@ehbj,s+W匫Wq7gXK{h9o@ZǷo^oൢL tkIvA-GüEdz#o8^3伷p"OSlx?渟1"d&\#bcoV[P!ݱrim7_%zb+uHϥn:٬9p%7;wHQ9VzW*UMHWfnMAoC Bh UZO{4p{0 QI㪵YgvID'=]1A_[-> pn2L)jWg#%吜cK-hEt4ISq9 +? Ag ˜'&]f7z\>L@by[|{gHw҉@sG6~d>ێ@O6,V)ZY g>Qz@N9;V}5vC0r'( w' #b-Z"Sp"paʜH*HBx <۸T5w7lX - ?H)%uΦ;c)S8swius_c27$ Tbs*u$<_+$_x/RhU|_=yP/…!nxKORQ }Z0a#\OH|p% 敶ɡ} :OCBExPpY2no]` `B13%% OZHě|oSe-zUk-f|(hu  U6}jfT.7ߑ+:ϳQ:*b0rQ7L*7=x'q`w C^B9ʎi e ʞpArVkA8ɰ_o| xȪkwzy}?8Md`N"(.~,>$r ^#6 Bp\_<(; /e)Vґ6xVl=L{|w$R]~ %!S v<7u}? Ps'K 'ȵ^IM ģX2'QYBL=mE'EDO=GAM|6pYQמvtlXIYr毋1_:uh|kQ`—wZ=I9MFR  1fʷYfOgfzۅV#F4LsnT4G—x~xVv/lu;[?tytv[?h7'2x=$.YBQ0#(nQhFZ)jn?8 @xʸ%f'ϗX*@W=pSݾH 6溇@ SR !\5$KrPZĵfH<@RbUefzЮy"55d)wUQޒt=?ʱ4xDZƿuؖF T-zR GippB^]Fp.֯k U?@M΁B7=7 W5 {bC;0U8J\y LƒfI;=mNj->XПG,_\B~Vh ^`"LrqR'Kɧ灑O PrW77(r@xD`0%=Ɔa/{u=m-֞+&߽9';Zh.€xC5䩖릫 _w 0- H!Χxk1D)H E|vYivA*5Hy';"2&3etPg6F2$)-Nhi[m7,DR_ KFx+rE˯|U@W롩T[FB%JJ.QYKyJRFbFrbF?/Jt+oF(l*1@ ѳ#9S^`G\mV ky4FqGzOo> 5#_?Ei{Lr#g_!\ D8דؔ1 ~_BaXJ+–$]0|Ib(|8DR?NH}/bpm~y۫M\W'HŎ4F3^'XJR Ƃw󀇤\‚yBOl3K=Ddȫ=yLOU#h|sW_FPf pT,sșwcPy1q\qZSHEv.B Y0QSv閝uيG=Ĉ(2{J.d m!z<_ xϑ"O}#=D"*p/kI:Xpk `B6DA8kMYQrqجH~~Jk@$C2% xK{wɵ”Q) W{6[GXCt惋ȏ[&! B?B'qWӉ.zxKτ 3kĊV88Y9?v/!Azz҅,EMs!ɟS)}W0\Xi{Oe SǨ cho#9i,O ` >1/=^)\`'jIBDߚen7}Gr8!zhA|[m*f' {6LJuLݹ2 -|W| wI/,={59>f!w'[ 綹V'B% 7>XxqR&FTbq*R17AtYW%N(Lˁ&&i_)_kH{hhi޴\d{Dd\Oȑ=l}$RT<+0qgT/kޚ1,mm-M[v`,* \% @746ʸ^~bȸuwRb3XSr%4n:UKQ 8jrCr)0C%iT1%Q S?xu=#|&_mwܷ1{UȒ-ZDSgǷzYn`W)tZ56 E0ХW AaB<4wѫObm@0:EHN-h_L(;!ȃ֥ZkɎKEhV*c| n[ѧķ?}E/ɉ8$L{ 䠆3kCr@IȜZ^ң_{v nRz|~3iDkۗcȉo@> {Ӊ^ 'xwgI2bh\g'Sѩv;t:$,BL>'(|smGlgR23c}[s6b <4>U HOۓ:_ qY(x;nrppJ9/c(n-ʊ5kWqW=.LTpmZ ) w&x6:Trɧ _|47=bюjԮ6|YmBWzFbZU9\V6!TVŅh4&}? K"xhItҋb̴dTDsa~}^P\-0?Zi)7=+)A@KψbF+˜^{,iO)⩍2o}۝b朥/, &52ֿZ)Vy/7ec4,k"X #YNng .<0*jpr. z3L[ñ,6ZEzv[1kA(eRdR4\{ޞ%'_=虯tw;I7ۡcɣNzV 1fT (7kq}=P=O[4ܹ->T9 s߹-4❰%fd e54h lگ*SPY(P+J}+]b gbrc/a<ʧM9{8`.Ti+[W\tKϨaHxpdO_jFZ+L ,CWRN($ڲs7z==F$@=03佃kq篠tAvW cė?3O-;3,'t޹x h䳼gp)~%0VtNJf?v?o^z8&|xF7چKX+/Tw(|I':ZgF0sn=Ԗ/+ZZ83ݠ/5ź2#j@]ơsg׺5i=I(}iA'⼗p2p c;o ېB +^Yv uRI2Wd%*$ Ot_n"~2فWK4U?qjPX"D7!='_YD"ty kmWbHȷO# '}sQ gb.uvS9pw< 29DO8 o gL?bv"fȗgZ'/q_x0XwK l!q7;*vU6,ڥ Ww f}w7?k [2ЯC7ǫRKvZ WuY$ȧ4*ü*jL"C\vM?b@o/vwX739I8ʥO23 ^=ki}m & tR{Rc۟iD# @[}ˏNF|^Cԭc0 VJCS1K3H+YA@46Zuu-H,QŐ]zvcM~S'\Q$8D+ XҐ_n{yJzhaH(pՃGB+m,Iܤ<;d-OMA`wآV2gLHy,w&tא@D k5>#㾋AFSuԗz|,L<6Ih#;a!z8:}\s1ۄ${l+D"bq{ުZkfE3,cGCaR-WĒr)$d$C\{{~7)P߽M|+ʥ]d_w5m.n&uld4e6{,RfшɰDDC/WLp/(g;vHl-6"{LgbEo|k>B 0|YVutj=% uY^ mU#y0_Խ`\'EFl#O0@@ɯWhl̶ iن^e[k:`rVAf6O(w_2mNoq/˃DgCzK/_g֯`ƌo}cF7qD{[RNr뢧좳9m~`"'])!2d*_:4z@#o|g⫝̸moG|7_g N@.t2'"9-$@}0̢l%l5O#w.Ir~N8c+A,τV>~-ZZ hR\iҰGԳ͡3&("i m, rBřrBs3F#i0V:NIp'J_>qJe 75f Jw۠^*%z;{zCCpM9+؉;^d=Ƌ+4ȉ|Z*K!P<)=DPhH !;6OotlfF=*oOd9dU92ʐOŢIZ R_MY'r \AJ1^$3/>#CD"Ò?84.3ڭRAz0<ˬ)̥Li߀I4N\~ ]xpQwk3p#@cޕ2;$q#N}{ՙ'; jF_޺,3Y4^XDcu4Ӂs}<嬅cS\m H,tQh^ޒM1GTb*ܶ&PzAKUDGq=eg6Bמ2=&Y܆? $*$zga%QEďH< ms-/Gȭ/X)ق|3,( 9i֑g:m.2 2[W4}(wOQul @s[mTR#y bڐZUˍju)j*֣F),@YWzJ$$ma=Eh^ol =dU͓618eմf0R>dz"[sF<Ƽoܕ,fOvnw>,u]w.b gG&]4j,ܵG'ڲτY>:&"#k @8cGg/04PcIP A7?҆YXddžg–\bq/7'6Qhk04:?0 -*sX#jTɦztl~<=.n$mPT8cMV o6#V(݈VB[a(,l>Jd\~V ^IOӬtSj_ßҀWQYb [CțtpM$>禼L'id.1Љ>.AF(HO $%b6dc7(\HVgZ쫻Wx[7<ӝh c?^Qm+٥ )lGul}a1pCIxȅk-n$VYkC ܕE[E=$0i;̹ qWyG7}ʧЂT͕U@xdkRn y]'p'O 0H*p,sVz FMWΟ}Q6FS˓L󯌁y6 ׫o}r6CgX;f"0BA XE|_ъ 1KB,t dV﹋gR*+2tqbYA *Xl\UoCfږEwv$Bw'@OQz@ik)o Fð~8e [7 8ӁX9Vjrω Q*A(G(?3Qo#L@vr\h=rLCJZ&ikhZ~iz0p\Lw|!^ۂ9edKU:oeV(qb2^O 0ۀG7˃l,:e6K[)V? =Ʋh=Mv/™fǍ,b) ?'pZ=qP 66pBfll| !o{54:bTKXᅡWS ShF nԽӍ <8в,ɳxzųeSH qO㒝MyS< k/q1' `\g͛`;5CmwU@yO^~oi Ho.tdGEr=n} 7 dΙ"!+k׵eٕ<֭5x)ugr%p )|5xgD`qo!=)ylrVxK]772,\SubOHɍ 0db@WJ՚cZH̞smZ>ne74!E,/Hњ}. K߷2оjP*5\x4𠒄Xy; Eoţ_q,;>W}F1γ "R x:ȏ Lj vS $)\kӓ\Ba'B& Ąb^8JߠΌ&ГXd'P[ *AMOcS"gWGIU>о9$g5u.xFϳ˼%wd/~'軽ս8ZK4#^da,nׂՈ;HaRY>~>2[o ޞrڳsɺQ+M" I$ѷ sZ|Gg|ڮ+*?q鴔 p7`Iȗn~ʊ B*7!N̾(ϥ',<g`xQSҍwq4`Dd.z/n# ?]3c,<}+溲{nj6cCIz_K |񾠒drAfNb3xk~,*(HS@|=G/ȊB5ZP 2 +L`zfC<3Y?%TFbg9mݸWg9}'g]:c}vťLKʷUFUt;R-l2(ԃtKӓ ӆ.u#`XEa1;#V]䣗6-`~ FJC+B[A+SPW`_+8Ǚ I P'':o}X҅0 6U7ķ@d-|޲Էڄ6t?u f0ғV k.7e9&ڼd̳ >US.Ql!X?M/Dw>,G٫~LrR 7/sGbzn t׺"RxC9ykMk±`J!bk}y v`άEBU؜ W)V!E4?cW:|KLG mTO EYbLA 5qe֩m-kR?>?4Y$|LߥaHRtS#COl1r'"mMlqРOx-dVsN91f]/4B,h \Dûq+"i=N]&4 ^c?<d"4|BU\iOgko>0īWpo)cipo:"=L}fgfYm뎾ʎ[Zi>Fl?mo֫kr{>swDIIQ%H Eb 0 @cC&7#)FlYDRIQdgs~Z9>SjժUUUU@,hrtvk#>%N]xN;kXOm U>4dlpm >--O) uP Cs1 )ۧ$&zۜx}vz+|4lHL#Dl# u:1][v*GT`?^쒒B5a$$󀂮ڃIa,nQɔdR>L x59{*vqiQPKXGhyCKJP 2HA{&wf!(H5 %\"cʹ;q*R'|{ &x(I*g!^]W^N/Y~5}w>j9ft҉X,ovҖjmlc#\GH I !aX<қǜl)ړ1ǡN-c>S#YN> V"5s뮾(P9FaEyZ FjeJM)ɴ'LH8܇0ds?Mac R { rt-cB^ށ 7o" >sK55Z_7Hm ]Cj,'ID; @ae. 71qP4M̰+Hevu}fէ|8>vy~gi ~+挝Mb\p|{ɫLg9y:;.&kYZWɇ&h8Y'<)[G '"B#?<|7~{M1wag E'?rib{V@ [ZL$C"*Ἵ{MK9F;’% F[x_,a~& G*sWq ABswWW8&c%˞}h6. Ƈ1K K:qt#'pMKGI‰/]ɅE ' 8'5Z[@{40 tV:H xke=n.ǘ2y+_pWCI=}F+C۴N>`R1w*F;aB@ȂNyb_5fтw{$Ok<|K9s?E ?4}.-Mk?8$!Kd ԏ&N !G] x;?C^fS!IaZK}ؗjB3EK!2~L) Qu/`uɛ;{Ya';")ג/&3|LIX)}o)]  ]%Bioix%xnjLQb 2{`JQ%>A:2$"`*DP`"vؓaOt) |4!u !H&C(+dBEI3ID|ϬLqђLgK /ޥUzo,Pr>3<ȋK75(~@2AV#k;;AfٲJއUb %'9Ab/-= a(2鉘nn$=:tg ^mG,)]|on"v0ɷi5LEKvXz_Ǔg>s 53$dCe2,y> 97ۮTѓ2T'B# ZE56< 4|qo9_) yvC 49aa%߲A";ofoHZe@1eBy#׹Մu5pbXB{. n$ $Ll~j{.Џ}zvr+pzv%!P@s4˥3 9{$3:+(ʺ2u\үG}G5ˋWka1G mM6as/}9gؾqO Q~/g?7QyKד;lҝ:dĝ *'z 2>sG( YrUhK2DpMH= ?"|"Ec4{&32ܻ&xEcJL~EsPxDhYSʙ ŝ ]F(z鈝BBIV8UXG8 mB%y!7^z. Quq;of=2} =zW.eN˜xDcUz Q*.x{&To"nI'VxbCCɸ(_xLHb pWw߈n][{ңPLC,bei'tDUՖJfq#R^s-ӑjUUeWZŘ衩-6ywS+X]XP~au ?O A:X^'6q+ ٽD 8˞Fb)!'q MJK,R ʁ͡BrW*t3vNQ8ǻZl1˴FR!˪*(h,;&AjHթ{tCşCU/XV[w8=^2c>W0ņ0rx0@02d5ؒ0rb׋mSdi @~ p!yU9 UʕoX?0UZCC-z#0 |2VWhܕƝUڊxz\|OkD\4Q];q& 1ėrT'QԦӸb垇YWPpeN>C?n s4V ITQm kAЮ=A &k +Aȟ" gkT L8*\6ǥ̡>16B H᭏P釱'm-n\zJK~”g :Ң/$~<>&EN_J~%L˭ ""]sUҙYxB[D;L2 ;ȳ=)-yP'4])AFg2j"BtCey dnO$[ʹyN`6##n 3\q?6Hjѕɼ7|7\PM's/  {91fNk-IW+C(ʣG."-^E2SغO;r.)..sO=0RgTdl*-/6OfؼJO84s 6m_U6!ӽ,:WD! OdT q44)ysꓳ_<%x jAMP|;&c$eRxR=D>{_ {;eg?[^z0 ӣWn4[6MGMM:XB姤Dbgg;QȞ_ "*mvs kÓ[g͒Ck0\/ \ErҖKy,w)'¤ֆ|֖+%W8$I8.r!@IDATWZĄ9AAG"1FN,? [ u=$ q, Il,BY}d}gIk슁]|2Dx+{ZX]7VlP=@hLn^]_fsbf 2ͦ ^ġd3ުFjRQ$4dC'B7U"Tmp;6$L풤574||.˘r/ z3hu^u=ϚQٶFgc&lK @=7L-2 ۝9라d’Y;X9Ai4,qU}7ɉ\_d*^ S/ ӧ6UQRsK&خ,4%)K_f,Y)G>l`e 7n})~OzHof?"{ﲄ8^!Ӗ$2G[NiNkQ0 _)!@RL4#'8 ȌG8Rt].rϸ4`j[!HJbW|K|vMtId#l4 R E×غk!r؏"N|j@43>V`߾C.9,p-A+n \z1\x8w1_,Qm]\E>;z +@ `&0i23#rnsQ&c3BI_ eRьP`NI=wYsakU0fۊv Sֹƭw%%c֥|#7+u5dYï$y@kKYzf9{ ɥ[ Sg d!G|ۛ)!>%|iiI2! ZjOxj`9_`A>)xE@iq{@\^++_pЭ]UWw vܗ o'4fF|4w 3YsKpE 9x,hھOWF#\ZZWն02pҜ i!Sdm";1qPAn]:%a@Udx# 4|T:H6 c}x>d埊@XY't]F6sǙ2iI1途67oL~:W2LWwi} {IL c|K*ޡG<^K.&վ ;/?ˇ~h|UCv,*v~Aџơu\VE~j>~mhDusn0I+&jB0X<5j>Qw'P <@ {hg;vxJ2?y5|T p${@Ad%4㙖>-ףcSs`6ΞK҂ͦ'=mY@@ȴ諾wW뤞?{.isʺ?z*eups_Cޗ-rx:ч_t9Ir~ ,>; 3iNawL(TW>ZL{8 X=vuo:/†Ӹ=HC,[chjxf9"K`37qbP҆o!ҧ#Ti>Y7b%-vVً8A6k&sKZ('Ϫ Mkw=xӸ7LϨ|>Oa(4r%FK1KZd8h[zLK#+v.-P3m8y6_kf  lN4 6G** 2b}&PU{?rj>#gQo se^s aj\4:!-Mu"/vѡ:q8z5U~B/,e~HN,3~c2(Z,oOE}]7m:E:婎Y_-|ct7=QSY>A0{jr1; 4 a<>}>tiMͩQIWcqdAɮAw@v?4:|5sRd05K y-M@|wvUz AӾ|K`1`a39ƛMnnՅ9 -}f̹@pӐD9޴|t9L:Ezlr )r qn04.ɋ<518YQ0JgGFVD`s0~#XWEY!Zk|tC݀.G:QA"X5̰XTGG .|?II27»0ZC<5oy1W9?. Q {m سoE+yTH]7VfJL]8%0g]b>h+(IQ?9U-)%BYTtfObXpwg_x];bkˉ=\='׿u"̠zk`4Vh2"|z$ TX-3|8zpy#EXZ-4f1M|cM XMb߉ ^@p}zzoY *?jD" 'Zx-7?D&vZys> fȅk޳I[4%xwZVn{w$NKʚ7yPM$L:4gv*vDQAn A$ETW]^H+`<ꂽ\u"jܳXz2 Liq8>_1vDs)n:fVS{Gf/#EZ #ꂙn"H|&Bsį 6987Qyk{)ub_w,Q bZd;?i1 #4h`p#_aeFEk4uPU!'[_K{[Cɗ4R;݀xH|v #zf8E0ħE"t*$<߉- NPl /}?6^QWp5ZADZ 8. (C}~]O V y,x3A]ϊ ^9M >O=0c=4S bA^l4:deLvh!0)&? WM47|xTW>F}aһ0`v =!՜[(9:ce~&BmG+ie)L{NWVHZR%F4/\;fw Pݻkwsޞ>Koηn)3i]תEp6Bw7l_;mD\1-p4,X__Y+ty)TK8PkF$2{3ή5uc2!s'D0y5'(w\ۍIIꆨe_=3(OÄٍ,HmR_w _WGQpissns-Ƨ`-w8)ȇjd@/\!knʻh{B|0-7L&Pioӽ0zP-)N}Ci(IvX5OQ'.:sx(]o6v HޠF2py9@֮nL? Cg &pvA<$H*$?s }ۭkz6.^gW,>d]{7^[=|Y~z'[1S?Jm̑f8t@\o‹[#6YkD(?PLc6Mugz5R^t֋F̬ y2\M?yj#qދfk. f3t@+%+i'`C1ِPZ9lBZF2`G%t1D3=?@:sBHp'v8A'KˎoےLWK<ᑡKA!Zuvk݀/p#LU4ړwĻt/7 x*΄}=!"c5 K|!(ڻ3ia_]gq,΂uQ}=t9Cv{E3IF'ׂZ4fd)xG#O D%ZueS6 DP;Tji"jmZ*rV>%(چDg79Ђ\]>1-ꀂB3=gRV[n>CYo{Ϛ>Fj0%0lr(;.˟ˇ-'_DoՀ?F GyBvq9N}Wd}KM'w"g23MD__:WjMƌftV%J*q Z:ڠnA37,C8Ookё\ hKt IvQ&$Wk53v_zDV MQLχye¯Ï /~=-M*F0a?x+θno3Ƶ~;xN>w+N&>+ěnJ(&('=U6rp( eY|Y'7.*atƧ3s{z&Uh r"\3ݾyooqlI6/_X]_pc_rzynCt`h7X zVt_* Jkgu==B+c(1Ҩʂ /.DϿ#w" TP"t-#wnŀNAȬa\[R8tC_s J-'ԓkE Puޖ#GCnNcOP% .=4 %xO ]+_FZ]g~XPPh{Vߣ_,?jz'cPLL Ӗ3g|MO4_IŪ\N+vcOvo;D9ގd~3?zѝwh܎+ ։9n?Մ؄9 3vY1lCpQgbx`<,| +i< ;9tˮ_f"=$:uI:TϨzקj7͉v3|SZ*\L|3|2p8C2^u!w^#F$yTՉm"" }/e4 Z r;ҳM1f 4nH}anZm7f\dߋLWvif2-}ew 8ۿ:d? GZ@5IK>d*\FG~GрBWCsZNr唇Y;G@pW\5~p1.bq.q)cV .?ӣݾf&棿T.D=CNd$wzm7#fܲ'eVs$dFK6XlcW%tN"a_x!7y; <8due.pGH9 1&+q~-TF%2a _9Lr=]:3 [ Xt. t1LBb 5Î9l\ٟn-{jcdogݺ;b>C>nWDʎrfH$J@ X U~=Z+'x3o@ 1b_,wH0hZ%ff#~7zy>wKصl[X">9_3Ynɨ\A9l "T.]z532-'jP)9C}ajTCs>G;Jp D !P@<ɬˢyfٟ1cCL4H\F]ׂo/ s:3b]7 xӂyo1|7l;1<<Ψ떔.aXJAˑf5G0^wծ”z)`}܃>.鷓g8$ز ȜE0wR[ʧ+FASBJO@f Ad@_v8d?͒߃՝Xr&"Tʀ4]ĕxV4j3 ՜CS/]qG OȫS 71<|]:p^9=&3tšTm*@=1{`);_ &:tpulB ,!5)@BwtU)'tU7q{&&`T۳ğ*[a[ ۣcW{1NC 㑒"fVKOYG x_2}kKˇ`e'=I:3O1n1cN+̑]"~vEisϽ۹TfL$)`Àts ~`6+a A֎1V#6Ph@ӜO0?B"Y'9Bj󩡇ܖ5tn\Nopd 掀إ4}K:LXVިU3݀8R5 T5@f\$m÷~:|ի u";ĄCQ@^T*;odބۀ& _G*XJע=n ?/aaEP~u̚CaiYP ;jB'hQqvcM) TT #% b-߂1Þ?xG0X 2*q3R1X¿w6/--ApԫS{ $3y9~T=::{.@ 0? 0^TܣCwYmzEDppoLWՁ+<$Й98 W!د̴S( (^gߺ geW9GF;AgEv6%M [UOhvߺӁ,{.7`?}v ۂ?s^ `SIu!<{}* (QQ;f!9 '/wqbj'3Iq9tlõvk JI=C 6OySj pn187x;uAwS w9du8@dk~2\IKw~g< m>by [f>a+RA:}o $ש?" M^!'k{9.;0ga3jF{cPLQ70 c{.;,}!qjA!"0[w`_quup1nfV{f]C?s7-BHp$ɬY<7<3À9YhؿGJf} QL{ I/oκTkh8.lӞXr?/E`x7>>R B H/c{:nKÊɌ5E/~Ba&jN),gs"@H9{NgEOŗ9 IRUmwo|%ˮ;?non玈7soėxslA^X<8qkxW/xuq= hx,,65>5/гSK{r.s>0Cy ?+{~|:c߳6):p2.2gqaLH!-Z]\a8jopuJKTZ~"Ħ.1ŦlEAؠi8E7&<ͺq.%i:;K/Mڅ Ѷg7 ># cs+i᳙VJlFuZ/cR[c'xC08Q׿lx f)]XiszZOkr.t9ODރ6<+/٠Nk&"GDT.%dl:;>~VUxTAJ8'h)tx?V=7R2|\kړ`񳂕aѻ1 |ޥb s8bf\rwc8\'A5كW_ QN!H\;vCb\NId}W|h}m!c;>Nv]vS\%G߾~_{%;3Mï;w?v֟6WG*!95̪1x O 94<#~1  n{ Rp0<9f}?>d)ZT# ?[j꛸#/`qKaN)MIOޏaf1[ Mɩm1鈕zaܨ&Ocjo'׊%.iW"XPvbЭc) V$s ;$nyE.mk'ئC WBf%=pQxl~=BT\#q$%n$Qsc3gH>--',]`okF(O)qc5.[#j1q-ɘŻ-KϨ9{lUXV]Epp/6ts.^b) z(t,Hk0y9 OA\v5`kuyfj 7+DvHEլuB]`>wވs1N& M{7\tc/:oܝ(B]pe%wzX }.$ТO1Tc }%:E>@mR R\ } };hOg 54*4u2Ղg<~tuTѸTKS{tgK쩠x&'$TsW:ęW[|^qhW|:nRf\&z3 r %,3%Vc[RƒOA=X8Uf$CI"<Ҷ[xLW+y쵖cr8@ 0 S0->\%nė.imp!*:]A kx ПSGz8~زZ0Hf\6Y1$cPo+J*s ء![b` JeIJ0k%'Ǻ 4c-3QdZN@yHY^EpB:]N5[.:Afĕ!{:x u !p?-ID#4ʆ[n5|nm-q$ \Fq%8ߗ Ωa5sdI@o4hTEL}L~mh\>s е[E)t97)wytu@ N:.+N7q"Ǚ;" X35[Dh6iі[ &#萸C̖x+$In!\5 E1S#q,X5eBx!TiҪlRIo+aMT,*rJQY6t%vr Er[@+`4KAĞ]r n 1xm4gcC$]AZ.d<N?X|o} Dav%.>}6Gh'z;$SʷtMpP-w(7q< &&3|i@лڱ9u\DY1y[?d'_mɈqw2FʼnWg&@ S=GGdmFi4uYljT7ngYT~S`3o~;xq{/gbJ;tC|.p`Rjw V-pXKs{,׃ֵվ9G*sC`W=͉b<#P]h~S r~.4 Ѯ1 te-?4^OΦ(,]KqC@26IA?pV?TFub#8bL:Ѓ*p`<&YSA/Keq!kgod1q*=CKG0\r5pn;fذr&߻JH3 +[pȇA=~\ȩ˞t,cX67[i 0 Rӡe@g{ oQz1a"6{|jfNs8<]o`93`6Cl  ?lzt. J #`lwiP1){4)â Ά%x<= Ɔ])L=.!&^ Z¯>f'YcP3 ěDW`CNtq8ti{̙ks[t* Upp2j 5"#= + ]"~ mY^Kry8@/W}`ϊ>Gù%!Q ^⶘а]u?8-]V+Xl;q c#G H7Ln>~4j;w}2+H58[§+cQ\.‚f"L6~A"605l!l挻cVz'NޫOpQoEGJIDAָh8Cku媂[m[ g/̬]GGU"ea8oZO su}UDp>*l9[=rjF]tf|3b ?xW?o~%'b>H R+Ӄ9 b:Zp&2M,)8aӡCzOȿ qԷ?}˒ X>!nW7k9 ɹK}YpeHƷfzTurM>WZ& 2ˁWޚ<Xk'-3w-%$^$n r>a K|ƬYl"N p3y`x3i|kӀWq= .}~ZŻ|15/*38|=t-Z~^c+TXq[{Ƒ؃aD]!A8~jc=MeDWWhw8i3m=4"0iB`<-(sIs9S&]gc-cC/lߓЭӘ Auy| s 3'4wcVlMi,3'yPB GbIE_S92.wFnY̴҅I >CEn65%{W(]U}&^AFLϘzRe?'toGtβouWawdB)wz0G"p &muw1Jm\N&鷢$fm,4( c˴]*>[鐞r F.1*MO;xIN"P3*+<>m0>mEɨrC~|Lj'-|HeY%|<} ?[ei:/b!NLpwޓ> :JWo0yog5bmX$+pYde܉Y T?xĉɄ1[zQB)z1넇ڼ-\5 u ]k~f\v'@RxTVd Ћ Q-:\UmQ|>t"NSAXB&Apq6:tgR\$0 80)>؟3tL7E&|5-RK߼b\Dp$5~>0I[rK I0:`<#ɖGMs1{& i/8Q߰J ,ͤi83gU/\e!ӂ- AILɬ8N4~/'z+D‘~ *Un Ye>]%0SB,Ȑ{|@KizAPKye%rVmc`U݃)[H K G[xb  Zfׄ*[wޒw/JkQvyOACaNa%xgarVb<`݋;e E!9%&$R+w7c݀&p,5iyev \ANС%pv"0!q^0hYG6n <G,]qv1g~(-8p] >,˽X[z Mg;MEk{Z%TN v[B'7ntz%PvcЬERƞ+ I.Ewj`K Q.;buLa}0wB9o!= ܡ<=p0}eH.g%1 )ŽrJo=(ᦧV`[ųzDL~!aO4-7s USDJw1]o1Kh国틗8&sg*/jN CC3oBӵlI8-HxL}`2sjbD,\իO=k4e|`qT3\ 4INX4Q`K$vF^5jkT=A tp{@]>e| %WYhNCa7m#v]S|ÒpolHZ@c&j=S`x-T1*j=p !!K77dS xv ةJ`tn3vC3{mwƲO ?1Jh*/)ToFY_Y;~6W}{z]I:^Ǜ7`ƜѠOv_/*UE^:c<~pKҋ%Р zOs;&pd`A , 3{6I5]J_n2pھp;P<”; Ax)lGS`M ^?PY>3P94M2Nˬ,o$=gIcoe73GA9E=et chbIiY ?=)Y@Cd>s9zg:hF+v1TqѻH2q =v0K(VM,K SXb-15組`:iִr\{Þ 3 )wۦ!|3:AUe({yG! [LtᩄJ 4VmƄ&uˌa BݑUWShEX}PFߖUw?s7i6Wh=ǰ 4Ephf[X]6(;-R?2dxeVd̋1L$mj:xfV:9rlJ0ZX]qK2AdUaiMrkzn١)>9<&C |>tR6F(Xu2®_j5\*m &艷>b zc(,):{ ryf[Y˸HgW0pLo<b-T6f9xHn _]Eõ{%1TuwZ!^<үBT#rW¹XtQgI):^Ova+ko1K a f>6Y_g^fğ$U`Dϊf©gRhMLַ溳X[}䚿[ nݮ=Ov79xq:{pApn1VD;H_ Im73w lKlSHRmy>֨>-uY'x`NES<:-Ƭם}{a)z`w$Ow!Z'iw6F/?= Pd iK ֳg";1IGtIρpisjNŴ _q^zVerW(ƶ52gnZ@/-Gd&m:-9cRPnMzet.-:9&1%gI$hQ:W3wCӒ9fn#-O Oˡn|+Br B=B,ӧhhwԺÞ'd'h@b0ql1I I'T sd06}Us&a@pӵOAHJe 0ڽZ*,q^ &6ީq釹/-B li WME>bP 0‰K9woͿH8t r|wτ )L~#05|ނ$p&RL!HO|ī`m:)=:N2e,b%~CɜC 4 ફ[ 2ϐly& ZrJ4twED4۰eZD=8-.L:&q t6䮿$>U /5x3d%Ok.-=EbyR=igoT ws: K;wqλ]{GrJ6 8cM6UMD{UrZx()3.EKvVQvs<PxhF q.19ʧXETg읮N" 9MTzuR$>_-r"8 ?-?9j]!39x28`i HHAQ;DyP&JHB$7t_4D";Z>N@7$ ,uTD @v|$* Xya^Zɐfy:@҃7MN'||@ E]᭰x{ټ0aGhq4t,\鸝5?-TSb DXKa6Y6cL"fp"T"nYL>gG4G}&~~ M-gXQٱ֣ !X6A،`8q*7S=:9p-CSLgZ1?IwaK 77gJn :7]$*Mի;-n54^+=j)|NK1/vIgN`s $g-m0M݁p>|)8םء>v_xç ŧ;7Bx" ݆쓮x;]V#8̜hKYx>{>S1q h#uKG 2l݃;>kmAw%z[,W>Z9NKu :>`pu#tjXL8OX&F14bI}?3`Ü*O/*o%|?A+spkn1N0N[9QtN ,6)?V[ ȫ֐@q@ G 7umEj;ڱ̐bղ(wZ82YZ)3,_faXuiь,宿a|L&}yf weؚ+Q G&xO81IA`NO\wH:pX^FT]{O젴d}9Kt\\A AI2Ϝ$؜7]$0sxHS`g_E ~ia:硙H[꒓ͧhhW/OĞRȓSK`5^O.C:R§`~ ,9w-7N I(3-cG?)P8DR_Q۞6{Y.;AtOQ{+b9+23>Ou 钳] Ā kӯudRa[pK*AR AiE 峒I7dH>[JP%M,ɣ f>ac偩M~} 3I@R S-K[q7T+J*!'^`LXˆ#.2/3h;#"z@En~hĀ9 :h:|-?ʜKII>{xDdL9TVVt@-zѴT%BB 6cŢ7 ) PݭRWVTVEÝ~_¯G'y1cv{21CHj r n*`U!l *jg%p[Cjj y t4?}5Bxg?]x:ymUF>X,Fb\$)ϼ5meDkuF`5:#'I'|E׌,ACx3!6<][Lck/嫿{8$OYt9;쵴~9`r`XtUz@X&fis0)TTqmS 0Jqs- (PPUs%5w;u9H|W@xRk4i e?cQᙿ7y#{>?avEfNeN%Č2RD 7E,CHO7FC7%n1EYFvvh#_m),At DcgR.d 5/x0%Hl ¥g͂x4-JAD WwZe(-OXo0Mr|se&GS#^lN?GjtkS 3^䃟/ۣo'PPޠJRD"T:85nA ׊6h$uQqL5H0Lo" Y|N S{LL3h \Rx}SP7 m Gu&}^F2D ʹ-~rX,LF|Ќ&4150,AU n袼!n [4Y1Y#9cQA%B1Tw*enP^>_OiN\2́۩@¾hQU) (k򫺎6t,׭E" j[xY #`qAtolC^C?MhOeN'@)&#?YdgsJ.rHjO 0fFvKmcd _ualn{%>#9.fr vFݬO͠Gv]yDķOfOg1~I&޵>x A C{C嗖iRfv򏹫ISA":*A'z>8RZ/[w(:ntmA6t&DFU!/L 0fIZpU&9IhwWѯ< 1ݦ >cnA3S}k4f[i9om뵶1;Ν?9w;϶sօ۰w-nQ+Ketl v*_(t $)!05xBk7aE*IK.22xz`Wc/g3 "(W Cf߸|DNOa X}>GcM-6.#doٺS@-$N[ԑRY㌝0#>6imf0g+:l:}#iyD&ih'quME?nP Mۂ&8D!|J 2T 2U9-OS7j($ V)+wp磣k ݇n4[:rd&n2OR F#}GN=T}N>DotX,IFކ"AIMցrB 4"]!‡}_zas{8ۄ8M#ۚX%Tܸ7&#јspG7_!/a e2@:cN'PBMT\?~JT'!Gbu=JUL_A꿑SXVkFR+ wI9L3:| xGBT='u~vvw-㵬Ub%/|"|Ox3~]t%jķ׎ eK?'0ES+7@J^>hE0'ܻ|}O]7_2`ȴ!j6k[jsYt넖Rxr`$}۫+%A-,s"k]` H|a=$u$s.jh=?@qg.<ջlO góD៦N"gCsKRȓ-A>$9CgS`PL}O^׺jyU =`y8"IG-kv$=[G2=9 2*Q?= ;4HuIr2Ɵ,^~Cx`Y"A%krzɅrԜoЉ=|b3vǿ>ֱm7\qbw#> \X@ |űi/>"k:sVp&/}`GϾmvxGl {d'hXKgkuv,M7!C%In**aE!VVV.SyZ0FmYCP?I`_VR#/rnRۀIv2E& d#,a %FQPq&ta_K4PZ:*1~RVUie,:e7M!&=8ѡN B0_z#B'PCa3~ݘS j6/Nހ@dJR%d|fP?m˛ۣ_u7/]X.Ϯ_بjщY"QW-ʞFG!T t!D~`$,?q-!C%*=8&UUd(oDz/{`.>e$u6"l\F],#uSfB}+sF2A+u 4K%?龶0Èl"`jooGcN(GJK?qW. oՈ鄽Zo >#x砈-xРֻIN7UAG<'=r>AYD^M hAڹ5zWԨ">ʝYcw2L$Qs|]!LD!_`o`;U֒B"(L[_畾~Fw #4vvxGwD Nn4#u /=j;#~97 Dq鵣+` 5t㯔B2tF/ /m"B|ܚ=jDW9}??ts69Mt 1&1.c2egohW8J0&}*/۽g1fAPX#E39AFg훾dCVʑ˔ܽv )6 8 5^ٛ=hg BpP4_ ߽!~UY/&_ zEHs'|9pa9>/_ Dlf'ʧMf ;t)ag}#tH4)".L9bO|Q_ m3(G|imQcIa7WH>g;U_8P] #՟c7!/!\w+Yq|M4n4_`8c?K JvϿz}yyv뛜ۜo2@̶'hi[;?,^gr.jw s{|ϷPo?]x|z28̽h p;U8`J> 20-P&>Pc*=TR8n/g2%pjN7GJC0:9~|F[!/Q$.xjlݬy { Zn0wOg]ZpU5VAB-`4aS cZ[`ud3Xhùr5+|&Q]^aQ z|tnW* Aí^㓦]Lͪ"w<ButYݒ=.u}]$'6ծ-W/pl:9P 'sKum^9vM⏯\`ڴ:ׯ.3}E, .mgC=~ܻ~]z a~M#xxPeڨOvacNgo1RG1QMy-D+ަ&^B8&JoQU/(8 G&|-o*_ [Q0|ok[G^f)2Xg{)EwK.]9֣Odνn>|.p*<$ag=ߛhv|WG}uRw޾:;^2ۢK+ٞcH{P>]Q_V~k[#Ot}y&OlJ-6pZٻ4`>,}qaՁUPvǯ7Q%s{B`$KxN8H`ʻ:a>T:c4AČhj'G00Ǖ;nH[C70O7 7HڕMpG'ݠι舶sLO6x∎݀!69|󨯛,gfJC_6QK7 *-Fn$PÀqgYg?rj˨ھ͗/C<wc}ץc|]@g$<*lρG/Xu  }@?U_2zwEaeti ĉG 2-1Era=vwgnPY(Ug3jxzpX%6nԆ°p*mBk&1,|NZpϨ  1Y!uPN Bi `+Nu_{;~r C3髯#5 ɗYU:;;W| d;wނ&#:_+rmA]:gWVwM!kߪ|W.^aT|ض|r]kiIe8Ud/*?e)qS4.ֵjzyਇma(iW/] 6FX_.l`}q^{3XRk;9F][=sd >z.~ʷd[=G -[Kο Rנ3uC?}8]5 ]]㴑pP Zȹi7҃]ҧ(V9@G@ӤGzľTL &ŻYccf{:2U.N*韧 EV"y|vJLwRaxNhAr6F 6 ۼw Ujo6`.|m"r2t n1jL~܌ E42FT0~ˎk;^)|V HRbV=XP DP|cPojw-_웯_H*o|Bk?{ضCkf C?zI.K^/0cI:}aJu}/-v Ô敗.miQ>§4(.mS>E]nhwp5e%{+̵o{i_NwTԲ{`עZ{.!>;~ۮC6#HAP'# />:RRUBR*%֐tFݾuwv-j~rEu%P/,^`CZ1ﳪֵ7_bHki2s{5Kmls:AuF~ d-|| 1kW^]FzDp {m IW?mrK{}ĻƜit0HiBяiӘ§8p(x" 'x^N7kgKS );s/upFfZ)sCpipjjm_9Y PrK0ѬuK9aEG?(#>Yzq/xP\c }%q^j_r%TSLv(8~6v3z/5ڮj.xm&nta OGgn;X] {y ? BZ0^kmĵ4Vn\_Ƃ}p..wخ6ɴK[(>9& YS~>buu; _<6V%Ƨq nDՇXACQG|Ed88iIDAT}i,Ir72xL"H"jɫ qD4sj0Y)ŃI(}[n.}̈/5GWG?ڌj@\ 0Nq lqpg ( qNc tx~U-RG|4#)`?յ]?M2J!wx8g47Saxfj`M(xu2ʾo}`0 S>z"rA{_PĿdsjЄ[Q*=Q0mev=c?us.`jT} 3ws~ p2.PŋHp2V:{r$A9$h:lhE|CS=09Vg3tʯJ9E8N')qA<MD@3l`W2mGL IK^b*ܔ\iVA=;y󳇤FkaZ ҷ~QۑipiLu"W-t]ۿPnr(%dw~D۴r ְMN9`kZ([[`^ CjHt_9EBjZJWscI_f02O@C ?uub~03`^̸ C\>qB톃`)$4=Ťlv(*wjUAPВd 4~f" 4ʣU6v#n@y.7F EmӞM^i`8dXZRy 9­hzjasU-iz>'ͿʜN9ۀV{ٽj'P &[oE(䂉%j _ |Ad,b /g tZ^4Vr*K3a#F0dSf\<܎j,UXA#kW*uyc-'Y˓Gi qgo =\w@X[жUNNeM $OiGF$*#^t`Wpm9*V?|ҥKaxU~_bv_8?z/d|VmC&;4,"80~0h 8i S6ʎ5 s+~@kh |O9UȄ*]s|YFieNo >۫5SE+_~\t|%gx0]%qGC#0X]0jq_@{#Sy@2N5־*2Y,i':/BkN'Ȼp߅ è[|C@nX)y0!poo<¿T.H; AUNQs7]]Z3hA=5쩀eQ8ñ|}23't淢p?t?q5lgy9S AEwb!st`p+WFwWsh @^5y &QUp6ͩdh>+/:D3lDE. S: \#_w{01"̽m*Wh O.a.&dho_G43>PYL9]A3w iz5F$*s{xdB-lVcy6Q;,,6. I<(l%] 2S "g9 %iB).1Up ,`A^jQ([0@\uWha4RqG6(0SSyNVsd=X8!BEW~a+tpw~8>b&Z0WPCrNhNֳ¶FдH<2;FRN|*픹>>bI|"_,IH!HSeia#%S U]X6r?I헑]w7p5U".v\PL癵7̛4RC>eK*b|0O4_ԥƔ5M|+tirc^8-_Dʭa'GRRuhbًbKTYfÔlr5LGcۇUs'!t!V{٫hעd!3~{){eh{TviޏDc ?~e >v}/!dp7߈p/|W !!qsPM#d7 W`ʇ)qA(~i|`Nn1PW; 17ĚYPҙ3D|=gt'>VY#wUD2]z2{De'0sq]_ uu. _RX]fFjGdĻƀf>2 D@h Dumāv|1"^:JKD2Xp]W8{bp@QBHh%!su ׏q8pH9ۚ0<*\╲+0K$0 n+oa0sx2&x ׁKawQ 8XxS pQ0}K{z'mLsҿX5e~xaхTrw5RM?F 6)}]w\_b0}4Hۈh?ZYEsdޤ4L/80LE@2%j k *}R(iYUnBpGٚ*GjfF"h!r#i!#- MgX/F+Uƣ"՚8]ᯊI}O,B_zxIPO-~'tOa,e>"kl dt +Qi$h S$2ܚVj̩U|qGU}{RrE>PzY !,,c& S!4y1_|ȓ`1'af҂Þ1b0~3or|s)\Jb$`o+ ͗r3`uPُ&QvsjJSn;ՂTs) LDNeN^IA` .#m?“NߴilWQ<$mVDn,R$ /:E wJIZ=Dq?;((3,s.4¬(xz *㙮CEsQp 4 N^ȁ|9H4e3#sI^s;@4Qi\75e9Sp | S܁̸.Nqw0eSWÑ"Lŧ d4M>HH|Uj I7J%>@85\3r?(R04jkq &Cv"Ԑd(H &T r*T̆Ce17!qdA\ߺYCEPӹC%[edט9F=a4j.H_Fz\04N-0"OeP(7N4pGLKpu[WG0]7IF#i*lDZ5ļ'/z_7IFaex9:RXk'qikADO( ISQ/𿦈=#0? PGi}X]W!6|gǦ25ТmRq.l_b2{醕YPgoe *n/ߘFΰ( ?@9(HJ?{w&TzU50dzh\o<4Ez`ߓUjkPAFND> j@cИί|skcE7Gyh I[>Itd;(]S,BuU8Ǯt#-zd d!)gd£ػ6Il_ź&^mj_n%PFuRdg~=(HfށmCkaȏ4QIMfv@݉(D|jikK6/]4>Ă<(wMyzyG4(jiODpxFWg^49/5˫s~ #+Nҳ- 2 Q v gЍ&/#n>0{fyiU˄#>hI_uqT'> ;I >?Ͼ{bH\A{N4>łiwl ̿n R]INS#\Į>'ҰX pXT8o.S`tm 2WsS >دq2w;a', r+̜Q׈ab˫ ù,$.1;Y dL??0#c_-@B2qplyA>FyVTWլA|9ٿsx|DmN%<4z~WV~s _?g49f sfmѵJȭqp66^?J3 7f+ ΁}}-Ȼ~"0oh;ĺ/8Oםg\2 Gq Ɯn?xxsDy+,Y9;Mbnxƴ2m\/SwQQG2{ވICc|X/8ܙLNPۀo /o޾u[f> {F}yײ8$EyWH)+bL_J#D1]9/t읞|lxI?i$/^g9gQD^>Z ! o-h_F3,\ x 7r_ m|vE̮_@VTj0y Xd:/cR*t1x6/CK/L̳nכtTZZv#Ͱ PѰ)<{P 1q/1XR2֢i~@fs-p*}XSGŪ.|Έȯ#/mɏJI͛VӬ|sxTC#5@±ް} Vdƛ80}-9=DXp$x_5=I2cqI(D- 54thsnQK<}$*Lɻ@F\y8[ | XDLh1c֋/]lm0W~\e_PP9Yq*Fd%D5| yZ>x!"_'smBq\4Fshòdb&&c_q#Z0]\xxEejĤSa {T]a$ DbA.tA<2^3>OzSET0KR>Wg?S8W7 -(_x>@f^#~ω]g L>͍޽x ŬV KxwR6?1X[ۭyBqA0 = \ NM^X.t^v"Ɲ=(nhe?z 04m< +s;Ճe'?bs?0SvyͣaIw3 1Rsxe *a 1%,7?K[*}.mG;o Jgl2WC,>u~KVN{/;0sq{mJ=-jn\Ui|6g3W'*!b Qv_pK4$ 5LyNBU‹-D<6d|xWb'UN;a'LjyUbfpóZ\Q)u_Wh)]dbF<`8WAQ}e?y?4LժF/if~1[ݚ]M:m\8~rkLJG+xN85Ec>/pcK.u`@i50鲝Qx67+a"TՓ0{%E|3Njnɷv~&'. RpUƜdФ*I3NyHP%_ϣMaB lI&S+M?'E0i7.6Rh$` Om#m`lNqFEWEPU:yg?B_c޿tcͳܟpeG֩r~Q]%~N~V}ߏJH=UKQ#b(ˋ>F鹼 @x%H<O 5H?PM/N ^ G)lD,kemHأf=R(X0-D.խ(#ȣ!3X.vU=}B|nʕ tz &0Eepqw y#]edX__N l^Sz+7cIzOOaN%;Q$ w=EJ}Ӆ  OW@GLlgQ%oS=jB7@d=D=q5}iJt^OO؉i{ AJ{f9Y߁(dg:GPஔ>N/J/8[xt[%(adk=%sC9{{q3G~">@ڏcL܇35:TrJuѹ#v'8xW:}a!%AJ?`}5%Fk/AT鑪x.0n6EwOV|%_ &z`8/^ARd^v`/1OzRd`PpM XŽK- yva͙B|-ꏣɸFyQe[Ք b0ZgNPY:#DV LIa|)8\ Nƕ`x{moзU4nF6pkzeI{U UEq~ L<2D*\klId:;9_y,ݨN"ez1@$ nPcXLk Spcޘ'W9JKUT3/!EyHR$oJ~|Oz&Ht aiTQ><|0ۇGn}>[fm]vǣj98:ຽ4`p6ꋭJ7gDŽaQwf꿟 W8{;3gu |>5k:Fp918O~>gtvM?o2WxO?49XC s v=p_~)7#~:sQ9i;ieM={ #ѻCq_ XmgX 4ՍU{U Lq3.#lcP 4UCxLJ{pg/BS j{Zp̂c Ⱦ ni>x!D-IAA}#t8+EGѯYeL (CﱵŞAs`?`ao͑WvXsZX[!*<ToL{  } qd3JAwy>٧~:/~6o~+ &{!< 8AǎFcM>=~'ayƏgv/9ۿyWWg9+;/_f_Rs3>䃟sv/Sk!i>` S?r+w@W]&' ޅ@ ?kiz}˗%!_%,ZRi"8H߻iEmU-@y< ǪN5Zf|ojm]hZ+C1r*Ӂ`W>ߔojrBCc6 pǰ(ބM<I܅p'xXL/Wwȷ>*/>1%`O?~lj5(wk n*ޔ'K+^XB9f{<\lmm}Fo`7N{ß$ se-^tb׮_쿂_~ CH+3 Ȧ*>{bu|D ޑ.0U7 ,MvJ%S^ӥwE&LVtCW W9q#]aŀ \N@ Uy=H>P焫v080g?% Aq;[|pxv]Č_&-W뻈GF -kyn/%R>^H??`D $)5Hk,]pPa"ªo h~op7|Ivp jUOA!j\a1v.Y"#,#%F^%h&2iI~~ W3 N2CSvgN&L_Cm]Cّ/qw9>xs|/=*>uiw&c~?#?>$]k< ƽb!慧ŭ@ knߋ7oGC E<~qbm*ҥsUK̙AN{=/n6bFyd7^5ceG<@O4HItzd;ǣO5<|Xq2rP1'zzo߭7F/U{d҈sZĂxv>s0]E|/&B \fLd XCw D@ϐ,)[,&>w1d߉.mً mT!bh.2QMAp9u*>̑spO7ΒM0%S ǣ8tD:3sR:w@i onpt҃Oڳ;D\^Wto w&O8pkC]?2޻wg_S `HXZYp*,5syL?O|_E=xJ5+*xR+y/}G<뱢 +K[-mr2PnMšoGówKܑe|{~|6/^H_[_C3> ~k?X>7I橌$'Ͼop)'%HI&UK^r Pp>`ooBj@{bյ񮷸^dEl / :_  s|b#:9"2ȿBOgb!އ"7ǟFԓ;[x'Ӂ[ ,Ĝ̝T\wލx@BOfÜR|e)f(UQ/iY|{:E?VYvl}s܏9ֲg[`n?,գr\?-~wDKKLv2݅o SsخwT] (_v.'D>fM#N-!OY/G!d_:0>~M{û=,voCn~پL6-@|?! KFi{o~O{LS#_bgMd񄢫 sZ(/Q!m (AV `u'= :K@y 8mu?0G_>mww+ `4VgK?ソ??Rs}@Q NWmc~; Dg>_Ł&(ָh{˙9WHbbd3JٲsWdC + onnUKH'HMǟ}/'̶Xm~0Уu_pih\0lk]_[9by&6gWKN1W{Cw~^yp3~::.>ug[Sxv~.xupmᾌKSkkGW8w>_t̜QWI^ԒCs~扌tψSRڞ-˟߽϶统~Vֻs7֗:ڽl `_[o |kN O痘!b3n/Ip}FK'2Y4>1;#QAe.gd{C쑿z&'f_nܸ~7̾J//7_(e"U@ ,oos#h0;k{k2p}; k3ڣ}7F ~_x3xk j/kfiO2O Y)}k8i7c'~#HXc,[ǟ!?㻷wXK|&h ;Rͯ7C.jv8<¶8JzAppicWΓ~޷0dW~&Q_tf~)@\dD&ͬ;=ffc**=r-fx G|$a acUyt ui)ڄR2.[`ej w , [@i{Q6嵘gx@3d3זm}ѯ<:y.݀\]4a㗐2,ڽضa2bfzv<3gxžE |+OhگȨ77v|7mõ-mtwٿJЕL{Y% u)'2g_FaYfQ;_M_Ǎ<1JX+Fb]iuK %̭ѭYt'$_BFbm:M3|ۦ<̧uk:}Ef#h;ffX=mnVOP;?/7'IӖQ5ShukN?-] GץtT35qJ0 [4'qg3 4N2Ziֿ/j`[ fa_dy5j׿;5boi|ݦ93gx13St\Bw|ۋpu>'5\2_`E0/K M7%0oZMyF'SyfExpݿru0!ehڢvQXt9PٴݷN-ًdسŸKJ5AO/i!ϓa3 |zom7+mؗIENDB`ic13C A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs%%IR$@IDATxYnu߷c$EHHY)9$JXSESy0n$A ~p# )N")HElN=߾{MUs[iuUZjժUî n ID;ߛ)7%:JuAUzPuJ^VVWN ^@z"ݴl't/]Aܛ SW!S#\Y^.;Z-GMxU=)zrߞvrW_ rk|54a5>Z+pV4\-{5n=}ݴߟn͵n⾋ Lf1LG.I??VntGsrYixI)ϻ)y޼ȿ/|z=IF幛<#~>'~8zq20"4hR%ߌK,Q?Q.Ί<'?/86ZIX*gs&]C -˧!|VLՃ?C-wW9_p]ʙ4CW>xM!KE 5OAF:ɦf R Dz \GU N`?rǤ;=)c )VϕY>W47)`lֳ_¨,~o?::zpp0x?nݺ=coGx?y`VL t鸛>F4v;"}{o=DMA+@m(wA31zRs 7+Ms-xD 9W(u |KW X(8Kv0R.. jRn)rY:Avu+톳Ui njCOd%1t%ۡĂ\1,޷WU |R4'eirS巕Ȥ6Z-q6vuֻãWW_ٿأ;э6&囗/\~g>Hy`gg~l0zvf3Onx ^}:Afa}ڳ*>nizko^薗 -=V(â1@WUbz9t;`u Aq0g,UEkF5Q.8x* D,*| 0PqD(6Au84t )Nc%CCzGDwBw#`m@[.G]IrXPYwAISV$ ع(tg׌zWaMЈq>ֿ~ȧ/|j:79)#oA aAUZr% |3daG#3.v4Roؿ2Ϗo⟿gox# @{wvv~~}f)z.̺EdN1<3DH*FoLAw̺;4nwThm,nJpLC%uJeĺF 6eiYǝ(zN- e^CϹCйh8…{u"E5(p p5*4%e%iDRڮ;˖ 4: 4|A_'tWY!?n!y,t(ֈ^oVu&ݳԳ1/M"|1ti_rC~+(1p(E#ϭmz퍖F[/bFg~^^;338! 3 0 &w2Ο{ދO_ۤqjNsJk@\D  ;⏊@/&a}A;w/S;m.95/`F =m{N-u=?pe ﺏ>~nVVJw |D~w{ 1p7g-:_)hlHȕ(گ=|x W=>>xq}n82 )܇[ ^I9Д†peu@9݇_ G7g~-<|:BV曆|Sr~r[V8E˫G Ct]^C  p0UଷO*g9uP~WVG>zm7z:{x?Vl #gmU|"{61^q?z'],ܾ?`}r}/x=1;I:F&RV+ܿVKjU;`/u䙢!Te;`tci=z<6F!S~ F+}ڛCmxY&o;BK8*(Өx{|+xuS;E6V>t{iIn8{>p_ GYhH/ ><Xإ[Lxܫ!&4l-܇awĸ`K;<66 [<8ι;p#uRS;b1&wP+]|M_uPC/șb nzCNnli;B/٢C2tV=>س۽tzKg6/o<?h'x]~_W~?M <䦂 nߙ1%J5:&a qpoퟣ6Ahox] //:ߵkOxp}&; THzGn]Jv8Hw2T9+{( ƌiLNcnmb6ֺ yyx|OW[}ݥ $-NW_i,|z|}m}3[=8-6 :F/fKOgc|_AwuCTgc6倏*-E&6ЁqT8ˁoD/5@3'kvOjыG9V:Yg$>2ϖrh1)r)t!LuisHEt_\ܒtqSS;ėg{ZT|zaoBw(ψ Cuzc!%' t,vׁn]2D(:+[x CKLH 0aFoǷowp{G>_^Cf[KO/=Hg۷G~}m{{9L+!VId|0~:Ro:?c{Is{\}ZFovgݥA߼\wcx-Gr"ܹQsyq.􁛆hE0y+^S7;KOnc >Ɓ7M9vo쭬OoN][>w??,Eo`wqcvww6vzlXS@SpG qPLl-IqgNa%vR7.g,CTg7b1$3VA KXYȆ۳aD/Zjd^[XX 0wvk#F4I /Fs-MH0Ȅ 2.} ']3E)/y!U> JW#qfGFG('Cxpe {lGy]KFr[o3 k>+u&܁e;-GSq<˭W=V{nʃs7C^:pˆ)ҾCI`ڪs8`iWZVhN7_<;f1ag__3:PQpo]_goyogX|tV ?Y’& :(3L e =ι1W=!J>VE|t{TJ\ۅU!B#z=<0ã0kÐF}^Gh1E ==C WGM+)hXjA4&*g ]TNANL'2:4Da t<] \SVE1{#QB㧮_IFuo@Ӑe E{ȣ֝=х_S>gyu%1* {_qX~d.⭉&Crև ;(GDIJku)~Xej3p]v8?ҳ7'ٷcw?9V1fc?bԵsPl`?֭ٹ}awggsD$5v}ֶn"'L* ԕo*1i`unLCꁎw}}*A!v=ȐlN֋;o\{?zː+/%`<_Z 1ۿs#sU_pxtm4zؗD72 мmh iyJwim[+{=(lҋwoOeJ)@ H:3g0)<@3r;p KL/S( FBZw})( TSAP*6t0_m `Qw0c\|*84Xtac0)jOAc~0[/; %UZ뱉JhrAÑ7,Dąrr"q>4:o,tfxK1D1TQ/=yy{i i zgK zѿ`t|i࿃ڍTifL]_N]|)fc,{YaI%E^IGUdMxZp9eag_k\ ⃄Ћ@ڠ!qʅ'o:Lੌ-5k M !S?Άvi[56s&W ݆LipEO =x ZmWabp}ed!#-yBMh ZxL%*hMN j¢a/MrqjӶ7C*P hҭj~~9PH}vȲLڃkLȢ/LK;p6=ixgV~kKw]3kßWq X(R푏We"D93jeWtlBb3L^ٍ!2ƕu;ֺehnƦ.>rwݛg`/@a(L$X)7w2NV x]EY2E|4cFì=2FoSb(`6Z ==(_:ܕ+| 7\xxL % }T\UEJq60Vꃷ{$_c^zTNoU@;cԋjd&435<';$dYX {4Z')JVpu 22rKr9Ky{&K" J3J:/W0^Y(*5x)a/=nnfj,/>E<>|pv3^D5 ŋ[K*`E[r܄S'Te LcDok|o20u:[u93 >@bfp衅.vBah^w[W,yVGFX2D̦W- cYBQG"$Kr+ /%] T5D 2eL ^{:q#<«tΆx" Z"_ . To dGE Z/?SgZm W2$6AD# QAZy…^dшdʯt[c,>7)x"-ad/ &fa)^d2iKtlZ󗎆[ǸB6|^O;hd5t&B&Al=zx[V>wm!VX/1˪5C)xpdD =. s&cC͵D@(# yxыxP t(\͐jI]6LK$"œ`V3o%0;E)< O)o(@'s(w XVdexTC|!xG nY]!k#}>tmR!pP<8뿱ޯ|~jb(Qx3p8`܂Ӫ( t*@CD Ή`8qYe=.pi0(Y:r@|4LbkË@>e9M({"K^EŴO)(`hWg,VQqVIT4Ð--6ɹ#&DX+H@v$圚l&th <!!mkzAB r+@5 #`k? iI#YK3QY{MeX"V3,X RI-=7dVۿ $L#K._sn]w"w2 bް ޹#Ok ll .tŇo>w4~ 1iTƇM=('֣pWKzMfWz"BbaU)ncֻn_>ql7>c;7RXpޡvy`yKx0u?'[&$ ›@r XN߷O##Nv_{^gS&;9tDA7уҏQ\9f5%N^mG"/. .?op|Ύ7NXC< Q cֲ Mw9*?DgM9r~ ; IzΪ[nݏ"*|Rs_9ŃR&cqGz8dz#^6P=0 FQ0Y˜@HYd9~}`ĖIѢђ^¹u7otLjjd4ZW;xS_{Ud{9)`~”N ,+!^=G`Zhě=7,q@qM#7/z;c0+j|f>O*pZ'po#5%Las4RZmKw"A0hT~4$ߝE <+oo/PtFw<^2 {! PL*j|jowUܣn2s: ar6ԥc[rIjhEƞ Qzfk1(YlȪ C@ 46x F9Jǡ̕v"*_IW3D3 tҀWl)̏(Sܢ֗ 4ܨ{rɬaw \?qXLxW]\rF^\@ӎ~|uu#KW\q SsG0)J݂vRm-<oUՊz+@FOlۀYgvThRVUSE[%By =CP*EeE1M3fH2l0LAtA+|h"Qua"-&o9j "_-עr;[hmNqHW{b l}Q?]h=w-4ƱgM”6oS@\o(WЋP#r0 ,)塳LOCH>sn_yaΎ?1{Sܸqm0.ܺy[6R§TTnXPJM `T)CoL4IK>ω>d 8*cE`#\Xq>/4TDBY'+ȏC-ħŒP^ZV1cݷlh֣7vKJ/4Rj9Q q"GB1kETNW8@ehbj,s+W匫Wq7gXK{h9o@ZǷo^oൢL tkIvA-GüEdz#o8^3伷p"OSlx?渟1"d&\#bcoV[P!ݱrim7_%zb+uHϥn:٬9p%7;wHQ9VzW*UMHWfnMAoC Bh UZO{4p{0 QI㪵YgvID'=]1A_[-> pn2L)jWg#%吜cK-hEt4ISq9 +? Ag ˜'&]f7z\>L@by[|{gHw҉@sG6~d>ێ@O6,V)ZY g>Qz@N9;V}5vC0r'( w' #b-Z"Sp"paʜH*HBx <۸T5w7lX - ?H)%uΦ;c)S8swius_c27$ Tbs*u$<_+$_x/RhU|_=yP/…!nxKORQ }Z0a#\OH|p% 敶ɡ} :OCBExPpY2no]` `B13%% OZHě|oSe-zUk-f|(hu  U6}jfT.7ߑ+:ϳQ:*b0rQ7L*7=x'q`w C^B9ʎi e ʞpArVkA8ɰ_o| xȪkwzy}?8Md`N"(.~,>$r ^#6 Bp\_<(; /e)Vґ6xVl=L{|w$R]~ %!S v<7u}? Ps'K 'ȵ^IM ģX2'QYBL=mE'EDO=GAM|6pYQמvtlXIYr毋1_:uh|kQ`—wZ=I9MFR  1fʷYfOgfzۅV#F4LsnT4G—x~xVv/lu;[?tytv[?h7'2x=$.YBQ0#(nQhFZ)jn?8 @xʸ%f'ϗX*@W=pSݾH 6溇@ SR !\5$KrPZĵfH<@RbUefzЮy"55d)wUQޒt=?ʱ4xDZƿuؖF T-zR GippB^]Fp.֯k U?@M΁B7=7 W5 {bC;0U8J\y LƒfI;=mNj->XПG,_\B~Vh ^`"LrqR'Kɧ灑O PrW77(r@xD`0%=Ɔa/{u=m-֞+&߽9';Zh.€xC5䩖릫 _w 0- H!Χxk1D)H E|vYivA*5Hy';"2&3etPg6F2$)-Nhi[m7,DR_ KFx+rE˯|U@W롩T[FB%JJ.QYKyJRFbFrbF?/Jt+oF(l*1@ ѳ#9S^`G\mV ky4FqGzOo> 5#_?Ei{Lr#g_!\ D8דؔ1 ~_BaXJ+–$]0|Ib(|8DR?NH}/bpm~y۫M\W'HŎ4F3^'XJR Ƃw󀇤\‚yBOl3K=Ddȫ=yLOU#h|sW_FPf pT,sșwcPy1q\qZSHEv.B Y0QSv閝uيG=Ĉ(2{J.d m!z<_ xϑ"O}#=D"*p/kI:Xpk `B6DA8kMYQrqجH~~Jk@$C2% xK{wɵ”Q) W{6[GXCt惋ȏ[&! B?B'qWӉ.zxKτ 3kĊV88Y9?v/!Azz҅,EMs!ɟS)}W0\Xi{Oe SǨ cho#9i,O ` >1/=^)\`'jIBDߚen7}Gr8!zhA|[m*f' {6LJuLݹ2 -|W| wI/,={59>f!w'[ 綹V'B% 7>XxqR&FTbq*R17AtYW%N(Lˁ&&i_)_kH{hhi޴\d{Dd\Oȑ=l}$RT<+0qgT/kޚ1,mm-M[v`,* \% @746ʸ^~bȸuwRb3XSr%4n:UKQ 8jrCr)0C%iT1%Q S?xu=#|&_mwܷ1{UȒ-ZDSgǷzYn`W)tZ56 E0ХW AaB<4wѫObm@0:EHN-h_L(;!ȃ֥ZkɎKEhV*c| n[ѧķ?}E/ɉ8$L{ 䠆3kCr@IȜZ^ң_{v nRz|~3iDkۗcȉo@> {Ӊ^ 'xwgI2bh\g'Sѩv;t:$,BL>'(|smGlgR23c}[s6b <4>U HOۓ:_ qY(x;nrppJ9/c(n-ʊ5kWqW=.LTpmZ ) w&x6:Trɧ _|47=bюjԮ6|YmBWzFbZU9\V6!TVŅh4&}? K"xhItҋb̴dTDsa~}^P\-0?Zi)7=+)A@KψbF+˜^{,iO)⩍2o}۝b朥/, &52ֿZ)Vy/7ec4,k"X #YNng .<0*jpr. z3L[ñ,6ZEzv[1kA(eRdR4\{ޞ%'_=虯tw;I7ۡcɣNzV 1fT (7kq}=P=O[4ܹ->T9 s߹-4❰%fd e54h lگ*SPY(P+J}+]b gbrc/a<ʧM9{8`.Ti+[W\tKϨaHxpdO_jFZ+L ,CWRN($ڲs7z==F$@=03佃kq篠tAvW cė?3O-;3,'t޹x h䳼gp)~%0VtNJf?v?o^z8&|xF7چKX+/Tw(|I':ZgF0sn=Ԗ/+ZZ83ݠ/5ź2#j@]ơsg׺5i=I(}iA'⼗p2p c;o ېB +^Yv uRI2Wd%*$ Ot_n"~2فWK4U?qjPX"D7!='_YD"ty kmWbHȷO# '}sQ gb.uvS9pw< 29DO8 o gL?bv"fȗgZ'/q_x0XwK l!q7;*vU6,ڥ Ww f}w7?k [2ЯC7ǫRKvZ WuY$ȧ4*ü*jL"C\vM?b@o/vwX739I8ʥO23 ^=ki}m & tR{Rc۟iD# @[}ˏNF|^Cԭc0 VJCS1K3H+YA@46Zuu-H,QŐ]zvcM~S'\Q$8D+ XҐ_n{yJzhaH(pՃGB+m,Iܤ<;d-OMA`wآV2gLHy,w&tא@D k5>#㾋AFSuԗz|,L<6Ih#;a!z8:}\s1ۄ${l+D"bq{ުZkfE3,cGCaR-WĒr)$d$C\{{~7)P߽M|+ʥ]d_w5m.n&uld4e6{,RfшɰDDC/WLp/(g;vHl-6"{LgbEo|k>B 0|YVutj=% uY^ mU#y0_Խ`\'EFl#O0@@ɯWhl̶ iن^e[k:`rVAf6O(w_2mNoq/˃DgCzK/_g֯`ƌo}cF7qD{[RNr뢧좳9m~`"'])!2d*_:4z@#o|g⫝̸moG|7_g N@.t2'"9-$@}0̢l%l5O#w.Ir~N8c+A,τV>~-ZZ hR\iҰGԳ͡3&("i m, rBřrBs3F#i0V:NIp'J_>qJe 75f Jw۠^*%z;{zCCpM9+؉;^d=Ƌ+4ȉ|Z*K!P<)=DPhH !;6OotlfF=*oOd9dU92ʐOŢIZ R_MY'r \AJ1^$3/>#CD"Ò?84.3ڭRAz0<ˬ)̥Li߀I4N\~ ]xpQwk3p#@cޕ2;$q#N}{ՙ'; jF_޺,3Y4^XDcu4Ӂs}<嬅cS\m H,tQh^ޒM1GTb*ܶ&PzAKUDGq=eg6Bמ2=&Y܆? $*$zga%QEďH< ms-/Gȭ/X)ق|3,( 9i֑g:m.2 2[W4}(wOQul @s[mTR#y bڐZUˍju)j*֣F),@YWzJ$$ma=Eh^ol =dU͓618eմf0R>dz"[sF<Ƽoܕ,fOvnw>,u]w.b gG&]4j,ܵG'ڲτY>:&"#k @8cGg/04PcIP A7?҆YXddžg–\bq/7'6Qhk04:?0 -*sX#jTɦztl~<=.n$mPT8cMV o6#V(݈VB[a(,l>Jd\~V ^IOӬtSj_ßҀWQYb [CțtpM$>禼L'id.1Љ>.AF(HO $%b6dc7(\HVgZ쫻Wx[7<ӝh c?^Qm+٥ )lGul}a1pCIxȅk-n$VYkC ܕE[E=$0i;̹ qWyG7}ʧЂT͕U@xdkRn y]'p'O 0H*p,sVz FMWΟ}Q6FS˓L󯌁y6 ׫o}r6CgX;f"0BA XE|_ъ 1KB,t dV﹋gR*+2tqbYA *Xl\UoCfږEwv$Bw'@OQz@ik)o Fð~8e [7 8ӁX9Vjrω Q*A(G(?3Qo#L@vr\h=rLCJZ&ikhZ~iz0p\Lw|!^ۂ9edKU:oeV(qb2^O 0ۀG7˃l,:e6K[)V? =Ʋh=Mv/™fǍ,b) ?'pZ=qP 66pBfll| !o{54:bTKXᅡWS ShF nԽӍ <8в,ɳxzųeSH qO㒝MyS< k/q1' `\g͛`;5CmwU@yO^~oi Ho.tdGEr=n} 7 dΙ"!+k׵eٕ<֭5x)ugr%p )|5xgD`qo!=)ylrVxK]772,\SubOHɍ 0db@WJ՚cZH̞smZ>ne74!E,/Hњ}. K߷2оjP*5\x4𠒄Xy; Eoţ_q,;>W}F1γ "R x:ȏ Lj vS $)\kӓ\Ba'B& Ąb^8JߠΌ&ГXd'P[ *AMOcS"gWGIU>о9$g5u.xFϳ˼%wd/~'軽ս8ZK4#^da,nׂՈ;HaRY>~>2[o ޞrڳsɺQ+M" I$ѷ sZ|Gg|ڮ+*?q鴔 p7`Iȗn~ʊ B*7!N̾(ϥ',<g`xQSҍwq4`Dd.z/n# ?]3c,<}+溲{nj6cCIz_K |񾠒drAfNb3xk~,*(HS@|=G/ȊB5ZP 2 +L`zfC<3Y?%TFbg9mݸWg9}'g]:c}vťLKʷUFUt;R-l2(ԃtKӓ ӆ.u#`XEa1;#V]䣗6-`~ FJC+B[A+SPW`_+8Ǚ I P'':o}X҅0 6U7ķ@d-|޲Էڄ6t?u f0ғV k.7e9&ڼd̳ >US.Ql!X?M/Dw>,G٫~LrR 7/sGbzn t׺"RxC9ykMk±`J!bk}y v`άEBU؜ W)V!E4?cW:|KLG mTO EYbLA 5qe֩m-kR?>?4Y$|LߥaHRtS#COl1r'"mMlqРOx-dVsN91f]/4B,h \Dûq+"i=N]&4 ^c?<d"4|BU\iOgko>0īWpo)cipo:"=L}fgfYm뎾ʎ[Zi>Fl?mo֫kr{>swDIIQ%H Eb 0 @cC&7#)FlYDRIQdgs~Z9>SjժUUUU@,hrtvk#>%N]xN;kXOm U>4dlpm >--O) uP Cs1 )ۧ$&zۜx}vz+|4lHL#Dl# u:1][v*GT`?^쒒B5a$$󀂮ڃIa,nQɔdR>L x59{*vqiQPKXGhyCKJP 2HA{&wf!(H5 %\"cʹ;q*R'|{ &x(I*g!^]W^N/Y~5}w>j9ft҉X,ovҖjmlc#\GH I !aX<қǜl)ړ1ǡN-c>S#YN> V"5s뮾(P9FaEyZ FjeJM)ɴ'LH8܇0ds?Mac R { rt-cB^ށ 7o" >sK55Z_7Hm ]Cj,'ID; @ae. 71qP4M̰+Hevu}fէ|8>vy~gi ~+挝Mb\p|{ɫLg9y:;.&kYZWɇ&h8Y'<)[G '"B#?<|7~{M1wag E'?rib{V@ [ZL$C"*Ἵ{MK9F;’% F[x_,a~& G*sWq ABswWW8&c%˞}h6. Ƈ1K K:qt#'pMKGI‰/]ɅE ' 8'5Z[@{40 tV:H xke=n.ǘ2y+_pWCI=}F+C۴N>`R1w*F;aB@ȂNyb_5fтw{$Ok<|K9s?E ?4}.-Mk?8$!Kd ԏ&N !G] x;?C^fS!IaZK}ؗjB3EK!2~L) Qu/`uɛ;{Ya';")ג/&3|LIX)}o)]  ]%Bioix%xnjLQb 2{`JQ%>A:2$"`*DP`"vؓaOt) |4!u !H&C(+dBEI3ID|ϬLqђLgK /ޥUzo,Pr>3<ȋK75(~@2AV#k;;AfٲJއUb %'9Ab/-= a(2鉘nn$=:tg ^mG,)]|on"v0ɷi5LEKvXz_Ǔg>s 53$dCe2,y> 97ۮTѓ2T'B# ZE56< 4|qo9_) yvC 49aa%߲A";ofoHZe@1eBy#׹Մu5pbXB{. n$ $Ll~j{.Џ}zvr+pzv%!P@s4˥3 9{$3:+(ʺ2u\үG}G5ˋWka1G mM6as/}9gؾqO Q~/g?7QyKד;lҝ:dĝ *'z 2>sG( YrUhK2DpMH= ?"|"Ec4{&32ܻ&xEcJL~EsPxDhYSʙ ŝ ]F(z鈝BBIV8UXG8 mB%y!7^z. Quq;of=2} =zW.eN˜xDcUz Q*.x{&To"nI'VxbCCɸ(_xLHb pWw߈n][{ңPLC,bei'tDUՖJfq#R^s-ӑjUUeWZŘ衩-6ywS+X]XP~au ?O A:X^'6q+ ٽD 8˞Fb)!'q MJK,R ʁ͡BrW*t3vNQ8ǻZl1˴FR!˪*(h,;&AjHթ{tCşCU/XV[w8=^2c>W0ņ0rx0@02d5ؒ0rb׋mSdi @~ p!yU9 UʕoX?0UZCC-z#0 |2VWhܕƝUڊxz\|OkD\4Q];q& 1ėrT'QԦӸb垇YWPpeN>C?n s4V ITQm kAЮ=A &k +Aȟ" gkT L8*\6ǥ̡>16B H᭏P釱'm-n\zJK~”g :Ң/$~<>&EN_J~%L˭ ""]sUҙYxB[D;L2 ;ȳ=)-yP'4])AFg2j"BtCey dnO$[ʹyN`6##n 3\q?6Hjѕɼ7|7\PM's/  {91fNk-IW+C(ʣG."-^E2SغO;r.)..sO=0RgTdl*-/6OfؼJO84s 6m_U6!ӽ,:WD! OdT q44)ysꓳ_<%x jAMP|;&c$eRxR=D>{_ {;eg?[^z0 ӣWn4[6MGMM:XB姤Dbgg;QȞ_ "*mvs kÓ[g͒Ck0\/ \ErҖKy,w)'¤ֆ|֖+%W8$I8.r!@IDATWZĄ9AAG"1FN,? [ u=$ q, Il,BY}d}gIk슁]|2Dx+{ZX]7VlP=@hLn^]_fsbf 2ͦ ^ġd3ުFjRQ$4dC'B7U"Tmp;6$L풤574||.˘r/ z3hu^u=ϚQٶFgc&lK @=7L-2 ۝9라d’Y;X9Ai4,qU}7ɉ\_d*^ S/ ӧ6UQRsK&خ,4%)K_f,Y)G>l`e 7n})~OzHof?"{ﲄ8^!Ӗ$2G[NiNkQ0 _)!@RL4#'8 ȌG8Rt].rϸ4`j[!HJbW|K|vMtId#l4 R E×غk!r؏"N|j@43>V`߾C.9,p-A+n \z1\x8w1_,Qm]\E>;z +@ `&0i23#rnsQ&c3BI_ eRьP`NI=wYsakU0fۊv Sֹƭw%%c֥|#7+u5dYï$y@kKYzf9{ ɥ[ Sg d!G|ۛ)!>%|iiI2! ZjOxj`9_`A>)xE@iq{@\^++_pЭ]UWw vܗ o'4fF|4w 3YsKpE 9x,hھOWF#\ZZWն02pҜ i!Sdm";1qPAn]:%a@Udx# 4|T:H6 c}x>d埊@XY't]F6sǙ2iI1途67oL~:W2LWwi} {IL c|K*ޡG<^K.&վ ;/?ˇ~h|UCv,*v~Aџơu\VE~j>~mhDusn0I+&jB0X<5j>Qw'P <@ {hg;vxJ2?y5|T p${@Ad%4㙖>-ףcSs`6ΞK҂ͦ'=mY@@ȴ諾wW뤞?{.isʺ?z*eups_Cޗ-rx:ч_t9Ir~ ,>; 3iNawL(TW>ZL{8 X=vuo:/†Ӹ=HC,[chjxf9"K`37qbP҆o!ҧ#Ti>Y7b%-vVً8A6k&sKZ('Ϫ Mkw=xӸ7LϨ|>Oa(4r%FK1KZd8h[zLK#+v.-P3m8y6_kf  lN4 6G** 2b}&PU{?rj>#gQo se^s aj\4:!-Mu"/vѡ:q8z5U~B/,e~HN,3~c2(Z,oOE}]7m:E:婎Y_-|ct7=QSY>A0{jr1; 4 a<>}>tiMͩQIWcqdAɮAw@v?4:|5sRd05K y-M@|wvUz AӾ|K`1`a39ƛMnnՅ9 -}f̹@pӐD9޴|t9L:Ezlr )r qn04.ɋ<518YQ0JgGFVD`s0~#XWEY!Zk|tC݀.G:QA"X5̰XTGG .|?II27»0ZC<5oy1W9?. Q {m سoE+yTH]7VfJL]8%0g]b>h+(IQ?9U-)%BYTtfObXpwg_x];bkˉ=\='׿u"̠zk`4Vh2"|z$ TX-3|8zpy#EXZ-4f1M|cM XMb߉ ^@p}zzoY *?jD" 'Zx-7?D&vZys> fȅk޳I[4%xwZVn{w$NKʚ7yPM$L:4gv*vDQAn A$ETW]^H+`<ꂽ\u"jܳXz2 Liq8>_1vDs)n:fVS{Gf/#EZ #ꂙn"H|&Bsį 6987Qyk{)ub_w,Q bZd;?i1 #4h`p#_aeFEk4uPU!'[_K{[Cɗ4R;݀xH|v #zf8E0ħE"t*$<߉- NPl /}?6^QWp5ZADZ 8. (C}~]O V y,x3A]ϊ ^9M >O=0c=4S bA^l4:deLvh!0)&? WM47|xTW>F}aһ0`v =!՜[(9:ce~&BmG+ie)L{NWVHZR%F4/\;fw Pݻkwsޞ>Koηn)3i]תEp6Bw7l_;mD\1-p4,X__Y+ty)TK8PkF$2{3ή5uc2!s'D0y5'(w\ۍIIꆨe_=3(OÄٍ,HmR_w _WGQpissns-Ƨ`-w8)ȇjd@/\!knʻh{B|0-7L&Pioӽ0zP-)N}Ci(IvX5OQ'.:sx(]o6v HޠF2py9@֮nL? Cg &pvA<$H*$?s }ۭkz6.^gW,>d]{7^[=|Y~z'[1S?Jm̑f8t@\o‹[#6YkD(?PLc6Mugz5R^t֋F̬ y2\M?yj#qދfk. f3t@+%+i'`C1ِPZ9lBZF2`G%t1D3=?@:sBHp'v8A'KˎoےLWK<ᑡKA!Zuvk݀/p#LU4ړwĻt/7 x*΄}=!"c5 K|!(ڻ3ia_]gq,΂uQ}=t9Cv{E3IF'ׂZ4fd)xG#O D%ZueS6 DP;Tji"jmZ*rV>%(چDg79Ђ\]>1-ꀂB3=gRV[n>CYo{Ϛ>Fj0%0lr(;.˟ˇ-'_DoՀ?F GyBvq9N}Wd}KM'w"g23MD__:WjMƌftV%J*q Z:ڠnA37,C8Ookё\ hKt IvQ&$Wk53v_zDV MQLχye¯Ï /~=-M*F0a?x+θno3Ƶ~;xN>w+N&>+ěnJ(&('=U6rp( eY|Y'7.*atƧ3s{z&Uh r"\3ݾyooqlI6/_X]_pc_rzynCt`h7X zVt_* Jkgu==B+c(1Ҩʂ /.DϿ#w" TP"t-#wnŀNAȬa\[R8tC_s J-'ԓkE Puޖ#GCnNcOP% .=4 %xO ]+_FZ]g~XPPh{Vߣ_,?jz'cPLL Ӗ3g|MO4_IŪ\N+vcOvo;D9ގd~3?zѝwh܎+ ։9n?Մ؄9 3vY1lCpQgbx`<,| +i< ;9tˮ_f"=$:uI:TϨzקj7͉v3|SZ*\L|3|2p8C2^u!w^#F$yTՉm"" }/e4 Z r;ҳM1f 4nH}anZm7f\dߋLWvif2-}ew 8ۿ:d? GZ@5IK>d*\FG~GрBWCsZNr唇Y;G@pW\5~p1.bq.q)cV .?ӣݾf&棿T.D=CNd$wzm7#fܲ'eVs$dFK6XlcW%tN"a_x!7y; <8due.pGH9 1&+q~-TF%2a _9Lr=]:3 [ Xt. t1LBb 5Î9l\ٟn-{jcdogݺ;b>C>nWDʎrfH$J@ X U~=Z+'x3o@ 1b_,wH0hZ%ff#~7zy>wKصl[X">9_3Ynɨ\A9l "T.]z532-'jP)9C}ajTCs>G;Jp D !P@<ɬˢyfٟ1cCL4H\F]ׂo/ s:3b]7 xӂyo1|7l;1<<Ψ떔.aXJAˑf5G0^wծ”z)`}܃>.鷓g8$ز ȜE0wR[ʧ+FASBJO@f Ad@_v8d?͒߃՝Xr&"Tʀ4]ĕxV4j3 ՜CS/]qG OȫS 71<|]:p^9=&3tšTm*@=1{`);_ &:tpulB ,!5)@BwtU)'tU7q{&&`T۳ğ*[a[ ۣcW{1NC 㑒"fVKOYG x_2}kKˇ`e'=I:3O1n1cN+̑]"~vEisϽ۹TfL$)`Àts ~`6+a A֎1V#6Ph@ӜO0?B"Y'9Bj󩡇ܖ5tn\Nopd 掀إ4}K:LXVިU3݀8R5 T5@f\$m÷~:|ի u";ĄCQ@^T*;odބۀ& _G*XJע=n ?/aaEP~u̚CaiYP ;jB'hQqvcM) TT #% b-߂1Þ?xG0X 2*q3R1X¿w6/--ApԫS{ $3y9~T=::{.@ 0? 0^TܣCwYmzEDppoLWՁ+<$Й98 W!د̴S( (^gߺ geW9GF;AgEv6%M [UOhvߺӁ,{.7`?}v ۂ?s^ `SIu!<{}* (QQ;f!9 '/wqbj'3Iq9tlõvk JI=C 6OySj pn187x;uAwS w9du8@dk~2\IKw~g< m>by [f>a+RA:}o $ש?" M^!'k{9.;0ga3jF{cPLQ70 c{.;,}!qjA!"0[w`_quup1nfV{f]C?s7-BHp$ɬY<7<3À9YhؿGJf} QL{ I/oκTkh8.lӞXr?/E`x7>>R B H/c{:nKÊɌ5E/~Ba&jN),gs"@H9{NgEOŗ9 IRUmwo|%ˮ;?non玈7soėxslA^X<8qkxW/xuq= hx,,65>5/гSK{r.s>0Cy ?+{~|:c߳6):p2.2gqaLH!-Z]\a8jopuJKTZ~"Ħ.1ŦlEAؠi8E7&<ͺq.%i:;K/Mڅ Ѷg7 ># cs+i᳙VJlFuZ/cR[c'xC08Q׿lx f)]XiszZOkr.t9ODރ6<+/٠Nk&"GDT.%dl:;>~VUxTAJ8'h)tx?V=7R2|\kړ`񳂕aѻ1 |ޥb s8bf\rwc8\'A5كW_ QN!H\;vCb\NId}W|h}m!c;>Nv]vS\%G߾~_{%;3Mï;w?v֟6WG*!95̪1x O 94<#~1  n{ Rp0<9f}?>d)ZT# ?[j꛸#/`qKaN)MIOޏaf1[ Mɩm1鈕zaܨ&Ocjo'׊%.iW"XPvbЭc) V$s ;$nyE.mk'ئC WBf%=pQxl~=BT\#q$%n$Qsc3gH>--',]`okF(O)qc5.[#j1q-ɘŻ-KϨ9{lUXV]Epp/6ts.^b) z(t,Hk0y9 OA\v5`kuyfj 7+DvHEլuB]`>wވs1N& M{7\tc/:oܝ(B]pe%wzX }.$ТO1Tc }%:E>@mR R\ } };hOg 54*4u2Ղg<~tuTѸTKS{tgK쩠x&'$TsW:ęW[|^qhW|:nRf\&z3 r %,3%Vc[RƒOA=X8Uf$CI"<Ҷ[xLW+y쵖cr8@ 0 S0->\%nė.imp!*:]A kx ПSGz8~زZ0Hf\6Y1$cPo+J*s ء![b` JeIJ0k%'Ǻ 4c-3QdZN@yHY^EpB:]N5[.:Afĕ!{:x u !p?-ID#4ʆ[n5|nm-q$ \Fq%8ߗ Ωa5sdI@o4hTEL}L~mh\>s е[E)t97)wytu@ N:.+N7q"Ǚ;" X35[Dh6iі[ &#萸C̖x+$In!\5 E1S#q,X5eBx!TiҪlRIo+aMT,*rJQY6t%vr Er[@+`4KAĞ]r n 1xm4gcC$]AZ.d<N?X|o} Dav%.>}6Gh'z;$SʷtMpP-w(7q< &&3|i@лڱ9u\DY1y[?d'_mɈqw2FʼnWg&@ S=GGdmFi4uYljT7ngYT~S`3o~;xq{/gbJ;tC|.p`Rjw V-pXKs{,׃ֵվ9G*sC`W=͉b<#P]h~S r~.4 Ѯ1 te-?4^OΦ(,]KqC@26IA?pV?TFub#8bL:Ѓ*p`<&YSA/Keq!kgod1q*=CKG0\r5pn;fذr&߻JH3 +[pȇA=~\ȩ˞t,cX67[i 0 Rӡe@g{ oQz1a"6{|jfNs8<]o`93`6Cl  ?lzt. J #`lwiP1){4)â Ά%x<= Ɔ])L=.!&^ Z¯>f'YcP3 ěDW`CNtq8ti{̙ks[t* Upp2j 5"#= + ]"~ mY^Kry8@/W}`ϊ>Gù%!Q ^⶘а]u?8-]V+Xl;q c#G H7Ln>~4j;w}2+H58[§+cQ\.‚f"L6~A"605l!l挻cVz'NޫOpQoEGJIDAָh8Cku媂[m[ g/̬]GGU"ea8oZO su}UDp>*l9[=rjF]tf|3b ?xW?o~%'b>H R+Ӄ9 b:Zp&2M,)8aӡCzOȿ qԷ?}˒ X>!nW7k9 ɹK}YpeHƷfzTurM>WZ& 2ˁWޚ<Xk'-3w-%$^$n r>a K|ƬYl"N p3y`x3i|kӀWq= .}~ZŻ|15/*38|=t-Z~^c+TXq[{Ƒ؃aD]!A8~jc=MeDWWhw8i3m=4"0iB`<-(sIs9S&]gc-cC/lߓЭӘ Auy| s 3'4wcVlMi,3'yPB GbIE_S92.wFnY̴҅I >CEn65%{W(]U}&^AFLϘzRe?'toGtβouWawdB)wz0G"p &muw1Jm\N&鷢$fm,4( c˴]*>[鐞r F.1*MO;xIN"P3*+<>m0>mEɨrC~|Lj'-|HeY%|<} ?[ei:/b!NLpwޓ> :JWo0yog5bmX$+pYde܉Y T?xĉɄ1[zQB)z1넇ڼ-\5 u ]k~f\v'@RxTVd Ћ Q-:\UmQ|>t"NSAXB&Apq6:tgR\$0 80)>؟3tL7E&|5-RK߼b\Dp$5~>0I[rK I0:`<#ɖGMs1{& i/8Q߰J ,ͤi83gU/\e!ӂ- AILɬ8N4~/'z+D‘~ *Un Ye>]%0SB,Ȑ{|@KizAPKye%rVmc`U݃)[H K G[xb  Zfׄ*[wޒw/JkQvyOACaNa%xgarVb<`݋;e E!9%&$R+w7c݀&p,5iyev \ANС%pv"0!q^0hYG6n <G,]qv1g~(-8p] >,˽X[z Mg;MEk{Z%TN v[B'7ntz%PvcЬERƞ+ I.Ewj`K Q.;buLa}0wB9o!= ܡ<=p0}eH.g%1 )ŽrJo=(ᦧV`[ųzDL~!aO4-7s USDJw1]o1Kh国틗8&sg*/jN CC3oBӵlI8-HxL}`2sjbD,\իO=k4e|`qT3\ 4INX4Q`K$vF^5jkT=A tp{@]>e| %WYhNCa7m#v]S|ÒpolHZ@c&j=S`x-T1*j=p !!K77dS xv ةJ`tn3vC3{mwƲO ?1Jh*/)ToFY_Y;~6W}{z]I:^Ǜ7`ƜѠOv_/*UE^:c<~pKҋ%Р zOs;&pd`A , 3{6I5]J_n2pھp;P<”; Ax)lGS`M ^?PY>3P94M2Nˬ,o$=gIcoe73GA9E=et chbIiY ?=)Y@Cd>s9zg:hF+v1TqѻH2q =v0K(VM,K SXb-15組`:iִr\{Þ 3 )wۦ!|3:AUe({yG! [LtᩄJ 4VmƄ&uˌa BݑUWShEX}PFߖUw?s7i6Wh=ǰ 4Ephf[X]6(;-R?2dxeVd̋1L$mj:xfV:9rlJ0ZX]qK2AdUaiMrkzn١)>9<&C |>tR6F(Xu2®_j5\*m &艷>b zc(,):{ ryf[Y˸HgW0pLo<b-T6f9xHn _]Eõ{%1TuwZ!^<үBT#rW¹XtQgI):^Ova+ko1K a f>6Y_g^fğ$U`Dϊf©gRhMLַ溳X[}䚿[ nݮ=Ov79xq:{pApn1VD;H_ Im73w lKlSHRmy>֨>-uY'x`NES<:-Ƭם}{a)z`w$Ow!Z'iw6F/?= Pd iK ֳg";1IGtIρpisjNŴ _q^zVerW(ƶ52gnZ@/-Gd&m:-9cRPnMzet.-:9&1%gI$hQ:W3wCӒ9fn#-O Oˡn|+Br B=B,ӧhhwԺÞ'd'h@b0ql1I I'T sd06}Us&a@pӵOAHJe 0ڽZ*,q^ &6ީq釹/-B li WME>bP 0‰K9woͿH8t r|wτ )L~#05|ނ$p&RL!HO|ī`m:)=:N2e,b%~CɜC 4 ફ[ 2ϐly& ZrJ4twED4۰eZD=8-.L:&q t6䮿$>U /5x3d%Ok.-=EbyR=igoT ws: K;wqλ]{GrJ6 8cM6UMD{UrZx()3.EKvVQvs<PxhF q.19ʧXETg읮N" 9MTzuR$>_-r"8 ?-?9j]!39x28`i HHAQ;DyP&JHB$7t_4D";Z>N@7$ ,uTD @v|$* Xya^Zɐfy:@҃7MN'||@ E]᭰x{ټ0aGhq4t,\鸝5?-TSb DXKa6Y6cL"fp"T"nYL>gG4G}&~~ M-gXQٱ֣ !X6A،`8q*7S=:9p-CSLgZ1?IwaK 77gJn :7]$*Mի;-n54^+=j)|NK1/vIgN`s $g-m0M݁p>|)8םء>v_xç ŧ;7Bx" ݆쓮x;]V#8̜hKYx>{>S1q h#uKG 2l݃;>kmAw%z[,W>Z9NKu :>`pu#tjXL8OX&F14bI}?3`Ü*O/*o%|?A+spkn1N0N[9QtN ,6)?V[ ȫ֐@q@ G 7umEj;ڱ̐bղ(wZ82YZ)3,_faXuiь,宿a|L&}yf weؚ+Q G&xO81IA`NO\wH:pX^FT]{O젴d}9Kt\\A AI2Ϝ$؜7]$0sxHS`g_E ~ia:硙H[꒓ͧhhW/OĞRȓSK`5^O.C:R§`~ ,9w-7N I(3-cG?)P8DR_Q۞6{Y.;AtOQ{+b9+23>Ou 钳] Ā kӯudRa[pK*AR AiE 峒I7dH>[JP%M,ɣ f>ac偩M~} 3I@R S-K[q7T+J*!'^`LXˆ#.2/3h;#"z@En~hĀ9 :h:|-?ʜKII>{xDdL9TVVt@-zѴT%BB 6cŢ7 ) PݭRWVTVEÝ~_¯G'y1cv{21CHj r n*`U!l *jg%p[Cjj y t4?}5Bxg?]x:ymUF>X,Fb\$)ϼ5meDkuF`5:#'I'|E׌,ACx3!6<][Lck/嫿{8$OYt9;쵴~9`r`XtUz@X&fis0)TTqmS 0Jqs- (PPUs%5w;u9H|W@xRk4i e?cQᙿ7y#{>?avEfNeN%Č2RD 7E,CHO7FC7%n1EYFvvh#_m),At DcgR.d 5/x0%Hl ¥g͂x4-JAD WwZe(-OXo0Mr|se&GS#^lN?GjtkS 3^䃟/ۣo'PPޠJRD"T:85nA ׊6h$uQqL5H0Lo" Y|N S{LL3h \Rx}SP7 m Gu&}^F2D ʹ-~rX,LF|Ќ&4150,AU n袼!n [4Y1Y#9cQA%B1Tw*enP^>_OiN\2́۩@¾hQU) (k򫺎6t,׭E" j[xY #`qAtolC^C?MhOeN'@)&#?YdgsJ.rHjO 0fFvKmcd _ualn{%>#9.fr vFݬO͠Gv]yDķOfOg1~I&޵>x A C{C嗖iRfv򏹫ISA":*A'z>8RZ/[w(:ntmA6t&DFU!/L 0fIZpU&9IhwWѯ< 1ݦ >cnA3S}k4f[i9om뵶1;Ν?9w;϶sօ۰w-nQ+Ketl v*_(t $)!05xBk7aE*IK.22xz`Wc/g3 "(W Cf߸|DNOa X}>GcM-6.#doٺS@-$N[ԑRY㌝0#>6imf0g+:l:}#iyD&ih'quME?nP Mۂ&8D!|J 2T 2U9-OS7j($ V)+wp磣k ݇n4[:rd&n2OR F#}GN=T}N>DotX,IFކ"AIMցrB 4"]!‡}_zas{8ۄ8M#ۚX%Tܸ7&#јspG7_!/a e2@:cN'PBMT\?~JT'!Gbu=JUL_A꿑SXVkFR+ wI9L3:| xGBT='u~vvw-㵬Ub%/|"|Ox3~]t%jķ׎ eK?'0ES+7@J^>hE0'ܻ|}O]7_2`ȴ!j6k[jsYt넖Rxr`$}۫+%A-,s"k]` H|a=$u$s.jh=?@qg.<ջlO góD៦N"gCsKRȓ-A>$9CgS`PL}O^׺jyU =`y8"IG-kv$=[G2=9 2*Q?= ;4HuIr2Ɵ,^~Cx`Y"A%krzɅrԜoЉ=|b3vǿ>ֱm7\qbw#> \X@ |űi/>"k:sVp&/}`GϾmvxGl {d'hXKgkuv,M7!C%In**aE!VVV.SyZ0FmYCP?I`_VR#/rnRۀIv2E& d#,a %FQPq&ta_K4PZ:*1~RVUie,:e7M!&=8ѡN B0_z#B'PCa3~ݘS j6/Nހ@dJR%d|fP?m˛ۣ_u7/]X.Ϯ_بjщY"QW-ʞFG!T t!D~`$,?q-!C%*=8&UUd(oDz/{`.>e$u6"l\F],#uSfB}+sF2A+u 4K%?龶0Èl"`jooGcN(GJK?qW. oՈ鄽Zo >#x砈-xРֻIN7UAG<'=r>AYD^M hAڹ5zWԨ">ʝYcw2L$Qs|]!LD!_`o`;U֒B"(L[_畾~Fw #4vvxGwD Nn4#u /=j;#~97 Dq鵣+` 5t㯔B2tF/ /m"B|ܚ=jDW9}??ts69Mt 1&1.c2egohW8J0&}*/۽g1fAPX#E39AFg훾dCVʑ˔ܽv )6 8 5^ٛ=hg BpP4_ ߽!~UY/&_ zEHs'|9pa9>/_ Dlf'ʧMf ;t)ag}#tH4)".L9bO|Q_ m3(G|imQcIa7WH>g;U_8P] #՟c7!/!\w+Yq|M4n4_`8c?K JvϿz}yyv뛜ۜo2@̶'hi[;?,^gr.jw s{|ϷPo?]x|z28̽h p;U8`J> 20-P&>Pc*=TR8n/g2%pjN7GJC0:9~|F[!/Q$.xjlݬy { Zn0wOg]ZpU5VAB-`4aS cZ[`ud3Xhùr5+|&Q]^aQ z|tnW* Aí^㓦]Lͪ"w<ButYݒ=.u}]$'6ծ-W/pl:9P 'sKum^9vM⏯\`ڴ:ׯ.3}E, .mgC=~ܻ~]z a~M#xxPeڨOvacNgo1RG1QMy-D+ަ&^B8&JoQU/(8 G&|-o*_ [Q0|ok[G^f)2Xg{)EwK.]9֣Odνn>|.p*<$ag=ߛhv|WG}uRw޾:;^2ۢK+ٞcH{P>]Q_V~k[#Ot}y&OlJ-6pZٻ4`>,}qaՁUPvǯ7Q%s{B`$KxN8H`ʻ:a>T:c4AČhj'G00Ǖ;nH[C70O7 7HڕMpG'ݠι舶sLO6x∎݀!69|󨯛,gfJC_6QK7 *-Fn$PÀqgYg?rj˨ھ͗/C<wc}ץc|]@g$<*lρG/Xu  }@?U_2zwEaeti ĉG 2-1Era=vwgnPY(Ug3jxzpX%6nԆ°p*mBk&1,|NZpϨ  1Y!uPN Bi `+Nu_{;~r C3髯#5 ɗYU:;;W| d;wނ&#:_+rmA]:gWVwM!kߪ|W.^aT|ض|r]kiIe8Ud/*?e)qS4.ֵjzyਇma(iW/] 6FX_.l`}q^{3XRk;9F][=sd >z.~ʷd[=G -[Kο Rנ3uC?}8]5 ]]㴑pP Zȹi7҃]ҧ(V9@G@ӤGzľTL &ŻYccf{:2U.N*韧 EV"y|vJLwRaxNhAr6F 6 ۼw Ujo6`.|m"r2t n1jL~܌ E42FT0~ˎk;^)|V HRbV=XP DP|cPojw-_웯_H*o|Bk?{ضCkf C?zI.K^/0cI:}aJu}/-v Ô敗.miQ>§4(.mS>E]nhwp5e%{+̵o{i_NwTԲ{`עZ{.!>;~ۮC6#HAP'# />:RRUBR*%֐tFݾuwv-j~rEu%P/,^`CZ1ﳪֵ7_bHki2s{5Kmls:AuF~ d-|| 1kW^]FzDp {m IW?mrK{}ĻƜit0HiBяiӘ§8p(x" 'x^N7kgKS );s/upFfZ)sCpipjjm_9Y PrK0ѬuK9aEG?(#>Yzq/xP\c }%q^j_r%TSLv(8~6v3z/5ڮj.xm&nta OGgn;X] {y ? BZ0^kmĵ4Vn\_Ƃ}p..wخ6ɴK[(>9& YS~>buu; _<6V%Ƨq nDՇXACQG|Ed88iIDAT}i,Ir72xL"H"jɫ qD4sj0Y)ŃI(}[n.}̈/5GWG?ڌj@\ 0Nq lqpg ( qNc tx~U-RG|4#)`?յ]?M2J!wx8g47Saxfj`M(xu2ʾo}`0 S>z"rA{_PĿdsjЄ[Q*=Q0mev=c?us.`jT} 3ws~ p2.PŋHp2V:{r$A9$h:lhE|CS=09Vg3tʯJ9E8N')qA<MD@3l`W2mGL IK^b*ܔ\iVA=;y󳇤FkaZ ҷ~QۑipiLu"W-t]ۿPnr(%dw~D۴r ְMN9`kZ([[`^ CjHt_9EBjZJWscI_f02O@C ?uub~03`^̸ C\>qB톃`)$4=Ťlv(*wjUAPВd 4~f" 4ʣU6v#n@y.7F EmӞM^i`8dXZRy 9­hzjasU-iz>'ͿʜN9ۀV{ٽj'P &[oE(䂉%j _ |Ad,b /g tZ^4Vr*K3a#F0dSf\<܎j,UXA#kW*uyc-'Y˓Gi qgo =\w@X[жUNNeM $OiGF$*#^t`Wpm9*V?|ҥKaxU~_bv_8?z/d|VmC&;4,"80~0h 8i S6ʎ5 s+~@kh |O9UȄ*]s|YFieNo >۫5SE+_~\t|%gx0]%qGC#0X]0jq_@{#Sy@2N5־*2Y,i':/BkN'Ȼp߅ è[|C@nX)y0!poo<¿T.H; AUNQs7]]Z3hA=5쩀eQ8ñ|}23't淢p?t?q5lgy9S AEwb!st`p+WFwWsh @^5y &QUp6ͩdh>+/:D3lDE. S: \#_w{01"̽m*Wh O.a.&dho_G43>PYL9]A3w iz5F$*s{xdB-lVcy6Q;,,6. I<(l%] 2S "g9 %iB).1Up ,`A^jQ([0@\uWha4RqG6(0SSyNVsd=X8!BEW~a+tpw~8>b&Z0WPCrNhNֳ¶FдH<2;FRN|*픹>>bI|"_,IH!HSeia#%S U]X6r?I헑]w7p5U".v\PL癵7̛4RC>eK*b|0O4_ԥƔ5M|+tirc^8-_Dʭa'GRRuhbًbKTYfÔlr5LGcۇUs'!t!V{٫hעd!3~{){eh{TviޏDc ?~e >v}/!dp7߈p/|W !!qsPM#d7 W`ʇ)qA(~i|`Nn1PW; 17ĚYPҙ3D|=gt'>VY#wUD2]z2{De'0sq]_ uu. _RX]fFjGdĻƀf>2 D@h Dumāv|1"^:JKD2Xp]W8{bp@QBHh%!su ׏q8pH9ۚ0<*\╲+0K$0 n+oa0sx2&x ׁKawQ 8XxS pQ0}K{z'mLsҿX5e~xaхTrw5RM?F 6)}]w\_b0}4Hۈh?ZYEsdޤ4L/80LE@2%j k *}R(iYUnBpGٚ*GjfF"h!r#i!#- MgX/F+Uƣ"՚8]ᯊI}O,B_zxIPO-~'tOa,e>"kl dt +Qi$h S$2ܚVj̩U|qGU}{RrE>PzY !,,c& S!4y1_|ȓ`1'af҂Þ1b0~3or|s)\Jb$`o+ ͗r3`uPُ&QvsjJSn;ՂTs) LDNeN^IA` .#m?“NߴilWQ<$mVDn,R$ /:E wJIZ=Dq?;((3,s.4¬(xz *㙮CEsQp 4 N^ȁ|9H4e3#sI^s;@4Qi\75e9Sp | S܁̸.Nqw0eSWÑ"Lŧ d4M>HH|Uj I7J%>@85\3r?(R04jkq &Cv"Ԑd(H &T r*T̆Ce17!qdA\ߺYCEPӹC%[edט9F=a4j.H_Fz\04N-0"OeP(7N4pGLKpu[WG0]7IF#i*lDZ5ļ'/z_7IFaex9:RXk'qikADO( ISQ/𿦈=#0? PGi}X]W!6|gǦ25ТmRq.l_b2{醕YPgoe *n/ߘFΰ( ?@9(HJ?{w&TzU50dzh\o<4Ez`ߓUjkPAFND> j@cИί|skcE7Gyh I[>Itd;(]S,BuU8Ǯt#-zd d!)gd£ػ6Il_ź&^mj_n%PFuRdg~=(HfށmCkaȏ4QIMfv@݉(D|jikK6/]4>Ă<(wMyzyG4(jiODpxFWg^49/5˫s~ #+Nҳ- 2 Q v gЍ&/#n>0{fyiU˄#>hI_uqT'> ;I >?Ͼ{bH\A{N4>łiwl ̿n R]INS#\Į>'ҰX pXT8o.S`tm 2WsS >دq2w;a', r+̜Q׈ab˫ ù,$.1;Y dL??0#c_-@B2qplyA>FyVTWլA|9ٿsx|DmN%<4z~WV~s _?g49f sfmѵJȭqp66^?J3 7f+ ΁}}-Ȼ~"0oh;ĺ/8Oםg\2 Gq Ɯn?xxsDy+,Y9;Mbnxƴ2m\/SwQQG2{ވICc|X/8ܙLNPۀo /o޾u[f> {F}yײ8$EyWH)+bL_J#D1]9/t읞|lxI?i$/^g9gQD^>Z ! o-h_F3,\ x 7r_ m|vE̮_@VTj0y Xd:/cR*t1x6/CK/L̳nכtTZZv#Ͱ PѰ)<{P 1q/1XR2֢i~@fs-p*}XSGŪ.|Έȯ#/mɏJI͛VӬ|sxTC#5@±ް} Vdƛ80}-9=DXp$x_5=I2cqI(D- 54thsnQK<}$*Lɻ@F\y8[ | XDLh1c֋/]lm0W~\e_PP9Yq*Fd%D5| yZ>x!"_'smBq\4Fshòdb&&c_q#Z0]\xxEejĤSa {T]a$ DbA.tA<2^3>OzSET0KR>Wg?S8W7 -(_x>@f^#~ω]g L>͍޽x ŬV KxwR6?1X[ۭyBqA0 = \ NM^X.t^v"Ɲ=(nhe?z 04m< +s;Ճe'?bs?0SvyͣaIw3 1Rsxe *a 1%,7?K[*}.mG;o Jgl2WC,>u~KVN{/;0sq{mJ=-jn\Ui|6g3W'*!b Qv_pK4$ 5LyNBU‹-D<6d|xWb'UN;a'LjyUbfpóZ\Q)u_Wh)]dbF<`8WAQ}e?y?4LժF/if~1[ݚ]M:m\8~rkLJG+xN85Ec>/pcK.u`@i50鲝Qx67+a"TՓ0{%E|3Njnɷv~&'. RpUƜdФ*I3NyHP%_ϣMaB lI&S+M?'E0i7.6Rh$` Om#m`lNqFEWEPU:yg?B_c޿tcͳܟpeG֩r~Q]%~N~V}ߏJH=UKQ#b(ˋ>F鹼 @x%H<O 5H?PM/N ^ G)lD,kemHأf=R(X0-D.խ(#ȣ!3X.vU=}B|nʕ tz &0Eepqw y#]edX__N l^Sz+7cIzOOaN%;Q$ w=EJ}Ӆ  OW@GLlgQ%oS=jB7@d=D=q5}iJt^OO؉i{ AJ{f9Y߁(dg:GPஔ>N/J/8[xt[%(adk=%sC9{{q3G~">@ڏcL܇35:TrJuѹ#v'8xW:}a!%AJ?`}5%Fk/AT鑪x.0n6EwOV|%_ &z`8/^ARd^v`/1OzRd`PpM XŽK- yva͙B|-ꏣɸFyQe[Ք b0ZgNPY:#DV LIa|)8\ Nƕ`x{moзU4nF6pkzeI{U UEq~ L<2D*\klId:;9_y,ݨN"ez1@$ nPcXLk Spcޘ'W9JKUT3/!EyHR$oJ~|Oz&Ht aiTQ><|0ۇGn}>[fm]vǣj98:ຽ4`p6ꋭJ7gDŽaQwf꿟 W8{;3gu |>5k:Fp918O~>gtvM?o2WxO?49XC s v=p_~)7#~:sQ9i;ieM={ #ѻCq_ XmgX 4ՍU{U Lq3.#lcP 4UCxLJ{pg/BS j{Zp̂c Ⱦ ni>x!D-IAA}#t8+EGѯYeL (CﱵŞAs`?`ao͑WvXsZX[!*<ToL{  } qd3JAwy>٧~:/~6o~+ &{!< 8AǎFcM>=~'ayƏgv/9ۿyWWg9+;/_f_Rs3>䃟sv/Sk!i>` S?r+w@W]&' ޅ@ ?kiz}˗%!_%,ZRi"8H߻iEmU-@y< ǪN5Zf|ojm]hZ+C1r*Ӂ`W>ߔojrBCc6 pǰ(ބM<I܅p'xXL/Wwȷ>*/>1%`O?~lj5(wk n*ޔ'K+^XB9f{<\lmm}Fo`7N{ß$ se-^tb׮_쿂_~ CH+3 Ȧ*>{bu|D ޑ.0U7 ,MvJ%S^ӥwE&LVtCW W9q#]aŀ \N@ Uy=H>P焫v080g?% Aq;[|pxv]Č_&-W뻈GF -kyn/%R>^H??`D $)5Hk,]pPa"ªo h~op7|Ivp jUOA!j\a1v.Y"#,#%F^%h&2iI~~ W3 N2CSvgN&L_Cm]Cّ/qw9>xs|/=*>uiw&c~?#?>$]k< ƽb!慧ŭ@ knߋ7oGC E<~qbm*ҥsUK̙AN{=/n6bFyd7^5ceG<@O4HItzd;ǣO5<|Xq2rP1'zzo߭7F/U{d҈sZĂxv>s0]E|/&B \fLd XCw D@ϐ,)[,&>w1d߉.mً mT!bh.2QMAp9u*>̑spO7ΒM0%S ǣ8tD:3sR:w@i onpt҃Oڳ;D\^Wto w&O8pkC]?2޻wg_S `HXZYp*,5syL?O|_E=xJ5+*xR+y/}G<뱢 +K[-mr2PnMšoGówKܑe|{~|6/^H_[_C3> ~k?X>7I橌$'Ͼop)'%HI&UK^r Pp>`ooBj@{bյ񮷸^dEl / :_  s|b#:9"2ȿBOgb!އ"7ǟFԓ;[x'Ӂ[ ,Ĝ̝T\wލx@BOfÜR|e)f(UQ/iY|{:E?VYvl}s܏9ֲg[`n?,գr\?-~wDKKLv2݅o SsخwT] (_v.'D>fM#N-!OY/G!d_:0>~M{û=,voCn~پL6-@|?! KFi{o~O{LS#_bgMd񄢫 sZ(/Q!m (AV `u'= :K@y 8mu?0G_>mww+ `4VgK?ソ??Rs}@Q NWmc~; Dg>_Ł&(ָh{˙9WHbbd3JٲsWdC + onnUKH'HMǟ}/'̶Xm~0Уu_pih\0lk]_[9by&6gWKN1W{Cw~^yp3~::.>ug[Sxv~.xupmᾌKSkkGW8w>_t̜QWI^ԒCs~扌tψSRڞ-˟߽϶统~Vֻs7֗:ڽl `_[o |kN O痘!b3n/Ip}FK'2Y4>1;#QAe.gd{C쑿z&'f_nܸ~7̾J//7_(e"U@ ,oos#h0;k{k2p}; k3ڣ}7F ~_x3xk j/kfiO2O Y)}k8i7c'~#HXc,[ǟ!?㻷wXK|&h ;Rͯ7C.jv8<¶8JzAppicWΓ~޷0dW~&Q_tf~)@\dD&ͬ;=ffc**=r-fx G|$a acUyt ui)ڄR2.[`ej w , [@i{Q6嵘gx@3d3זm}ѯ<:y.݀\]4a㗐2,ڽضa2bfzv<3gxžE |+OhگȨ77v|7mõ-mtwٿJЕL{Y% u)'2g_FaYfQ;_M_Ǎ<1JX+Fb]iuK %̭ѭYt'$_BFbm:M3|ۦ<̧uk:}Ef#h;ffX=mnVOP;?/7'IӖQ5ShukN?-] GץtT35qJ0 [4'qg3 4N2Ziֿ/j`[ fa_dy5j׿;5boi|ݦ93gx13St\Bw|ۋpu>'5\2_`E0/K M7%0oZMyF'SyfExpݿru0!ehڢvQXt9PٴݷN-ًdسŸKJ5AO/i!ϓa3 |zom7+mؗIENDB`ic09PNG  IHDRx AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  @IDATxIdIuSܸ9DUu ʞ-P JAPՠ@R?w\tj!Zq#J$ L6X]sN1yq̬!;3#2=ng}g1sp3; |%0lW>R >RC%I`'vI`'R Ym;wI`'vpM>vwI`'vx!%|!m!nn]|[z'vI`'OJ '%]; $N; |M>AI`'vI~'.ߝ~Q t/n'_Lnj)<6v+xw; $N; <MzqN; $G*#|'vI`'S^v\v:1.?m`m~6^g%!Bv+Bx; $N; M^ԚN; $n!KN; $*;?=f]>oxٔ?>#n#|'vI`'S Y/;vI`'vH%wI`'>pEFhusxQ ,[]ޏ;y?>G!s'ϓ`HkGNFNF_II}7|-إ}n$|U:ONv$Nz &lWf&_Vp KI'[/?; ${y߭0YKt3E 0qM5 [&ڟYӱ5Gw2 QUZ|Ȫ%IKR䟞 H}+n~FIlisi_h//@'ן;u3+\|Pnutky^\qO6_o ٦8 ?l?l؀{sw+[O~6vۈ?_MOo#n;m}D<>xokdzh!w_Do2`}zrg.?z,?wW?:@O wV{{|D`ښk g~]泙cfp1:?LoGt7t K9]c"{?<6>;<|`18h_ ~)`X=^2͇cfKr8aZ Vc6跜|^׋[o;NXX`5}\H r W jjZ1ڤ%x[81~F|ɷR`7 v٬I=d?"QҪW3f5@!++# e6ΙВ 5~̒OR V`82}-Wj"9P>jW˰)yDӠ:{ַzjl'+&.#R9Tz,#Fd5Eu5=d8.:Es0DWU&ڦT:.hgJ꥔%[me)CRiKښr0wk,&6+ e1p-TwOqQQ=[[(qQK!~)S[z!@T$`(jܮ~\\G=o|7CFzY~v>2`|yyyyMݑ:hVâxBLǃXq`-=`jC^;u/´".L[f GL$ 0|5B?M j,i'ѺS+RIp!?M-?''Aɣ^IAj3n*?- f/oF IjF.h<.u"< Y[&i;yiu{>\l옰dVwNuJlY4DXnɠ8uK Y%#FKq=[w ff|'ѕE[cpx#U~)K:"NV *_2Ll98z?ܿUYAwI3=pYr6ƽy۾Gtݿxt-l>h/ۛNuExgwI:h]0Xҁ, ,@ g,Qxy7 Ks=vֵ BV_ږѹ eiv49d0)qHn)#%lJ"ӆL( tIR~Bgzp1g"L' $Y[[t}/LpޤZ&ߖ:t!'6񹣕T;~~cLv{a'=ak:uCGDYoݡ;8=}}05?3_K/Ym[T+Gʫ~Zym > .4+=+%]G-ۚ[?Q+fm+;|٧WHK{iICjS"&K'p~tu{~:<;]^];O?{oQ[œ/f~7KỜ?w...~; 㛛%q o%_sbw&؂2w3㋳n`g>`ؙ<=mګ%5 W 1^pf=͘%g&t̳ `?gBgkvmwMѼ}ufV;6ͯ @*Bt-"ۀ 0X\5ZQ$֎I_0K8m0[ԒzrTB:_{ijfz:!߂%eْ:B*7V BK[l4mMn,[v&WTml$WbQVE $n |!UI^-CƻWiiׁk$>*ح /"'DYq)YPxo6>'/Y"μW~ zPRVds?iq aMY 雖wوP+6L7:6f4W~\t6;fkx;`;l^p /գ6p:$3;8;;;L&"܅jks_ԴBن'Wi(z7Owoh4e5MÁ_pz k5au;=[c^G[o.m(<9|7+:>_7=&Hu@);;$dqe7bD>Y5h-;D9l{Ϝf0ˊA+Mu[¯F>db@7CWȲv">G+5a,op {.li'KcB@7-"Qז?{&*I ȷwOG~v⦉+:RnDk3:UU_X!E uW[p+ℿx7+ɧ7IaG,S(qۅvjWt4˾o9а Ф)KX+@F.YOhIO6 JRf~$Xoedd݌98bqa 2I _dRJ}@WXbD&i\q0=T45)s$$C/b<aU5CW EnlTzOɣ֎.vux\t9ce2챧Xd6p}|8|'cpY|f''''#N|_DSOk:2(l5=}Rqmih(s:m $Ys }K uœhpp΁՘ÅL8L"yj8{1Nx:6+֧v wl?~<w>|W_YW,8 4nzخ_MAHuL-2ECiP{4,nnS&+rG=? n-C}Ȁ , 3y+d<Ӆ>Ndry8illށ BIk_nGˡW" ~I`p)6)lLy+沆/@YDRxluv3a6p{yЅ-aL ]ac> e'NyeFC6-ה2_ ` Q72&⶯r)dBUϙzSm:FLC5aȿRϻLX-yY`>\\̏ǽrvꃧlph58k oTO7ވwp;>|𵇓tjέOc/8|-\ *-*iR8`p q =xk\;"OyDͽ 7Gyky@]PN~vb}cKLaGui|YFz?L뷢/k>8<?7\n'! m`pU]5}.sYa 1R{?K?| Lnߺhψ m# 7t7eՉi!LnW}-v=}ël/lsiNCa/Q =~ƙ!-n>y}*f4Ζ3C0:t\FJ%G:%-ЀR7>vLgή%ˉۇ8 ON^:\.@qИ޾\~\;[cKmИĞG-flw{wOwkfozųaB- KwsNo"nvgjᕧXw2gLP/xoϺ xȂGyu~rwxO[ Y##b/5_Sfp0I[MYa1o1zUG P Q&`8KSY8A~ouCS搹+3֙-'nQh)JyŹ ݝPLXF?zr3x;B;x)+? 1=OR=eݦ\_s/ r(oǷkvӕW&m]w`~!^g]|*W/^nxfSU<;)xԗnUyPQߦ>>?1>N{xx1F\Q>7CJx_Zl" r9;0=3?%`6wSv\0}g;G?Z\yVF>E6fpC*KDL03޲ըDc(S w&*&VMC:7qj 5lL|˙S^s0qAerGL{anN$r?c2pn9h6DŽiȼ2d )μ ix\;T*ſ ;8 Ep +nbBwc ,h)s\`WيG:m"<3'gX{B*"WCb &&l0#x&IKóܘx8"&e92Z}^o-2Qi͠E7*++A{=n'ˊQςe'BM!ws)D.o>?Fr 4=կY^1?UNsdsrj³Q[93bzT*]-zJ!Ky#0 Oo"?]mNxZnaάߪ3Uަ8M~cB;!M~3wF>~>GO^QUn8S&7U+u='g/Ce@~t 舓;@gnhK>RZuU;e^EyJ @8@|V&^`01egXW^x`4hz8op} ;~ߘ޽~?';?Ώ~s_{r.yf[E5wыHZumx}T)_5ЖSrSf<{QPEddA=i?i#F ];9`yL7Ѡ\:zw1hȘ[ʆKU q{܁1ҸEns+xR1=Q.9A7e(0%qG(Gq6TVQ '?NRš=lHBŝ|Z$M)o/Q#Nfay0Ϛ.:ZЏq6Y^]5P}I撿=WʊLc L,hkS_?Q^# }I_=F>:(o.r7Nn%((okSۜ8Wn7n9܌*YG#O1sXEѺo3v\N7o1?ǜ0Z&mĕ}nᑾRgq󴺽(iH#nWكb~\>O/v΋o5~ʒ慝8s2Eރ2ӧG޽|ݓX\掗9W޸GEIգgk&Eю-˝3P?Uvr0;@d;={Π2s=Cg[WG}peS eea^0(iǃ15pPn;cOSO374I48=}tk[t{PtSm&ξYͲ),5X^w([ ,n)ʌdxhzxѬO}<:C+2YyO0C^?W?o|nv- =^De0=FnGGXwr(xL8y$1uX,C%cO'7vUk(!HRgB:&Ds(Н؎`Cw( OSW:E_ϏL0AveaRLX 8}<{0{ef|v31V& |kctD{.ik{YY[jy#[Feƴ]ЕؒߚMt>aMwH{.je۵/ѹSyL]ßmwB}OMO275s&3V<X ),b熂 ) [^,y7+vëheϔ\x~]޹k.GJm,QjVk~>r`<]o~s2b˼g/ٯ/{<^)zj;Êa1g@xQRɇmvWtwφuIqZw#-Ⱥgyw,}\r=`|} ŸpLƔ@ZJ s0/2#3ijY6pN!& oqZt78UO1ݩx!Z^wFy[uՇeBNON= B*Njz8:ZGLtӞd0}&JxW69W8~#㻃RQ#8vã;<1xc'RV4ᯯj Δ|` U &ب0VoPs=wc'+&ى\#vi(Km &? >= bG GI+ڨ[&0vrj;)0|=:Xq4tzgyU˿*ze `&f 7zJ֝;/6{Ľh2QSV21yrG\W2nymK#EKx|h[QbDž\\$MkR c G8" pR<3_mS8~q3;̃1(_vNYvY^ht( r[OX3݆|d .;yhIsR) K~0xvr4lY0*SM.i_ţ(3 |MKܢ 7J.exQ^'-}ofC==:'?JUjuKѭk1UZ)ʕ.kODW2*'܄Z&S\mev՝<1STųnfU6nF~|?'Ka+t8m;~K̔"3O\/|?b!8 ϶&m/&7mū>hLڔq<8H"jVj$rJy9:A+rɤ ܭr/ˋep ySDKy <^UA v߯j$X^N9Ǚr}Q'3ѦsC" $v2 0ۤ>,gryg G&ӗ]솕=e_nB x ޏO00 yd|v>Ow2~_MΞgQ.n*Z}YE pw-gk".w%,l8e׭"ٸ[~1/YE/kguƴeɌI|2 pP?xh,i`s:4vyM÷e c: OribK~%^gEa[҅*y҉^X*x<@tS<3MW voR[ $byQT\Y>c b[GbFȈg!71\)/x)s yCvny@u>z=S6ˡ$|NroYX&GO:RO(?#A)߁S/?1V2#tLa\S> J 2ۗ51 Fc;xwH3Q|ɷőӯ|o,[`臷:|bcL|!7Aۏ^~~%ۃˋ'O^0퍬LcMXkS5(Fź`tp_-3h 0ѷx5USҙb׸˝s3haH `Z1'n>U5n4]5RM%_=/qmylNOЬކm ۡ%xĄ-:3gَuVt~Ӄ' 9+o-Okygn|f^آp|(bb8R (vx)^~DZ:nIKzCl,L@eޅÓ7NT3;Ŕ!bO/> ]eYx^e˜lN7CRvL o<4'U-7Qn),7;Is;S^tD{NXI:!O]w^HWGv1c>ˋYWF,G~җE~вN'a~Qu(Bdi>՗>%GymSʯ/wV};ިR߿G#-*woca_=sxLo&ʌ\H2Mh#OzwcW܆L;YR[+˹OݦlE窓xuٿ?=Y<ѭul뗘ν5}?9K~קʾ1tgtN|k|߅2ao !0{@8õG7Їq+f;%-h44;%*z윒 (lW]sH]:OR7wC%XGDjɒW*_\Aބ;O9Nr#x#,/i=[9X+;L˝z+AHlyZyjPL(NDΖ:ALF99:&-NX thkߕҗRMOӖ.DŽD{`},|_W^1,SJ(SO:@j+HNVΓfwiԃ8/'f (3}SDҏQK'(9砥%L<=[yˠQ&l AlY7\AdBABOϬ\?rϛVז,VG|%_\v[o)+tUS^]P&ؤa8_ܑ \rV+e@a}ߌxC 2g]0S0|B˃u+!e8NPs>roƄ_\tPSb|~&{Ah1uz\'QQLjs[B;mYЋꈭ$O߈ۚs8QT ke+yZ.Gz:qQe:L yGItf@1*: 0pҸ ;razglG`ٜpǒ-;mEAVAIj;Y^gNY1 +/Q#୸ˎ-/L,QOfh/ʕ DBUR޹i)'Wn4gAi8RȺLHhZ>Rl`9]n5Y78U~\Ynr壁T?4˟&PP,K]ι@Vm k@XYCgyjy &ZyDEߪ 2wGc+;t6<)>)>,V5M4úw(r_#}[?~-܉YU2US!RVj ˯ G ХeI@_ S*|V3 (>b{%{鷼e\`sv:gj/:e#cb{9W/۠Y2K^(;?Ʊxil੘@|,`Xr- eCڸ8{IJ/j[b#In4A'; _v&& -.*^Z<cS@9M%76Ur5^GJ:V(7tFWR/agG1X:vB@ 8xhD҉ܻC3 ;?ⱄ3J% ߾\!|F"Z5}p0O\:)І OpQl۷,/1 e,h_K^#|%) U2vQǃv/ yv'5&R.o =:vzTʛA Ⅱj[^䥴r6;ësh\+dY"Iq q_YcK`nlbWp<7vHPig=^d*Xcϩ Sߨ-6[IJcP<6K8m8prbMx5V/}᯲)>VUG?,SgiWWYBp){=IAe^샎2i[,~hQ85Y- ' X#eV KXÓƽz^~:H^) ?^yht:ut|}>W 鋜s8`KNLI[\%0hO ,-85:IIJ\mڡ>3Ti7?+s6Vfk& siS: hT6cC2ƣX l9P/lOJIMJ;Dl+mɿf~J~$E Eo̫5ބrA:W m*?1ϟV|%W&L*F0@,98xعKmp[Mc|Xn +f-} .@IDAT(pWz+eԎm-܋g~{7h)!yF]Z8k2I'&kp6[8+ s`AHm%  1ɯ<_sgM`2r4{?He0U`O}~V%/e,љ>m#yRYnQE?g${~&x{ܰphV{9۠ 8'u*?cpϡ%[0Ls1=s|Df?۴{tz忊^\'Tq9N{ݸETzx~vE(M)E-HE -MaImKZMH i!t tI_\j.cZ@[NW]$zI#XK\>3 v%LLKI%(8c\Virq^zj 9Qd!.?9:HT[Ddh>*[hYtLJb \ݎ%b:łW~H nPӛgFV:lw;bL:'Nr`x>eOKIyT- ƕxf5F8W ]Bvd?'8!ǒePp^rj3,•I~aYq̅p𱉃xMhYe:FṖA6çjGP\be~_` o#&2y.D=cuk9}N+oru+~)ZA,,MO˫Kzf?'+܁է.3eƞ6Y5䷅'$?a-nEf6AVUkNdE_>ϬG"= Og0Wi%vm pS_14|pg0c> 0|bosU5vU֛WFWӃC&̯@I[xK̜AW-vM-~teY[y H5qJ_o<&Ní$]ކLfe^[\k}jtb:2hg1 G]@ghzi0M6rIFtU$ӖJ ]p ؆"Lɮ8ōbRK*w[ʵ_=|}7>xd-x s>@7ڲղ|5ܛYe؟LN??a[h}W?Q2hUgYQw [2y77mPtQ<mkV:e[pq 23s> eKM~~:i*|1u˯6Xn"! j{ d5h˧N B+~@;ښ t=rJ[>X }yp1/mx>;oxVvîU8.M+/)QQ~Zmj%<;U;K<̞ocp0oK<SɊy7i{s$Q) IugmQiZ񯧯-]= -]xKé=4hv閤#͖߾Zx7b#~ct\rZIZoEBQG,9%]H~pˣVpsY"xe5(!NW٘<;j́v V;^>7_\+<7ֹ'c> ~?>wѽ;~ygǼ}iJWtʥ 84(,8*I jH1BA:H].WƁi;ӌ=sV7s, ymB79DTȫYE3# #D ~@Z7BǗ+[K2L􂺼7hG?vov  mDm⹉v<[WS ,{U@6Id?9ӠW#n4嫬g=M\L-rҜ bˉD 1JpWW~-z Le҂ڞ@Ue&-<5n,85*r⯔iZ~YHPRq~'Lt{ zO~{ w*?5K$ѐ$ , iO_!;6CDmkحK.qewK 3}da;g Q<Ҏpc'-o=1mǣ˼ϯ> @>}Z䍋~7̜)-_ҵ^|O}zvqϧƧ71Ϯy[_\=/Ot1<]GONog9\>mTjM'Rvd7=I^| }AG-x=_SܝL(<\6z3~׾чw[h׫[:PpX| ߽|AJ|$bOKʈvhƘ#x^VSn@0zX'VU8]V_PD"!4~<;}>Z#bti bAwvHkS;O󛯲juUӚq9&wj &0Z4& M*gX'P'Oca&7)t6V^3iQ5vt,ӾtU@VEz'qXT3+IBe Z!w%-3e Dm푶/&!hFȺ||Ǫ'f@A풟?:z I}|dYWWuPǨ~i/uS;8Zd׶!UW=-=3yTIӴf/`b=@CXS }MV6X^.eCYVk=\8k3fO&G*':MO{l>g{77­8HbTH嶦6:j F "u*t䝳lH34QH]:zWIM_G{ У}5٩ds<H#Ֆ`9^ӇCJKe4G/QՋ,A2ˣs̕w ӆf[9D 5mKK?pid#d'r|I3Z8f~W6Va&_׆)HOeQVrTRM!^DTQ~TREѤo/H]*u*CE)*bQ4)sMӷz}[Qh0聓[?nx.瑮6#& (l^^[09[6;^mpiiF9Ikz1Әrp^r`SÜo V +ݻNf&ꙫ[X&۝+?tg=-ܽفӭ} {vyb4}֓oIb׏?>7bpoΩ:5O٦*ն̹:hl3{/:ղ52Q{0A c@,3`HIW[! >L|3uq38{ʻN6W׼'B/ 7;ent Ru v@eϱ+ kuL(H|hav1蒼:_M~"kB݇@:(#]f4&@G?mBdɿ]F]Max "0X D˻qmzX Sʧc G&-ȣ1%GߒxIz+$P,iVmeXMQ+ozU 4gR߭X^l{Z0GY`?x߄7m;V 0Uց! -OR"`+)wW])zDd-?0JZM+ ߼Eݘ)SN[yͿoдlZ[&c`N|L}2'4[*ͅoqxkzOÓ{xKu_ſWbI$`[1Oؼ~T귐h4P'9In {Eq}s,7j']54[*P;Q(ߧ/8N6O .izE{Z4(+w[5qSeMp5KƱꐙW#; T~ Ħ{`uOڶ%ɷYx1_R& qr\O3c-QjDt'*p͢,Dy+ +y&^jbލ¼09W%>7Ǐ&TݲND!7zGrRl⛏寥+>D~ݤDa˷5:TLז ]>-2ʕ,·p15ѣ_$|=N -Ғ|H[´A`#k7$oX|~d i)FD&PV o=: _g"%H4:l04 B{5݆5}iVb;߼B_ 0Lst-1k4?؀KgARv&oh@k3.|rt$PZe[^5&OE@7iU`!|Fӓ $hIZ"ud2#Gw1Z!d.BP3 Фl҇.4t-; {:Jdj^Lx>-姱vN6;*"u-fjiǖ}!;{GxL0 oSK ( hLS7.v3~nr+?0IfK:J!wԽH/EOGfɯ`bj}{mѕ%h5AG;Zdڂۈ⵽aW]5zӾiIQ$%6:jiF j&A:HNq~ESeQǩ9^~N]2IIƅc\GM),Mѝjvw`v=n;o.<}\cCַ _On?y{Q!=E *1mV9r f&5jęJ36Θ kCn;rWRӞP<|w46[Cd,)T 1hOw//OMf*{<8w8^1!zy6/u;FX֟ZVȒ]cp(˺n yjU#f[+<($,ƴ5Џ#h%O=)]i&_I]".~3T0 tU$hz[2sbmЕ̃DQ MumbJt,je1lK`R6,#BR#C ld~<4ڏ%Ff=vr5؀كH$Xw"K.c \de24 G~ւ Ӵf6Cɷ KDt[o Qs] ~]~L$f%ZHWn&?(@7Uw'A+fK#*6`c=&|{T hOs#XEcZ.-)d+ﱗubf؂-xEĜ\]`n#20B xϟ}LV@Zn`9 <r˃^>yƲwj/~so DSM|5*Q1ޛ3|yqɇ7䪢8yZ1U (Ԛv4> cGIYk5jl\I[\—/Rp-Ȕ0 e`sMCgA)n}}t|l|N[ר/RC2$^5))ʕzD]Z']vtK3sCmR,ʤD zЭ 0 ݃/"b4ck`l= pn5M]Uw~+ڙUErg)2E|8NJuZ 8D}r7@WLdBi~CA-ߊd?QG8/f3gyս&amJ IqGv隙2wUF go©B%<;v*g˶Ku(Siү`c8Hِ_{&ԳBt3ytHBáVa7A6sY70AmS'0X/&ك F'U CH3όˡz;#S Z3+E5E< m۱^_=yY"FWʇ#6v7/N袗~;RwĪX(C=j+]>/ҞQ9{7ݥsH =V4qҟ &eI\ ZV7h'KdOua:}=)bEX! @Aŧ,$ \zCB!/Шr)/+O`!kdԭ~^)֏z&ӇۛCD<_wz=#}/`PQ'J\:}>%Vp<7쉳y2P[Jqhv8r->TQZb ;xC?M~M$;Rv^(S3CafPˌByfmǶiBcEU!}籮0 C&Uu nԬvpF f1^? 9?qu_DFm@ۢ f1(| c=[&Cv^U)姨qbΈipI/^tJxY t(b >َ Fw1 yM2Bć:X 5*Ź}XM^^/gQ&(V:NH6M{`m,y*0g@Rz`~ Q.y|]5׾ҙ]ioaW%Hlh^%#8 xc `R^ģxx3`cDNig˾P{X| u {JnZv|nT~s}ċn2}/d(Fq彰NQ֥xTO,͗J4$o R"P3D"|ce[dC(ŝq J>xH2DH}t Χ۶ݝQ10^U4V)M">ǂ5nt~,>=./Ѷ_ī+}DH4ҿDo::нߟ?\}{N.tC+P@p;k@PPTT``&P jNCС==eJA1}X70PFHi$E#a$#  f\P ^恥鳻\߁Y ksW59ҭN&;łѸmU[^ t$.&V"[{mBxVꞘCO"Q|Ê_ؚyDb~vbeKe岋3w2Z Jtf.sa پ?GfavOu(8xA_{lOz|~~bw?ʼn%Ζn`;8B*BT6Bs|H7SxOxB^(;~39<Ј=\q4N.eV!qRu/7VQ^0[ ~6he0RZD?fOrU Vzu}'/?|]z}|SuLMje5Nj?*1,zõ;9Vzy.PRgo0I} iI/x͑^|z.?@}\[oƷ\Qmr I_kzLMHaV M8-iԈEYyXW(;8 %0PSضQv/p^UH\ʋ[6:8Qlx?"TYG/0 y23ƿ&G"x h輻yBucwI-VbRy k=Lu[!{]W#%Z4R[@!ddRL䲴B-Q  b>u=t}rƘJ1y1Ķl );^U:i a8%P(H?HඖvG3m`H.6*ex.iajXx焐i b:l/ [ԍǗ*N*TPeu(0XYm-mZuB >:*298Q") 0:Ac`x9|qBDD4C?~oh| = ~N쎌oЧ07p fՅLxL*"zKQJTIkpTPSyQ>vf\V(Y@&D8֏=t[PÌlv},6y1b|E! d 9(|*Q.H4t} g'Udݝ^"s4 ~n9jf~p/W!483_$J^»B.|9PO]Su4fP־Qzz%"]gX0zWy>B ѫ`$V .km6/| 5͗0 ^P¯X 3\G/)QGmˊZf.ˢ Ɔ:"r< #[L's FUڵCya4sh!G_Pbh+=B7_-e1!f{FJuB܎Їn`d@_R1h!ǺR_>t/Ti7@Z/!x!ミB/n|uqup%Wowzwlpqqt_}3awJ/9S}YEt+YrӑD"UrWh÷}M Й5;u zC 84JQu"XbS(`/҅/!l9tGZL5g$:9?| M6CBIlfGLW>]&ݖV!ozTh|lOh>Ɵ)mG}n X&݁#/3)Olٟ?$nˋW<;yu }H -U;hO/W UZVtѧ:.Pi = 9oA Y3(w'z'VG7WC~k_dw`wKKY^>!XKY܍xf@ -ht=֏qY+7.rZU,&F`iqDm5NYvadx"h YHwV"K[[m,GDME;%07$QWeO:Dh ׺@IDAT,:=b䝷 P(7CՄX\sjQ>4I1OzK(/˹tꒌQ0ׇ7FJ\0,BFyܟ !X0NEHMh;{uR4D(o.r%jSV`0GV j} &">ڿE_ȩ[|% gڔ"1ˎl*7AܟU C_qy WSv׸%\^r{1Yc; P];r-b=\ ?Ӌ,~|tw{]v)O/??>>Ҫ2J/yo蝦\*Åb0 ])*Nz~ˊWiָbh-.B6@#KGɤݒH^VUJ<%Axntrwp֗FN^sBOoɄ| @uW>8 %l# W՟cFjks#y'¤6L6JO4E"P:2ۄФuG:P8R0:|yB}zz@~7]+Hg &\24:5:UPC@xTCp ]"OOTPvl0`lLۏKxd1.6-yTB ?md= $_eti/EqC۲hNzn/-TV AB=Е ʼ, aJ8p/|yz4OmƞIg Q,(C=>c[|FCB0< {?ZHx]t@\|fq=FEN$cNxQ0)Mv\"ȷ1 Eg|wp0#X@+~*if:UL +u۹ݵ^Į.Djpw J+sid |z|E{?҇?-)\~Ja)XnaՍ +83>BZ:n\/]Z،5 .p?hUND7K[Jg,ə?!K xtI &V*YXk07tm3m8T3i *KC*m+mB̔ר ! $ADJTv{.(FLY Ol,Ḡ@ 6M} ǐ ,r3gu2o_5jK&sUHcqɩV\.'}?U.XCeQG#vӞ @dJQu~2NtVE|Cu mle5|GÊj5R/΁:p≼`{/TEiOܓ=uˆ fT 7KXP N]ŵ5jKd{6^7^^|?etD}Uqݺ x\o`!}0|E[3sEa`2#1`XS#UV-_/Ou\ϒ~x~à eɻsl*cH?^F[iA0w >{ǂ^ieW> (RV<Lo$OjEXJf1(1:4|ɉe]ʗbK`E6oLYZG?WR ?Q̖=1o$ 婃 p28kG^#J0R\a$ _vՖi*"g@$T^C?q&"c{ںZynt.Tn{5`XA2:Ǝ b;Ѝ?Z@y.2 &SlW:=p]~Z_>mC d[TnC(DgtwJ?K{(~D/U:N{5C(hV^:D_I# L+Ԡ7)u2A.}:fda _Ew؈APIqi vE QG>Vyc|cgnȈff/#liKV_7e9f>[@/urSsWj9N5i⿼|1D;:㓓D)HkYy1 =M`ٍ+*;.̅y܅f$R* V"bI ~J@7 FX =q Ӗ NjCLIxaırK3ϰK\T,5Dqm!*{Rh5ؐ @8\Vt hJT(:2luRmbǨ`_@G&5p%= #X,>"Iu("N.%:Rq$І>s-Iu&`B7t`/Z8b#հP5@ 5v[Dp*_sZaO|gnጱvȪVYcuU3`;b0:[ `DZ}fAQGnpPi*Ic_Jʬ[SkpF0pan]/#}9i+9X=y1~`tO]_689J_R'q в.svuC4GGWwחW>֙%=I_uq6B&: x(UCpQAϫY + Q߱[l+J?xZ ፖ9K ~_xj`_0y<]d-?ۿADQ?a/HZ{B6d%0Gn](B'W[EBƊoC~5~V&)H:x3tG3c T~Mm틉8 ~Zy}([ -PWFG00Ǎ=SEŇ"V -LW!E{$Î~5{4;9+ VJ) SP 5*N! Cb. g!AxL/da{vyur]ʕ!@/)O[I)bShW۸+.Pqі{ fL?ܒG!Z#K1C'\*)0c&@gӅI3+]6ea* ˾#}Lrci鄄~#S]=Wq%^[O{gL64x\_zv<8:;9=t'''7Oxd СYx9{ mt6,VlTZejW3ɿ>>Y2q])VʙV1B+M//ѠM4D^ ,AA3fhxWCG$^-@91cCAA `ozʊl=~G|ҁ=1/bOЁz&L]۔/q"ez35UbYe "58rƙ@;bnA CvE_w0Vk%VC4)M_e\)i"H7E1ՏtD0WZaR?vV8ixj/XmmwC$gERXkf߼:D*ÛJIH&~S[r8VR֯I3xRw]l_@7=p={v{B x1rZ.1|`IX h]59qOkbi@ Mc<&eFp[(ޫeݩhx/F1S}(zaIG Ċ_ymȴ9s3`u ; + U=EM}u`4g&[.cDGrk8V^;c3o߾G)Op|OvJnu?pzⷐA?]"*'fTj˵~ .O'h{@;mߏbJм:lIx׸Cl.@gF; Pٟ/}[2D|B 0vE#c Dډ!?~TaQ2[ʷO,"qd [VX]>{aBrOB֌no0i+o1+Ȥ(.o L:):(s%ޫźFRTx5s>4T_/W=1|mJx/`S3Xvssp_ o,<]5]㦫)K] ~ȹ?Eue\{dYxUbKh1FFc@*OQ~8 `o}4eȧ%u>=:ӸOG۫'_;,~51N IG [9O(0J!߁tVl=:d"%\J-;=< ;*Ƃ~+Z٨QFz0R1y_< \ ;Hh{ r,ނ6kۮ$DjXf@1;->(qseo" Dfq@=;* e5U6{z=2X{AY1k1K2W #Die#Fha݅S??4eǪ8ư!EFYP}Fl,nOJG}؇x;Є|֡kEjC{ WPyG ``s0]tBs-_)R=yև us}_Chn}gS| GY2Dև}ߔBZy/PlUl/ƗTr:\fy^~x>M< Cp\.<;9U `! k]C@/??O_xA.d6D\ 3qmtT,m8~׬s 1a!ncoB ltבuaZ+hQzkyI|0La59< CgE(bHz]$`OXT).,?)Ƣ ȉ +x'q)$)_ NZB-3jYPf/ 1 1Uz6xZД0 eu `K<WQ un[b)ːхjp#+\6Gպ&P1HߙvlNS)QicѪ}BgӰk]_;>;OO3:|^:d!(MӈTq麊h2]xPZ: 㾠YW1t\ðIjqrcд= >c9xz8k~Şx{t݈@^-}ƦITV,@z۴w?%Hqe\5d! K7NS-%)دrׁ2 q=U`Ap-=n "K͎Z]cS:*^?ٵ:-ָ0kaFErKD5E t,Shy v+Hlq )]"G8gg%{0|[Wl ˤ* TP'2v L! z}6ŮTd qMj`o7uga(%MaU{)͋6P-rI,˄i)SO S.!SLۆB+VT̈J'}O ZdldYQ_p| eCg>Uڊx#YAc0"x%ʼqӰp^᪯ .M+ۛCc]:}83>gPA}27^w*O8{xuNr*3Xn8 p6^=i7-3Y&e`RtT{kgbHl7=,:Y#_~1N:!|j c4Sz>+hpfw~\>|v{ˁu-/;&oJ[>؃>@>{9ytZJ<.Nӎuͪ:P33.طR\[*^nKó1GUL,aÕ35_L|6J$ɪoOpJj?ΘԠqT~ʄuCr;&T^[!L_Zb_kbdR,u(Mb_eS%ƲRvSWUn&[P 1.~(t( B{Cg^RuB߭*Z'5`jeκa$¤K 7MroZawD8zB' ~J IfhkL"O| oAkͪߔyy&̺Np`}d]j' m{./|+C?aGi `ͤC):֩uMﶊP'}03g:ex<:dG<ۀ+d<'҆;D Ž@q.b-46;QlCP%NXTjRpgޯ59rEɺN|$gtZ3̮5]⓯fC}/py9:;])d%2_{VA:i"Ui>=UF(_{oAJ(UgJ| oRſj%&omp&B5ҎTLK/}ã$FDpQma1m=|\s9`?_?kAքNY mYb)0E^|rQΙcVrpQ7u0vS-IwP8_qh7ᛃj E. P!m"-qe5 B͔秒(/g=q8c`zYhNM*vׄ!nx}2gmֵf¬C^ !I)|'ef|HoU@PniΌ5bt'}vm(???//>rkO2F=cݠiTGzME?fg>EΔmǔeiVYIfVx,k 169C1x¾9AV{,٘{Ē3 %,+ڑ>~pn$~HrnnчmPNR.52G4ؒC'P+J%^6 NPx,ŸP{5ƾۜ9FDZ:)&f:΢S.ҢAOpeA &M :WD6)_3JzP!عs6v%QTGBO:E#0Go*)Ie5#()LƎ dWo6g'om~'?WX½İ`T3T>eh1I+_%aA;N4 ^5>ol8'Ƙ\c6zٻ)uӬ~R.^Hw|dcMY*^j5v#f|փZy^ܳVD˸Sc'n~pp҉Fp3 No'!B=!;Hok]PQlW |TmUή )cQe ~v0]TIӒw4:Vae[~n;fcx@L+':TX2iz͛AEH#}x`g1p~J +OW%Eڃ p$ " 5KΊ8>ɨ\K':)Y:@p64өX!%;5]qYt~\ 8 ]?uֶX#ψb؞> Ze)? a+2_jw*Q\Ƿaʾ)Rdć7DZ`10ͦ"k!FP.jZAbGO㸷pŸq:zyl??J`CҎjVS.=+ˁ s{2+  e>1Y𘎅9vCTl<7T0l[lIcӇ .|W>}bwDޞP^˷O?\Բg7ɿ {^4$hǒ#*Հj.5pEyUV‹ Fp?lO |͖|ĢbZ%ǴdALo#;D,wUնޔT]1agO5(T@].h1-9)Ƽ@5KT K>R'+O?F[v5[.2cRn)P٭cavmʷa'yܹtX]=JOYw+EfΠ)&pǾ YNaaӅt lDK[M:X^rt9`Kڨ涃N>y"zu?>ѻ5<6E_ӳ!BIbȊٿ4t+nݴ,<1q὘h:|&Nɒ"ʍ7ښ"<awTBON#2;,4}}dMi6O[ pܙh`Z?B)yؽ#AB9< 4Q0bY0)nd78MЗ;i%!ݾor!Qh-^EC}xMo{мVU:.%LKb|@X!7LY0W??`[K LVKd'b@ E 7~81o%WcXs{#it[eLȔ;D;VUf%zBَw>Z΂|O{sZ Dg94ŦAp.m{s}:Yz}52&nV_ 5k}gi[C/kuvn!p3󯿕c^с^d~fj7p*?{o\ M, \,bDኽHl e":68$>èa[P0[r6wqԺVRd{)[GW|mGݳ=_ȗ RL[8G0\[7|B<2qlcDY8HlN2slA`A<3-:n7WIΖu#!hxeNvjnʴ> iSu`!%i1;B¹> Z7#}rq(Va41ʏ*׏_yGXza)0\%}tqOx)jS& ְۙiNucE5 >!hs xR]$_Sl-<كk"^_%ch$pTk}}v0R۴GwǺM@zXMs]||JkXfҦPl(Uɛm&1[C=vج-2v͒GtQ|2U:!k51сAE鵿d\Xߢ3>ylv_t|P՞x:uѦ鄡MB?ASs}{PPz|Oο~RY?T% Gؠ?:pw{ȷx+է.犯ono/.ή|v{t}gjm7Q*TɱK[4"1$S+N ģUeA%,J654E +N.9N?N^ Bv4hKkR@IDAT>Rg}ƗOE?nyFSA *"JG#VO0mcv|} YѰnLq{>s=-r*6.#k7Gۛ{$G77:;|i4c@6%Zz E`MX5~=$.܈G c`Cdx]N9S ;FI @ph8|D8 H<くcg.F^o̖_kRYc+?p[m:I298߱&G^Әǝ[a3Ci[q@'{o ,:36C#L=1Tr#:LaqǧpIc^eWD[Trb o}Kaxz-ZG&%-Nk1QiAil[W.Fg+=yxx`瓁𠳻7.o)No#OD -:R(. *QMCK#ZψqE/ts l++ι8D44տ-z:#d,<0IZl}zXި\g/ow\ˀݕWGB.]6~J_r;ӌ#1 Bk'8;9I}UR.wob xBNE|Έ[9h;8vuOH\'.寋&)?1} Y2zQ)@-[f4)lepv8,¯ ~?U$Is7"_PB%W +wS1PcB܌`3ZЌ0=܁8 8CK&ɚx3KkY#E%0Dh3}G&0r =nOZ'Ͽ=ȑPn&|7q=EL n'1! l9{5PjBvqл"{*ѱloȐtmk BW[g<'&~7gcjqoWkCƜ{ORZlOX&`*]RQ !!Iɋŀ f;Xs{s:8aXa+#/"‹7,M*<-3PsPLb8'1T OT^YX%) ىg6ɺGETDa'uy`_"C6ƣŀ4ׅR %ZN7[8cL$⚥xQG,j'& x)j_gtXZz)yU&Sgmn"BRWz9:N`θ_BKy5Rڅ9nh' IS6,~8dm&*C47@LF6Z'#~6g|G /$]kMA1G2 n̳bYGJ*K/fh%_;N̯8+U?+4ʋ|}i1\˴WX||AɟEnܗFw ezTnshyĭY""g~^ Vt+ y\ѷu)%A`:!e0|!gz,CtYtF괳q댱(AtFf"˫2K&AG MpCnNJdUډհ&K%fgc[o&Z8o`]S_g#Ukm.OQBٞ2UWcLbFH0֑cFfԈC+_BM&1:a-[W*b&,fbtQOun>!3W|nծN\xh#)-K1-y I-fR1[ s;T|+S. $mU2E默r=?5XM%=3~ y|> iRgo/vo^#ӻO s]SyZY٢@sbAgi9  ̏Zр3BȓQe I(ΓΝ><=AP! ,KߔUy[?f U;id?F܊2sZ/{'fȤ5e/Ȥ +F@zVG-tqEwڪڊnC_BjZ7;ԥ&7_2oUА$ KGӪ %%Íj_}j{XUv6I>dMuA{{XE^%xo 0ԭU/UZ7p_Tw_Xt OtgBY<.CrMIAnϑ eŒ:b夒1#uͻs|Kz`ҁVӴ?\?лS*f=Nnc%~-jh:ӿF 5Fhuݩ2zܭ_*~p{zRRBu*V4˽c?c;i-T}W%R$T 5fr`z@F^3UU2L19w 6uG&Z3Wx^; 27lNu3MR& ʔA>0ŀ$0L,I6{S9y+vS5["#ÚbŸcǎ -:]\2tI#X.J[Y+ZӖpD]ZB.qdjmWّ,U.ڨ͓3`I-ҵIv}=-7¤kLjSv6ĔsIO5KE+vOvO>ߝOvgQ"kc[a%x<xK&P e9bx+"('WO th - GLA02jàz5Ԝ/DqndvaZv ]_txM&Z}ߩ,)G+&mi(h)w?fw_.Kʁrwbm0v2_s寢\9n>)Fi1ldM{΀}qPlψ>0jQlz|I?<@ksA%l\^4Ib;9D˧vVNa("7GɾՒM+,]**adJS.YFx #: BJLxq&_먳=9q `&S!}0xwwEyB }ē;i1Tzz ߌIgh1!liDazE`}.L؍~i6<$rK,/OmGG"B^.DdKw}Ơ`@QUK;Yxvf@7eBK0n=, @j@Myyz96%'ճŃx"_x@.SЄ ȄܥaaSs)^rH۟4I ǟ VP}v!uq@u|wcG>I齶mܓVaF^ŗA[.ݸ}aR]BfOSրohkV,<Ia |V;z`-ȡaW:ωƖ~Eg@wNekgV}÷n$KLXpAv^l!:&.6~_[[ƙ @H:ZMogYoQm 辻hV{wW΃iy`5Sv̠3!pI-TʉfƻkK} A_lĮ(/~T%?m+ Es&Cmq#Ѻ ((|ղ`p>e rґ'`\r QoUP~0wn5,_ls]~-{w|׿3~:(XVsz"WMLfc U&~ᓘ0ti&MxGh SJ``&"=aۇwsJtjPP9z?s`dY>p᣹L,Wũf0V' jaxiWTM Ncu@//Z,'>T wd TU"Ib r[>{S6"q-' )_ˠQAYyyCsT{߼}ݥ~! 9\IW$۱sqWZoģ׃\B3pC>Rp3L<(K~k-K,q90t>)UYyB3F_;̹HP' U>';?z?2^~8/[8'vN DKU)l-l{e)෿Ґ#(dlSTw;6E2.%-mElզ-19UЩbx+(??ο!2) :Ab;|]0_3z;007H0c-E<21hBl#PVq󧶗6 oZF-;VzNJɐ?*%uy7ooP0MtkV0ecB'baq<;dz1 Sl#6x?>};v q]ae][C3OF=ڽ ֮rFrpX}9;P \_giӸKH~q@ `nr ,|:fa;XX{s諂͟7a3d2TԜt5z\*N|gOوu@7KC|Jhbv~McDD݅wgĚXid%mSǯkڻSB0fq &LZPmNZdFy,ϐܜ%ys^oXvB_% NzfIݤO&+!t==S (k[LWg`ņN,Mk0]A?eT%6=?;2ӖtҖ.UT;G𓔟9ܰٓ{6ch0~w SehkU@Xu=r)#ǟl̍"؈v芑ONk%ZH򲭴 M[xIljt.P:8<2Ss9?ʮSz.HHݘH`msώiW>ɼ p\mZ%M:x'YUVOXB,?˳|0kŠ`8 g3$EFU8]KB 9lw%<ݻ7u*%qDDU]8nSƭ<@Z^ڻ0hi:\ܘ6ZK <wgnygB3l;M}b%}R7>_r٤Bt;=EX-l5u/ΦVhL۬kC䐭Zxܗ{Iܒ"ްVTߛ?NOѳL2bxhTA+eweO񳑻v@("Sk* KG4~sJg/>A,Y-(&֒U6dܒ+n޻9cÄlٕ\1_+b>\ -pVO F6ED Sxc^0p2 IЍɟi=/4oLG^\~V*3c?>N Blt?mpci[fg#+1MbߗoP:vc]6"SX YZ;Sx Z^[E36, u{o<7fHTtJ*G`%Og/SS9iEORj0~zy N }-Bj)C (%{W>7>ڻ~J~x/~&6t< }~xC7ulKڊbk}O-^6\9󡦁(8܅s @/!S{9M%GQY?w`d.F:;ӽc\is Ƞ"WMno$&7ɋ1K߿d2,_g5N_+[Y`Ve{:ɣe#R]ADao\,0#;VUҍqXгc I{mӈZ^3q,  @WFNqi|sUMg}<8'Ֆ3}$f>/b!W}I ؤn %u'`/ʶcpY.+k I)QrL%)*@lqi*kOޙe"٪u _gqmɗ_|v %Kn{>>LWBK۾ikڪ?0#mwΈ (Qqg }!T(ub3$q`|5 rpO[x#b u7݈qR1dYx&HbZcZUH=өQ u?rz]Y!'el&W->(TTîq>ThP'YYpw~L tß0;us {SCs<c3aos.QT{M׌/'H nBw:a+w?{:Ɲ!=i%{M>;@ط<} (>䪣mާwWV*]#V)4~^|K=dR&¯:KYMy#2/MIyOK'4,O5G;}݃OjoJ׫רꚧgk*x%2pӸ} LC3ghZ2lȩA:#qjxJ0WMĩ1Z vP&0s?:!?|֡S&M馷a~D|"a) v䵬!Y.2Bd0TN:/^z5g@K[b0yj5eco0\uw?:{;M Ҹs}7[A;|wʽ%[:`"Qr"JbXS.qxŇaa"*m:IE8,čEbgJ},3k-ȿTJ& 9\.z:XVDN9C<,p*+}d]'KyLg#UFvm +v3,gRh¯i ږݦ5GZ(XGR@IDW_۳SRĨp}5T7m85]^[n'qLH;:5 t~b*uϽOq^OvhCsii/5~.t2:t0yBt NX%t,gp7ݺL!.ajCl7ZΒx02+iϬڳ1)y;5aϼ;Ho|I |/3qv٩paטԨ1(P2! 3hhUq`tG\6-SlB .sØ؍:Bǟ#˟|ot:5 =92e ~{y_[xOqDv&_jKeXivwϠf:l R [~(@ARZ2X46%*f%_k *zugys-Ӷ4ر$nDxblv3A"d-s^ɥ1*o0)s ̣W6) xOGK'yu;3)ySal5_J NbRF{-SU"hT*U,وǯQb޶⎺k *ݍTXqi;N'f +8njT޴xfhFqShdELN,LMԥh׬&K0ʴp WF`NJ EGaT[ݙ8ZOBHծʰ8 l {O]CIevJwx*E].g&ҫwHțB%H7=cr+|%KYi?Dm}$`YЏЩxC^pB[>; eպOӵ s=HP_uSEI(_uhڰ46>H4~*4i SL7AI[/5?ۮNbBS ?(Gɹ̠l̤f}a[V9Y.oWN֍h~>x~^ߍEg<*0TJ 0VFsL= *9W/̀_+9ՀM6 ys)䴢ƼK xthvMT*n~Ӏh5<8ώx6~uYA:to;6yRkw+auz$gD{>\x-K+>"CC.VOW_|8Uj|I2IXװj;6\M'l5 >(\ figسgus'Y906'c~ - ˯!ʌ%Ew{ƃorp㡎G~ ӡ 7@y[lxhyhcL詛v^&M֍I'5:Xͽ.\k ci, D)/<7F|qvv%qv܍e=i9K77~ 7Y+gB9 YM^A~H˲ސwnx.t.W, շX22_ b\ONXu00=~)0Ķʉ0$QtJ8{sC+B Š,fAKq@@;iy4@OI_Iwt.ag+kKպFR:O ꣀQhL?0?|Jg +$xx}2;hj[^|ZA=jK.Sa)]Z B^;həT a% m9Dyb(DХY ,!0{y:?{]`6~;ݤط"L蚿h8y&9"w_|L}i^]Mz⽤k9BALCli|O\\L C ^ҍ%.iIvB޼ڞn]^>ٖ2"hjG F $GE年i(yg?9 w&@7i!" *\ʼn8Ú.9@q_|l$G&ׯxo}ʜU["#iM= 2y1%ѕ:RIݒ9&bx$zBZ+%SKAHH˨?۳El `ɬ^2Pcs#a?TD'8P函#7K> AJ?H?kx#NN",-IM])C?࠹7BU `4ny^S*0 ޭ=x};>eA A>Pd쇌{ @rD Na╛ kD/-TW'E,#>E 2 <}u}NkTf<:+hq{ NL_p/$5 Z,/vx_`6(5۞L;n(/Z: Ecӗ 0 4W4n,\) axq!fUo` ="# nz/2QI" j7߆" -X_. PQZxƭ?h&.B9+ipI;{m *'oU4E5{ 'O%qs#9Ua#aK B iNpx*P1=9b~#&kUhq˧T-2RZg؈wDz;M47H3ytI6E¥8pwe{s@[PIUx/p8 -}KMװaaxM6S xK{yNq[9v/_iV[Ż!^LL ;2nאNltzlU0Do7MWwtҮ9nj078}_}Sނ]l[BZ8Ցo ^=KV f$]~BІwɧI:~T }-`GYJreaoM_+b ɚ.Ӵ( =fy4ZÑ<ӕG\\ X_ ‘>O]Сv 0q%'Ůk]_#UrdOK|`VdLOa3b` ڏ5)_e>/?yO ~e@U"ݾq;5hoߺ=@j:I8y~'&ɫo)>o|(cd~ 8 ;ٿ̈́N/0h&%Ly@O n}ǝ|E$07> l%iHj%:&pZLַC t$e%.AM9eb#LT߉gwN,%G +u ȇuS%L'mi(~}E:NRWϒfkujhd0R)؇'J:=+ mygN}Rw?ׁyviw{ק~/Y=&GY:_W@=~]N28X@X%,y3g۫ MV'7 g` @ m;>r"&nTv-QO"r^ZT5` @6nkF2lY'>ΫGtlh"kf5f#(u.Ħ# j$T箟ߏ>:}¤%+ o"F05Cmg$eJf;̄C{w54QO~3}`s0Բo>-Nr`$Lu2>\2E(#z9̲L,du:ABZP@w 7)f G 6"5RNi :&^3(qEU=z?aw~aODVLN%6Y0z5 ky@IDAT-8u sC{2軯7Ҁ6+ U }YbL#S6e=~.v,T.M.LL^b>1{ݭ,/2J@&h& )Y,^w%SVWkT^\dK.V'+CI)׻t>)T+&^oIxT!&tQpa={KM+MW5q4P1lC~9Ut\ pُO0^)_Wdͦ/vp7+8\˗\m;aw8ҭwm|s[7O+bXӭ"N֝PQwIS%s@cwxD,fؑ;D@fbftfoˢ&2$gBzww~M~1-Κ@;9`ri~5gySCO 4wYo]P|4T*얡d'fwՄx;?bfx!_>=`"kwAF؀=N0&g9'C,%Γ!13#Z;*y1rHe),6m zoAҰ-Bfk6kK[m ^jܔlV:ydOJ bMtA4Af%~W?oJLP ې2p?Rȟ0j9`?q :pvi_sLuW*zw)-Yt} (E}v/)TGE`\ [N@N~rfeUՉBt*@G|l($\ *2|I4P&3TOlL/v׬xq*: \ˌ @xrR-*AuDyy͢GSF . §QY?w)v!۳y+JL.P DN&\nWx4EY0Qki$h4;$4AgƵsdJ:Wك>h[Mx95d=SB eX6蝐3&V~eCI )<#rY-'8@h! `}HO_mtJ#e)W@7ej"M ɤ.ŅO-t 4ۉ]xa[[mύh?{ @Y{ɑ։nid`SvԔKUg9yzW kGV;TՈ3;F_&s+  (nTRu}jm4H^7[?4F-_`j/? uPgRv2l ߛ hc}y+CKܕW^6wZe w3|cliE{Ν a&`*'o+@'8>\H O}ݳ3_F-e^-.C5C˥;iʼnP&kAK[]0ocTl9;z?#S:G+tizMsǘ? 4E7#S(e`"F_Q`)H` m `{#< d R^0qW-A [p#/äHRuIj͆NbI<H!hy:6\O|hS}7L<:JV)IHS>sO b/s.vn+On<'7?t7r̨u;|>[{c`y8ɊVTG C[ (x_g\}C$$W eԱ^$} @vұ|NNx v~q{ƌK} ~r2AP RwC#|5m2k\;%fDʅmB`c`m f5))N^`P玠)VqQ_\ E1 j-B;D,@*<y/qB2Q.GVBI0H.'.LkIFF :AԁMd^툉g?я~ N?}lN)ʆ+ޫgv/~nYϲg;_4wL~4OK> uи20ވe o@Q~+Tb% 77Q{w^)u#8'}ڃiZ6sK8w;7?GcH`E :N'X ncz%yuxƍ׻ WIވW4س_}@p5F1sOaU!c`#Chlht?­(Z,+]^+I4SFUR]uJdX\W=1 WP?N壗rͅ ?u0-N(Z5n7~Fk@2L SQktߴx\ W҇ |H14?yM\C ˫t\o#':綡w@<?cg?sퟍotO9~s}s3I]m]lvmW_B7s8vb:2X;fp 6" 䣧v}_~Ϸ.Lڥx&IMcv[hj?i[%/"zIM-=ED<. s'BtUGDʍnЄ=7䤢728NnW>ˠ2?]'O2839m)0s|!C|^]h__s906L$B$F .Pr aq+캫kf,K<l-ٳudgը @@7aMe]78P_Rߖ)!}A/{ȯ:^MO`ˆUouC_{vwrC^?n&-\1P@WS̜M,K}󝵗|f1V@i7iAmDM5:Nx :ZaWۡ1';z:dSC]Ȥs :+:EHybv_~lw$.w&'T y<P޺%/# v^]d/cБb-A3ᙚ ;X<-t 'u -zV<UʿՅ2n(_; /7zkHRǩ|G"Z`U|?s |ƯY`FbO= \7F5ZbN4˧y-dP2J4oo1LlAeS<661ӿwYqovc<^Q__>c_'YZ13`>BFGf(]6AsC@=³숒W250c>;ݠ]XyKiϾ9 9.և⚘} *7} ּ Ahzb$*`wt|&>)}alm/6mcǧǟ 6q7/"k+\Lؽ[kJsc` &t@>h塐lT!L ⚲Pq ҟI\ 74]slUZ=z :^s Y!xaNtS&TFnX**B4дGq.i!3I?BIW,\WT-l9uz9Q)>U 3H23ܓ80䠙ރ d&!hS:1I6 J -xli wBC#W74$!'0 9ß|;~Wο#6}#p= m ?_𣃽bb~;k7ڈJ%y~q>Dyj#ۮ e0l"a{'+H rwT!P*y?}݀r0* k:8^cU9m!yrPD!p!a˃84Ήvfa'7 t7$s@oS62mo^wүk ښ6z_qx{Yk ߗ邯5}I={A9{{OC)Mw 𶘆ݙF.PAKiVW-yph +M!xWJG.MX^H:nҰm:{I09J~A!u{~ʷe] qt,{G4n>ӓ0]B(}߿f0Bw&!w]Z7݁zizC5rj$:¤Ǘ%)cCqBv8']2?3~Ec[ʟDTǩJVVt\{ˀ7xXY}d7IH)_V%ޚ@}ӕX XvOTCQ~O~ 8<|x+#{%c6w_؃oO>})|%pǁ%w)A%M=h}⬑ZًL9FdSQ2,6JbdaQ YG Ӏ!v{i\G̔ή7 `9MBOU?X7 !u~207 31^#_=&o{-zU6 7,=j{أ 0?Tώ81'=]?`6ڎ&(~I%I4Z7{?#>c7Sպ?_|ݗ.}wk2$%{SRtSWUl.מc$w )H"`Υ7 = Ϭ[O? l/"чoG=/U!hﳜߝ sAic!_IԾ#/2S`w|@QwDž) |#ص~k*[-3HJdT?"҆I9d_|v>J$^#0prf@BZP6iQ1V30]RDF!U_5`Xh[ڈz'&4qgMSa{c:W1? F<J=oQq2's/FEx_;t1I2]yI)nVQ,/{d6 -tq0 e#}B:0Z3؈Sb_g4_C"|1Yu20pI2p-A߼ne9tH@h=F_.mqյ։I0Hx|&({O#?H 9ЛPڊ]W~%/lLdԺ1̀ aٌMh6OQ/BӖP&T"-?ht\q0.Gz؀}%mTd(biYׇb;KPUwӢmcL:tq\]ǣߟ~{m9˦dCTtMd,>sP2άq㤔[$>'eKܕTa;;Mtǵ* +[+q+`%8;z;q;T8.:_?kYZ V8:?o%H/ !Êݮk߆uo74~>oܛ܊^|nwWJՔ N511>TM;sj'n?c#v{&W 0FgJyʳy*wͤCM;;8i5R]6ꏈ`'_CٯwZޅ~15mvy#NluM4Չ/lj:<#E~Տy";'?`o3u€mw;i&lD`$jlz{@nOӴ2-B:J딫F6̜ \r JQ^4=)C|Dsنf  . qNZ G =.KAT7S~kUt#=Yԛ;+i{]5p{) IY㇩ a.Q'tQ5:a$MrQ\Q t#樂K&; y9$bye7+M QQ_5y9w]+ g}璿y߁WK\2u%.˙_iV|wJyMGJ4tU{6 m:il ˰7I껤oɢN@}DC]'Bܓ1p\w`+ˆkVyk mvH}&Qd_ i[־ |pcJX'joBN76w{9$$01H hY?gw?^{']@o~?{jɏL#]P/5]v_:A'%fuL>(!?^ Iѡ+žU)# OzX&N~qj2Weܫ2-Cqҙ뗰,bl;Gv@|#of~{aϦ % 0N$=e ?E/Î0Mb'.{5y)|h8-|8J:ʳ{ɿM?G]˂{#z_^sh2˕0vYjOHR!:ohEG^rRl K '͇#{L2#5$$YW iӃF׌-De׈3лBEg\33^:.ҧ}oYmiz žNHǹ?BmyӖ0nDwfM$]cXќ> tyT 3HSVVꜫ&ۼ&x=#&Fn!Of%; s {`Xu/kFo}4}aYi}%5w]qӐwM #^q̋qkQN`WC>J-<- |{IwP*Py@zm܁CN)~gqu~?ߎFvX ~] +qw7N`eg[puRCSܻ!CbӁnM_qwDVq+?{_PuVROÁ#[LiFs30uډF$"=f"p>ؔLS ٓcSsewF}u!2% dbcTJօ(gĈ3M^dDj{]ȑ[6Oy%yг ^_,IfXjʷd>QKK~[?QfJ.]9UGIUqE_SC!bu$`N}xB0!ua|KWm+4%?8z1]!\WYM.;9N<&Xf).[]~#bG膯; leNxQ- #+>.;Y3g)N|32輸àƾ|P7bѾ,s]6׎͓μGOόv f['=%2XϺO oU. o/Dej~z9#3#`,yLe})t%ۀG8hS Bc4<7%nЉXq㙸A rثځ4ΰM+f7;;w,r)89+sg?cf#5nG<_Wg=Mb7ܹARy,6ןNnUZ]^\Nߑ?^ʢkY=h aVg%'TIp]8y9B9tq20gK.#6<ٓGezfO1 u=%mUx 4m,!j5w\a,i ػW%$bͬ諐*.\wǚK%ņQ|Ǖiْ0ΖD8og* ؎77Fݍ[.aE1%nH*}OHHp h$.YLߛ4V~]OwO~goa?l;+Ή.Ez׿ tW_W`ǹלw ̣c3.B%~*#LxIG@b}A'wD czTa7F],O't;u-gY.=>cEL3ࡣ"WE5UתP lۤ+5yNT'~lw#J02Ƒ;2| #$o7hѧDZ?".ٟvpck'o^a []]՛7/yULxݐb;%P=gP;S̬J|&8itr8 DbԾa Y H2Xܜ,^TQ({,xdͽN{ ?ٻ?t?VgG B>˦s|e>{N^5&[?N(GpP#HuOI@KpӚ6a 2,C/s B!}ވHA`{ m)pަ%t:j#Mt$\<{ ҈FV!i&4zq+ [8dyku#'lYv| 7-H}p(03A,Ym}81m`go6RLn(waGĪ^KW f*p}}gؑU<p/= oS_}xW}o_ֿj(PV"S'?{||:WҵE: NQ p[(6]0Ό.25dM|DyƜq~̹g/v'l|l'ׯv?|Q} ' bW::. @dzϰKoXU,ĽrT%e$ 0juS:gtI7TΧr93l<L ':ld?CB:-3ԕ]w>3 ?{;oM~Ͼ 7}{@ꗿ7n3c 'qti"x5](zw͟4@ {n&!^&:(oXdQ'SH8όMFoEOSeJyǫlw' ogY:=g?sտC~Bo_}z!7/ #Tmy[H+h\X)F\~K[h4t1=l͜q:N )A}M,R{UCI 4ŪNQg>aG g7O9wo(W4N8K?F/3uN~3oYfX'uf(#Qq5j669(KV~|G`Hu4a҈!p6 J4F5LZts`ܞ*Ϋ|16?b _Kz.A_ɥbM[,R~ؓyĨwlGwqەF'dԋ_|w-}j״ᒟ\пq(e@ɞQ]6mԤ\x2D0qOϾ\Vح>:D,+MIWZ`v`/M.Xb]1RJ?1}&jN͟ >^%@ĿewMu]]E6<]݁AOt탓߉k? T*f3/Ǘ,-?:>~bܤa6{/|K7+NXv;^elDXH3aziCL&dBL)I8EPn A3U=dvaIp`$"o4j5wRqWGKJcf/8)}L[@97%q>FBt#K;a9ѧ=Ǔ<]= (2%¿, 0.[m">UCNfDwܑax>9Ytˏ&Q{[+,La2n"$Wn-u!bDU.83ۅKGw-vΟHYe&.e ID玿׏W|/>Gc|y xWTgע&8Kz\.,g7R7u*\o4;ttOgPNoX]Ke`1-MaO1kx$CGO#Q[X#R2h+FD8d_GXɩfv: h&v$0@IDATm6F c(;.0@. BZVhQNƷpQSj&B yݢsr/wA[d@j$ sU#[VQZLsE`ś~$ Hohе@ )UY ؽLL&u9,qZ^ q, %9s%G!YBFk\I[@N/z/gG^6mi`UorOJP]Ă .ѻOK3>_g|n_g:+k~/$O>[|_9^:E}d À%?x'4ă 0ivA$u ;Q>W e oÒj1"JA0R eK2y͍ e;3 ~9Hh''@ 'rJ"9ӈ+OߥZ3=;J%}oj*7/6R~%e}E&#t Kr߃˰@;$;t_MV5>geNjX^ lRi(Z_wyg?Gn_#ľ1'Zk.3ȟ,gQ_,/}\z̙\yVv*N.Ù(!״ ~V.be %@rvޱTΧdCaezOZtIh\RWۅo^Zɋ Zղ/2Wkw}¶ɇMࠍ@k.%A/zun oH*+6A5_7q 9aF+,O@Ģѐmj{4/ d{:Jc}aF};t7ʪ؇uCGVbC &)dԔd8L:u݅/@,a*a" tLJJ?*C>j8>#m1aia*4.rDuQT @6F3?pxYO#O܇AY2nX> /6މA]ҡ 4ݬt o??X U ,(LGS6W~W6ٜI]*0Y1 |/k~Z?ֲp<_M&qe&䕾z_w?zp @Ahm;k@Io6H]'u!D2OڇY Q^EuZeЌǗQp_f8+)SքG#! DZa>.roE\EL⨚.Wb+{`i~kPW̮飲i8$"cOU,0= #aZrTڻ;e}~$6GɈAF`DlFB9kBj`VL?ᨃ+F>]*ǥf}gZV})9wΓAT!O&LG~2Mg`QF0C|g7 C5=`7oH{eFط҃Dc=[/!2b(EQx,"v7l ]ǫΈA+%Nv_ 4e&KT|dtqxH=D>|~GS~<iŚbDy_^S h_kwVVVX똻{t'sx $4.KpOLHWR/Qy‡Q!?#@ +N?Exf |ꏊ3CQ 7Ǟ1ƒ =~VL+c;qoH3g"u5.#?Sv?@HΉxU909󱞗}_g?'{{ˉaw:эW^8޺WUi~kIW/(WE@g ߝ*4l`ڤV!+^j e`I/aeBaZө!e=yI3〻=;զ!DAdm D Uqv7SJI E} ?3bUsmyqdٓ.ѡ*L3x͡L^_-G?zGoT\R@&ctQ9zn,'l$Gj@c'J bͯϦfᖳɵ6)M#Ah1bql!k{&dWM n0.aJWˊCUoUA\1uQ<:(HL)?e.g@8}&Х¥r׳w~D?v,>O+.sۀ&otӯ5B0}s6{~*JJS!,%I+AۺT4qmϢ/<(]+mQ  W FB E/b:"a]&~ʄ[{P5$؏XYw|?O@2d|C;=aIo[ad<27@?j[/-J;~WؒS/Oɢ0б:Ѣ>y:\u.H?, ]$#$-U39rʚA?x-AIr"XfZL֐b e&~jg'zMXgO;$fs<_zQF|Bܰ.y` TxAAb9/"7D"-sV2դ(cɰi2dS[\`_: D k'M8'3AFU2(N@R÷,j4EP:LEh=)P +S)6`7JD_(;bx Js(ϴx|AFzf{fjի nJVVҸ"д?؆}Q|BJD^p?QlmfDɸumb ܐ5MAf<|fY d?Q؜;O{jN_~9Wpgfقq__?Ѐg|~*o'ӈ3Ҏκ0[" s}6Y4 Gjٕ/+b'>\&6X=;=2+ʆH%EPY3ZǠ-h(?h.6(> LpmLFaD tL)C* jك`RWP\y<ూ1+xM|9w>7 o\H@ :W鐟?s|>,.b(5eD??3~v+>חzk%n{I 6w3Q M^ )SKD rP TڳAb]c.Թ8sCVEi'&mI(^ XvaJIP? i[3&2|[E̦M&aIS8N?Ty[3ي  lOBgB:aJCF&>4l& U[OdΊP*?9g\qeݢ8'|d1#yQ (ɉO&G&[.Ҟ-/vpW=ә3GuJ͛G9'G>+{A{u"Z4iajӝUFO4d:_+U#LA74W3n Z lKރ|rɯe[oyȺٸ?wKW( eB1 ]R7:N?|CM><=wQAJ+T׺hߋ/Ojr{} U*휦1ǖMy-ecŇLqO/L*`aƵ<%5 IyO2$vi+l } >v'uEj˙PzCKkh叉7SQo'=H(9Ud\+h`K> +iu&#|D$,`W8Qq %, iWFKف^ 9T;y9 V+˚tFC$7 a%l? %sG~|o[i/|8d NȷEo/h_ue} t ) z.hkʘ# O~ew}:" j_>wWwZEb%GV Q~TP[ʶi+.zWGIH 9pX( 1y_ o5}1p-YU%@%[/d1zQ#]NFC6H)jd}6"W>Is`=P|WۇP(&%YA_{GN?G9?}=n?\Ҧ$.}?sՋ:'8_=y W&$|c$aŽ(E_2@Nn4L88W%Zխ 酀=Q!9 "$c ?g6 ! df%0FD* }awY4 brL0 -s$CdA·~DR|2y]\+rT\Q~Q.:7puwC}j3Bp"#Q 0jypl܃\tCհM*$2gp,Sp~&g~&M(jJ3㕅qR}U/PɂPPީ>3<| cC*jz, -%I+ǒ{,}U:UKFǗ/wܜj"J? b'H*wx^zC#BqFh=A_ A3 %)Ke (l.ykR\pQ:0hFAG@~|#DЋBO|DtDٖi'CJkiwy}B~*3{vIxEdJ],t8}"-GFt)%!|!!RBhx%o! y -{9-d n}H9RHP~auMhs)`9 +jL`ᇭ+IP&@nOR 2vZD8ҔpcX|nPt{7F>@d ߢKvRZ82`rY]] yg^*8j*:= %Ar(N6ikK4E:\#=ul9P&%u+ q@(8\[[6=Ia3U a}.⣊pTСpy''YwPl 8zp@aGE,ؤ5Y~TPMu,sc胏_ߛTgMInk(`@=?^ }Ggk#ڑx O0aُ?\%lBU?y|J|fh<;@8up)7|LAuO> I!@USMu~36QR(xk?⣋{tAD"qA8,A*?4T K+vjRgLV2#03GpQ?h̐=(N -- hZ? Zk15؆nQל&9 ¤Oޘ8TtM0^;`PǖQ[5x[~_?I>\0+jPRr_g{׫}Y]?Hmz[ig.}W_KDAdy}E@C?%AeI엀d,#&xCFf{ 2`+c/6ڸy$!3Vv̋lFeo##ԊR<ҭ1v1Ӡ5.[g^} Zu7+:C8F={ ̞4Oim|M V& CvGٗ\~MgR):ӷ`Dt TU;E\p y @詋~| Xn[-)ǐ`YK?L#^=YӇIgQtSS)n+]SOGX[?v-vB>ŗDU{G)~DȰ.})";e; Z|!F`t4 ExBpp!N{ww?3~ٿgH|𧛺3`R˲Aw%XQZ_ǯoowOsЏg6M$`L0h/aS~uO_I,֩ ۍ񆧺wdM8G`ŷ|. _rruLSݰ7e2E8dÄjDv( Zn$ZYʥ|cU DeʎGˠ͸l6Q :41v S2aJWJzS2&!m/tK:?޽wbw1,MJR!w(z@ &KC7ynM wU,_kɧxM745o *R4$ XRA 4l1REʦv~@vJfC#l3XZʂ*6XxjtqcmloO!B*dc#> GF8nyWoʠ")fnO@'`C?gߒ'pa@Ӏ)x& &6WV@j[H@` eG΄W/Ae@ם hń:m&9w;=g?A?yO罧ՉU_Z@z.j_;u zZbbn g]6:LL *[r+؆.UKt湝ߊE/a&y@]MPB :Xe(LFMt5 S!W]eoܟ@. [㨇L@@7BˀN9 r P2F秈C<7:4Amʣ0 Z !A`s%=0a_xTi8:Ϳc;a$?IJ$WWbL{N4W!wuoCzDF9^7ZA!'6Y]>Կa$'r/EتH}j.G/Ę-T1z@.1п#sv <=+E>I+Rp0UL)&?Ҙ[z9(=mK7ЅЇ-JY;yD4op'{V4 V~Gt zpe1 @v&}؅ء)<8S~,Z/U=c"5lp4cU^^ %mX]Xf浨,f9ڇ=zҴ3ʮ8|o!xĝ#xi7`.eM8/Dp;X`"RL~ձo5!`{zRVC)<LFtiF豈W?z?Aj`dD;)~g3l־: !o.,%+A1&".c~ ؊)F!ןTfc4<>H5Z3,ѷXc+s8AbkņD׾[!3V< o9# :rs WXSpa#2H J+ dtsr r/'OAk0]G2~`kЗqo%6qp؛OyQ׀ehP~B_`>VLg-aOƷS|S$whʿT ctAw|Z  32A!ÏeltBK< ' (ԾЅIǤ aX ~7gZ‘5Bk!E_4[UQFAl*x邏&:C5~" _.K%"gIe UYkCˌp[ :u>KrQ~q(5 L`)%X(nD3UJ!``1E,?y+ħ!~զ/.5~ZV&g_]>}5k0lcy;ܥݓ0~6+[m @(/8輼M>nkmEg:WlX۞L{[i7>Xc_Ij0+2nǝ&!J/AVV=!l&e@l}D) V*+}?s?Yz?jĻSA}M~_g5IМظNiHVB6; 0)=,eZX}gTw~XV'݉׊Ça_Ivo Ge%u@JE`wGԙ<|"{֮bĀ{Ig =ˎN{D1li"I G+/MKP&N݇a&?v3H7 +Kasm];ti ~$`C9iR UD 3tKghPmTIE8ͥa0S`%‼IeዽBZ:G]Vp/w\E+/$ VhBS9Lny/H|#?C#6h”y'[87@]O񄀉qz~qE}٩:$|>O }gk= -;|c:Ѝ4p x*:gm`"<NAXof5Ǯהٶ鉬/Ey?1RN ěN[{Q!j´)Mc";7iOaeM#M4XY7ރZiu"# XAmk~? RN; ޞڬ32S c.ԞCk/~L;a$ߗh`Upd7FГ>Iq%yJbz @'GiA@@˷TCC>w21u%6AK`_6LwG-dfgpS#PEp5l.Qٲ)0F|Daзvdu~wWS~r`O?w{>[.&3NǂHWkz|OǪL8Vɛ%|[WaG|`c9PV'3 D;d]FM *ّũ.i Um 2?4?ON"a؍[bIK,{_ˎ|{7}/uq+eפ U,qf>r0SEcCjxs jn/͞h$cۯeͫ;z50&9!6LRȹ fzZ^'`8+vYWݎ6,5XW}cvw},x.ʑ>cI D'^yZ5Fq)PXQשLT u vFy^#X/)Qq)ݤHsjU }:"+} (ǀجuS&xGP4i*ny]q^d+!)j)}kOPi晷ti&×んAƗ䛕r t7 Ep`%g|BŭW ԍDt)Vu,+XC 6Q3Pφ'~~}>>H_?,E>toyA9?gCg\,3f sjNo]R4Iu0G.UlZꦈ8 wG\= ->=O{:5zIIh7oat_Bx(,VL}yq@Mk{b|Ҽ&keMy𷽎=<4F*O"g=n9"Od=+(?%I-UޢQ]&}RzvY-<&V¾ӴoWGw|n>pNYͽ)'rdl#W$bq5d0ɣOI]a0%=307FW'U[ҢZtDοtr+[.x`=VCf'd'N,b}|ß͒P|K=,pGzϪ&9-y*M4Cӻ>9 _of O9+j qɼw"DN|lq' r1|UUX,BZLe.˙yv)* rO.:}ߎ;Cu7t0ob7:x.i1;}9 ۬lCL`Bڔ!R" #\$;4"QGVUzO#]i%S&ݿH=PlW`W2KSI֒ x)eDЊ,.D0!DD`DsM)"Pfti +~&B5KC\1@ D'ErOMgⷸ VMK6--ۡm~ aEs|+U?lɊ"7,rcVǻ#w?a7!eSxsݓ S=:$gZW<6.m6̪r7)w;xL)K^%@l*-^duFM-{w lrgLgBh<;W/v|o]|V I*"Zz&{H*r,r4aiAg,"0Gf^>X[{-L0= e6:}^"-?.~_{xΟO?4w_.E,IJ!?=k%~6)Ϋ<S63`J0c idBbz- gl0s__ Y0flЃrK2ȗMe,R W0 LJ h{&9{-t 4lP=#ޫo#T6Я@gaВ |d{eg N}$].vf vh|F$@IDATQ;/a5!=W,' w$_'N5>#C@dGP! g 㠞] K(,.4>T tq&UIZC>Z:?Ҕ 8|t]ᩄ!⬙g2쐳k&]<.RiL/_By}Q]gW9C7r_*uHȠ՛OEcy @$d8t RQkRd}W)% ّSrӀ %"ui:Ÿ-)aAG Z@@bq _"de}wpdwv`;&ҽwW6j~~O9_/7.-_~iRsoo#H⼷§H*p8ۭi<&6C'ZeͫKpr ߾*na AK~ Z!V<;/J9,RK"OǥI>ϬSlb{.4|'S'7t}c?Rh̢ zpm^`|;lx$yE`MoC=coȞ+%c>#LɸߗվO[}$ yd(y@?~{X)찒7rvb~{+]p.sim-3'*U8O 1Ԅą"dBb~럼 [Ȣ㐉.M) TʜNTN55]P+It@sS[2R*wx2QJ`. `]Zo ]1 #eP&"_r6+H!Ek'&^*J?rf%exJ~-лN]!:|wX?{r<~[;nU6]鍒K6rKZx}acd\%$0$a|\)>ndTA.b&27c|0# VK|@ VngRUď\ed"iR; ?~Gf`"B! Bf*)ܜ _eIM2Oð8SV(0G[m;uuҭe aO㻎 1!!(&L;%8Ni},v7oyAKF2p mJpN[.8;i xB2;'ʏ]CM/*1 9;JGDZd_ițԑ_1%8f6G)x: o5`!YgobOZ&Qn(eaqW\ CG[LDos@a!ʻ}}(w :t?իA=B uβoAd?,_~7]=/_yU˜)I9- b0wIMKk-`|C*2a 酴a BurAN}i[e -cӀ/yAZo96%/ȸ o7'Ƒ3Q ` tW> guD{6^@Vxh+,9dgTB$=TwDL p@ZHD8KWq;ix~J\mYрV,c|mBrezY'K Yf?W>=sϟ|}'r[Ցݹ᳒&?Ї+B4:? >t['aRy÷HI'iE6HهOnzb4%MÄ'N|~+ƈts,3X+,_#ȗ:l[>Xy,|Z6ߟQl;S)ìKu לכ8¯}or_<2 $ 5[vrHTMo%joݱ->|>b!2TY"fN<8|dh2v+ӟzl>&1e$Q^nxlS>զ;Or6 ss,Ȕ^Eվ͠o;X؜鴐_X&xKޑ6hitbw|\25#CpcC= oAc.>~AFw7Dm}~:|H0 :7AEiU1l\_<;|#i!LO,<5㥁sֱzjvǎ!A3 b|gww]n]CuٳуVa!rBXvE'NxO'!+k%M;pghf?g\Q(4 >i~;9:!NJ'D^i5}qz9W[$H5aʺ̴@@?̬'2beY0! ႬI{ar|+ѧY34+^EWm:p3oBC]F<'?B9v*2DAQxk2"nC%b0)-OY6?Nl&::B_+KT}r%}07aeNHB1DS}c~x[| G0țjh!V{hKH3DG\Ct~zG:=Ywan;g{G [>p)w`/9FAuqK1V\z_E j>28R`wF2 @Ė /b5[v0JMQ7Z=V_Ф81q@E?{ }BQ+Nfv0C>#6<2jWRn-y-LP̀>zӥPHiFI\TYRPKR =ArWJ^$ r/ibūӧ-џrdzʏ-$Õ̓$~L(V@x71EBr+hAhJb0ɬ`Yz (}]]oMnxU>SQ1PEj:2Keg_qɯiC=GK ?y_5!o;Hs8goҾkٗIO=QOq/&Gb3n0̺(⚡Z8dՁBar͸h!dr k~A` 6 MlPZk"EYL/9D@pQ& V~5)@)A0)?,u|wy/{%'<άl-lJ!/U~Zx_9DSʡ }c~Fx!+' MUN~Җ森Y,E r;.[Gn *o4GzC`2τ]v@4BLzPTfR6wt-¹/yZ:yM\.՗u@3z -K%!wC ?jW \LVOyry #s=+VTƱgg׮|ِf(I8aVCShlB4^gГj\,z3ㆦi͠)"clC;3werr,$]ҹWC<-Xя^v1aOj"gzj{u篝Dc_?g}?ҵO *28 f2N;ۃxsǡx]kO^8.Wlb|Ap3 Lhb6px->$~W 0 8.13O, Z1vP׭f)`T 8ʟl_yodJ/ voPSª6^Xa M=Kl̍ qncD;i6;;?1q#Saqe(Y+O5Db4ꯚZ+w48ցI`s8 Xg gC\wy.+ut#m;Hjk 6bwqe2*Шx8VeN -a[JmCt ]|Ly4>HDKQHlpB?`-C' /m§g|;yί#}Y]/+%CwU?{xZ\K$e%@j"%''Ag&겍,ħچ'x t)f.֦ǠO Fv!Ӂۂ+5QF?m\I+z2UBtmo$li(#iR.xQO$siN zũ7=Ah[`&[ئIZ&OaOeALWvs癰XoL3P6/,{+9{?~n?|?&L fΫ](սo|>Pa7 f(4 nA4VphI 2q  `M0]m Zظ4DDᙞ|KoG h qdD}m#(6 /<ce W1@tPѕMof]Nhf 6Չȱ/͊Hb܈Ĭ+U~:y#Fd Ie:䇰C@87)(!*SubbcdG|,+|}:?deƤ3pع:4J2.G&@[>!w -JU[L|~MJV4 i# RPx噿3h0Q91Bm,DA#@В'.5Rr G?G'hIJʋ~poDI-i"/zۥl'x[e>?4 :ڡf ڈLg=1x:$놉> ]ڲԒ }v<{s2E`yOϒiIh>#>'?|I@e"Pu$`_Yg_玟y}!Jɫ }2.E^$92Do,o2p+=IJaS/~ xh|nK%<8F] /lu=YΫm$69q4 P ;0@54=0S/18H:I#ɯ9!Vt$N` l~4-OWTUʰ,IB 'VtOիEj"/Wx?H"&"؆X&葇 WKZ4>ФCZA+4ws֏0eh?H\v_FClz&Ojy%sJ@&.`ijR`p{˕j 7C6_Vܸo%Bb~v5(Rw, b8z#=3!@'': HBWNK>ө&z' )䤫QOʇm>y0I-ȱzfςI,h0.4z#ʎ</v8BW`"pFaxY(xh1Dc|B1"Qe#-^;^>3{l믇}*i|?9/}6A{Σxʟ,'Jc0t^^1 ӆ%:k^8u^S|0m˛h% 8tI>Z-S{X-tLp:MR&$hsgޔ[$mZxyrP_F:?䩍)hd˷W'N;*,Ru|0#e2AY-O>D`=+ŪR DO;)gUZM'{ IJvuur#u1M/F3ML?A9oRWNuIa&=<n`/׎<%aVqN`d[ 8A/S^>P >}גɛ;̉}Dú *:C\~'$&^ H|wOGy Dp4VfR~a70h`mZW"D~<)Gާ #Iq5ísPBG})1 Ra̙&듉,%8|oM&ǷFSVGF!]bߑVԠ49쬟t~_ג:ou|ӑ*7wp)?D3D kf#ۙMUcǶ4e=@a<:p'Ok^HVNW{JZuOCLJF۴#':CF~WEg9iCJݝ&y @/T¦jPti/BHC&3xX.;(QkX4wkvyڳ^J4* # %NRKs]y(GppvXGb2*v1 JHu`  E}6p+ĉ^)P'xCy/nP؄C_ՀR Rn[uwƤkO;9O}篺t<3$:c'{:tdDL?l}x7ϩa]I3a/2Nqcte%yUVT3v{ES Ef˯$p^#*6 7-o|~q B#>‰6opgAOPцVnK<6LSvmW(dS*&Jy[meNsidc؃1qM͏ ӂ#IJM;vIy|'H;)m5IlآtC9֗"*|St~<]&/?2M8Z<P;|J OBuHԜxW@? p)_] K8A~n%Zx&liCքm]!yE_Hܮ8S<Q#;xDԃiuqg1A,܌c0@f KyT.1#{ -3&fwt^{ >hg/8W+G5SR;tD &G$aWhOfIwݗq~ Nu4'kڞA54mH$jgGD&o"K@7 t\zYEN!ׇcJ +,Rfp/s҆$O:y&_{7P۠K@tnwrk5Ik7@kN4wd{|CD;1h- ?rwPˍ Bz4dzg|*p (\s,B~@<:`*3Y>tTGmrW.ԉrbZIxho#m/;{g+rN67cFo;s|d}&Lxwׇ+_id௳{'[>z7QAˤa`; 8EJH\ܥO痕uֈ`u7270w}fIL"Ƅނ0?J]Cʲ݁< YZ]|/ :^qCȲ mȖiCoax? YK__*AnM V%̡ΘFi3w%f8ׯ ɐ1-Sx+fnKўz8۝@pyswtV#{DG? b#7bFG3DwϴRAQvZɻE.ng y]K|Y(q N'p92FLR@V8ǍA]}=-DoW [&_ B=] 5BWH@ NIvGӦBGn,+/ QD2Hv"KHcZ-bPA<_S,#?GL8k?E:cg{4}觼ܽߢ_FHkSwsk_{>ɒN5?ՖS q 2/mJ$eK(٫7iR~t^\Igei/:/Q7ʍf6$ܒ9a6׹.UрQ:=S5E\̒EZ%0gHq(s*dkU^{XPrʯhQZۗbs/IԛHJ]T1AR !pd˳[ C (ۆ&)G4^Gia\c:d#gY_|Ӳ㟸,*t\fy7=w|]gϘD)oBl(tW"eK3>nd+<8%(e bTܦ%C$&SȦ\gEF)N(:R(F1Z2VxGa=W/ٲӗgKDGpH`xf{;Fa ijm .4a9r5z&#b Ce=$@V{-pJ7a EkNK@6X*q?{bm7k`@ }c<=w>x__pn)ͭu2[EZg.F?iM]X޻,6& t9e2g0/8fiɖư쉯w'<{c!e _"/0.qFǥ;PGyӠESH%rHĂ[ YV %e;)ӥ-H`n1< IJ>σݤ y#-bZ޼Fxyqj`_黻-3=ǤʨŞTy2:j[` 1|}Frw*߮ײCm\D2>G=z'_קu/e~!>9cU: (d>~[x)L[:Sgs$~32ȌoY1}ZL* +2! EDgytÙ.$nsp3nro aDj@xHs2c?г $S{`&by',*H>]w[;ζ| [Xk̤lq~Onu!a *62 ߷O%!4%ćv7П]ޱiV FZ#|4jdbyhvq3c+"vw5-_7n|B&"j3崥=6_7ۭ83Kf:J|A8T{}q/F/ׯi~7ݣX@¤] 6ـOF:런U>ؕO@ݪDJ?a0w ќIx(a6q%4;֖Pծ;rtq´o7! }^ZN~ 7X:D%( 4x5FaI$s5}=eQKxMwOD% A5E9=hH{x$T =V.nVzWxqٿI<{w/ws=i6h8 UΫ0gz,Dd?o4n!/l)ʁ b KȵlIW&s /%}L@$>3eS>f(@qC,S)/5p87Syȍ5 AK9e,_g>as- ZU5R&lp?pf0^1:A^GS-VR%Ю JBI||WE4XB3BBsj_blS : ?br\(vu{Ǐb~Hdi6RmK]e3vXSF6y/3A{Q {fTZrhmtRT9.y6B^0I C%!*i~ToXvqFC˘K`HS\7#TJn9 #^g0eX@`T:ȱf5 #2 =2mqOR@_;K}OO+|kz_~ye^~__~VzRμi [7 F~o->K15 a>姜nElH;ʊ6l4KFyk!X`>dT]-{ٳ*N8 ZvSGP^~EP~v()[K`z1w3TԑbW > ~?sxU_l^_pkpջI0k? SJa~ޖAD'jUAQ`őK܍HhikӨX\~_yƒaaIlOTM;y4R"g~z+8>޼s|9ErG dArKԏxu-4?`IfBU҉'ayhtәSçHHȐZU`6KF`FY#;~fū?Ef e v,Ӳr86G 8GGM~&?g?>llq %`/oޫw_&rd⻤qJ|A7U5Y̯ x?u9q%(pkd6| !(hU^gejwȋ |h{Yk^?\=_T b:Q:*ްi6'4A?Ox !  tГ1j҆ûA qA60h避% }/ޟaٳ>n|߸:5+}a&G; .B_ˬaϭވIԇ՛Rgp>?SȦ}8wZ/i_ӵqаBV-<-?x^qxܩGylQO USjx,C0%SZ^ÕqMwK ve2۾#(aX XzG=C`z*ùިTGiVlwX[`Ӏ ʊP "zê:%vA ֬EӰu٥Si O?ﲻ0Qi• &12סY5>7 mBm~NJ%6&y3tWkh/N!p*SkxQy2No?w",2F% pO]~NsxNN&?~Yf}X&s{apqZ[FSSdA"WLkC7$H'@GOZ ؄f6ވh%aEΥ.Sǐ؁A>EF>a c` MD4bw$&N2i!#?&{яݓſ tʞȥ_$}G:jU`^wͶNb@CTA8#=c&/ [Ғ'g~[ ^=:/_\se5W|o"M%Ĥ0P5si!IדF%X8j N#oVYقLVd0%|7nH_ c0n(KJL1z{)) %pK%D[ +1Nf'8J,;?5M`j=áWlS`P4V87}y_y_;\;a3 O05Ȉ0ҡVhyvV7z~e'E3+JCTm-CY mfΪRkf $y_gĢN[+Vio ve eyL[!1x:~ m"C:H8M¤Qg 9$|9P,od, _:ȯ*#I 1д>#A Nx#,vWZqm_;RWuuBI9}.9#jxF%. @sddX )kTNJM;qL({6|WLۋ 7nީS'r7\ŴlaW2f:aoD#J?ypߒ*gZ`iBT~XVHH0R@J@jN#Y?#0 Ϝ⳽s\,#.tPt[:Kyϖ / [ !]:xOZӥӦe+e!83$ 3`mg3ٓ)Ԅi'&1a@/uU9=a^fYm-zn$.b-M{X\ˊ%98qHE27&$)y/k n BЃ)䀻VHF G" d//}sV`LVgכK/~fB W ]9Gt#1?8{c'g::/ :kGuQggrf?\XGaExʗu2E7\{QDTW&Ԍnt h(O C=t 8˗~NJ˄DˌT5ÂxT2B2 E" ycI.VG~8g{Ǽ~>{`>|}UAm,ԧgO|ek M͝վ)輸>9Ns&Ͽofk.:#Ah 8pOkmd_4~ jWjo7gm ir Y]'[L )mz>M,a.4_U Rxkk-ndƐikoi|}6yL6, Di&pwy :t6Ppuk7fGr["lhC8M b6?ߏ%.x o\WLJ~߲G)w, OXUQ{L(v#dcό-H8cR$L]꣆tR>yS^_Q@^., 3"%iQ̬ ,?hcG*3 R™:|\ZښU1˖\quYw^c,ҧ3x!{'O<{@OG] 3~_]#_/a|#+rKw;߿ }߱93z/T 4ҤB6ْMwᶜYkt猆8ʱ Gx)1ka8 *rٙXxx(o8D`,<;&Uҽ%J:,#o ZOA輓h'2͏im;:GL{g$28vIOjzc+~R6$!aKNsıaQ>X?cKǏekڳ-lkys2Ul oJHrhqapäI4ÌT[|]$M̲ 8W/F˯%OvKgݴ3Eo=̊令+mۆ5?LcxT|GHNՍ`HO+b=t9\JmƧ2ËB:by+if4-%D8Pʨx;ڳ*.WD.Ƶ@T MJo kp00Ѿ5qZt\gq3g`?ْ|; y5xk{ Heen>*wF"3@H$ h*fn1 1yZ@s32'l6_Ӷ}yAKz̕J g|$g 'UJ9\'WS(Q*\W[agcOȧ Gk?{wϞ nf(]x cυT Z;\|~Z:WϩAKVp6\o]Qisݼ732Q%^ [͓qH`=o*5< k^?(́w!۳\M\QX㥬>Dw05uۺ >lI#B=(;_W?,+4?7b԰ٿɚZB)#|o,Ky7pg]&4nUW_2I|d R^pYMyr<{/_1 3foV?i`n,xZ}C~ȽZwǛ'~ʷHfFGxh7~ɑ2-kfd\MHR0 /Z2Pj/:t:( ƪE(wv[oͦ| 6u$Ú.22dѝ|Aww^ԇ6I]k5RD_[}R\4L> i(ZՏJ\+VY$ Pt0bH0bב3b/M8ִK,qM oZѻhV-I ͒*p#l;Kk+j1looUe4d}['D0E+ ~* wԠ;3t)Sg%)Sp~/4¢ɷV'F|5XxC{ ^ .jҤoUmG[e~_7 cq>xw;9W h{DQ|ce_2uJ gUrAAhC>$AoWwPW=?34ܥq*nIC/r{/[/i Ϥ}HzȳH]6a|ZDw L/zT3cÂ}9I([;%M[͚F/1q٠ŒG(1%<ܜj] p GPucVtiCy  ::^Tqi/C#/Ԡq{ Kt8aG7J-z+GĠ`ڌDXL1W2B lgŘ7\ w%4U?g{=G_{oEVd=.x;g/k} tC~t,>tnmuL}TgIӿ y_|{ eqvsY~0f3YR{' )ki^A:R(QUTe{=ZJ:1K΃$fi46hWT:CJ~L<3o~_r W8dO[nY;|y߳deBIx,' #q)R'z\qUC:Y g߃rGP.ݫgaZ}r%Vq+8&Ӕ5h:SFu]4 A]a]ʨOIkqCA&S/ɲRb{g3o4_?rzx]1\Xӕro.=FH)ǻ],ɉ{? }** I# ivP?jk_Jk6u9awP13=U/4moo6?ASk בmhǞS&4?~'W0igXvm [s1w|58p5>C7#ăh G9i 8%4mgrp w~rrWt8*L3?66)J]VU'[Wo;Lx8+O|}9/$]s״ A Ä>Weޅ^)eH>TQ#+$dȉcŠ[*F!;Og?kz3v~q/({BiMMw$z_5W}ɸz94, 8h$unCsDE a3qƋ2e_4cɰwC ?D:],ֿ+ŢC aY]L tuP!H<( ۗKt@):ѽGP/ߘp : UAW0ДOy' "WQzg8񦅍liެZod]< u$)hF9D/d’L(z7 (X +^K Ƈ2"3{@Uf9tt+^Y2 x!V&闷&9yl}Ŝ 82N-~TGDV j`GQ2v{1Q^ɵ\x+̩BV5dl'; zOEVFF h:)Vc3%Ӡf yuxQ(u UFX4Y6ϡmV{ Ҹ 8c%"3ɦ?Џ| %St$52')̛S[9yp:3zH:r5S+ 0};l [{dyV__w7`+.{0~'5ծ֭ho]įC| =ٸ )n{~{gCmuʫ=l>Wl]4¼ξ5׻W^SQɷ+][+}G!t/CCiY)?EQNȸ=cH;mǎP49tmIf\iѵoo.N냘2+Rgɞ$QlK nvX+A A᪟: *(yf摎c֫r({ gZѡ=}*A}eo1 0uutiXC3 cR)]D j8el`Cn |LOA.ɲ D4:2>}\I|`9PpľR?r0wM80ʮXaoe?3*5lOuUO~"̼51=)x3=y|i3\o]YHt㖖1.3=Z w[qF^R~zƯgx%et:; Zdd:82-_E(ʝ?Wڊ>iW (HFބRjL=R-[ ^<ˈAeʎz +!vOR>U%-E^~p;DE57MwN$aJ.ހg{3lV 0F ''H߁?#ex2C,*n2W,y 36]#,bq͙UA{2'vX/*c6]6 X?Ai0yXa C2vCAg,Abf@|϶e4(.3?cyɿ] p|Yn4\sgjF;s>=g+'00p;`` ˠ.~b+d.IȡwU];e3&&6"0@5 Ʀ>haa۽Hf@8ϪYM]^vk;!oNAS^C)31-->!EN-~|.+ҏ}lV>`+ U4wٲFFtȏʗ`LqoAs^&H[|~.ިsHNAgwFKZOu0iK+N%RI]N l I"΃`9ЇWpEJ"@ itҽY$18}WI{_&*N~0bpڃ^2lAqғO]( GvzVNܳ >[®5d'yaW{} 쬕D;;PCbJ؃3)v7OKN4626K]t ˤ' gh &玢w– PJ*.al8֋[~Q0Ci.*+dmBa[<[ě6ܦyꞿWrhyn: %_?Z0~ ݊9òTg-ЦI5ڛjÒokcM09z%NA2,, ?7/ -7 h/n~!.;nY5A>02mb_W&87OKƊe2=J@:TKGd[F#gVZ0mӴm9OhbqZt):;ƭ/XG;ii;@ݚ}Wj/b_Ӱ\iU]WQK75W쇙XVq&j:bEFӍNUA#VFNׯ<_F88B>˩+^R+ '@\qOG쪒^i `khۈ2IҬ[ w Jm<~u[Uބݷ|kx0p1mYbZp3iG\'#> }RO;@7rDNs8~A.aH _U * ,m:Bm& ,L曂h#}~ [V7W//MY{HCI:-6ȪTQwCjksn!`,H\Q+}D" d)j_۱r^Soatꀊ1(8K^OU!\w}ȏݐ &@`Wt4,udh2t`7cj?S=%;~eT|˃у- Af5)[ngz<>MTpÙ,)+bߞ~`4~a43{ۺj}+pFU~[Νv`ρF`SgdqR,D-LC0@!k ^OrL0j0Z7[~i:/޲!Iyddj"mb?>FԻHk4tMFzgͯ\-_AkbmnH MqB!z99{M!Iϩl"g-Nq;_{ɹ]~d3M37=VeT;="^jzt<3{>:pYv9a[ |-W)J.v㛽̈́b7ZKoiܖQb|5Bx=<[Wo;ȷ `^`ڗyL#θ?3`) B$\Ieؙ=!#V#>^Tώ1=iptsOr@P8L<6NIqY%ӷ3%F@ypF8c d2'37@={0)#4\qXJ˄\rnpgvsz[ 5fBWl=M ]X" f<*&z%v'_3̱AC;\m Th,ZLcXs&᷵U!߱Ixi֑t{s9Tw-P쨴&u6 aqt:{[L L^.%"vw֍uOM0[G3}!zoy;hֿ_GcϾq ;.^_] ޯ8r?W;+ ʶecڞ:Vg{gHCVp뭬T>=& =6GAy4I#}t~GNOA:D";4]u)@K9WyoH/?nl/>:9C`ɲ\!Hj !Ӵwy:45վЄ8캇 a'8}msZSO2:iQ*AA]Zr-Gq6g#!DxҾR9u|jc/(hi[,Bj4 <(*^C .&nDfZOLҥc@ ƉCOO?.y5tCVɏƶ&pqI ڡc@\~=8O3o Ojs~N餳MDye;|yNˁ ~yP}P'YƊ+iן6~ˁ`9h!K&'j#2M 3OtYTbWƑ^{'a6e;/ʠeq/CNIVlVIg9)ziR4J,0,oI_Rq*m)Wq:OV8:!k2hu}aL[̀`EO1TaIդf3Gbsk2puC@n8rՍW^[.]~8`"ꯇzt>)[a e:yP9/n ,䐨1[kJK7&W$^$'twh}bŒTbEz?yJ< ߈0G#NـVx1 ނeΕFܭjuE͓ %88_x.y_ }]ʙFr;[/Kg6r:CuK7Ү[ Ftΰ"o}JcUt=WL/,J?;Zު?iHûB~D>FKp)k|۬vSf Ҡet$hֶ;;;>쬮ui7 1tl}%F7aQ ?:X d.:xHt~`$ Igr6Q+T4Jҷ5 ? $Zq/]Dt',Qvt3;3ęԫ'YÁZ{se&t&Mⶱn ~)hgtg4EOdZQu*aFC靶~j+2/Sx["D4>kh? Qufj_Q$Yh{)G<~gDM?ll=QKj{hp8F|LjRbţ *񍛂S{gr<#Ic2og,K:} ='WTa-O݌RG KLep8LؚfC3;9򜽟FTD\[g%Utna#:5?:X  HQ*c.NJ(OeTaFˡ"`7["'TX-JazucSu|h;fLcCwۻ:(#ṇs:ba!@7^V F [p>zO{n>$d;?hNh,yY@K;x <z[IP_b"?l+O3.1rPACLP/)k+;Cc?"8rii'Eʘr\&Fd%$o# C:qGw a%p~ (a GWxZԃΏ5=IYV AvK9#f*1FW*ʫX^U7ߧOåj7hg?n^r5ʌLQ:0InWwO C:zePfq,Q/v g~}V)`<`:VwV'L&7&kh/D[ lG^yh0꥝? %~pAM 1Lx/Yv?$ˮA9 \0 @ '#4*la,xo! DW`{Oڹ~&`JZ}Hdp{ ^Fd-Jh+y^M/q~B1 5Pl=W" 6ˏ+, wKKxGFΔ6 B53 jUgx^t!o"ڞOhAI/ -J+Qkk^ 55y1I%B0-H[~!ړ9%Iuٰl{Fd[ FeD01I|@l]]8|.ZB\apND\0Xk41QHd=tBqH"1^͵t|U -?xmd#Èro<떛ّgZVHӪm+ɒhX P9 dFN$ltz*<.\<;U@o;[۔z?BSg3H٪/m栟ࠟwUdϟKA&$Bma`{Ǘ^+|LcKT3-\CIpȟZയ?i;[C-QY3vR{ ~!]Jߑ64xXA/{!i~ 333a°V 2\ꍇ]F?MxggSxo[wq ΆA ekIr0*<ôCEer^3[ۏ/txwedFx8_z;0+Yx!J)71"暃_4ƎܸHAmOW QJ00PZFU_;|=ݲ[$2*˒/̟25unJK&liL.4 e A/^/n{\iWt H|㯇Πx$lcݲ[J;S>xdʏo;}@:MtN7 vgK^8j,LJ쇡pճQ:(.9,3bšw)Q4V^#/rxLT K޳ \qΆ ~C % - ËgJ P,;+QZ(_+d o eEwZ0ISM^$"jg8 .<=y ei% NIZ0JL{~G>Cg\4kr`@ 2s@@_+៽0[Mtt >/]~jAo#Sǀ3ZC{M|^ x{Uȣ~3x HQL#<^iLDfbcovF{W&gGNf ~޳Si‘e3&—/{{*rr-goDg[db`?]m_[2(`Fa'=+|A;ySR߇3&!Q9HZឯNrFǶ .w&Qf:\<,QP{A4+ë ?O}Zm+af|-%q?Β$ :z񌷎.?Wk? V,Is%HX$HT1ο+"Xڭ42tqih^r(>z[ب/a|F:(L* &bWc,Ówj@F:m4RIS R_c"i†<)o~Hi;凵XF'6pƷuO.xRT |]̈́22<Ȫi:Hx֤}!COm3s@p7ʐm]Ұ$L8xܢڡD_h ҂]2zLspwsA[orSV,l9Ĕd­IN9`p)g4 PsQz3ZGhS@ӟ؄sP"EA9(gt  C25[lR^8@&Պ.On7a*׺` )9q!^s:(>jD(v,e@FxϥXpgJ&fQ}?`e;A>5-@L1)E#:f j$̺jL"5YCLCxI'J^ *:|:Tn((Z?qm|3+y[hq7(!̓ ҝ6 }&~<:SȮ%ٵK82a"c]1V̐k:(Ta+w4*)*ůhvziKs!f!j*!rVt`m}t"xىx:vr9ЬwhoMBXipbqjlOs~Ji r'/ 8yLϷv<?6fʍmb9_`gt|N'M Тqc$ |W6N8pu{5: ,#g O=ɶ̕f`Z]RщvY1\:q(G-iM~5[<o!wg yAb;4-9ɧ4 P$wOvGǟ|z?rߟsC"?P(d)g 5жNكg] P*(9&!m? r+>c7,E@uIHanF5?k$V˨Y56qCPJr#E7B44=÷ŸҬ _ʁjccvƒoF׫M' &cA}${MKCaa /4^s-p q#t{̼ TUFAOTjwjF2d ߠ+l=ObDڛq 3ڵOđ`abەrwyB,Ps9[5Iz*+p|HJH]eb/A\:|%kV!ϵp^?koPV-B઀HPUrH,KMv S`,7cсԙ18(zkvO}ge߂vJ|-Rayӈ^otf-ӿغ49` ~~eAQz6 ^~W~< G.׃O~֘wQ?^ t[ǥSV䇍yX3MtD=qS"z%9}Ga<yLꐄ)V?֙9SġcEւȾP5l%2\B$ՎU\x+,!.vBsI[پ?38 h& b[,v$&L ;Jr:%14fml(XvWd2aا4BRaH$kAlmps֕SL*853te+˘ 6 :K/ԸsfC |>[6yJgS=QFiXzi;l2u5= fAbVIj(,5'x9CO.pҹ":O0Uid1# y'h '1J̻e&]!Sɋ8g6()[#2篒AgAv}Mtd%R8mb%xt*P΅A+vU)v1zjc.}3:3nA2:.œgl!x1f{ Lv@$Xf&?yp >u6)s2ES7w5Gn[9U\u0\:>4AD% J(jě|+WPf M<倌w3 3p2'*,ۨ't?o7uc]qo:/KC폫Whqo_-Pl$J1Em)ܻAD:g@6tzރd^VMѧ!6U -.dBp㨬z|@|!z߻,g]Q)lJ1)nrʳYaRO̓ Kw+?**2fN#MIy$7 ݪ!=5MWқti9ëÎ 7}w xy M~öN˟z3/ ^zkx.H[h=xM78ekTz5 aUWiړ.6O?:itW;n.SkmMDP*[X0W -3x? ڼ R Ct>4$M7.`ΓvdLf71q’;)ȢEf :1 h8c0`I0xZ{/C WTCMw"Y;Bwx#-Us4gKWd޾p]2[xltۗ4s[嚓^u*˯ ?<`غ8&]P#ƸV-;Sp$XڰojLqFڽu*ĜX13dy`/i=W~E pB' GaT`'K^ЈyI'|0`Q(6Y꟏Ry`@ܦqҌ43~{rkS+ݢ[ҝπ\I}kHo+K-dyXh4٨ՑM~J +L B3~Xqf`Iث1O%FnPw]=lx;ኛBf 򢕐|v6J󝟷Z Fgp3ވ AEzD%I21rj`bt]hDO`O8Ҳn_ҡC&x"?[tV2F":?{n1&+SCaɊ o$зM6H4_xd|5 f??"o荄g6CM2qHpH4MA+Y:&a'`G*E-g# 49UHcv}N1t؆ ,8/(8:y9fiө}]983ܚ=R &Vҭn/:@`hid=H퍱5z-zn?~i2m)_*oOց8)?k.q=] FvFYS}'4ĭGE~Y0 %Oî%Q9(d.iJ|иu,ܦ({V}/: xgn%<7?FK:#^;//?woҦ9dzSvfM*sF ITj22cHwCՂG60AF4MTt^:wŻP*TBϦ]EHSTƞ&2)K3_su@\/5kOk CC1 VY;9 &lR\hW~w6X ͭ^^g+'@ދM֤bVsW#Y c eH OĻfR Da>Iy:t ;=7ս~w:yl=kq}:͜52aא%WYc6. 57&mbt;%"1VׂX[S8/# ^c{ }u6ħ@1Pr[+2+J:%S^2 Q"~|p>CyԘ'Pİίr~;|>𒦛wzW3OǼ܉F=W][KP\YXC-][`(8,b-dw+[|viXZ[2ۘJ[#$PMKLBLi乡?sKF?NY":I!+c6#< ŨFQ.#bm@i~ :*/',cHN`^nc}_€c,1W! ?#<)#/2bkGLē>O4쪖Gb듛 x'da&@*cxG^dӒ~6z?>[eer=VIL}ΓɘhFg.>USU[ʟ"{tkM}HyWkb­s?o .y#n?~dq~+D pvpC7 ȜcM39iyDCY,\j\=S&ܚ\\O6~@qpx ֦ q,r*j/1+>~i$_D$j9j2}i/ U tԈvB: u`=M)U܏ŮsVKgM_ "I)dU%p5PQYML8J]3j! #YBľ#]p079̥WU`=ګEUu(̟ i ,^A6̓&cwOӵLQ-jPvG~zd͕%龦ۗ7^ݙN@j,-[-cx,18=Yx9OSW\yL)~nV 2 ۱;/\yRб$}]3.T<L̉DW _b>2@ ol^5",*#oP6ZϒQhmKOL6y)pGL[~A7p$n`%%G: wedzrEH,6iw6'~Vfy;]&o|+W$_`~ް-= [}T$YM]~'Knb<;q|+,}Ir I Џ -:j rՖE̕ +nQL0gkd"ʩ>W~_̬iNZ>N~ ^sx_<:ǒ8WX.7vفph Ԟ*l8m_ϩXNĜurF=oZԘIq +zZ&?krkPPؠMgtw%WHquR!yFs8) cs$YҦCd ufTPz"$x[}K@/G#AlvT4n^QĊޫwd W W~ywFcKUrV+%MͺD}|)O`O*8('RznIkG:-{ӟ`NS՗ vb74A#cgF\*fQ *f |`ąʥ_WM<\jWDk6h 9#\bըvsyXb @>'+Ү42R,x't8 C[\b~:%$!4[Q= +* PW)z'n#0(P]@h;ƅ^t9("Qe_@ݣ<1 TX1ya)%,ڰNP|gp QV?O(V[ֆËx7?ޓi ݂[d o;h8c'h*Cy \|o Wrcu}_ '= 5q K_Yi3 :di=zh=ۓD*EV)'O[ie1J~{TyrXJpMq=<1Q'ANL"qS x:2Xru瘍R n<'/}2&6? Ǟ  ?Q!ĖCX `~CfLqԗ*3XJBK*R7( '*XJАMv@لY*v~J'q08'?g:',H OᏼGOmz*CaO8 f%'-~2 ŵW}AWww/as 'u(]Yo*D/n{Ml y(͉*<z̰ NfWĬqů{޵ MG*_+P&"yA:[yT-$9@oF$|}RZ ]C$ e>W >h A|u -9܃W'A@-TbsIA*NiՇ 2I%/@l,[ae_1WBI,+ͫѠ |jG?|ůG}H+n C+5l*p\%12pn}ծk|3VFU,rqe.h%\4MɤG a  4(V̫U Ŷvd4?&Y_#XS.R}T;FO0V_: 7p8ѶppgrGv5 c7WbNq^q%XxVW ljnq")ڠ A~|U* ~~0HǕ^KfG¸I!1m,\'(C=e#|EJd}eQ޳K&[7vr,nVe -ͭ:+a^׺R]%4l705В(z_ l  Qlzt+5~Zݫd~I:F$ˆ t~I*`m@oUd+dqd}rĂ~K;uS=WيBGFӈ|WH)V3Yd9#'lb7?9a2D_;/x: w2[I zoN?y`*_ǀtJ(H| e溅A6rD>&F)H$' 2#wxABOH.44axyI6Q6 K5(aMeFdnh\I/JtȢl>WϦnHzYA;bBM:vFT^ܺ%;Ɵ[oXF@:hj?;Ae}O~-wVt~%~!$ %|ЮGyx4N6wO𠴠 X2EUwxjdY'{%rC Gk7!68D2q8;d$.13Də)ڝD-O Y:PY$IDnxLAޙ|f #YQi#HQb+tᝓ>8CyZO]Q6NT5Oh]Jkզv @Tb2YcSg=V$.8 bu!T>iXҝ]89٩bB?LX՞A&/^脈}%0o@D' :X~2;dӚ!Ah)cNSlzMΙPAA\FbEe=)U~cb+\6a'?,TTb62-*." =cЙ:xb8Bc | ms"G?_O*)*'?&ib cr9_eX@rGzE Rlʡ@1 vIS#? ~ڟp fEdz<̐vr"VkDp_Y5,:x1Aс{Μ&ElVsmU,9! QftXzʚhaSSe obhIO'&~ŸJd󤢓 }5}K+V:OyB3N" @S&26 8VwYQ[6 '@)JJ)ʊvtX@@"KJ49K\;'1)sMnPn 3]Tu[S.[-{Ϻ'r/ʹts19N<3yx++8uԂ? jXi:4`v~N܎9t 5HԛU%^pohy<6 1hGO;,T XJ㧘ڏ.󆅶2ʩ({_W˴e\7]j3S`>Ⱥ y7]vb|X4ej] *l *h`SNJ;Zޢm;*cvYMD3QE& W$! M7p,E@e@,c2]q{XڒaG(>WՊw>@sꟍ;җP+>8PiG>_JL* O>@ѧyXHW▘KͰ}zN^s2Cޜ Tk°#</뾗=XN U^Ӓ/RAhDlG;orxZ3~EoܝW'}nRżQi8j'|B?tӨ =^EBpi{#.Y"dWxNZ@lHJ>z".[&g@[,<|Hx8 |i%tD;7y&+H3 MRfo24Mm rԴ%X\U0&!rvMt8\~5-=XSo]NRKRֺu[ @o^SP`[:v h$t^'xdv^GZ\">ëxMx^l3nށ<<dOlSD"Ű?̛h({4:䖫9gm 7<O'}uNiH~gә н((Է⌄ M+`6]6 Sɛd@@[|6FRO} *bU ;^nW(pïҠDfdW6ާd[~ $f$cX6,F 8B#=po;S' LHI[ o6T+*no н9/{F[?5 񦎆 XR@_B>M#GG S:'c\I$~ e\89op6|ɷn|Ȣ+pXf݄}2cLzҟ{LV`G=7c&?LV~93r<3lXY~w_ѺdzI`WV~KdHFpz$O:|+~?_)!v/֌Lfx^4"m# x; zļݕ?S"gVYS7BaO>aVvBGY[FH NRY{EPKm-ܤSW/(̎lW7m'"fM+$fRj@NF JYaiާ}*0&`!Ik 1xe8p]}mp9§ ۸nP!iQ[ Q*z \2>/6Q3ѷvml 1pp7[|,RSغakTŃ_mRv}G `s,\McHgs!*%NՈ P iKucuٺg S:ߍ?tY/z@^_`:LJq'Ayඓ?:.:"[HOoUar$k86a ,\V[0e`^k@e?6૵ofp I5\IOg>-cJ0fTt)?z{W|:{בr3݃e@V ) 7?ܡA{j bW8]M$eK<6m;-'7'o-9`bs5[rbvY n \ӻ}¡3NB%˅fhy0bB 8q`DUahO9s%r1 G]\u#R^ 9}F퍬zuSWqۃ,_EўXo>zp% |[vD*,}y[Dx'F8Q cHBXfD)g]u0.>ȷS|MadP§pV=@#YH{®-7B(~*!Y|0ҎT#nGh@?F'!Eg~2Z'yJfl!M| B$y8e zHWRL!6eG׬m"ڱq{{G몾招'?*>~ﺰq &9NRP$ŏ 'OیTAn|THF!$\YɈa˩ At\J/}ɃzR2#E^M]P_9UϴN0^WI @ҟcXit4GI i~x4P  lQ9\JRDzBlG0F{:0y>WvYk b[`-G,š\֡:d!K9`;2AAJ[ pg ( f uNH(~Y(ql."wDl-5"e F:/;BԴ lkyLP ᶯVYexqF&nJݱMrbq= m2W8nH-fP\GL9,N՞oq|-xx!5o+ ``kt9HЇ;zCc\0Yri 'CVS@4h1d;IƬDZE}`SNƦ~#SL _9n_%p;TjP>R(ZbB%@kN2 t[(AہdjTq-)F8ZhۆƘc1> vy!*_c~x3IǤidT>pUn߿k_BPﯵ3d#_$xm[58]\!uC?|6 ̐Ch* ' n,nK@Q#(T@api@JKn`EQ4Q4F넮 E!DXJ=1R+9WYʿ+^*, E)1lf5Tջ]bfqCAOBN1}9ON:$!Ru}U,RݾJ>'>IfcZZ+} 58x:akVӈ'4.i]{eqBNj&/4a6WE“~o~v@ \F!"d6ne1@nS0/`_ ^*K`ORB\-z}>8|#/Aؤ3c14Z.%Y!DJ幑OcVntك㲲PX'P]eoy| =vg(9F`7T# wzdx4 ?Ĉ"+D>P f)v٢L(S/=i*_W2C]螿_SĔ#c>{ 6'b0Oon"0v2BӞ;qLf8d'>M8o eݔvcw|2څց'6]x5;s3آN {r8A:'|)o=?Ӎ^SZg~%4Pɵ^6f+75V[z[Ƨ j<%"n[z,VP~s뾉z%EA.z֡+32f'*4:ѡȷ+Ё~G16 -KyଈPJn70Gx=LQ!O|*O>z"uwߞ;LnH/kmըֻr|FԘHVvIL.D62)kג6e[q7z'0PEQ'q g9z۫c>A c;#aclu=8N,VLR¸eI/![{*%~ F0׍(L1,F%Nmo 6 =<)bXHC>'+GtIʙs2{0Љ>TcVH[ Zx/, |] X¹[&г!:{"- 'W]Fdd`.ݗ9p^ 7  )L)I][^N[p܅R &|[tLJש߶|XCʈRq=G).ީ/w?U{Q߿@'g'/)c{sw*HoLHoh!,֌> X)#`a*{'T j5(Pry: wP3x0|$ |!P\XNnJw\`q?J#WoN޽JN={'8},C6"Jjy4x8A(ojVs?v n  lC@>,2z2t,> Rr \{3o#핯U!f"~`0hB"kb&?4\Dc״ ]{Nޱ YmL1 ȶEZt!@ 6JK㴟Zt`WWJ9KҞ\@*Xc>J+ %ss2=+-R~ k2m=bfcaAc{.K<-u5?CkڹNxXyzs _tXpn~WIbC~H#ϕsM LNNϕgg:{슮?4ay xi-· Ѻ"¿_e\ n6ĄaÙVNĎqE_ `!4(3W<=>sgl$g/鉄 ={}Dqw[~2B@'>%B=y +| 3RNKͧ,_O s[9pcp[,%wc4F]y"Uٛ-0y{R.Sg ܛZP ґ~ JFrL Dʨ{ =l[iG(H&@:>s~#e,fw}\'׊_^ޮ?+R;zXNeՇ߿x7W7W~h4\ksׁ9#2xMѬjt0@CmRA?0C al&ƲO1% ЉQF]1ⶣ#TM"1r?j#F,5wb I7rA@@磻[@?{QX#sL7S-QE\#~H"P)2Ph u_[8ꕶ{ LXn%[d ڲtt%YK&ɩ%6uÙJ^BY?b` eA[{K{F)3JVYv[Xf8nnu Ho4p}7nxx3U'hAŽyԅe,AhK =hM>jCv,UN2fQeTUO*wjsN<蹂%^ad;z=N[[ +FKRމ|==!- ^g1m"@LHB+!|ޣ*qP3,vɈ4ـ\8d'q IV^ ѓ|2DZV/MBfaק[R(OlxL..+uwJ20^tXF!5| Jb?7e`,4NL`,ACZ 2+YZtrц%?={VFsV,:72ɕtщpE~N@ܓͰ> #1!q:/`;:o/[_lq|peG2Z#:+ҨAk*!_I7G"PmYC)pT?L4 P0C %'>iA }-,?US iȭC-c\Ԩ/A~ț]] \[+t}%X)Dʸż0֞`9Y0PY-GQ9,YF!~1V1qo` 1#h]:86ņ#Xp1F@$P*5nۢ#6u봥 h؇*j|SACD^}Ǵ:6vlaw[GrzI%KÐGY F h^?yE'U*ޗ.7#v'لpLtm"`QX^,q@OEJLx!XGUrndU ~*Nx9JiZGk4UnjC"S|0._(oI2su-Ԧ/%wӅ8·,VRjN/t k eئcB o !ΛqG>#L7z(OĈl Koip!+DD?oV.taG p0=Bs_z|Ub&d'Oqvt{+63Kt؁yɺ!ݦ @o!cc`Zzr>='4eƟMA<#@Ќ>O;_ SA`!&䌸yYa[ԕ~.?]MɵR?'RIe;=O UbOVP2[W @WD_  W&faB%< ?mÇ, B&L?Tm[^;2+'vy\c@L\L!:Ǯ3j?xvWGQ~M"b/(X)_,6hL}>eʣpX2{OۣixE{տ~3 zhOqL1ެ{]48f=̳1"ʓaoLtʒ,ʒ F2wf㖯[Tu Du ΝKkTTZ.|ik$C 籇agnٶŢͷKnR+Ȫj2nߤ$ƏoYqI=6EK}&co%g(RZpwQl2D.EZ'f'lt1+)h{lCʢ윫XWG"MÕQ?"4a#m0U.'oҾS7o._jp~'jQV -W{BZ Ӕ/;Ph?/JV7`* MMFk*<א"pL#/^;^-1M?+5Qp;AVi: AsZ Xcq jh̢Vtɿy %M,< )rBfq8VҜj̣%3̃},AFFnwbRGSJ9eA;6ea_tQt4ߣoʇd6 ƼӃrrQB3V'Bk4~q{SwW1=<hj IJd~PhۃATk\-yTf >K#nl,/u;揚xW^?1OSeZoQZ2z V~Je')Ka@q#zvɰ ɅwF0iC b5@'X7'qҍw6MN#2!q+Ѓ}eSr )ٔ& ʷRQ:Bb"W68u`D1:-`CRZ2F_cb' d6A3qt3B:oG,],f=!)!bg!Z.&bHPkx,F&ا"C!(|!B%#RҔ-ΪJ_%g}z@.Hvi {DlMC~g*t;kf}|IR7mOmLȵ"613%e,\_')_k̈́vHi۫rp]Q ~2N,OCۭPO~. Ʉ <(FD2&b s#RZ'K~xARbu @\|JYóOUlރ .ċW= ]GA{Cp,z8 tl |hXxHdȳ&ϻJEH$3cWO_ϛG޸SHb;Lb{MTo,*o: 1G~S0n4a!xebdQ|2/1e|I!G U;msa76U\Y"6"lÒZ􎀅4l@72$XF ;.'f$Rd'#[O{X[OhOd0P ThX#G5ު>%*@h?u$ H7?Q׼A gt 1-%k !_ ƷvwQ 7qH̤uh&QU< W֥?-a%\<4[emQo8R QrGƪxV_ Jpd`|(? "~j/I+Vٴɽ^ )Ab%gsd&۷x~j88GJ O7DK}+H%q+Ć/ To QC{F5S+ߍ+-/IV㾽x}r'*WZVoZN%jm˧DD} 5)D&]3VˆA6e#K^pƏ!Q Y0f?=@ROtUbsO`Z臇@Y`N .XT?oA 0߈:*x,6iry"BgWtJNŲGQulQH+t* jc|4;a#?wtO~o6P]Ahf dC"#Hbϴ;$l% mby.v#}ĸzU0l=$/*ζ_l[Wi #RKmxx8oh,gTH{>&7) e)oH%xQI&$<Ł\q٫'@ Ě^y$-Ǡ͂- LI8i)_{x2 o \M>.LJ@Tz4 f~*S/ǃJO$9 hqʫWmo ?3bB[zo -F :xt{QL$0> \{ s]0%h` njEeX+[p z+2˩RHfp5b&Ol$yY)V%VtoIFV=BtCɛK.16H =Rƒ#U d="^I A 8M'^\+!PZGY:|d“ r sp2. h.9+GQ$?C ]EObޢc ҶXQinLBmD̲̠֗4m$+JxgaItVEEKŘv1tࢗC΅FLkū`} 9InCQԱN2e*mf-y~o`NA=pF^mhOB HeNN1a7=d^p ?Qy-}2Йy%[-Yb~1xnt˘ ܳjO  6yKK(K|j/ {Uar^KyPK@6=D{HO6Bo?5Vۃ#=k1”S [搦 7]Fs+1Wj*'ط251Pzouh5)ԌY`juI×=ULFy +⊪hI\B0?WlIhJS)&m7jesP  Us\Ӑ6l]6χz˟%C@'dMx߼ԽjZKIN$4-͊QztJGR#pid* wBayH&PTbd?[P::)`Fbp=Ǽtke^`W[!>%)6}б1ܣm~Xt㵙kMW,[|^yw~ۢ} 4B{8qHlB*8G9_4F^4C)87b[oVdoڣ0n ΰ14"n5XgT1U^볓Ճ\}T>\||"~&'wqV&'Q  MPic*MLp2QMKC-<97-'yc:<!Zē&?Q_m%L\-T|sk֥[6H]|ck(+v:\d3B?,Z3V,z;ԛG: ^ vGkn-v1US(Mvաvڮɖ"V~+?-U_2H(RC:̏O̡!K,x&f7h?ѽAzd/ye&1Jw#0/(i~\nOXaos2"Zn6?".PʴI[I1݄nCelbS\X&m!FyEbpsoM$}k; ?DQƣ=n fB\}ԏ4+ ^Vt- 9 o= §OxA+A{s,? |Oqs6 2Um#ˏ%* 4R^]~'o^+5=K 5Kwnt\΍0kR i# x{c`TĎʶ4ʿ[}"` H},P!P6- ;,5.&z !3iH1p'A2ӶO!@׮:8ڷ]fx8+hpF[A<"X8.B5h/a6'Bɐ6pOm1Ati({>=A\,GC'f7 4ĨyR#=;xjx&8Tp Æ+ E%F^Q{P&ׁp؄CEՓ7WhڂlWlaPv bߣS6 qXnmQHYדu'tcߟ9w%QCZݞn' zDkhE(/ m0lW |Hc#El g8P9 4t b L -7 S6W.;J@Y!E_>CBq-l%J焯4RD^@lznr +Õ z\AFdtG ]Yn "Cm!6Zn5_!|lNE< Mb}'"*1p ;OPn]!\4!eՊgA\S{S Jf7AepGPO}(vP[sݸs "eN,O Ec.MP'cAѩZ8H!T_ P@u2!O,,zeLC= Ɉ ;t"TXzMzwkR.dy; h 㴏TR41@1jv&X۷6Sd;7MS;! }7oߜ'_?_᫓W%n h5W n4h7N_OhhP95\fV0lL>q:HS4 E;:` ? im|B*K|G08Ƒ0@h3Kz UC|X?L_[=&ވB`G>p OPz6!kdlb= fl Hla_I|541vCv`\ 3`rYf~]ji*XHV͌=huTTn`Mʻ1y̡6%#B@n:A>|@ ^agʣa) δ+µnRLw1 b\`Ppw v`s=#@IDATD أC0Yx:z-O̲,^lZ.D#k”tڟ.xTNl`!xI ě]fm[j^Q|Y 0$tfD2 9KPc` 7Cܧe;JE'U/ZM]=~”J0NTϴD\xHǛTe#$nĩ+z^ўEz~~gNu OQʟGJ~7J׊#Zfoz{0˃=bUwUa@ TTH+?Py A _34%o# fU):}Eb5#5o}}Bfh1Qa/O7;y^ч~2|ys @ރ7jeZŲ~ǑD1eI),>ߙz6J+n?{ޤWni֝"de[lll4na[JK$޷ݒՒm=Kf"H\pNWY"I ʀa>2|rdMFNTiMş׺:ZuTo7#R-$\㬯1yr:$0Nۢ6YFD] 4RэE ܦ< 3g6Gyf]t]olOʐҰ$\V"z/q7Q􊟼I1r1*іZ9>Z^YUbhlyY^<~I7*P:BF&>w.&?ħdM6v7օ[Sd2wkRb-cOY]J> Q͜-y ';oV?At'?=͛^\@ةĿG&n Z38Wa[>PC= ΃yY:wU 5Q` [*HT5q| $d5؟:1h!P t űϤ$=17W1 ~˃onxxϡFƊ<&+b+L>kk &"Nd9|k嫼Q pG;*B]eUgi596='&+PϐѹC\TژBSep𘴣vDit+/?n'7GOLf ]PG&q!OT)=Jn>jт着,UT%:k y$r. $Qfy-LT2 g;9}m5Ar>:Io:2=ޯ7R~XpI|j\T~'AiqBLj;i ~hӇפ ͖ e6D03 /D=p s{wcW|<{úb0~|vp5ߝGc4gA䑧?]=cw$4(:D [Z|@|N8V4^^\'~Ur a̐)&S~x)b DLg]7kںdcOlpĐ@FoT I@( D x0;$e!JVpp0xztgr3' 7؄h{ "T;ɸm$mzg 4fv!5ꃀ?S|~VV Bq5&%Yr9Ty1ywF`2iGEpiUb ߨbQξŒ 쿕E5P&{n޿Ӟ` W|>0gnƯ?||V/&3 ]];(M3N_^qL!(ըۇ"ww%Wb`.TS|je* 5Vwҿw|5ϲ>L+Gy$Fy$& ڝ2},ylv<MVy]$hbW[%Jna jtfV L3v˹!S" kyL }Ѓ S?77%ͨ;R*苺:E(`M ̈Bakx>E['Gs<+{4g5.e*Sk~2;bw<#l (v_ 4wX#NK-R7"K.I(F}eOy A4rD(7A!Q БFB`~G]0~x df=d˘xL]&fi KDӶg8EN;Н;4Skzx&R4Jmh'cwţUĦ+Hg&y&ـl#.5tKw6 [^}ۀη;Z=j~*"S~0Il7)OLFyMK T TH KԝYrD$M7v#%y-ԗGTH})%j NnA7]3Q-'M.q7rz'ef☤}Oqڷ؀yRwoqtO.^XBK: p@^Vz<^ҩMgw ÈP(‘eLB~iMwkKV7RZ+8Quް=_Oޒlrr!wgL]\|7bpvwL>yupy<9uSya?.9@&Gn- Ȫ ۥSA!:`k+$JPNè uYq86Y3>plA,A^|xߞq"iWo~uw|3:<r*x{,2[p {s =YVLIc)\/vp)WLYAPC7[#;{ȡG_7ufqP;oV-Mr@+( K&nQ?P~~?=SeXB|/^tS?]LxyzAS=?Ĉ+KlQ;%vS̄w-V8r eA<,ܣ~B&!WH69/,Lewn< "B!gt{N ?$48Ƅo&jZ%kCP ABzB6jSfQSrPnek3E"m F;蓿Tp3b&>\ q1s^@?r|c&/y)rqwܰ#חLvC@ܡx.X~V|35`,b Ss% >ҁӀ+x( HDC>VS%y3a= ['U䞞K:k~ ;fC^|H`[4ogW8փ;NRI;K&&;yUZt3oyɥlN#E=٘W>T&7vaV8v$"KkيM`=H\q.dh| G؉}ק]+N*l׍~]ƷcyzwSWnX82_![F3n$[dIx_ȬˡO&dS%dT[ n uc!_4ˮm81^XRKDM2b"nFlFKaeip;BVܫD-IChi$UQTi=ɀLƣ0 qσ}8h}9?W*mJeAlF(߾5wh@ c ~;Q⢏gIiK܊b+kgo_vK25J1xա~x )_&,#Y @z 6{ LE+݋qO6p'0]ֈ4a QN`F^ZQI-*^xM]n֚ᗅMӧL;ztC@ 1<#k'K۸UKC8?u,94_Bz]0cCKecKfC3ӸnU*IGSo}& L^sIk8&HD6/i+3q7&\BZ/ qI[/6l& \cK xhELnJT;|[z 34|rLW[HZ8zK)aK _=oZI\UNzo[Y4@.y&s*Ȼ? :^c$eɗO]`Y\QxNe2mdF'"MaJ|ֶ ڃugGE&tdKL y9.ȷ}K-.W.]Cg8E!ow~ ֮ `,;1۶I =^AvRַO22O?gdԢe$)[Bd<屐&~g1\ZPgLzp*=(^Zx"H-{yg55(..顄] X1{⑺ȓ7-|$ip{ivoN1,==1Ѹ7߿?x?e*+sS Nyw n2? *|MWGAw\7I|M64EynMj!܍ZC? [lYE jU:ԕ@lh>:}J=d$paR]!Q"ǂ[ӱ%bH'%jbDKMI&P}.|:͌BJ.LbgF .ArY~Ꞙom~%QNk}w]|Mf3 lRMG4|@эeLh3Or]ށSd$+=&]?ߏr!q /<|uo\!B ~"|槇_^P(V /ygi˒0Bc Ҳv r۞>v҅WwL=$j@o&ɷxoJnj'k2#6[Ǭ3qG|8P;Qk>Ě7xTʎJ]}N~`࢐ `UbFl#nWE˷c:ڃn-Cjm a~i>;ܵNkd?HST-bu+ˆ8f2,!xO2M<2⦈CAZ/u.Kxw_1W:Y@e@CSNB+D%t\Ʉgm*n=&{oI̸ϕM |m>Jjo !1iiC,ށQ x} $GO1 iI8xҞrdS$K WIɬ#wgH:ePcۿ+c-~^3iɲw} (|k~z B KsKw )3W<]>,Vns:'ԎnnC7WPceҪp/]Dt Y&~I>v҅όe%vz;QP-$ۨu,Vא#I\x1!!)44oKTmO3?{U Pv8E],W+4<C}e4~[p (s%N 0 N]:qDװZح^EB6]D$8CJ?: `A+D-FL_ngi%7ᱛfeU"nV}] BeE(Ja!oKBMŃ.}5 kn[ MX7;aX /i<.@[_wǁge @xa2%_[߾>xrz Kɺ|2rKO,ϥΏ7eaUyޥuy ڀ,`,ƗbD{<y&MeP&W=7,@#m#cYP桿GPGY7x˃oLՇoIw|>|9k&))_}uP=V9jI ڙ|Hߊ"9ݳO>).ԑ?5hN7LwP CE0pS8)"K ;x74I^vhz:.H>H^ S8i-JOz3z^KzxI[u8PF,t|1fvn v:IEbȫ~ya/-r&,Btvәr%A˛E5Bĥ֐fv`{0V`봆M:dM8"&%aIʀ'e)W2 5yպj5/ D~28OWqsODo'RYvUd[{ˌpoA @QW>U\Cƿ4jLtH\疀0xÃدX3N;~ࡿ|f=@V|e~kTaClĴ@br5p/ºx3VIH$;tJN];c_4াf6yeG?wPenyK,<_ڻ#f| '#%nu;cGJh]< &i'<󚩣~+fW֑Q :LlON?0moCɢ2- 厢"yzc #1IMe0A}$#4XlmR~2:bAn?7S++vQ;7$qFȝZ43:\룕_,+I.bzfCťG$)ů>Z*%q 쿷)|9r6,N6Fgw˹D3FrJׅk+(/gE!JK9e;Oo([u((+KJ$_y)G䧜r2$vȤދq.S~3\K7K0od`,Llm\r|ČJS)<#vX"o>sbL{ݘO:1U>i̵3, @o/DNUN&v̨\d,FyG(HhQ %>%Jshl#%J/bSHRxsC[yސgtI>*Ys ;²RZr7e#,#]d7v? 4#?u{ab[_AB8GKZeٱ'st v6SD-0arEuCm: >C3M^9OU^u/+ߘp~d4|}Yc_ p^Nx%~:}[/ow(#y&4յGOXn"Dcp\LVE=%;9mz̠K@XСn~̉]E9Yuږw6֮<5݃J2a;kA:,ǀ!}o eޱmOǸ2:1 lP5tu{ꭓG=>H+@A.Tރ_"e>1ZZq'uW;[=YT9Q&TmDt;V2$u,it$Fq1 knb+j: ebT!<0rɄ/JoVC0%CFed_ `V~Xu&=EFXD'ڌ(N|Fe2lf2 DqɖHרoe_[vy7~9k#a]Cu+:4j%|*=u,iH?A' t>#ԯم9E:Y>yd:һ?(~{\:$V gG`?s e5.Gnc#ɖw iG//dw~xjrϰAL#*7l;>852X?)>{ #8?.0Cz. Q#~N h鰥|:$S ü"vм:l7RwmyIԟ@3C"RW^1-;w/km ]#Աv0Fy)-&_ (cB3xD(.Y3XDEDۙa2++"AY~$"ݐwRz::b1kw8 "o$ 컳HBvVSWmz pTTFҭ?7dZsbT=6Y` lnG,=k ?LyCP,y_vH 4Op֐Abt(ah)AȊ~iW~pqL=Fg4_gQh;oδ'ez~ L靿lcNx}vWNΝ{.g}^\9xpr>5_rm0zo2*<5\鿐{ (\OuB޲?ESuگ!uIO$WH1{G9i ?-2gԅ)IbIl]>&Hv&.?>q(;׊h%PIJƃǔi  (7bm2e ĵkY~iX~ 1:8NtYugh" hV;WuLyn)4OeB"O %~`=5j.{Ⱟ%2l>++_XLEE7NLeN`9Õ\P)<2x հ㮟f sC"I[Jr+BDžE\%復liBCu|YIJx_<\ w2L<7< ;dVEn+KJl~.LpXau7vee10ɦPT?iY v· H'~ȃ8|ӾXɉQw-9(VQxX`i%s /Ƿnߜf/yMUQ>F@ +OhV.]7ez;9A~,Ԧؗtz?'[uOP+U _wxuG~7@{,HrmdxL0?čr5*Ge dg0FSտfgQ6;:*áĩvɟ@G#Kִ{0P{G{&U|(>|O҃#?~[P2h蓋PzDŽKW>l7g7Tf8Q&q;`^ vⳇ4ݲ,is)=ī6>;{fe v׃u^ATqEXgr[}qf]*,! J3rڷ>}I? {ʓ J%{B V'.ٓ3%ڟd*'}>Y,ަۿ)GlWG =߿p&fmӸF(9e-s;| uK]_S냇`ڗ|׃@<%l; *[N@~p9y0MHnITexIs[h-ՏNohͭg\G4 Q!SY *S E ezGƫxg%I|"_1z3A_1+S1 z-_T2ogЌ\^"Ž2&¯BstIQJEDBea%T>ָTّ;cebsy^dӾŕ ]߰KrcfjzH~&~$6XaAtq$FBBk"<'Ǡej2f~~NkB _=k~u['>uK&DLbꊥytJl3u?']w*O?%hSS6 nazK pikhK}s@0_ԁW*px}ue")ѻ)Ҕ]q<)A(IÓ u' l/N7_(򊌳/WznTlMٓvblhٙB4;\:6:<+= "8[<ώ%8v*εK#$(RbgyAfk=oE:GquG\Q7x˪B%HN9_%BrDӘw@MщPqRm/k}(te^ [$qq7`ɅK}ƆчP[W"H7r)Bq";1.TScUyE|8T*x6#lGPe2\Rw]GV6$7v }c n֘vo#VC~?%k^~dYN +=Z0~p_|>|/%VRwh]P:϶ =VdTvڤٱoKTNQњzP񫛴<3"YS*)%xTGs%e&ʃW}_]% |vҏH2\Xbt1-cbI lڈ.hE7BCWj1®nMvD|>ҠO}S@2/tՓ _/)W3&^>/a}s}\˂ /a|WW{ 0d' Lj -~d9OƑ/7Qnډ}vZ^T |T,,o[Ga-<џp ĩUv{ߩr\wQ}<_p'n(O.WDNyrYp7H0iF.oA]`+˛@YBC9he@Oif1o$n YҶG/wex>nD݁ LJsˋCy˷5 ې=Gy?@r2=)N'Wb ";Y'$%YT77=S} ggX˩}s!_ 7W ?v{>}aak^f"[٢J []fG~hå-WV H= :&\lxOJFuyȣ=xJlw6eVQBi~K\půWc먈HXZ' (V1?ŵiIO̕ >/e~Y'rJ9\Ө3p^TB2j_x% ||G?aV-{ !pC#LZ^v?0 z >>6ωg |[*0^tg[y Q>`W.h浽 KzC}/^7+Er_r>O@;n?a+2t9ԅ-p}5575FY\'s} (>E2=ڹա]#U| iqGRO.Ȧ~ j2}IZCKU33\bO1(rݐ@H>n8 ,S}{7cئ aH#eAٜDZ0eɏ lsmQʯUEwms 3vM(G[XxO~U_񿇧r+o8|*y"m#nkG겚Eoup;;8w:qKٟ14?l_NR^)/;Ƭ (Ž[~p̔Y+u2dy Zu fO;n]>eݯliA䧽 y']϶qߛZ1v7 .ξ|Gq|3ݮR&mW~J.| sKnU<߶ Px5溊Je_?N?dp/;oxW=}==+wW苉uϊBT \_GyW~ yՅ[,ȠЖ/pMX)O]FëiqMp@IDAT%;t|q>|d.p ;~ӿQGEz7jG!_\R+N\4摹p4WTX{`A8V82`\|)- GD~4 M [|Θpo/a`'->RW#@S G9a~8{G8}:Hy IO}]&RRFlk9 Yޫ;L=gchit^uc`׃Cujy7~|G8%r ׍NxgPϥ'9.z~P:k舻g+nU~o+|N)vđO;UwLO;#qf}TC|:i/Ǵ@rfBB4gxhqOZNRSeE%PuE3E ?)>SW~xkys%`&DB~+Wq6 Mk/=m7X޽Ս^e>:t뺀4^Ο\p6ϣdI~05E3ŰW_Ʃgu'&QlE9@ uBy*ZcxyC>F舴3PaXQ6ΚΜ|'9r@䭸\IMd{Oj^N/YAlmx(P5g駌o+Sf./ {}S0g/hT̷ff@M*ߗy?| ksdqa¹{XUqe+W}NN7> Rӡʾ gz ˯g]ˇR^7.ʴ:^ے7{ Tׄxx51] /GyW~N4SN~"Tʏ-啨ó; :/;~(E<NvՏ{j /3G6{y(7;Z 3<_Mڗ>,]^=x+R^^嫞|l>^qʒt_R^A+kÇ7s_~-r(U-} ? &j{O ;&t~\P\)QBW@|ݍCbbQ3hʏx 7on8}cw+hO^ jԟ?w %]Yg3p}}ծ(@coKG9 TCቿYm.yݣK@!ńB[5cnqJΣaQ<3^`]M2/8ړ?~?$C@VM.>RtrH8:|ըNi`\Ҡ.y<آanu Џ*y Ud{B[:7(>}v<e Kl+?e)|"|wo9dûMP4+?w__}?M<>5;t C&.?.ɫ_`{!)GL?w/y~x}ɏzP_?PGS.acj[| :x@tw+t8AŪCwۍiևƇ;gc>s^<~[Q/՞/>P^C^}X^-o>`'A{{ gX۵<|S Qʋ>LК[B}\hB8!d Uڳ. F}Uڛ_ͻǿ,~=ϨC]}Uk;LӃ_.{ԯ%0$|7L/δ峗bӑ~}3=+oM`{~yM/?"3niSn=aLcBryvc&}{pH~?9U\ W\{tF;g7hqСځ]ΎF?4֚'fDoӅb蝸P~ⲔWnI`[tZg{,ix,k y&uSgV ~g 'g4&v4S-pw ~Oe 1y:5;euU┆s%70| ;;πH^#wB?ig4S`H Kqȇ-,/s„4g.G;LtG8c;tJgmyOX3{]6p8,c[N(1rZǔwW߻9Gl#)F{V4 xZ߫|e{YWY'~ T06`'ʇQsc+ooQB#5Qޒ$Z|j]]8\ ^Xe:SS_S(qS>r^,h7ȥ^۞ߴgpxlR恟Ln@a`ۋ؇Pɯ9R.?p1ArRGys`K%9gZ=qRRΙ$ɾ'a3!t¡W_g?KpMA-3Yq e)nG ѩ7L\?6he`}J*\iㆦ~fOqAө; :T# ~-QM҈BG8%⎱ppi B-Ro$ha +`?桟Jg#%"CpO3֫,Vl5Y{d0"ke oggt2"aqrG t/OeT3t4[euҟ f?fPrRpg-yOgp'?L4L2et"dv2PvIg7?:!(q :q%D"+2/ )d@;2ƹCJ) NУ2.c_qV'Q{oS^ ]p:н?Jg}K>W򝡿OSծFդb}?dR}>qE1Uu w3$>E} ^|Iwα94O.x|Sޥ>S=Q*6M39?LԆ/m?p^U-a'MyI 1 d!ʯ' ʰI Ʊp@oNrm&}<'mESrr7s0e(kET ` [3%-6gJ8aʓBC}Ry7T@[ +ܘp$&2T`G:;cߘN;_fYM|{dQ6yyE {v΂O߼0r*w*S2'-oޜ\J*_!y[ t(uF#~?d?o|qpuQ_-wS*@+sl;#3Py85lG*-< ~Nɯأ|}z^U> |]r*#Coɛ;f=k^ "o^N@;#Ly5?Rv?9L~>Tt ?Rl~o~Ǿeʕ!wl+|"y-/ޝ#|E`˷/J۰?ll~1 ~Csnj|y+oL1502-=5FRV[X¿|3 ;PsꗺK>x [=?n}`k}D./v6E ӟ~6#|.2 N{FBeڵÿ;+{ٝ7gxޘ| ?hs7h'LmJ߫cǁi LR~L6ZR)6&d_L\sT[4ӛ7~%+'|?!а{9螿?÷zaKՈo^~MwwC9םX[Qh`,(/Zx'q9qP2R^E>&B|J&Mȋ;n'I[`I^Xڰx>=R$k_tknC=wl?W'gkh-7G#M'?%F>kB(bԯa6=@.^ihNiz@xCy`X6`eN(w`N@]xcr $%^<3U/hiX8 @PȃV,xg$yiubC?#􉳛λ]KM%ם~!#8As&YzZ"Ht #Ȼz/ ccW@h5bw^+cQ""P7Šwr2:87[ \ :8Usej 2܏/##\ -C]A^V-a"tFC\\*8) Jʋl9_j,ڇ:aF 8 ?9ʩyZvq1{=zM w O NyXw(f55FqI1Î;-Ȍw R-(pB׮l$4uD8s99ӖU6aH&}9Wr:" >Ϲo!5H__}:|Be;L9yCxw}#B,y (_]_u<_A!ܡ 6PLPJ@D[xِA}ŔWqL}[E(`M| :ן؆˒'yDɏ| pNwS^evyUVʏ_Շ(9P.ͱW{la' ?v'/oQejy#yOMPJ#K8XsSG}Xiq(J>t5ι^,/?.{h"`37~ n󣬶G>|`qx"Ԡ<^xX9gQ;/'oWPOb-Kgb$ֆyPesoɆ08aYNefpzt&dIK:$QtO8 o5t]m*=Շ:ȜPNA>.{*?ݔK/Lo* /ԯϑE(',R^%JƄ1A)~w8$2a\ TB_(-jĄ7+ 3_ |UC;Nr=` =)[|Y& ^XJW,՛zo۟!K &_s,>mIfrZ! #4gcS~0l(;?-+JUfGnmܥ{^ٛXADDy.)Q lF9>>.()Z^] S\nwL)~ϾؓO7H&Nol5bg _ϮP7B85b[ -Gi#y:/U*d~|jP wѼ+lSy d66ecO+F{40iTwr|;`;aC~Ncg=<0> ],7*0fm;/~=6.Ost㘶up6pu-w`m N[\SNВQYA;|5B*X1Ѡ/xbͼ Ʒp'ÒO\ٟ c#A#?;}ɎG}3~vQemrTvl-eE?Ԉ{WI[Qlv2|T}'Yuw?3xkm[ꍃ{\6^K/g5shπʦT)d<`|}`'xX-`e"HPxj}]wI'pȀ.|U~ ՑBYSOߩm-6kH`jsNZnhXb%:sO;LIӸXtڻCU2MrGZ NI{p~*E  8PŘn6Zܗ`ITS0+9DlApٳ[+Q!X,\&4 M{\%wQDQxpL0Ҕpٟ(E,?»3 FA1*["ψ=|)xV&,\_-7q(H6G KMy ʃk&l3T%MiFM}3jηasا)8iCL蓢> >Jz D_'Ay-v ae>!jЩqxuwPeG(Y}G55!(FW.bzXmuw~ [f:~^OMi_,Y.O)6݄|;qPPTs'LzRS (Xq9l8`13v]wI=ACSҼ{@fZX{ <"S+Y ,zpdX\?\TުBځ8;jCS\‚|i]&/ Fs@O ]-}`v#rvG72F&G|/\GsN9e;b]b(ۖ\yo$;ˊ?n? I}/MSQU( I/wpB;VcWqZ˂H6sl?]eY6{J~ϛDAo_0Ee{_CR {,ܳ_ -iƊHtR< :ω,=gH{Oskw,x~8^R;W} ӑWtùЀs}f%$]wp$"๺yGh+=|쁂z;}ͷDGOnt#i>I?.l&)V!"5}g?~ӥj~Nm;lfIhq#Asd6]&x|,lf4W,jODxSyhj1f󛨅}xS5}p+uvf|7S6]3 G)0~>UeϋМO9 syyc^{5ݽ`o2GXxy<)г Xclf)0%=HD28/A3ߚ>"b?(y?}~}k<ӟK2ݟ*$g_} d>O"~"{OQƄ Pg$Xwrg`c>~g6r?z]7qlg]!f|L $~Y\?`|Nɚ`/1Q\?5"ƌ-R> }{*Jsw#s]=q;pɿ'F?)˕N >֏ԸϕDO=s |N%,i;>?7w3.7_7 7t[{B#Rs, q7I@i?Zt[3:2"L ~_{,;{h f>}g |g qgs?e~kz͏~Yyy'ۭ%B^o |q9&uw gNcI`TX%o-L&AQ7GMk|.Ϝ2Rs//yL33w;+6~?j>?S|W {¼]gɟGeJ̠vq 5Ġ >ZQ&K̓rgX,lO{lXWJa8JT[@vna*ӵ|^|7t UU ୃxl# ឺIX~9>kk`wߥ+./|!?Y `f$g_k@w`׷]ǀٟfƯj'yҷf2Qa>zf'Ȼ ַ=SٮY5Y5 jGt>Ϙ!?7| w'N؃z[ K #1Qg^Z1§b翠Xy tA-\V]',S1xM}Xy_&}OY=_"sx)/^R#?nV#=}++{VH~~'gY3>ki]Ex뎾y'|"0>Eǥ,z|x兀OdVqGnɓU3H0Y]ylI/| TFп 'k*3²"sFV?ܧU7(V5kF~Ye 1c=-ߟFeJWR ؏vj۾A>;z^ɻW@1yϏ @^U,鏘Htظy*8цYvX4Ty@TGU$X:4XR-@Х?ezJ k';?0w3?G櫡o?Vk\2s21`s}j>~WO' ?Ǧ } @RǹğۓgSnG|/.Ǿ\طzFg>;SJ0yIµ ybφ!}9/q$A@p ?`MIdɲIӳ=ʬnBir+ )ܰw$E,*+#2 w*8^3<<+/3p{rϢ)~Dc0 Wk < )şx{MWtz >t .,+~F,T0HrghÕ~>B}xR?w_g}4R|xmLHd7_$CBt>ZR_ųt7o\JQ  ,UZ={ձC !$T &}0hn:k' Eh7ʂ7PK 1̛Ge.(B',Og%>>A W]qyƭ4yw: 0pFjX5\'߰%X6Ǟp׎\u??^GؑNt{CN';;IgH 7iP[}e%}b3;QIm\VF}Q& K5V āĆ~P%|J!yG < xDŽ'Q~_yG ^B-em֙iT-aG4~K ?~][>$Bd'PPI?!0N4p5 &,u(R"&p~CW9`J#IyzmڹB#Ő&t9] $ڊzl @ڀÄ/)8Rg- >O6YV w+Z6}Ve9J{8rIہh<Ф`lЖStdzBF7f/iV:BF_B:ێзw!E#;{v%R)a&(! MvWS=Ca]{=fӭt'jR !i =,`/]dpBSL?X61#a53 V^64U+|aRM$ֻQ5~oY_ o ]Z!@^'M i#ӋO.tUQSizC(_[/O[|O=[H!7*۩8޿﫯CI;S?  WSw}.}@] Ox#@B"iGtxJiЕ!LC\@_bV}J8:E^U:1eoꭟf$~GN*رWø(Jء|X9iq 2-15PuKLTy.4t9B+e*7LtlcFjbM5 -]4\68#r.Ӣb`vMX[fRsѣ Othkـeveshd2%|dĈ?A8 +]e/>ť*WI\KQIT" E qR{~_;n q_9^wzl)GgkOfeW>hD/T e64}Iq9Q,h %6Cj%MW|TH@@#VRC롿ru)nZW}T; VL_#Wr vs )<,MUG83?/84:`ߋJCu2Oi{%jͰ9 ^٨!l"=)PI*p-g Ԏ`ufEKlNX9L;7߃]#{i^ps-wA-AS CX-Νzἅ-1)gI &SE>뚭1ίDNŘ{D+1뭀1}%?.k sBYLPN'֗QW2 U:6R+.VxT%UVո Q8]5+-{uouL'ᕐcoc=S9U m9pX_|M> 㸗_"I ;q ڏs :9x&Ө6愥6ٳy`W&CZlP%z(i)MfevܢPDt̎t]ws]=HF1. [eUg)ck<D{ DtխCߥ됥C5"\"=E4w wuD9 q~"OtD2ltjqX2]@2k Xc=LaE_3c)?3zm'>נOlZGy6 bY!L/@d6_px Sef5qI@Iqꨕ?)AG҇}J!LbE_QɗL_'Oqfnl16j#MvO`ۑhQ0ttӤO%O6Nf5ބNAW-P18`hI,ʄCo4 j,<#(LB-ؠ($rN+m@u+UWC >cj ='Sơ'GOѭw6@%+F2 A N"NEDC=SkҳOۂЈQ܏u.dcWFV Ę v!rߚ R.5YS].龜Oz]Pnm Ԑ>BǶp~oqd14q:p}Ayb&= ' ɉ(GlǤ|̙CCs?<L ӌ :?SgH|;'cYX:<SBЬ,a/ H[ؒPQ Y(=fYTb` sȠL|T h_;j#!n$ׇ4 }0 6C! JpO]p{̑G5v8Ѽ84z㕓&o~CmaϴewmҌD'_*;Z} x sPBI%l 씙h}OWb?!ƙk|ymV!;gpτDL=_J~.':.7&>Y& `t),D.깤(V<3FW@VtJۈAII"ʮށ5Ce[lyUp". 00l8ح/ Ҷî9exOq!PĆe\e)6$]'ஊStCrax|K,yCXE[H8JD:~%̱d_CT @ Kkqm/7t_ Vd?~^̞@IDATG~Rc_޼U5z<}6"t8W]N~C_*=&<P\ X<nojmuXְh֡"Az5]Lv'=" ^ -mDI0`("4}1U<dϿ=` \-蔦( L-b7o*bL(x~ /1~ڬ75To+9(.0$ ,@%EZ{ti#l%2 25UptW:"}yM\:VÄ5aO mz%鸯όҟ!V B8 `>xn[[&0rx20JA 1rF6ωѐm ^!{@sk1kdrr齤8nC*"sy="+|ѓ"XFnTl)K'{*nSи 8ʇs{+vkrPg)翍QIԗbd NJp}JV\8fkÁԊ~ɏ9?;8a 8xϿ]iנ&Hqx q8}].-SL!× @d= a:lKfEa_H>)/9PEon \l ''پX|A#Siܠ;;6c'qXrg8c؎&1ػ-Gm"]66hgq{-H:8|FN7 rs3KqفX ,W;قur,H]uWzvk\*t{B J yT!/rߨ ]Q!*>+M~]^ynYMagO^yo (~?jĤhsO-޹/q8\v聑C'cI0s&AQyժ 2"&; >w<"WJBސ0g4iбr-,d /[hI%MrYEx0~e=)pPA ValdOE!6V\ nIS\-}}gVV%[:/ 7%"16bd;=kK?6@ Mq#ĕu{ʴF 6 R֘\A:4%nuħ4TO׵_Ư } @*@f?OT a\'~+?ǁsiSWZ_<?ov i*[WJy @x);K .Bњ(HНtCKP ?`Rk4nRu*E¤2IhIjelQ=!1Y<řbqS:io'tۋ'& I!o7Q lƳ< @7s41yvoKG\4[*Dm9˜ mw"SPZ%e0 Jxb->kBкt$+C( Lcթ]\qmlU1\/%tMVUU7k,QbqO!B@/>0 VG{?(ÁoW^_8wc+Y=\1uRDt[%c1huٮZgoN܄=!̽~WL9f l{?WjkFVDJi|ItYXi5^(ОOR EJс:CQ}2zBT]0ج412 uT"c^'NGP#Ŷ9E8dsǙĽ ~VڇoIk;v8{/݋ {;púl-2ta̮I:>)0sߐŌ|()֮pCcGP7&Q=t򀎳m5d`I)BRpNUCYʰ|YL,衚;n' +I-g QeДzLO 3`Z6;X\5[ pS될kh =nw[쀕 )_h6)5 ^܋OsS9psSo ȏPm?w?٦g"]& ׃z4o#>C\@4Z}l|jY!#+]v-ҏju@'[y_?Ǚ+zͯv_N|)ڶbr!*mʘNt'vBEp"VDص' hE6a}JՒۋ=0t@-ѱ`elk@vpTCV'lsRAV]Ql_՜jvB-BA;cVgw_\E{/M=uuJ 8'C8 ̂2h)㍝qIn_5;{*-3t)h%utOT*^gTqA>KN'$4[k4{mʟ_'46!#n ϳCo2NqL<Nx!.\[U҇}e2ds\βjʾ"= %Cτ]A:-[ՖcX[?oe4i*1׎F=_CϮCk{p:O/l")ܸݰPM}<弑y'_1 `J0~:h`_rƧ|$p⡏ & E!=&!6}2â:Ƞ?LڻEn`)!#vN;^Rq.{Ė̴;"&, Rk*~2>el!LZu`x/ wa9!)*_qp[o' ɖc&ΏS=&cqU+_'GX,+ӏfzr+ynE[n[4C^gnPdUW;eAIb ^ cӡ`k?)Ńԝ[~bgSEb^|c{I$v(g0~o혙ݽs#qߟW+}c|x0'ujL$NE|y5wB>:-13Ft8,F"&O8C~6RDƔdUe]SGFSL!"JK_r0?BQZjPM3#eH5) qO=MDK6Sad'㯔YhJvS+~b>>Ty:K}7&ț_|=&x# U)>Ag*%eģhN) ¹8ńa`Yt3&()OS_irO4q߱0ga6*!+RG\u pmz |\UFH0 @FWL1J$!me[Jv3җ_'AT~ G+u˧癷Ux"j$:!p d+p 3kגa_TIkO#ƍ4/!2-8=h)93;[UݢYa0i4*SCf([M\(Q}1lA6੏'8bAZtWl *ЕW3uCμVeq VvF! A&O KPOg! 4( dSz 22:"?&4}[}W.}w%B 1*l_2{(*/g4 #l!r~ '˔Hs,=fvc *>訑eyn" gidJU-2K;} jvV8bZs;&طq9uqs8J{2lq6݋t-d2!Dz9LoahB3uNs }ȑy{x]evt3bMel,zPtʶZ*D;&I!^X5{=A4x]-F 2a̲=>Đ pkٺ% (g{Ӧ8  ͯl:~Xٱj~J/q8~Jƣq Iufu]m{"| B,+pARdԄuV e&-֌K3UGro~Əezr1 CJ B`#qh*2UK8ԉyz'#*\MӪkxU~WN[WJ]nA; o4teF]~t.6h G'I8CADzQ$WtL䖾GFC?K6Їc9/\Ekl ΋(sU߃˲xSUƇ=˄CWV,]gfEV 9cAEpa : M\|&i ,A3bPfhB UG&U1n v0V٭pJRvJ^S=lmjw PgKBR_J#•(r)@j*J`\@<]u5{Q@#]H i1Xu%QLOM}-?cTy4 v>}BqC'qn Wo2g].a5?Vɣ6  B}~{5Vޞ* aT>=?@ p9C;|jGTɨj^>{VGɯѡO郮O¿@?mf',}>/ W?*-'/_O$+F^_]&\t1!j(Wxx)+d{zy {fv9Ʒ(2tD.DSg*ڞxOZ&(ML^rY g97f yoЫ[j7: ~=<6ӪWVRCWAThz3}:!?ewOѧU-oE]sGQ>8K^REݰpG:mȒ:DsE픲(Lמ)dHՔWǜizN;Q4ϑ4/8н;S>&{u+y׿u BzOOr?n =1jetŤ#x6uIhymNCMdp;yN /8*(:g;!.C@rÞ%<r&@aDDټ"6ep-d*= N-gx(Xt Ywi Ͱ#n(ZM}3e^veL5˒j[99j`[_ {rpn?Umoh^7Lm.iShPm٫(H@9.$d|BC]L5& -}ՁY=i|E8Ch+ ?21uM]={=+ c<-S*Tɓv Jb-<=&5‹Mɩ5!TM:*mSp*V}ǪpћbFFICq> %WER SAxG\%bExpr2iW!Hm&Ԏma)"HiX f*iNyo/ HI8)SFOQK]om/2쳾<DUM`2x{Zn;Y=ato0px^2%RyYjg2TQC9he= Gwءk,ಹX~h=YY wa-2bDhHP : ԥbd=Bg~h̀$|ⱙ6KGG6O0/W!Y(- ̮܂m:4ZߑDJ\֦Mc-XUVa,Om׺v[Qu2ӓ׽-  H>6x͂3u!49 FD-!1CewIh65]nwi31:_ι׶&Kj1?N@s6l:4Y//yŏ/* ! NzܱBӤbhًeK(/[;y21^C,mS9 sr2$EP' O$@֧ 1-&  ZO2hځ;ڕ{Y+XHN8<Ά(hI2NkK/m8Y$)f%قQ6-O:PbOS JS`>(HV05 gH}9=ƲD{T*L;rHW5o1`x ^ ,':lnӅC9K/`jźm^zBg+ -#j{ "dL܇?{C"BI]w%hZ[d1{@8.pϟ.d[}nKTL -M 9D& |L,H'Qƕم@LZv(i {"awھɈ&zXƾⴉgl$鈙 M8Sm:Pٍ<)_!ʶwR|b dJa<l)F}[cepSd*5^L-/4۪!ӓ5{|EG3m/P-?w~*9M,%m@SV>*M[񐊀^v 翼PH+\ПW@p҇wlVvd$!"? :x[ LDFвf{EȊ-gZa׬;̑$^8wMvB"a آ]xOhLAqQVS}.S5N Ɛfb>i~u=!drxzwNv3=`Ti!X{礲lӪGTثra,;>;;^yuB紣A¥ow_a.ѝHg~Y9S<5ppl(0  ?O"gIVcW #;?$gfp{ŷPU~%&޷]^ݹSNOޝ_EӧʼV3F*w~_]wk{qߟwy+??KLP[ Gc kX`7^|@K $cr_:?Xny_|VnWi%3P\ʵÁ>>$!|Ȁ]|_I7=ZDžl=g9>iξ_\\Hk?zY_^D\?;:Hӊҽ-:5ox)NM+vAzxx-BY;]`яhI(yzk @ԁt9 Omd?U>Z85Q5аz.\M2AkOw8v^l&&=ִtYJ{pqu=e?ub[FJ~ع_!]#H{c kX;ˋ <,V0X5dƫ[Q4B!1CKU H`h PL0C; *ߝ^;ס7O|'}V巼sͮo=Rׯ_OJL <(T T^}&4,SeQ`4 N^2ic85pO\\`Svpqq>N;krL np7iFOHِ{ꉈ3Aٯ"6`oK^DÄM8߼ݍ˷|׾/˵t=^i| \d繵ӓˋ= ppqqo!NoN\U6$v @+ ͱ3C1qc kX/Yr8VN^/csSˁ1gà/Q=R!)cNċGOPqդEhpŀA;=v _ʿրʡ Wg8=%ӓ7azYvVifxNQh vx? T8#|ٺshЬ83Wc85p\ҕ?6B8X{PH^#&0sۛy [2 0ˎ˩ɺqErʨ֏F=;b:,XI'no+7Bܿ>CH#2+ i`~ |kX8`;SSwtӜ8L~\pCӑ4GSm3f-t!iLkßvAxPN5 V| xkW&.wpy5pc k%j 4->랹HA1ĦJz^`͐ پ,\gw':N/{kNooݜO_ Y/zoN~WWD쏶bGkX8P|Ϧ|b?rw,b,g; qn7BLqkyCW:lWNoޜޜ{58y<8Ml@*>bp~Oo~ x/ 4g*>&5pc kWּx+MLXp&S)3y q"}Dܷ\8?|?` WNiMx&YSps}onnʘ7Wf~M6Zo`J6 Sz_%zm`CTK(OR<,R#:O?H|hrD%8؂DS6$D~&Іs>N/>Ya\Ce$2qe eQ7p%@AJF_$A3%{dwhioS]܇^CHitg d;:;{w$ Wk<zxM!ix$_ͼ!k4$I)c9-Me׹-AKVz<=$U,y(TO;nm/v@#KR7zZOVxi׮yEs|ByTr;TQ4gbj\N-oy)]0 y>1kWox& 3k2`'UeYarҢħdZoCa^(4P"VӘP>,uOڒT+d-c5VE#@`NRBrԟr{e>U}巺x! :DO!K }i>p t8R*VgBN _~wl ~yB9M1xD'aI4W]=QeNxUY~AL| \3C{CG/AOO_NN3RI`Q @A=J;4,sג#@\N@8khrZLT=cș*h?s.vRc e~l)ħ|v><)s/q2@@ u^4p%?Xd Q)ml/D*Xg0  MC^o%-%f_AISϤRY 2'5WS? pWMtX ~,c#prb(YS;q:ؙ* }zFa@l! r}b@Tq^͋>K ![[wOnvvu\hq./0`xǁuԄ=Q3[}\}٦cVV8騯T?KX)V-CdrOlHDwcDlPH4ih"ڙ ioY3kN!9zʂsr26G~R[!N Ct-}j sKtKUHZ}of^k% uH0dkA?B.~MT-_\}#*`pZ2+O}S`Ͻ\X]SzRiw3md#㘹ASPdn { 9vߕHQJ0)Q)_[h<}02Oq,}NYV *sd]Mrd,øg TˎuҠd:\C08+ɟ#'su% j$`9k~Hz[#:>oXau )`[qbd9tqz9BJW*qn(o FL90Yy/b's*|/.q͜q 7AA~ț4'$xS;Lyj4ˋT`ǚ[x%I+O>~M,WD˗SRT!QVw{Uϳٓۂ#(,^i/| }\ H4xv@!|H/tvZ&y7( \"Po0qoŽ5llDoafK\녨|p  t8xAH&$[M6X -pδHO-I.|ЊPbl9\ǖJMWLKtccKH+^[gtrI?uɥOӧj4z[{;|ڋ.xJ(x y0I+/2gwy\\+A 4݃^IX"*k$:;QC^rOu}L 9U"C<폟G׬::GeSzwQqca/(&FRiY"yľyD)=92"6NM@_mi!zt^>zlg#:$FozR;u}wNߝ_<\|wr??^IOTjUgNSq>1NFFG0e2A41^{4}%,G#_yic8Ɠ[\I0OXA"~78,Y Ti do& &=>x,27gLʣp2gӒgѩ d}_'|ПGP4- \LJCmEͻ뵿og{_i_?Ma?|qu=>e2qÇ0QS Nvx@IDATp~{q~߮UP p± @MFhZ䬻.kL&X DP8y%kd=DgL J3L4z9t&YL0DuCG2){=c‚1 <2Bt5M)Ed3S_;V4}ntH)YJUW04{νp[aDbl&p]Ơ;&5mVG$أHѴ$|5uZ"S'G&I+ 4$-R""_d,kH\΄cGP%(Bd%_z qKyNM.`OmDP-k/ʖ\I,Mw B}_e3H"xW4l,{ig|Pɵ3jZdžGb8gC'TZU{XdKgxطL/[^I F?{/Z>lDq2brV\"-!44b9J; jLĸ rJixY8n{<薇NW'r3ŋ<`9X"OccI1I}?Jˇ/|@C$`7zW Ɏxn$[149é9Sˣ,rd&~Ηe, ] !3k1ul tC^cc0 1y b"QÀԹCIZ80o&bY@_Ckrq-O)nS 5aIs5YMVY NZ'5%*͠QG{Lu/U q%>޻XXOx͢ BƛВBJB7bͪ A2ّ)%SquGxA/1'#D,e<EPG*uS;%;֙V v^fCIGr(x%>^!zw1`̯|r-; U+M|J%F,M|M{FwxDOys2+:5K0cgf5|*^=6},(HPl?w9'5}myvSBȋ@}"q ˣOI0U>*eX)kfe,فN:ڟ̽Rl.eCM R'.Wjr83$1zv d> Q-,xW."L7)o/t"4]7{@|^LE t@@^G݊[ue6bcd<okVJW^i9_]Pv>E+~_NNzfSU>Hc-O@/' nPN]6@xėoQ/b'ȩqaUհtrЪs9T嫽U}6RȳR2Wׄx&#dY88'QWM#>OƁƧ?S{cO،ɳC_eg& ^Y)g">Mk}$dO Kۀ3 JQRCuXr).AQKZ?4y̐R(IԌlo3!ڐwPw5胗 LT1p +A^Rݱ.a'D gpitX/Ӊ7W:"cR둡pr-,OW޼@Z‘aA`/SlG~{Nɥ$G'!rZ1[(-4u &4Ntϛz@l~Y೅/z@-T싩E%5J:Rg%Ayd̐1y4mҦdC} 3;*`!x:eNtk.t= OͻϞH['f !⋆7Bɥ^!Hb2Vy]oiSZei5RN6M1B5Pn[|ծ=\ixܲ\_I|Ģ|reE>-PqYw$T ̀@΀EK㊙Ur(U=.>GN5!8}S`:b(dtBFL - yry0_tpqqr̬H?'@F#4[fds~.rūwHT廢9#6h΁AɓExf }!3Uq(Y$O:rfE1&>4ݓ"AKzB&Űt)InJ0煞LPȕYNbKV# d(Rh?yF" 2JÐU XيP>t.&9HGY̬֟'`"޽(wj3ڵcvweiŽʵL}0jbO5dcEZV:N^`)]C| Wz^5AU)S'=E=âv E98d4oh^jK^i UiyLG5ٙ,Yj^~ċM~߫;ik>J~$3A7'ӿj'W&t1YRA|{pJ_>NɃƳ0|&RZ?UU,5}(e=:С LeA>eO1Y5/A* TEr[B'6 $q25\0!)Qx`+!k/&p\F[N/'^A@ާ^iL 0y:bN3 @҄V*Տzp_Y^bApU*!>&Oύr4 -[2~իW'oN5+K(V m!4L.Q:A" ^Зv}Akapfe S6ẙ? ) !Ηfٜ3A*8HMcѳK~ KL!+LBY$vZ|gt(k4)Ν[$L΂$M挚lU7E]Fo 2۰+5-SA~7?iI]xSy ^'ߦ o-dׄz]n!Ԙ3)W{B@9Tb0HD\5\04jـk{&Y%Wv!B'`WB,#'|[[[7_IĹJTiiFZp˫|߅3iB{77zKGx9ޜ&a ǁZ8G&T`B2 _yJ~S,^3U4 MdB_4A{+V@`{U\} ΐ.\FR[W gdHSV{MuDi .r`eTF[;qZ0z֧G^F^(<*Tm mODKMCcڈ(K8Ubđ,Kl@[@QZhx׀褐L l ]c#(4bpqwаR?^t&aCwҡ/=2T }bvXgxgNU?Tӡ:^wL",흶i4914" ]tWl-,YώlmE.@AoSe\j m,1gveIs3y%,=4&|PVbh FHRsk;uE%PT=gl MtLmؓuqOq%zH?H~Q6o Oۯ}+7BkC rYm_ :}0hR$ UJ 0g$"\@p+87}Lc` qJ9dpo~O7O?{z;/|?/dH8f" ٴ^[t}{d_+֓ ;t|wz{w͸~MR;{gV0iI_~N? rgq ΢Q|2k@pKs_!#S8S9EڃC')T0ZSd,S3U".q/ "Jyf^&OX aŤ'Gv~z7k &hDLlO.a#fWL }  @ #usJd2!ϰVE\˱ jҕxMƮ։뀨 H Fv4lj굿+}=W/r;DZܝy3_~zۓ7îono>de͢lꋞ^t '!c@)ȍ*繡v#ߍ&7)h$v4@Q vv8R`(3G:䃿ܠVF!>ģFBC7>| cƛ= Ai[+F3aQbMs>ʡOhAC,a}K~km+{[]{Q!/k?fY8QHhE[qee:짢ʠh۔lO+]X[R.J/!x~UkjTIqECVs)7,쟇Z='o_,|GPbB;<|Up[Upe=^}UlZAӣ-xjl W6E Wg|[ڟ c'O^*X3BRJ0 Pf`ĢOpj? _1-SVML_0@Y鵞ꋉMtx!¹׮+SqfXg.\[wClw95pO"4:ҊuB| h,iywe{쨆_ c+pS9}KrXj$؈L@t~{vzOB>Kx ^ѻ׻<'_hB$&-PEQy+~&@Lb1kX8{W5T_G?бX 4tý{j.|a/Է|ݏ곾ovN7'_G}+9sO^ů/ǯ^鞿և/?h:_Z~ $R}wvOug_Q>)& >>7z([9cv;2ChD38o|uPׯo5x:5pRZK[ z܎> vp [bƒT xO?~xN1?}h5_C{ Ȟp9  +}.oAbo!& wzO+~N%7־=_(g /6iPN_ށFdLS b*w`sI[/콭;2kX8keI3t?7ĠBGKעؙr|׫~wa{0`Y~Oa~Ͽ .Eqnh'Sv?ջhG% wS^l?>62:C 9]S`QL#ćxb vF!x7q#q{2|D$;5pc kj);x6yǏňy|XZxŌ <Vpq_ >lЂpb{k㷼߯?Oh\ zU=N[X+|i@ ;c8ZdGM-8T@x~Z_-7zX}}qvWUËMd^s^?A/}Pc5otk UDIL iDZ5pc kjV"(Ex83N׈4\ƈblqn (ql/~G|\iW[cvI]}1LL%> j^woT;M\~pO?K?P*&xz?^?َdɑ#"תb7!Z r ]"y&OHCa"lꥪ*3#?Y[nU'܏O7_;G(e|i'6¦F "9\#mx-kxE o`nɟ/D~u@\XtٞC?`*MwZ :O"x<Zס{6e0zǞ4oWF[:SqW _0 }\ne++l:kaH뾆7~we ߙ@rѱMr @}Ѿ>ý2 Ft||9S& iߟK>^njjjj#`nRL,gmBJx |}r?;8 o5g=?l1hS7Wqoː؋^ 5,lj?NÃ8Q?ߨ QMh:KbW@ϧv;sd`gZgԏp"%8\n']23w VGoD zE@xq>}Qýο裀8g8)hw`\rjjji=!vk5'Ϟ8dTq߆> ,fD{_h`79>_9=jYe`˝:W8v]6oTR| ~Ygp<gfѱ>sYg W%Th#p8j=+#O&2Lp`'F[cNx(,/|'m9*8 ⯡jjjj`T˘?ǐq%Oσ⼕|UXd,.־7pi&yw?&I3?j=ߤ|WO'}'4xi`OrL~ή:D`}ioW u_JnLkx_.kr4aHkd{xM GA7F-y ?y8j Nb\xgo~ѯ~})2{r8 VA +߶11᭭{/MOl QËsI>6okgg|? 3:{H>L g1kV`sOxA Stj.^H4ACOīް姮XV1m9(BrQu"Ԣ=MX-AA k[?m`GF(D/&h:=nG?mޅh#"#"o1bI?+XI$ߐW<+ukQ~V(W yL`ײ-&1(CB#i3&L榴%r.S&YClv@xp1gTƔo bgg`] $ĺNށzr;tw4d36gG>3C_<΁GV`X6ؔwaW uh@O?1[ÞS :8@5ۗ'aә|3{!Jޙ0as@_LJ 5y+0a`qj7q`c /t ;vS9zń3mD.Fe%ՍFh p d@܍tsÚ&^\ncq\ 4DvYs,z$T&M S B $15Q,s v W"DE220cCl \xz%va<23q_!5g1khiI#+u,rΉYTMG V#*3Jy$Qt0A@V'8#izvOxKqjv 8>LP1[,5}[㲱gKh%.W>76JV dCp`:i?2@,}HID1F}f?S@Ƙ/=r Y9v6EOn `)^/+pxϟË PC@pnлt d0M3Ӏ di888@4 Yh4&]l$t#Q,Y&M> U rϓB4KZ?9 '#6#Z^Co@B'g0W5L!Dt&*PcK  `| Qg^#!ի(e9hGSUu>h[2 +n~]RBn_SW(6%X-@]m&=4SC#/ADq`ؒhI=bl8F mpFc`[kJ@ =@d4˙2:Ct0ny) a+Էrqb|(!D!ey&CW+fklK vJS6Tmfb,I3)/S[LǶ4ɃǗ6zE@_:2~U:3{ð#G@yegC5 f9.?~{ߛp>1TJlǺ+p^yå|ӔF"-_`+U?F`~?K&PјѰHԠ'ga J wH¼&"9梬 aMn2$II#0/\(He-s yIT(k5. \U%Z%^9RHX@?Z ΐ.Ys80d҅j0fԟ;SA[&)yK 0j0Ζ'5{V18P+#=1O:_Mg)#jϦ?pWg]4YΉ~W",eqF*_Lbe<=NNY]:(j Ge)^S{ g`ܨ,x` īb@#;̀hr tfl48v;'ڤ-8P3?A8C\9\G x\zG uŕr]UoVk;’l%b}ϱ/TCH9[e8BDWl,I z&8hvIѦ_"moO4HB|ȅO!OyK>3= Ө_L*k6RS&5+k?"7!dHf@9v7GEtݿhWXmd#͎(LYBW W> E@uɸO|.L+XLAg6`4_PRrz?n0镞|$]S)hwC!Qu\~Gr>Qx})'#p.n@SQF9y4QHSHn<2&g_zgAk>'#Mg l=|jj`r/duihSXmN4+a>)Bo7[7+z5SK}hkJ, 0߀ o~+ q=D`M$ó9w_y OШ_xL63xYTo<ǝesu:xAO>x- _M-2/ϐLoi{ ?-NKli6y}!x.m|/{fL h7 r^h@B(cy"NIW 2_t 6H@p`vկ>mnfi➁Ɵ0āMhV7B-kw$3ey}aiƅtǏYo Hi+^=X{ XT3,€|͏x1_υ@JP8ALAy-h@Q8'z7g|25]÷(<փHkd$=/8JjӚϨYV~ !?ڒ #{/ Fc?Ԛi  /RLeɇ#A֮j^D%)6Q! bi@=gFZ}VLgFz ll4{(9c]> }6FT{90FS X%|Y'"~*UơLn{;HhS裏>Qy,R/UUfh&F-mJ p*c 414 Qp,M:=KDljh?vفjP5P5P5pk5@?<}M@ \M~@, :a#ahk|)3Rd4%Ls0v땾:( l:i^S*B10~KxMӍt/v{'O~΂8n`1t3{P6a )%j!7 N)a{Ljw*yyuc#*ѧMu^667//3iS jjjh.N9G"X%8[D=٤L_d;W}ꋣ'+@^S`}M~tPe3OֵA]:tUNa= g/:/_N7fz:.^Jd#&?*͌gAya9H|$$uTbxa$8X!-vbv5 oP5P5P5p[5\0`sxf 9g #xC8dkUBk>7DhB|x_hLC(#hETqPgМvw6R%nyW7n:YgIin]D|Þ=?2G. -̟яti).c/ 8 +N92TPK2:y%XmV T  Ч%S{qnM_R>H?tR SV ].5'y=߲'@F# jv> Gvc+jF |\x8h rAO Ucio J܃Yg.A5T T T  ea.%[rb*@}lĕLOiˁ02褁w1\A 3O濽T6^w[cW˿6O .nr9* 5îwpOL'F3`XJ,|\ zSM.(ذ$[5P5P5p4d5JqGM{c!{/_g"%_f9/L2/ٛ> 䯪:ф?=OsHjjvIvdMS]cPmƇӊdpq4fd}#oFe;$f@3jqLH. >*jjZ4橈ǐy_3t8Y]q&=v3~X4ƾ1zvrY>l3)~7}U}ur&hB- k TFDF{8.S0$v$BnN6IDAT`ǁ $$|;<\+B땓ѰgoQS5P5P54Ұyt>"4d;X&L+l+r Eg?AJ+f? RmG9GO<:MiXޞ5oʎ'iC=9?pw4~$m?ǺpܡG0GCrgsKp:zd9:9n js`&Fn` O2M?zÞBț3Р,/)S~GYDġD=oO[ޢ5dh&ŋ6NNOO74~]5c9gۺ_ȑ"M% 8q䯘LuŔ>P.oڗ^x5lgZzzFPfԿfvο\G4_3#BT/f?ikěI@PFqNvǺ? d|0dek{, l(VfTƚP (Ŀe-<ߪ[wm!ZJBo;X?]՛:oF+xSD8}ٙa3(Sns;w8'"?93#؟(G7~nd^2utB hr@ UUUwYN-ɏ%Ul 2V .J#|KY?uM}-ܞw'Nu񓣳ߥڹJEM_:ד7q=ZnC}g?٣`%ÿYdVq6. 3_Sz-%m'/B-jjZ4o+%Úڛ} ȳ/+v8 7'?goݮhώgdxvOS"ߓQÿ'GMWч ,+G,x1PZ m4RثW`'PgP5+Mw58їǣx;8uGOw/VX6~36d{{{{yvvv~G 1;kFy(mU٫ޱ`ծپX1p_hTF:N}'sMkJx"8QZ T Vz.9XӧO91f>Hh?硲wWu~O[BE' d UUUUoZiͺ:zřvkD?O騯/4](P9;ߢطNK,{4C|ķ0@84X>ASG2394~cplpn魂=9 C4(ǀ}jOS l ϛd6@H@8MBt*LBMZsUA3Ƒ'Hb$&K"⒬jjk@ 䕃:z"'i= OBZ)8_kPHsC͚j?h7ߋiwϪ͆:Ex`iM_sHk:nM' 5D<1odo^\8Țq{7e!869`<|qg/ȯZPb`ll'A5[eq o~lq M?DHͿԥ/:w[ I Sft BNˣGitzg8uѨw)\yKP4אajF4}2r~$"`^-љv h6d鋳17ռ3lj_ߔt (_{&A\Dp饐 KTB@Հ[\EJgaD[#[\Si]ZOu TYigx:jm_Sݑ;4?SyFQCN+N'[ƭa`Eip\8A@;cB(~So 07I"NؙtFӞ?5A)m*?7x'b!cȣeVUUKܵ _6:h: fG#= <[\tfΏiD:CMA;0/:4vk4״3mj D3ի|ÓɉFl1WFKlw9 q ᢴd 0ߏ!7v!x6|>)<h20_8;xOp) dC]]-ʲĒdfiqԃ`;^r|W T T Ӏ2d0=h@7-f4;dcn5вr:2;՚ 8 5yy)/!7ΗJs+]prEx~@FphyyZ\iBt0՟X֏G9|I'm:i04;J /2@tp1=-yrXx(zكy٤i0@.ҹ@`n8'N0OJ%znP8]OPnNWX5P5P5pY o8Isgz Kn~dp<`ΟӉi%[(;2)5]s}txBN'E"?h4h( yJi8ş8o^E]w;iE 841ͼ7N><~}YVe)q@e`?UUUoPyJܗXEfcl ti_t N]q9/4wXP 2ӠCdRt|.t~Op)5A~grjjj i}Ky7gqyi@yx~nb >˳ rs)7C/O#&/:s:q7a: u 4yBh<FVsߖ i偳Rč7W`C O~8҄REy )u~?dLF4#ix1n:?u~p\MWE<9=Óh^UUUՀYi}w}q\f࡙\.WN#"u[4Z_uO|!e{״Jyx[6_UUUW@nɻ.m[- *;e˱|W)e:k|Wr/ǍkVb=`S <oCx0;nxQF8wk}0.i ys\,Fb~pr|V T T T ӆ44'7ZfKڪ4Uy9 Xǔ7xhl4ܸfhbjjjVh 7T4o[PIPW{,yxr:uӎBxg=5]5P5P5p5#uEˮU T T T T T \?R2yiIENDB`ic12&PNG  IHDR@@iq AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs%%IR$=IDATx dWuOKW=잱=v ccCP5 !@"B$ H@%(`H ', b{03{W?^wwXʝyu߻ܳs=1{K'Aq4oI~? S TOS5OZNC=z{wGh O[X=0b y.%,َ-NXXx-`,MVImq!8;SiXMmja; i[ֱb2fe)˧6[nNAl2nՖF|j%,8J'mx c? lhޯ_|iR-W?yԅZep~nXlcۏl!xfwUm/gѶ[V6\ؑy[O-UMv2+ّ喍`QwO-X#[5V2{,Y%R?O/[#ghl.ewbsKSvIOxxn1p˗_{ ?Z = @KgAɄűfee;϶5'`: BqiLځ&ȎnGWIX=w4?tsb0FGa(KT~ 1wU6] xᵻZ=.H>v?ϤS#< ش!R-ÙR d)NU%@T$%pO?l4M.)Q!NjCP6 p5. Ja剱vU+wf*`/[lvKaD3%FxNgݑ50!pcڸд`d9=jvxt=R,IԳxBec K Vk6^n>/ {p{v3/rEeA%4# 8xg ;nB+ 'EH`jopLk|Gmzv xR0xH+%6ۉ336IO.Ǖ(.))DN7mC-+H".bTÀW_yײ}FqmmsL#~#UK8#kV>V"%RWp~٫   ~@pL#(Wm<8B3V%1OXRdED?'%aV 40xJ0촓L>4Q\Jt!DXRTJ֊gwuڪ䖘1m@KV2gXΕŶQ4 %L&bE@ɳ%',s\}X`ʬ0bň/n% Dh_b(z⨕~u2M(Z &ExaV^c,[>XD11+X^[Ei[nR`u8\"G׬]l0pf&:u VU.[j=u5 +aůo=6~Ǐky5G L 7BM(@HT2ɬ2ں͞RdFVU! KK8bFPgL|fN{p[֥Gk_m^+~V:zONdgݍ¾|R'p+UkȻң V#ӓ6t:c}YY-Y!dhf[. U_ ƃ nUcА49zT ~&ٯ|Z{}dre݊yӪ%d%âşlr.G< y~iFGғd'1vp~z0< e̗%8!5^9CN֛mW*l3P#_&FxͺtO1$Bۜ0DY} fWjlxrLQWALL*:Sx\[5 Ȱ\%Ek+٢gW_!Ĥ`d%so}e_Y#> $ichҔ6PUc5wk a{GZR(QݬWHq|0 KH53B j5Jl#0]o\mϛޅٿnɏv ܀t ~x }ݢ>); \ƃm-= Ќ;!tZ2:R~&u{I `N-@[ 6D_h rMȰyw &{]۾ok~Vm}ϭhbR]HL݋3HωS5xj. Rz 拨kΰ[ٷ*ۑX!)-Pp=he^v\QRױZflO[>leKf" !0H&HxoDm 0C 1"ң1%".F^Ȉ1W6(]tc2}O?تW,=n`]`p1ܡ#K-yL8Vz3i*>og-( )Hc)^O -,p\WOC;mf-N{C7[Ա0!R_!$ &ܹQ,խDjCI0fN43]3NF,6K\DJb^d5ʷY3nOuL/}/+%[y;lkނgm_?e_XH!=i@).On!]vwC8O59Whq4/TҸoԤA͋'va%fhf^3^ŠAa_m6pUV lρ_XT%zYvWKh:[~B6t.6t{]6زo-=iFiSD4*RbdFuF튡rq&;d2:c#_+7Zj`ȖN~}CRPFYI<8Ixe4 f2PG$4%i<؟k,I,Atzٕ;Pvq[cծ[aO$bb:(^r~:/}N}V[#͸pv]`u͕M\(ȫV]MGhG `ˍUYh#Qz 4z%d.W`}7^d$e ,=>n/5דE4`!߹9ǵ)H`hA[X@h'J[2k./@gOōҸpb*:׈X 8u JXAq k;A1 RzD2 7 uV޶&a*/7=b-Zcq-pAb:!^Lð~7θ$פ(@mVw6'O" ɗt&}1-[zb ``V$x~qʕ*U lY25~}cBr}#}P o4٠4W,U^^!ź$ Ɠo2cmEΐ9M{HYy$Z hv:͟^ >)9ۚe K )19܇9~j׭>;m롖A 98.5QjF% lwlhA&hQ_Tuܠ}=_kQGx:"@jEcBH!]CmPk\CC܆HvNM֭" RܖD]OtOu \=8rn  yT-U~B)gP"Kt`GFURE">Rn)uՖ8ٲ#6)[l=GWlj!>-UJxBfklh-n)>Y=m]vwD+c#[{}Oxfń2ңɮwrKM7m]fk̛:}"},c%FhևEߴxh^CYf}TUѬolt ۤy!Lw9=XuI^ػ?*X*޽6VswkğZ|е*D6o 8n#ϱ%^9({vs>T{;7UD7KR`a.r$Vi1iud<w$*`I\*:z,@rfe>J.v!DCĪojB$n*Odp= HesEhwmyRH?N6gPۅ*#Z=~))ʦjf^E{DKJGՃG1fѨخl'mRI XZ쫓-f]V~T3Q,yФˌǃ"B[J)kv}e6t<8apd+90ȩ1{ spC ^GBU#cx&VxBT\T}Ӫ.s6-|‘,yͥ"!JbwcWՏUBk/HȽ4\ -bhn!jkW,j꣏)K[\F\eaa 2k`_w(g#kާHxy\`%&ؐn :+@@^Y~dԖ $VWWmzfT &Nsˤt/DάRtAa$ хu' '$R4[/$E _0> r['=2zG'_h~]s56:6J`liD. Lܵ%\e~]ؐZ^\?U= @or|jjʾ/СKldd#KX%CWYlo v=tEnh[Yz@O]H 9t['╿C6;GmhlgW(#Q}饗@ WI azⱱ4 @g]d 9>LYW3Ss$=S_{4oQ.QV .i,[e+Q@Xr x,Z95g.V?_؝9>E7όWf ٲy>ڒ;"rdc]'A3^dX\H  \^'=BS28'lVȴu)4It_Q?̞`tIgnU%X%3aLiP~IENDB`ic07r܉PNG  IHDR>a AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs  @IDATxmYU;Os9 u+Jte)Bhm;LVz{t<1uS@ѧ]^fh`gK1Kv >S~x!ӹ|}VFIORvkw6_or^ZR[׵^{sm;K6-??xСq%/O_o/~/.F8~f6wߗ*ј{ژg(? y3)w}U{C>;b|x.Ʈ|Rnٲ%>hIggO$ן;X|-Wg5Sx/xX,Wt&)nG\vi9Z FV)6LPj5Zu~W7wyu(U|-UHlDok D1J[5np:}R^.9--rusT}{?_}c{˱fōyLۭWTWiF-&k}k?؆ki\7.Ү𴏧oGP G_xXY^`_/_e_Wr*HXiDux 9=12E6 ,7t0 ?Kgf]\D7Q-/-'Q.F46@Y}5B4WV/kj\geՕz4w&Q)nV,4uX7+ԓd}6Z2ԆFj,,8*[g`L2lQoF 3ѱ q샿{ˉ?{g&=Xz#,W~_ՁW>H@jAruH|޸ݸ|4jB<Ј |i0]91_wׯ_c:=or ^e s+uܻ{$~+b!u{H31O2վZ1;M{E{쭷Oqqx3པ]i+Gm_9 go;Fs`G/ǯ}>(Sq78=3Z3P-uX{e&׌]b~2X188}/XZikz+zKW[+APTrА=3 0_}hAװc1cWL 6b\,#1 u5;:^Q)r/HߝQuv Hã_Bq{胡FZpJUGhѬw=}h~F5mƽTt%f[ L!+D {w|X #~)բ߉#1s{ Ũ`rɉصcc0^8@A'cb疸/ǁ'y[.f1W97nZ+ƶgC[~ha~w~gM<ۿ1$وj iQ()kƙx;ߙDطo=ϊVe VOFy-Nu[?s4bAj|`+D. JgNDkH*CLFMspVG]?8M.+90~3;1.J(gO\jG1k0CQY!Ň Wm* ~O,]\xr)^W^~=Uer+"|*y o⦫/>{]m~ZP Xh_BjI`GNmA7ҫL>+z ҨA9H~ˤI@D,Y&ą%D(I߼Feƙ`ې~fl pdR/sYsDZ:x*}^X5 &jI>"뱈?|*nrS̕8 Ѹ[k['HtbG^}{Jj뫎x?M`n$&s04c/W/?R5.T$pkKۥ-݂F %8ϭ& _-S,h&\2TKtп:TUex(GFX@zs05qSj!hMU !J˶T TFEW~`\M&SWhvdO.5}2i[uƽcubWK=l{7^,74B~U! KA${A~c(rǎ[ۦ0 D.+uEu|p}R6Wf4*i#N"i6HAYIx:AT[)aMÙǾUۚP4[ֿ@ӴhM3LMs̋Oo)0D ԜQjз_$F \ 2qe[mӳo..Iajdg;jwǽeVsC<M< u8 ⌵3 "nzT=hڝmbn9qA' @WqR%"ih?OՍ" XW:ʚ .7恛k0`Uk LE {lgyjFÏ R&т4u *WF_Du BM3J0ڬN_eʕj0$:YB'udf!cgvD&0ܚںhS(L0Ho٦O|hKOOYcdHNJ#Uhp.QVtWئ#ޣdϟD!wk_ )7ajS~G)Pm^A& }L딕H $FzٷN~~qDJiK6PT[w2^Խoj&ضēP2N0Q a0eƇ-_N LjJ,A%˫HصkcLjQ]k^B-+| k\DұL]ƹrˆxYe*kPSHt$#">5lN `$ӕ8;XQ:tʲj25GFS@k(o+$ dfP<cUYap̤y731VRG`/=/:?F΅D۶;SUJ`\8z]!1:\U(^eJArF#Y$Gzp]N*-?|tJD!&ۦвLJV*ɠ(%[GJlO@Ar2 ` 3eo QvMmȂ`lQqAj¹pV`Z%xƸ[Ii v ]%91jVɞ8OU?-2= CdTKcH m@JUT/iz|"Z!I%?hp0\q05!9=nKx*S5om|p\7R@T1r%\a9: Cu H6o~ -gdA:DjGQfa ,*60` 0L'B gCdx]J6,c[_Tj:P˴ DlQP j:# S(R8$t쯹H2 tFK )4cNCuEFu&r8 Ă;ö"aI%8#Bh= -LWG/kɜ W/}ZRN`DCz&D9 t:d(&8/4q>eciT!1j M⦾Xa)Jd f10HZ, 9i4a 4мCZ!LoGi]j(~j1}777mWktlzuk)ڳ0{]boܖ]dH)`y#pl8SGc鑻1ZΡ jJI1H AE1m+a͏&Eh31)Ն% VCs$+'ͫWck:TÑtqDWƦt59lpMDIԲD~)cU&\do+;q>J^$(cu\`C8f aꠔw̿o-g=TC;]?^^ 0|⽌Mvg؇wb!f;vN)HM L Qh&G]'4M N > onXAP ޮR,-37Zgt z|rh: XYɪN !ZsPLf uo{-u WMf 7\qML}}q2Mb,Qf5!28 mW8e ӭ 'ˤ)j{`fΜALgVcGcL8g!EC605X<͵R$|\ ^`|:HIX&72N@*"Y~ l:Ec %f?cqd1L!-}UM "cna>1;33LCJ{ 2 S6M{Y!So\5;Π8:t 3FUM3L}'_MLz1KA[ʫ#h*3u"ol W4vu1~ e ķ-~;'?q/E \5 t$U”*IPQ!L C7D^LI&wI58-&ZVjL4y8hѓL2$9N%lV)eb1ZñYbƬ?d6鳲ˏ!144q<,:ytLU2Y3#9$8hS> LJo)n=~WA M*rxUt(Œt9;s0Cr~JB COŖҁ V:aMZC)d3׭o]?0S1sg{p{Z3&0 c wCRCoɾapO%Z@ؐ3(gD09um~clr2j; cLJuQvs2k=OO u%!#̇#Ew 8U*i$)tUqb4@ģ'N;Z@5 QrR''j; >%j =w03=T \U FIjP۲]`lĆR%f=*|Ϗd.5 8?xUB\D±ќ1;`3cW<)&Lm:'֤Hm81cEK|Nc %xpă/ ~xud}s[~-kĐPk.%Ӿv098=_=?^{3O2*L&ue$8w=@L1!dJɐ~FpI۹U@S2%x- >vO5<ؤ#wÿ,|(pfJb Wnɸr ~L>[d[LIa !}hL8BM>!S(# [kNf8z2@nJ+(W(p+S  G=6ߋ"93SsMe$aaxٶtg)S݁CI:%Tݐˬw_h3NNuL)D7^;nU>LE'@05Φp~LܘلUs1Eg40RP)BuWPzjYuh$F=7vU* ݙ|&?fe(+EΘ0 Vy 'ZLb/(Rq!`-cn3:v`L{F1\ m:A*K?e <B?#*8.`PMoCMaWl&8v՘Zq|sc'kKkvӻ\p"+(Z&AV@p$ "Xť" 0#C.K߶VbhXWc5v]4qc^g24+GfjIgͼrUS90fP dK:B4b4b[.VM>(1$H'p}wHSځ!E>hn[n!jٵ ĞW>hL .sO1(00$$!>jk*v{zj &5r H+hJȖُS1ݺfߵ464h黿ߌFm)BJl|F!SR15=KŌ8(w ?N@(K0^;v7\h_R$-)+&aHǩcEA=5*9h MpH?\iAEr4 ܎ĩ)V3>LUL9c3TcIXRsz<3@DT`پiXc H cn=?߄&@;?0$*IN =k|MC:AUf |)0>Tig sKCƐc)ԁTC(:-W heZd8.$ \;+||OUSHtTE#3!gk.ף+'ĆZuDj9JLМ+i۵HD\ <9཯c=O]ӑD8hp*mxkI9q?]v28фOMŷeA|2v,0Lƒ7BU8mS.8W<#p֥%0oQA@Sq.I~)Mjvx~ Xٴ?8$J,$0C4-v somYMc1=q/XmYmb֐A9<̌.蠆򷒟aR[٦;YF %xY#VQkE8-؟\54IYԡ]gk`W+f虎o?qgEhA=tn %rr+TMC8];& ATMS2xm9%Xp@"4Fs<=DDMGdcM_ 3xs[+qebYwc00*s9|8bP ~A//߂jbTyrB;_YHu!]-ߐuie\bcT) IJKA p-CbBb{8[1s\f &6s1E&,'z$BՕ iWRR$D6/l4)C1o-|XH6xݿJfU v\J7F I%lx@;"Sv$?g8 \Q0 mk2U^:4:2Ld&Ӂ/,O!9,ik-k!0:z2@Q@ڔ|J5%#[$gR œ"~NJaY8O ({ [GF`H1(HZj( JxR*LZsCr׾{<' r^7~=q/ID&/\{եI 1AA"`f3c) ΅1WqXʮv)Di]mz7cRN$%<]ޕ@%|.yIŧ5gd2gO/KeDA^GoP E;SڵpPaJhhb]98i$I˲%D XX8id;|*JƑa{ a,Y|؜"*)<'~T _I~8>C(F$A\ف3޺ϻ)Nh'= QA&@; 2 \6i@'TdD୲b$K:HJ{MWl%[eER/9'c#$rXGnr GJv6t0u:]g+WYXPsC)p> [Q !7Gu<t(=N6JGj0k)24"N*lmNpaIwC5`R2UՕTp.rPZ8 F xd2(g N;ƾn8=$eJ,7?n~Xz2!e"[ǩXb9/mز uKCq X =ݚMDPɩTpW:VƎc3(Uwb8 xPk}r}]hERjaLVQKge/s&fuL !`2ʃx >~񾌯O϶ @g"JJT9l37FLV d9Gl9kKذef@^F(Ŋ{GN7D5t@N@ _xߌScXld3rq'WR}2~MǎedDZ:f̐"KFcz93)@,Vɜ> .lVfYtB S6z.۪5iOWIfdLT٫ )IN@j2&qx]{s|0oنӧG.]2F)Rk0s'mc Go~2KŨ>~&r~$" + ӣ1J+!dX oNI48~E]i$a؆[9)Id }l50>hUϾylu?SSԵA/3qdG: [wpb 4 Wu p<4C's=ULA/}MmSn㤁[KZsxX)>'J"iG#1%, M9 -eߎ_ӥ$'RWh^rNyc`si6b6펵/|ilA/D_bӈ>G[ I̗(4jLӮG3 7u 8t"0O8zjcs9U;TxI{ff&NYK:!5a:Ր iłɥ,e>-AOþC4ѧ9"7/Dz:4`bBWQG9SmzM;nWEڍ$> R)]R ޺Xfǥ diE )qHe հL@ܹ\ED9RVjrضC1Z5@P3SF',8Ƽ =B|x% zl=2[3NΛEUiݚ|VyUW!&'ؚ{^a`R޲]ɀ`\uY#msl &upF2Sf_-"d"iNPR}5(:SS$N>oSŇؼ=x+4̒1_2J mRf`df^<)8q/`'%| ښ38xg>/ZN] ȸM: 9ʘ $Ѓ !N$i{=Pkt䎝!,)ffʮE(;fX:h25eD 0f&)ipddWiߴ跋e12O/>ʇٹobY$SSEWс1r )2>fOi%j%n 7<\H x q^$D}tI\Đ:,H;y\sAS y6[ :$!HX3 "C<`0W\*mo~aV[ij* D[ 9-Өvsp& [K݉I2^|i2)z9&Ƭ9&!;h7܊Bx")r,Μ:z.&€TO\9>1݊ǖuE ѓĸ_`B[9q[!e`J}ADĝ>OGsݖG7ӗR-9QC%#򞮖P=R- N-n])3JҔ0.ϧiBdùGsơ^&,}Hn?)89˄y=2,5m2[wT24 b}O(-IDAT8nqglJ.4EԎ8Ptɣ'lrl}Åih*)ƪk'~6[÷=*-Խ\LC# dtsDr)9r:c>g>W&yĂ9ǩLM̉腧j6F$ګr+} 㷍rܔ 7WȨe\ '7L&ΠFJN\y3|=sB1ޅ09bj6x^R2q}mxYV QqBA`f6>, 2+t%N8g;04%G;> MvwA 3C;&xtjE(h fFvN p^l9DOyW_gxDN+{zX&ou\:ի.]|'P}?ZLԱ;e;:=@c owrQMJWGiڑ(#ԕ|I Kҕ& oYidx8L$Q=L aa)4)A/\ZrGgU.&P*Ӛ SeoQ,"mh iLFS:\P6Fݰ8?YJ0ږc/:tX+[Ʀ~˗le _`Tfb V=QSru W_ھ…ѪDP>%d ?fgEDx,Ar4Fj}4T2`1VGRg8 P!4\\cSʹM&iS" k.$zfu:'NT1Vdd=YnQ4}`d<}hx<ӎy-/c~셿= rbs8&@"@ն+C. I㚒6JD̻0tm>/r7}a߹y0: <)1N1wg]QGŪ[fT&l+T23Pu2%OF0Nn}dцh:X"Z^082 huw/˩/':)0{oGZ2i K3D?J\:E' MwoeN2u̺'DRxpۥ_=3U|+K| vAjZ 0t0pr[vd*C_gE҉Q*:n%t&D6WmZs5n%i} PFpѤ)n>砉~svh:K$l o=〩RS$am^ &x F%ޛI2 _~ KS5xFj_XR^1o4;s"r3ߍe@YgF ",_& H[OA=Sf֌'0|!!:DIFS's9kl/%ULG=of> ,ie @A?:ɸ['TC]gT/6(DQd0"i2lh.<@WD*_aR/l5_&8rQ.8z2*-`-Wr.QmfQwsߘ al iۙxW>D5&+B %z% =&Z|0UeԗYx5O&D0F:-[RMJ$1濴,Ŷ!HLVH+m抢`sQ)ܛc6 7p3hVL; ,5k-J)JS"Sǘ !*a&Y>M{ESwA$cy|%kϦ۶1yYl![(Qukl B`5R0D`u,[@s*?7B UBd,VQs!BSl FaIK2E:gT.,\^#E}2_j32tAl?=M@ڧBUre2@s@58Ew6˲,"BlUJW1~5 Ukga+a,&C' DJB*5SOP#H iEK\q8? :`FNͱjLƔk <5ZG<^@V2ɕ@E!ݿu?ky&*L<y qa^$pt*5:=Պ;M5RY}8wJ`EYsUpe5Ub6lopc &e@dIppB[Twl1F"[ǓKNfztבp*1OIӥBizXe򇄧Z@`{NB'[fэiF *Ff4= s=j̣FN0%|BXN2%S`c/3[6ĖWw 7[ő3P)W 6C#3ZAkLњRũ{3YGwq@B'p@2 K]:fL5/8\i"ՙm \}~m¢ JPܣp u:-01 nr;"ϰJ9I ktWY42*pI"1Ր͆cπ(\5btK[*qMXs&ė,qn2<'dh"b/5 sV;BTe~\ Rl[?WgTpf' _4 JwƗqU-&1n_;qPUʔŸ2'  ,]Z:7זRL{c i;wWUo%iAQz{q/͋Fw Ľ+h[XB1'-%_qerj+}J]+K\VE9ȮI'ڃ1ChA\exKmNw \Ҝ$+vOaxI3q;AzݧzҾĿ-P9+]yy5YANnSdr =aZ c~c- sa7d/m+]ӫ1\Q+af.L4h|ސ(µׯ?"̗327}sڼ ",'E  ,m='\TE:w=uh Hmz_:G_sb(fǪ̂cҥf2u 푝B6IOĘR~ \-BLܒ.C;2s`|x.oNuwEzj=9JHAD$F4{-6oȅ]˥p>3>rOEEŷe6"vh6 rW락-,ZT+gt7-+a△Mm2쑱z"]x:%~{u&9C[_f3e~RgMby>>S/tx厞 D@AXKښ⩠yp Nyu_vuos5/u~_XSsʓm2.IbII|E?~k[Cި/z>&K@EÅ&Qضc/$ )f?UU2Q ,t.t|^_瘱W(兌&SW $|U1ˣfu`wu7~GOPȉ&t7mmlv豘^Hl  &75"1f$U"u9fXf Aws7~E9t鴟N%'IL>uaYg7 йϚ䑤?*E9D_ M5',dS\I.~uew~]Yp?4>mgO~dU,}ܺcw>|8G;۶meswPogXz^'egy^%#PLS9 CafRGʼn/DV-/te O_?G*8_$.p!.+by橸'1Ξ J:T:{C?Feqx D2K @ ='SQ1As!H‹ 9w%Mb["^BK'2Z5w[ޑz_-5=g[dMO.ӄ|=lޓ$3S7oi4co!;w;ؤ0Jei 3FJ@Ub(ˬa$t\z<"V:5O/]#۷V |wo:|Pn`Æ k@ʇT c+6V<cIJ/.)bG䵤_yJ5Isj~œgv!d3rАg5? 2?~>$oK k+sFY Y ͗QqC_-⟧x\6m'O_s(_w}d Fq3Csj 0 B 4JtSBڕj)[|&7rCkUxrKTU((/OYn%Y ǀw9qL2%*Z752d"ܨ5[h(<RP'LOvd46k6& y.F2J7D&5j YDڟc%|Ey1$OuܲGǵnLb}gE~)Ė]']StKXچ틹l;AJ Ҍz> ',ԩ_UH=3Z&$!ү4;@_<|} }(gϥOIc<댼S$#q=lϻfRAt b/_B%xel8շp*oJөOtXqD <LcY^'}}WC9eI$)Dޟ~:v?z) 3Qq@"(X9^z%t 4voyjB =!4")No[ BK qljyf(GO}A㤀ױMj_!qAԳ?Kb4 Ǐ֯naрgy9󮢎;X{_;<=?_bPu(YWqѪ y)BAě}(`A&V^´fk qq)].>U>?1/AI肐n,a~VFK]N.ԉXZ ynrɣ'sl߾=I t~_Vع<@+‰$8W>pw(2,Ӟ&|ͻvJ{{-]ϲ{ηwZVm}m/{eEaߢͧuen݁lcNG]\uMG~vo]Q(7>[:` {RGOGH^\(ʊtVu}=m#-?-_|s82cY}[Թ }щiW [~< 4 ja(3XfG\g`81=ŨD ʣy ^x pس{oh[r%}YbtUIt*e5>8C\K5~R 3~zP0xkyxwy}j)34N̉=+PEau%>ݲ.sqhm(s˒y%>Kbw -K -}-_Ua/j~U:.7** D 鸂e\|O@Et!'ȅv=:1Ab*oɆQ&&̈́'` } f6o%:['΂gO1w %9HK>:'~ [!w?eۓ4 8:#1~AV)?{\6mn9ӟT|wܒ/|s^}i8KlIͿ򬠿q3H«БBk VI/~\@@@Id%pU^]7 Ѧ E(~@|$0CJ?|qPOOpoxs˻R޵NW)LZ u˻Bk%r sO`Թ^18= p0Gjpu,WL9Y؍Xa &`mqħP]'0&7voW9,~8;γGS/sat/Pa~bśoU 6z޽XT3 'gSvO CU+ry͛A \Ϥ!_cK?t@H9P|`Wane?~<_DT/3cH!pܽ͟-YymS,kǩ7ႎ2ۻwEm]&~ zT]!"0<~5Wdo3̋gg7g:?Vqw?λ0rt D@8w2"A611.²f߲Mq3gs2U[lbuh&7ldǏeJ)%lo4|mxʒ/4)5 >]|ܜ㼹e|#oL;rh<Б%tgL;)׮];}z>!jP}d\+*9:}:6M1Px<% t!1N {n3!?/_ -)xj]rŕ/0Z\9_VPJ5&: r13gfcfj&Cf-'d{wdz={bǰ[ XG Ee87 g,p\%OW~g] +%F%J+ej^#tHx2y$(T(B"\xo/^{1b4>s9awggKejL کM^*-/;mPYj:P+ OA~&jMCkQrU7v=- [n Yj_=[4`#>΋Zg?O=\ܷԢ jՋNgEA[o(l D|(_,?m?\?-1%c&OU[JŐNB˻";Ë0li#:<'"֕B[',wtbT_ h { #:.OWzQsЖBAEFHIJKLMNKQZ!)4,9FHGE>Hn9@BCDEGGFHIJHOS! )4+8CFA>jмM8>??ABBC@^IFnFENP!.<)8AANSDA;99:;;8Yx@CDBKH +3 4A@DLMTagnsvvttyH@ABAI@!$2+8=@><:978;AM^rqAG>*4*7<=<;::9:975446;<==>>?@BI<&;.81.3767632K6)%05Ztx|ys|x7)!*?Ղɼz7)++w΂»z9;5ςøm7I1-ԂȾF:$[):k H?/&(+-CKLKE==- $%(&#$$ ƅ _E;=<<=<<=>>?Ia+",$6432212123344:J5#+$731210..-,+-121233Bv"+%610 /.4DEBEEBDVJ-/00AU.<#6/.,-,*mrZNJ@US),-,>F"* 4,+**)!!"IǛ."()*++*$,%2%""! Xɂ !" #65#,%1# $S !45#+$0 Mɲ+p A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs%%IR$@IDATX Wm\g~܏f)MACUTjD1`R!(UJG5Z ??PAiTZ6iv;3;39wL-_<.y^;w{_ah$wrRBs sZy=et ٳyڃ[)LT=d9'b)<ˀJk8 bw#_o|5۠K? ajEibWFT+&)l+@VB(so[^xO Zv|(5u`;8D  呌Hlzr8gVsB+ػM_w=sŝn|i/qρGiFi"Mr$i8I "0b #>և}8ܛ!5ߕ 6\yqiPէV^8Bs&eSYp&^)mtKj1!uޤ oDbVpG&qIlm[* T Dۑ> d&H]TgcS_Ena"-B#22'uwۆҞ08)|Qenb l7̾*R>O{=}ho..0~Pyԥ\60^$\Gsqĸ)-$(p lF `OSOGe !_Nɋ9!܁qD %sVlԑTN!FBN8›я> DxژY&>b$Sܒ(tYh+'o*P4(3~;qßA_cEvI΂&R(jk}z( p_Qm9 ' 0u-XWoVk|Ef 3EoJ+K(ӦhhKpa( Kc4oO|~ uAsZ铄K&޹t:#ŒtMi@VGgEDYN;΢UGzȣkCLf Kܱ1V rLN"$硤9`߷="j< U|WdIB.AGmԬ?&"Q`lz^~&)+7B+84Y`JaA䤍ꥬ`x ԋotfnf 2rUdpasc_d}F)q#izk'gq.2$GF3 xiڜyTr=lv`3XG+1Km^E#Yx]'tk-UH,5tWD5D".3)G[ OSSxX\\Xj̨ZvxLK)!Pyj_N16Z[[l6'EԩS80?OwH%{*wDJ r01uڹ(1pٹ&.nŽֶ&0núνӴ'1lȳϴ(1i˺˰%/jƼʯ'1hľ '1h 0m |Mπ ̾7rŀ µ kR87>FS]L<@D"05:98775469==#18@ >E:.n PNԀ ÜI7s g3 !)+*)$J7>JIwKK^(*3775232.56:A,10-9M?Hs8-09*.*(-(=[')*6)+$!Ux2##%0*).}d%- -&]i-49>O(()'%5@+')& &--)* (5.,o HNր Ş<7t Z #s8mk ͛ic14PNG  IHDRx AiCCPICC ProfileH wTSϽ7" %z ;HQIP&vDF)VdTG"cE b PQDE݌k 5ޚYg}׺PtX4X\XffGD=HƳ.d,P&s"7C$ E6<~&S2)212 "įl+ɘ&Y4Pޚ%ᣌ\%g|eTI(L0_&l2E9r9hxgIbטifSb1+MxL 0oE%YmhYh~S=zU&ϞAYl/$ZUm@O ޜl^ ' lsk.+7oʿ9V;?#I3eE妧KD d9i,UQ h A1vjpԁzN6p\W p G@ K0ށiABZyCAP8C@&*CP=#t] 4}a ٰ;GDxJ>,_“@FXDBX$!k"EHqaYbVabJ0՘cVL6f3bձX'?v 6-V``[a;p~\2n5׌ &x*sb|! ߏƿ' Zk! $l$T4QOt"y\b)AI&NI$R$)TIj"]&=&!:dGrY@^O$ _%?P(&OJEBN9J@y@yCR nXZOD}J}/G3ɭk{%Oחw_.'_!JQ@SVF=IEbbbb5Q%O@%!BӥyҸM:e0G7ӓ e%e[(R0`3R46i^)*n*|"fLUo՝mO0j&jajj.ϧwϝ_4갺zj=U45nɚ4ǴhZ ZZ^0Tf%9->ݫ=cXgN].[7A\SwBOK/X/_Q>QG[ `Aaac#*Z;8cq>[&IIMST`ϴ kh&45ǢYYF֠9<|y+ =X_,,S-,Y)YXmĚk]c}džjcΦ浭-v};]N"&1=xtv(}'{'IߝY) Σ -rqr.d._xpUەZM׍vm=+KGǔ ^WWbj>:>>>v}/avO8 FV> 2 u/_$\BCv< 5 ]s.,4&yUx~xw-bEDCĻHGKwFGEGME{EEKX,YFZ ={$vrK .3\rϮ_Yq*©L_wד+]eD]cIIIOAu_䩔)3ѩiB%a+]3='/40CiU@ёL(sYfLH$%Y jgGeQn~5f5wugv5k֮\۹Nw]m mHFˍenQQ`hBBQ-[lllfjۗ"^bO%ܒY}WwvwXbY^Ю]WVa[q`id2JjGէ{׿m>PkAma꺿g_DHGGu;776ƱqoC{P38!9 ҝˁ^r۽Ug9];}}_~imp㭎}]/}.{^=}^?z8hc' O*?f`ϳgC/Oϩ+FFGGόzˌㅿ)ѫ~wgbk?Jި9mdwi獵ޫ?cǑOO?w| x&mf2:Y~ pHYs%%IR$@IDATxIdIuSܸ9DUu ʞ-P JAPՠ@R?w\tj!Zq#J$ L6X]sN1yq̬!;3#2=ng}g1sp3; |%0lW>R >RC%I`'vI`'R Ym;wI`'vpM>vwI`'vx!%|!m!nn]|[z'vI`'OJ '%]; $N; |M>AI`'vI~'.ߝ~Q t/n'_Lnj)<6v+xw; $N; <MzqN; $G*#|'vI`'S^v\v:1.?m`m~6^g%!Bv+Bx; $N; M^ԚN; $n!KN; $*;?=f]>oxٔ?>#n#|'vI`'S Y/;vI`'vH%wI`'>pEFhusxQ ,[]ޏ;y?>G!s'ϓ`HkGNFNF_II}7|-إ}n$|U:ONv$Nz &lWf&_Vp KI'[/?; ${y߭0YKt3E 0qM5 [&ڟYӱ5Gw2 QUZ|Ȫ%IKR䟞 H}+n~FIlisi_h//@'ן;u3+\|Pnutky^\qO6_o ٦8 ?l?l؀{sw+[O~6vۈ?_MOo#n;m}D<>xokdzh!w_Do2`}zrg.?z,?wW?:@O wV{{|D`ښk g~]泙cfp1:?LoGt7t K9]c"{?<6>;<|`18h_ ~)`X=^2͇cfKr8aZ Vc6跜|^׋[o;NXX`5}\H r W jjZ1ڤ%x[81~F|ɷR`7 v٬I=d?"QҪW3f5@!++# e6ΙВ 5~̒OR V`82}-Wj"9P>jW˰)yDӠ:{ַzjl'+&.#R9Tz,#Fd5Eu5=d8.:Es0DWU&ڦT:.hgJ꥔%[me)CRiKښr0wk,&6+ e1p-TwOqQQ=[[(qQK!~)S[z!@T$`(jܮ~\\G=o|7CFzY~v>2`|yyyyMݑ:hVâxBLǃXq`-=`jC^;u/´".L[f GL$ 0|5B?M j,i'ѺS+RIp!?M-?''Aɣ^IAj3n*?- f/oF IjF.h<.u"< Y[&i;yiu{>\l옰dVwNuJlY4DXnɠ8uK Y%#FKq=[w ff|'ѕE[cpx#U~)K:"NV *_2Ll98z?ܿUYAwI3=pYr6ƽy۾Gtݿxt-l>h/ۛNuExgwI:h]0Xҁ, ,@ g,Qxy7 Ks=vֵ BV_ږѹ eiv49d0)qHn)#%lJ"ӆL( tIR~Bgzp1g"L' $Y[[t}/LpޤZ&ߖ:t!'6񹣕T;~~cLv{a'=ak:uCGDYoݡ;8=}}05?3_K/Ym[T+Gʫ~Zym > .4+=+%]G-ۚ[?Q+fm+;|٧WHK{iICjS"&K'p~tu{~:<;]^];O?{oQ[œ/f~7KỜ?w...~; 㛛%q o%_sbw&؂2w3㋳n`g>`ؙ<=mګ%5 W 1^pf=͘%g&t̳ `?gBgkvmwMѼ}ufV;6ͯ @*Bt-"ۀ 0X\5ZQ$֎I_0K8m0[ԒzrTB:_{ijfz:!߂%eْ:B*7V BK[l4mMn,[v&WTml$WbQVE $n |!UI^-CƻWiiׁk$>*ح /"'DYq)YPxo6>'/Y"μW~ zPRVds?iq aMY 雖wوP+6L7:6f4W~\t6;fkx;`;l^p /գ6p:$3;8;;;L&"܅jks_ԴBن'Wi(z7Owoh4e5MÁ_pz k5au;=[c^G[o.m(<9|7+:>_7=&Hu@);;$dqe7bD>Y5h-;D9l{Ϝf0ˊA+Mu[¯F>db@7CWȲv">G+5a,op {.li'KcB@7-"Qז?{&*I ȷwOG~v⦉+:RnDk3:UU_X!E uW[p+ℿx7+ɧ7IaG,S(qۅvjWt4˾o9а Ф)KX+@F.YOhIO6 JRf~$Xoedd݌98bqa 2I _dRJ}@WXbD&i\q0=T45)s$$C/b<aU5CW EnlTzOɣ֎.vux\t9ce2챧Xd6p}|8|'cpY|f''''#N|_DSOk:2(l5=}Rqmih(s:m $Ys }K uœhpp΁՘ÅL8L"yj8{1Nx:6+֧v wl?~<w>|W_YW,8 4nzخ_MAHuL-2ECiP{4,nnS&+rG=? n-C}Ȁ , 3y+d<Ӆ>Ndry8illށ BIk_nGˡW" ~I`p)6)lLy+沆/@YDRxluv3a6p{yЅ-aL ]ac> e'NyeFC6-ה2_ ` Q72&⶯r)dBUϙzSm:FLC5aȿRϻLX-yY`>\\̏ǽrvꃧlph58k oTO7ވwp;>|𵇓tjέOc/8|-\ *-*iR8`p q =xk\;"OyDͽ 7Gyky@]PN~vb}cKLaGui|YFz?L뷢/k>8<?7\n'! m`pU]5}.sYa 1R{?K?| Lnߺhψ m# 7t7eՉi!LnW}-v=}ël/lsiNCa/Q =~ƙ!-n>y}*f4Ζ3C0:t\FJ%G:%-ЀR7>vLgή%ˉۇ8 ON^:\.@qИ޾\~\;[cKmИĞG-flw{wOwkfozųaB- KwsNo"nvgjᕧXw2gLP/xoϺ xȂGyu~rwxO[ Y##b/5_Sfp0I[MYa1o1zUG P Q&`8KSY8A~ouCS搹+3֙-'nQh)JyŹ ݝPLXF?zr3x;B;x)+? 1=OR=eݦ\_s/ r(oǷkvӕW&m]w`~!^g]|*W/^nxfSU<;)xԗnUyPQߦ>>?1>N{xx1F\Q>7CJx_Zl" r9;0=3?%`6wSv\0}g;G?Z\yVF>E6fpC*KDL03޲ըDc(S w&*&VMC:7qj 5lL|˙S^s0qAerGL{anN$r?c2pn9h6DŽiȼ2d )μ ix\;T*ſ ;8 Ep +nbBwc ,h)s\`WيG:m"<3'gX{B*"WCb &&l0#x&IKóܘx8"&e92Z}^o-2Qi͠E7*++A{=n'ˊQςe'BM!ws)D.o>?Fr 4=կY^1?UNsdsrj³Q[93bzT*]-zJ!Ky#0 Oo"?]mNxZnaάߪ3Uަ8M~cB;!M~3wF>~>GO^QUn8S&7U+u='g/Ce@~t 舓;@gnhK>RZuU;e^EyJ @8@|V&^`01egXW^x`4hz8op} ;~ߘ޽~?';?Ώ~s_{r.yf[E5wыHZumx}T)_5ЖSrSf<{QPEddA=i?i#F ];9`yL7Ѡ\:zw1hȘ[ʆKU q{܁1ҸEns+xR1=Q.9A7e(0%qG(Gq6TVQ '?NRš=lHBŝ|Z$M)o/Q#Nfay0Ϛ.:ZЏq6Y^]5P}I撿=WʊLc L,hkS_?Q^# }I_=F>:(o.r7Nn%((okSۜ8Wn7n9܌*YG#O1sXEѺo3v\N7o1?ǜ0Z&mĕ}nᑾRgq󴺽(iH#nWكb~\>O/v΋o5~ʒ慝8s2Eރ2ӧG޽|ݓX\掗9W޸GEIգgk&Eю-˝3P?Uvr0;@d;={Π2s=Cg[WG}peS eea^0(iǃ15pPn;cOSO374I48=}tk[t{PtSm&ξYͲ),5X^w([ ,n)ʌdxhzxѬO}<:C+2YyO0C^?W?o|nv- =^De0=FnGGXwr(xL8y$1uX,C%cO'7vUk(!HRgB:&Ds(Н؎`Cw( OSW:E_ϏL0AveaRLX 8}<{0{ef|v31V& |kctD{.ik{YY[jy#[Feƴ]ЕؒߚMt>aMwH{.je۵/ѹSyL]ßmwB}OMO275s&3V<X ),b熂 ) [^,y7+vëheϔ\x~]޹k.GJm,QjVk~>r`<]o~s2b˼g/ٯ/{<^)zj;Êa1g@xQRɇmvWtwφuIqZw#-Ⱥgyw,}\r=`|} ŸpLƔ@ZJ s0/2#3ijY6pN!& oqZt78UO1ݩx!Z^wFy[uՇeBNON= B*Njz8:ZGLtӞd0}&JxW69W8~#㻃RQ#8vã;<1xc'RV4ᯯj Δ|` U &ب0VoPs=wc'+&ى\#vi(Km &? >= bG GI+ڨ[&0vrj;)0|=:Xq4tzgyU˿*ze `&f 7zJ֝;/6{Ľh2QSV21yrG\W2nymK#EKx|h[QbDž\\$MkR c G8" pR<3_mS8~q3;̃1(_vNYvY^ht( r[OX3݆|d .;yhIsR) K~0xvr4lY0*SM.i_ţ(3 |MKܢ 7J.exQ^'-}ofC==:'?JUjuKѭk1UZ)ʕ.kODW2*'܄Z&S\mev՝<1STųnfU6nF~|?'Ka+t8m;~K̔"3O\/|?b!8 ϶&m/&7mū>hLڔq<8H"jVj$rJy9:A+rɤ ܭr/ˋep ySDKy <^UA v߯j$X^N9Ǚr}Q'3ѦsC" $v2 0ۤ>,gryg G&ӗ]솕=e_nB x ޏO00 yd|v>Ow2~_MΞgQ.n*Z}YE pw-gk".w%,l8e׭"ٸ[~1/YE/kguƴeɌI|2 pP?xh,i`s:4vyM÷e c: OribK~%^gEa[҅*y҉^X*x<@tS<3MW voR[ $byQT\Y>c b[GbFȈg!71\)/x)s yCvny@u>z=S6ˡ$|NroYX&GO:RO(?#A)߁S/?1V2#tLa\S> J 2ۗ51 Fc;xwH3Q|ɷőӯ|o,[`臷:|bcL|!7Aۏ^~~%ۃˋ'O^0퍬LcMXkS5(Fź`tp_-3h 0ѷx5USҙb׸˝s3haH `Z1'n>U5n4]5RM%_=/qmylNOЬކm ۡ%xĄ-:3gَuVt~Ӄ' 9+o-Okygn|f^آp|(bb8R (vx)^~DZ:nIKzCl,L@eޅÓ7NT3;Ŕ!bO/> ]eYx^e˜lN7CRvL o<4'U-7Qn),7;Is;S^tD{NXI:!O]w^HWGv1c>ˋYWF,G~җE~вN'a~Qu(Bdi>՗>%GymSʯ/wV};ިR߿G#-*woca_=sxLo&ʌ\H2Mh#OzwcW܆L;YR[+˹OݦlE窓xuٿ?=Y<ѭul뗘ν5}?9K~קʾ1tgtN|k|߅2ao !0{@8õG7Їq+f;%-h44;%*z윒 (lW]sH]:OR7wC%XGDjɒW*_\Aބ;O9Nr#x#,/i=[9X+;L˝z+AHlyZyjPL(NDΖ:ALF99:&-NX thkߕҗRMOӖ.DŽD{`},|_W^1,SJ(SO:@j+HNVΓfwiԃ8/'f (3}SDҏQK'(9砥%L<=[yˠQ&l AlY7\AdBABOϬ\?rϛVז,VG|%_\v[o)+tUS^]P&ؤa8_ܑ \rV+e@a}ߌxC 2g]0S0|B˃u+!e8NPs>roƄ_\tPSb|~&{Ah1uz\'QQLjs[B;mYЋꈭ$O߈ۚs8QT ke+yZ.Gz:qQe:L yGItf@1*: 0pҸ ;razglG`ٜpǒ-;mEAVAIj;Y^gNY1 +/Q#୸ˎ-/L,QOfh/ʕ DBUR޹i)'Wn4gAi8RȺLHhZ>Rl`9]n5Y78U~\Ynr壁T?4˟&PP,K]ι@Vm k@XYCgyjy &ZyDEߪ 2wGc+;t6<)>)>,V5M4úw(r_#}[?~-܉YU2US!RVj ˯ G ХeI@_ S*|V3 (>b{%{鷼e\`sv:gj/:e#cb{9W/۠Y2K^(;?Ʊxil੘@|,`Xr- eCڸ8{IJ/j[b#In4A'; _v&& -.*^Z<cS@9M%76Ur5^GJ:V(7tFWR/agG1X:vB@ 8xhD҉ܻC3 ;?ⱄ3J% ߾\!|F"Z5}p0O\:)І OpQl۷,/1 e,h_K^#|%) U2vQǃv/ yv'5&R.o =:vzTʛA Ⅱj[^䥴r6;ësh\+dY"Iq q_YcK`nlbWp<7vHPig=^d*Xcϩ Sߨ-6[IJcP<6K8m8prbMx5V/}᯲)>VUG?,SgiWWYBp){=IAe^샎2i[,~hQ85Y- ' X#eV KXÓƽz^~:H^) ?^yht:ut|}>W 鋜s8`KNLI[\%0hO ,-85:IIJ\mڡ>3Ti7?+s6Vfk& siS: hT6cC2ƣX l9P/lOJIMJ;Dl+mɿf~J~$E Eo̫5ބrA:W m*?1ϟV|%W&L*F0@,98xعKmp[Mc|Xn +f-} .@IDAT(pWz+eԎm-܋g~{7h)!yF]Z8k2I'&kp6[8+ s`AHm%  1ɯ<_sgM`2r4{?He0U`O}~V%/e,љ>m#yRYnQE?g${~&x{ܰphV{9۠ 8'u*?cpϡ%[0Ls1=s|Df?۴{tz忊^\'Tq9N{ݸETzx~vE(M)E-HE -MaImKZMH i!t tI_\j.cZ@[NW]$zI#XK\>3 v%LLKI%(8c\Virq^zj 9Qd!.?9:HT[Ddh>*[hYtLJb \ݎ%b:łW~H nPӛgFV:lw;bL:'Nr`x>eOKIyT- ƕxf5F8W ]Bvd?'8!ǒePp^rj3,•I~aYq̅p𱉃xMhYe:FṖA6çjGP\be~_` o#&2y.D=cuk9}N+oru+~)ZA,,MO˫Kzf?'+܁է.3eƞ6Y5䷅'$?a-nEf6AVUkNdE_>ϬG"= Og0Wi%vm pS_14|pg0c> 0|bosU5vU֛WFWӃC&̯@I[xK̜AW-vM-~teY[y H5qJ_o<&Ní$]ކLfe^[\k}jtb:2hg1 G]@ghzi0M6rIFtU$ӖJ ]p ؆"Lɮ8ōbRK*w[ʵ_=|}7>xd-x s>@7ڲղ|5ܛYe؟LN??a[h}W?Q2hUgYQw [2y77mPtQ<mkV:e[pq 23s> eKM~~:i*|1u˯6Xn"! j{ d5h˧N B+~@;ښ t=rJ[>X }yp1/mx>;oxVvîU8.M+/)QQ~Zmj%<;U;K<̞ocp0oK<SɊy7i{s$Q) IugmQiZ񯧯-]= -]xKé=4hv閤#͖߾Zx7b#~ct\rZIZoEBQG,9%]H~pˣVpsY"xe5(!NW٘<;j́v V;^>7_\+<7ֹ'c> ~?>wѽ;~ygǼ}iJWtʥ 84(,8*I jH1BA:H].WƁi;ӌ=sV7s, ymB79DTȫYE3# #D ~@Z7BǗ+[K2L􂺼7hG?vov  mDm⹉v<[WS ,{U@6Id?9ӠW#n4嫬g=M\L-rҜ bˉD 1JpWW~-z Le҂ڞ@Ue&-<5n,85*r⯔iZ~YHPRq~'Lt{ zO~{ w*?5K$ѐ$ , iO_!;6CDmkحK.qewK 3}da;g Q<Ҏpc'-o=1mǣ˼ϯ> @>}Z䍋~7̜)-_ҵ^|O}zvqϧƧ71Ϯy[_\=/Ot1<]GONog9\>mTjM'Rvd7=I^| }AG-x=_SܝL(<\6z3~׾чw[h׫[:PpX| ߽|AJ|$bOKʈvhƘ#x^VSn@0zX'VU8]V_PD"!4~<;}>Z#bti bAwvHkS;O󛯲juUӚq9&wj &0Z4& M*gX'P'Oca&7)t6V^3iQ5vt,ӾtU@VEz'qXT3+IBe Z!w%-3e Dm푶/&!hFȺ||Ǫ'f@A풟?:z I}|dYWWuPǨ~i/uS;8Zd׶!UW=-=3yTIӴf/`b=@CXS }MV6X^.eCYVk=\8k3fO&G*':MO{l>g{77­8HbTH嶦6:j F "u*t䝳lH34QH]:zWIM_G{ У}5٩ds<H#Ֆ`9^ӇCJKe4G/QՋ,A2ˣs̕w ӆf[9D 5mKK?pid#d'r|I3Z8f~W6Va&_׆)HOeQVrTRM!^DTQ~TREѤo/H]*u*CE)*bQ4)sMӷz}[Qh0聓[?nx.瑮6#& (l^^[09[6;^mpiiF9Ikz1Әrp^r`SÜo V +ݻNf&ꙫ[X&۝+?tg=-ܽفӭ} {vyb4}֓oIb׏?>7bpoΩ:5O٦*ն̹:hl3{/:ղ52Q{0A c@,3`HIW[! >L|3uq38{ʻN6W׼'B/ 7;ent Ru v@eϱ+ kuL(H|hav1蒼:_M~"kB݇@:(#]f4&@G?mBdɿ]F]Max "0X D˻qmzX Sʧc G&-ȣ1%GߒxIz+$P,iVmeXMQ+ozU 4gR߭X^l{Z0GY`?x߄7m;V 0Uց! -OR"`+)wW])zDd-?0JZM+ ߼Eݘ)SN[yͿoдlZ[&c`N|L}2'4[*ͅoqxkzOÓ{xKu_ſWbI$`[1Oؼ~T귐h4P'9In {Eq}s,7j']54[*P;Q(ߧ/8N6O .izE{Z4(+w[5qSeMp5KƱꐙW#; T~ Ħ{`uOڶ%ɷYx1_R& qr\O3c-QjDt'*p͢,Dy+ +y&^jbލ¼09W%>7Ǐ&TݲND!7zGrRl⛏寥+>D~ݤDa˷5:TLז ]>-2ʕ,·p15ѣ_$|=N -Ғ|H[´A`#k7$oX|~d i)FD&PV o=: _g"%H4:l04 B{5݆5}iVb;߼B_ 0Lst-1k4?؀KgARv&oh@k3.|rt$PZe[^5&OE@7iU`!|Fӓ $hIZ"ud2#Gw1Z!d.BP3 Фl҇.4t-; {:Jdj^Lx>-姱vN6;*"u-fjiǖ}!;{GxL0 oSK ( hLS7.v3~nr+?0IfK:J!wԽH/EOGfɯ`bj}{mѕ%h5AG;Zdڂۈ⵽aW]5zӾiIQ$%6:jiF j&A:HNq~ESeQǩ9^~N]2IIƅc\GM),Mѝjvw`v=n;o.<}\cCַ _On?y{Q!=E *1mV9r f&5jęJ36Θ kCn;rWRӞP<|w46[Cd,)T 1hOw//OMf*{<8w8^1!zy6/u;FX֟ZVȒ]cp(˺n yjU#f[+<($,ƴ5Џ#h%O=)]i&_I]".~3T0 tU$hz[2sbmЕ̃DQ MumbJt,je1lK`R6,#BR#C ld~<4ڏ%Ff=vr5؀كH$Xw"K.c \de24 G~ւ Ӵf6Cɷ KDt[o Qs] ~]~L$f%ZHWn&?(@7Uw'A+fK#*6`c=&|{T hOs#XEcZ.-)d+ﱗubf؂-xEĜ\]`n#20B xϟ}LV@Zn`9 <r˃^>yƲwj/~so DSM|5*Q1ޛ3|yqɇ7䪢8yZ1U (Ԛv4> cGIYk5jl\I[\—/Rp-Ȕ0 e`sMCgA)n}}t|l|N[ר/RC2$^5))ʕzD]Z']vtK3sCmR,ʤD zЭ 0 ݃/"b4ck`l= pn5M]Uw~+ڙUErg)2E|8NJuZ 8D}r7@WLdBi~CA-ߊd?QG8/f3gyս&amJ IqGv隙2wUF go©B%<;v*g˶Ku(Siү`c8Hِ_{&ԳBt3ytHBáVa7A6sY70AmS'0X/&ك F'U CH3όˡz;#S Z3+E5E< m۱^_=yY"FWʇ#6v7/N袗~;RwĪX(C=j+]>/ҞQ9{7ݥsH =V4qҟ &eI\ ZV7h'KdOua:}=)bEX! @Aŧ,$ \zCB!/Шr)/+O`!kdԭ~^)֏z&ӇۛCD<_wz=#}/`PQ'J\:}>%Vp<7쉳y2P[Jqhv8r->TQZb ;xC?M~M$;Rv^(S3CafPˌByfmǶiBcEU!}籮0 C&Uu nԬvpF f1^? 9?qu_DFm@ۢ f1(| c=[&Cv^U)姨qbΈipI/^tJxY t(b >َ Fw1 yM2Bć:X 5*Ź}XM^^/gQ&(V:NH6M{`m,y*0g@Rz`~ Q.y|]5׾ҙ]ioaW%Hlh^%#8 xc `R^ģxx3`cDNig˾P{X| u {JnZv|nT~s}ċn2}/d(Fq彰NQ֥xTO,͗J4$o R"P3D"|ce[dC(ŝq J>xH2DH}t Χ۶ݝQ10^U4V)M">ǂ5nt~,>=./Ѷ_ī+}DH4ҿDo::нߟ?\}{N.tC+P@p;k@PPTT``&P jNCС==eJA1}X70PFHi$E#a$#  f\P ^恥鳻\߁Y ksW59ҭN&;łѸmU[^ t$.&V"[{mBxVꞘCO"Q|Ê_ؚyDb~vbeKe岋3w2Z Jtf.sa پ?GfavOu(8xA_{lOz|~~bw?ʼn%Ζn`;8B*BT6Bs|H7SxOxB^(;~39<Ј=\q4N.eV!qRu/7VQ^0[ ~6he0RZD?fOrU Vzu}'/?|]z}|SuLMje5Nj?*1,zõ;9Vzy.PRgo0I} iI/x͑^|z.?@}\[oƷ\Qmr I_kzLMHaV M8-iԈEYyXW(;8 %0PSضQv/p^UH\ʋ[6:8Qlx?"TYG/0 y23ƿ&G"x h輻yBucwI-VbRy k=Lu[!{]W#%Z4R[@!ddRL䲴B-Q  b>u=t}rƘJ1y1Ķl );^U:i a8%P(H?HඖvG3m`H.6*ex.iajXx焐i b:l/ [ԍǗ*N*TPeu(0XYm-mZuB >:*298Q") 0:Ac`x9|qBDD4C?~oh| = ~N쎌oЧ07p fՅLxL*"zKQJTIkpTPSyQ>vf\V(Y@&D8֏=t[PÌlv},6y1b|E! d 9(|*Q.H4t} g'Udݝ^"s4 ~n9jf~p/W!483_$J^»B.|9PO]Su4fP־Qzz%"]gX0zWy>B ѫ`$V .km6/| 5͗0 ^P¯X 3\G/)QGmˊZf.ˢ Ɔ:"r< #[L's FUڵCya4sh!G_Pbh+=B7_-e1!f{FJuB܎Їn`d@_R1h!ǺR_>t/Ti7@Z/!x!ミB/n|uqup%Wowzwlpqqt_}3awJ/9S}YEt+YrӑD"UrWh÷}M Й5;u zC 84JQu"XbS(`/҅/!l9tGZL5g$:9?| M6CBIlfGLW>]&ݖV!ozTh|lOh>Ɵ)mG}n X&݁#/3)Olٟ?$nˋW<;yu }H -U;hO/W UZVtѧ:.Pi = 9oA Y3(w'z'VG7WC~k_dw`wKKY^>!XKY܍xf@ -ht=֏qY+7.rZU,&F`iqDm5NYvadx"h YHwV"K[[m,GDME;%07$QWeO:Dh ׺@IDAT,:=b䝷 P(7CՄX\sjQ>4I1OzK(/˹tꒌQ0ׇ7FJ\0,BFyܟ !X0NEHMh;{uR4D(o.r%jSV`0GV j} &">ڿE_ȩ[|% gڔ"1ˎl*7AܟU C_qy WSv׸%\^r{1Yc; P];r-b=\ ?Ӌ,~|tw{]v)O/??>>Ҫ2J/yo蝦\*Åb0 ])*Nz~ˊWiָbh-.B6@#KGɤݒH^VUJ<%Axntrwp֗FN^sBOoɄ| @uW>8 %l# W՟cFjks#y'¤6L6JO4E"P:2ۄФuG:P8R0:|yB}zz@~7]+Hg &\24:5:UPC@xTCp ]"OOTPvl0`lLۏKxd1.6-yTB ?md= $_eti/EqC۲hNzn/-TV AB=Е ʼ, aJ8p/|yz4OmƞIg Q,(C=>c[|FCB0< {?ZHx]t@\|fq=FEN$cNxQ0)Mv\"ȷ1 Eg|wp0#X@+~*if:UL +u۹ݵ^Į.Djpw J+sid |z|E{?҇?-)\~Ja)XnaՍ +83>BZ:n\/]Z،5 .p?hUND7K[Jg,ə?!K xtI &V*YXk07tm3m8T3i *KC*m+mB̔ר ! $ADJTv{.(FLY Ol,Ḡ@ 6M} ǐ ,r3gu2o_5jK&sUHcqɩV\.'}?U.XCeQG#vӞ @dJQu~2NtVE|Cu mle5|GÊj5R/΁:p≼`{/TEiOܓ=uˆ fT 7KXP N]ŵ5jKd{6^7^^|?etD}Uqݺ x\o`!}0|E[3sEa`2#1`XS#UV-_/Ou\ϒ~x~à eɻsl*cH?^F[iA0w >{ǂ^ieW> (RV<Lo$OjEXJf1(1:4|ɉe]ʗbK`E6oLYZG?WR ?Q̖=1o$ 婃 p28kG^#J0R\a$ _vՖi*"g@$T^C?q&"c{ںZynt.Tn{5`XA2:Ǝ b;Ѝ?Z@y.2 &SlW:=p]~Z_>mC d[TnC(DgtwJ?K{(~D/U:N{5C(hV^:D_I# L+Ԡ7)u2A.}:fda _Ew؈APIqi vE QG>Vyc|cgnȈff/#liKV_7e9f>[@/urSsWj9N5i⿼|1D;:㓓D)HkYy1 =M`ٍ+*;.̅y܅f$R* V"bI ~J@7 FX =q Ӗ NjCLIxaırK3ϰK\T,5Dqm!*{Rh5ؐ @8\Vt hJT(:2luRmbǨ`_@G&5p%= #X,>"Iu("N.%:Rq$І>s-Iu&`B7t`/Z8b#հP5@ 5v[Dp*_sZaO|gnጱvȪVYcuU3`;b0:[ `DZ}fAQGnpPi*Ic_Jʬ[SkpF0pan]/#}9i+9X=y1~`tO]_689J_R'q в.svuC4GGWwחW>֙%=I_uq6B&: x(UCpQAϫY + Q߱[l+J?xZ ፖ9K ~_xj`_0y<]d-?ۿADQ?a/HZ{B6d%0Gn](B'W[EBƊoC~5~V&)H:x3tG3c T~Mm틉8 ~Zy}([ -PWFG00Ǎ=SEŇ"V -LW!E{$Î~5{4;9+ VJ) SP 5*N! Cb. g!AxL/da{vyur]ʕ!@/)O[I)bShW۸+.Pqі{ fL?ܒG!Z#K1C'\*)0c&@gӅI3+]6ea* ˾#}Lrci鄄~#S]=Wq%^[O{gL64x\_zv<8:;9=t'''7Oxd СYx9{ mt6,VlTZejW3ɿ>>Y2q])VʙV1B+M//ѠM4D^ ,AA3fhxWCG$^-@91cCAA `ozʊl=~G|ҁ=1/bOЁz&L]۔/q"ez35UbYe "58rƙ@;bnA CvE_w0Vk%VC4)M_e\)i"H7E1ՏtD0WZaR?vV8ixj/XmmwC$gERXkf߼:D*ÛJIH&~S[r8VR֯I3xRw]l_@7=p={v{B x1rZ.1|`IX h]59qOkbi@ Mc<&eFp[(ޫeݩhx/F1S}(zaIG Ċ_ymȴ9s3`u ; + U=EM}u`4g&[.cDGrk8V^;c3o߾G)Op|OvJnu?pzⷐA?]"*'fTj˵~ .O'h{@;mߏbJм:lIx׸Cl.@gF; Pٟ/}[2D|B 0vE#c Dډ!?~TaQ2[ʷO,"qd [VX]>{aBrOB֌no0i+o1+Ȥ(.o L:):(s%ޫźFRTx5s>4T_/W=1|mJx/`S3Xvssp_ o,<]5]㦫)K] ~ȹ?Eue\{dYxUbKh1FFc@*OQ~8 `o}4eȧ%u>=:ӸOG۫'_;,~51N IG [9O(0J!߁tVl=:d"%\J-;=< ;*Ƃ~+Z٨QFz0R1y_< \ ;Hh{ r,ނ6kۮ$DjXf@1;->(qseo" Dfq@=;* e5U6{z=2X{AY1k1K2W #Die#Fha݅S??4eǪ8ư!EFYP}Fl,nOJG}؇x;Є|֡kEjC{ WPyG ``s0]tBs-_)R=yև us}_Chn}gS| GY2Dև}ߔBZy/PlUl/ƗTr:\fy^~x>M< Cp\.<;9U `! k]C@/??O_xA.d6D\ 3qmtT,m8~׬s 1a!ncoB ltבuaZ+hQzkyI|0La59< CgE(bHz]$`OXT).,?)Ƣ ȉ +x'q)$)_ NZB-3jYPf/ 1 1Uz6xZД0 eu `K<WQ un[b)ːхjp#+\6Gպ&P1HߙvlNS)QicѪ}BgӰk]_;>;OO3:|^:d!(MӈTq麊h2]xPZ: 㾠YW1t\ðIjqrcд= >c9xz8k~Şx{t݈@^-}ƦITV,@z۴w?%Hqe\5d! K7NS-%)دrׁ2 q=U`Ap-=n "K͎Z]cS:*^?ٵ:-ָ0kaFErKD5E t,Shy v+Hlq )]"G8gg%{0|[Wl ˤ* TP'2v L! z}6ŮTd qMj`o7uga(%MaU{)͋6P-rI,˄i)SO S.!SLۆB+VT̈J'}O ZdldYQ_p| eCg>Uڊx#YAc0"x%ʼqӰp^᪯ .M+ۛCc]:}83>gPA}27^w*O8{xuNr*3Xn8 p6^=i7-3Y&e`RtT{kgbHl7=,:Y#_~1N:!|j c4Sz>+hpfw~\>|v{ˁu-/;&oJ[>؃>@>{9ytZJ<.Nӎuͪ:P33.طR\[*^nKó1GUL,aÕ35_L|6J$ɪoOpJj?ΘԠqT~ʄuCr;&T^[!L_Zb_kbdR,u(Mb_eS%ƲRvSWUn&[P 1.~(t( B{Cg^RuB߭*Z'5`jeκa$¤K 7MroZawD8zB' ~J IfhkL"O| oAkͪߔyy&̺Np`}d]j' m{./|+C?aGi `ͤC):֩uMﶊP'}03g:ex<:dG<ۀ+d<'҆;D Ž@q.b-46;QlCP%NXTjRpgޯ59rEɺN|$gtZ3̮5]⓯fC}/py9:;])d%2_{VA:i"Ui>=UF(_{oAJ(UgJ| oRſj%&omp&B5ҎTLK/}ã$FDpQma1m=|\s9`?_?kAքNY mYb)0E^|rQΙcVrpQ7u0vS-IwP8_qh7ᛃj E. P!m"-qe5 B͔秒(/g=q8c`zYhNM*vׄ!nx}2gmֵf¬C^ !I)|'ef|HoU@PniΌ5bt'}vm(???//>rkO2F=cݠiTGzME?fg>EΔmǔeiVYIfVx,k 169C1x¾9AV{,٘{Ē3 %,+ڑ>~pn$~HrnnчmPNR.52G4ؒC'P+J%^6 NPx,ŸP{5ƾۜ9FDZ:)&f:΢S.ҢAOpeA &M :WD6)_3JzP!عs6v%QTGBO:E#0Go*)Ie5#()LƎ dWo6g'om~'?WX½İ`T3T>eh1I+_%aA;N4 ^5>ol8'Ƙ\c6zٻ)uӬ~R.^Hw|dcMY*^j5v#f|փZy^ܳVD˸Sc'n~pp҉Fp3 No'!B=!;Hok]PQlW |TmUή )cQe ~v0]TIӒw4:Vae[~n;fcx@L+':TX2iz͛AEH#}x`g1p~J +OW%Eڃ p$ " 5KΊ8>ɨ\K':)Y:@p64өX!%;5]qYt~\ 8 ]?uֶX#ψb؞> Ze)? a+2_jw*Q\Ƿaʾ)Rdć7DZ`10ͦ"k!FP.jZAbGO㸷pŸq:zyl??J`CҎjVS.=+ˁ s{2+  e>1Y𘎅9vCTl<7T0l[lIcӇ .|W>}bwDޞP^˷O?\Բg7ɿ {^4$hǒ#*Հj.5pEyUV‹ Fp?lO |͖|ĢbZ%ǴdALo#;D,wUնޔT]1agO5(T@].h1-9)Ƽ@5KT K>R'+O?F[v5[.2cRn)P٭cavmʷa'yܹtX]=JOYw+EfΠ)&pǾ YNaaӅt lDK[M:X^rt9`Kڨ涃N>y"zu?>ѻ5<6E_ӳ!BIbȊٿ4t+nݴ,<1q὘h:|&Nɒ"ʍ7ښ"<awTBON#2;,4}}dMi6O[ pܙh`Z?B)yؽ#AB9< 4Q0bY0)nd78MЗ;i%!ݾor!Qh-^EC}xMo{мVU:.%LKb|@X!7LY0W??`[K LVKd'b@ E 7~81o%WcXs{#it[eLȔ;D;VUf%zBَw>Z΂|O{sZ Dg94ŦAp.m{s}:Yz}52&nV_ 5k}gi[C/kuvn!p3󯿕c^с^d~fj7p*?{o\ M, \,bDኽHl e":68$>èa[P0[r6wqԺVRd{)[GW|mGݳ=_ȗ RL[8G0\[7|B<2qlcDY8HlN2slA`A<3-:n7WIΖu#!hxeNvjnʴ> iSu`!%i1;B¹> Z7#}rq(Va41ʏ*׏_yGXza)0\%}tqOx)jS& ְۙiNucE5 >!hs xR]$_Sl-<كk"^_%ch$pTk}}v0R۴GwǺM@zXMs]||JkXfҦPl(Uɛm&1[C=vج-2v͒GtQ|2U:!k51сAE鵿d\Xߢ3>ylv_t|P՞x:uѦ鄡MB?ASs}{PPz|Oο~RY?T% Gؠ?:pw{ȷx+է.犯ono/.ή|v{t}gjm7Q*TɱK[4"1$S+N ģUeA%,J654E +N.9N?N^ Bv4hKkR@IDAT>Rg}ƗOE?nyFSA *"JG#VO0mcv|} YѰnLq{>s=-r*6.#k7Gۛ{$G77:;|i4c@6%Zz E`MX5~=$.܈G c`Cdx]N9S ;FI @ph8|D8 H<くcg.F^o̖_kRYc+?p[m:I298߱&G^Әǝ[a3Ci[q@'{o ,:36C#L=1Tr#:LaqǧpIc^eWD[Trb o}Kaxz-ZG&%-Nk1QiAil[W.Fg+=yxx`瓁𠳻7.o)No#OD -:R(. *QMCK#ZψqE/ts l++ι8D44տ-z:#d,<0IZl}zXި\g/ow\ˀݕWGB.]6~J_r;ӌ#1 Bk'8;9I}UR.wob xBNE|Έ[9h;8vuOH\'.寋&)?1} Y2zQ)@-[f4)lepv8,¯ ~?U$Is7"_PB%W +wS1PcB܌`3ZЌ0=܁8 8CK&ɚx3KkY#E%0Dh3}G&0r =nOZ'Ͽ=ȑPn&|7q=EL n'1! l9{5PjBvqл"{*ѱloȐtmk BW[g<'&~7gcjqoWkCƜ{ORZlOX&`*]RQ !!Iɋŀ f;Xs{s:8aXa+#/"‹7,M*<-3PsPLb8'1T OT^YX%) ىg6ɺGETDa'uy`_"C6ƣŀ4ׅR %ZN7[8cL$⚥xQG,j'& x)j_gtXZz)yU&Sgmn"BRWz9:N`θ_BKy5Rڅ9nh' IS6,~8dm&*C47@LF6Z'#~6g|G /$]kMA1G2 n̳bYGJ*K/fh%_;N̯8+U?+4ʋ|}i1\˴WX||AɟEnܗFw ezTnshyĭY""g~^ Vt+ y\ѷu)%A`:!e0|!gz,CtYtF괳q댱(AtFf"˫2K&AG MpCnNJdUډհ&K%fgc[o&Z8o`]S_g#Ukm.OQBٞ2UWcLbFH0֑cFfԈC+_BM&1:a-[W*b&,fbtQOun>!3W|nծN\xh#)-K1-y I-fR1[ s;T|+S. $mU2E默r=?5XM%=3~ y|> iRgo/vo^#ӻO s]SyZY٢@sbAgi9  ̏Zр3BȓQe I(ΓΝ><=AP! ,KߔUy[?f U;id?F܊2sZ/{'fȤ5e/Ȥ +F@zVG-tqEwڪڊnC_BjZ7;ԥ&7_2oUА$ KGӪ %%Íj_}j{XUv6I>dMuA{{XE^%xo 0ԭU/UZ7p_Tw_Xt OtgBY<.CrMIAnϑ eŒ:b夒1#uͻs|Kz`ҁVӴ?\?лS*f=Nnc%~-jh:ӿF 5Fhuݩ2zܭ_*~p{zRRBu*V4˽c?c;i-T}W%R$T 5fr`z@F^3UU2L19w 6uG&Z3Wx^; 27lNu3MR& ʔA>0ŀ$0L,I6{S9y+vS5["#ÚbŸcǎ -:]\2tI#X.J[Y+ZӖpD]ZB.qdjmWّ,U.ڨ͓3`I-ҵIv}=-7¤kLjSv6ĔsIO5KE+vOvO>ߝOvgQ"kc[a%x<xK&P e9bx+"('WO th - GLA02jàz5Ԝ/DqndvaZv ]_txM&Z}ߩ,)G+&mi(h)w?fw_.Kʁrwbm0v2_s寢\9n>)Fi1ldM{΀}qPlψ>0jQlz|I?<@ksA%l\^4Ib;9D˧vVNa("7GɾՒM+,]**adJS.YFx #: BJLxq&_먳=9q `&S!}0xwwEyB }ē;i1Tzz ߌIgh1!liDazE`}.L؍~i6<$rK,/OmGG"B^.DdKw}Ơ`@QUK;Yxvf@7eBK0n=, @j@Myyz96%'ճŃx"_x@.SЄ ȄܥaaSs)^rH۟4I ǟ VP}v!uq@u|wcG>I齶mܓVaF^ŗA[.ݸ}aR]BfOSրohkV,<Ia |V;z`-ȡaW:ωƖ~Eg@wNekgV}÷n$KLXpAv^l!:&.6~_[[ƙ @H:ZMogYoQm 辻hV{wW΃iy`5Sv̠3!pI-TʉfƻkK} A_lĮ(/~T%?m+ Es&Cmq#Ѻ ((|ղ`p>e rґ'`\r QoUP~0wn5,_ls]~-{w|׿3~:(XVsz"WMLfc U&~ᓘ0ti&MxGh SJ``&"=aۇwsJtjPP9z?s`dY>p᣹L,Wũf0V' jaxiWTM Ncu@//Z,'>T wd TU"Ib r[>{S6"q-' )_ˠQAYyyCsT{߼}ݥ~! 9\IW$۱sqWZoģ׃\B3pC>Rp3L<(K~k-K,q90t>)UYyB3F_;̹HP' U>';?z?2^~8/[8'vN DKU)l-l{e)෿Ґ#(dlSTw;6E2.%-mElզ-19UЩbx+(??ο!2) :Ab;|]0_3z;007H0c-E<21hBl#PVq󧶗6 oZF-;VzNJɐ?*%uy7ooP0MtkV0ecB'baq<;dz1 Sl#6x?>};v q]ae][C3OF=ڽ ֮rFrpX}9;P \_giӸKH~q@ `nr ,|:fa;XX{s諂͟7a3d2TԜt5z\*N|gOوu@7KC|Jhbv~McDD݅wgĚXid%mSǯkڻSB0fq &LZPmNZdFy,ϐܜ%ys^oXvB_% NzfIݤO&+!t==S (k[LWg`ņN,Mk0]A?eT%6=?;2ӖtҖ.UT;G𓔟9ܰٓ{6ch0~w SehkU@Xu=r)#ǟl̍"؈v芑ONk%ZH򲭴 M[xIljt.P:8<2Ss9?ʮSz.HHݘH`msώiW>ɼ p\mZ%M:x'YUVOXB,?˳|0kŠ`8 g3$EFU8]KB 9lw%<ݻ7u*%qDDU]8nSƭ<@Z^ڻ0hi:\ܘ6ZK <wgnygB3l;M}b%}R7>_r٤Bt;=EX-l5u/ΦVhL۬kC䐭Zxܗ{Iܒ"ްVTߛ?NOѳL2bxhTA+eweO񳑻v@("Sk* KG4~sJg/>A,Y-(&֒U6dܒ+n޻9cÄlٕ\1_+b>\ -pVO F6ED Sxc^0p2 IЍɟi=/4oLG^\~V*3c?>N Blt?mpci[fg#+1MbߗoP:vc]6"SX YZ;Sx Z^[E36, u{o<7fHTtJ*G`%Og/SS9iEORj0~zy N }-Bj)C (%{W>7>ڻ~J~x/~&6t< }~xC7ulKڊbk}O-^6\9󡦁(8܅s @/!S{9M%GQY?w`d.F:;ӽc\is Ƞ"WMno$&7ɋ1K߿d2,_g5N_+[Y`Ve{:ɣe#R]ADao\,0#;VUҍqXгc I{mӈZ^3q,  @WFNqi|sUMg}<8'Ֆ3}$f>/b!W}I ؤn %u'`/ʶcpY.+k I)QrL%)*@lqi*kOޙe"٪u _gqmɗ_|v %Kn{>>LWBK۾ikڪ?0#mwΈ (Qqg }!T(ub3$q`|5 rpO[x#b u7݈qR1dYx&HbZcZUH=өQ u?rz]Y!'el&W->(TTîq>ThP'YYpw~L tß0;us {SCs<c3aos.QT{M׌/'H nBw:a+w?{:Ɲ!=i%{M>;@ط<} (>䪣mާwWV*]#V)4~^|K=dR&¯:KYMy#2/MIyOK'4,O5G;}݃OjoJ׫רꚧgk*x%2pӸ} LC3ghZ2lȩA:#qjxJ0WMĩ1Z vP&0s?:!?|֡S&M馷a~D|"a) v䵬!Y.2Bd0TN:/^z5g@K[b0yj5eco0\uw?:{;M Ҹs}7[A;|wʽ%[:`"Qr"JbXS.qxŇaa"*m:IE8,čEbgJ},3k-ȿTJ& 9\.z:XVDN9C<,p*+}d]'KyLg#UFvm +v3,gRh¯i ږݦ5GZ(XGR@IDW_۳SRĨp}5T7m85]^[n'qLH;:5 t~b*uϽOq^OvhCsii/5~.t2:t0yBt NX%t,gp7ݺL!.ajCl7ZΒx02+iϬڳ1)y;5aϼ;Ho|I |/3qv٩paטԨ1(P2! 3hhUq`tG\6-SlB .sØ؍:Bǟ#˟|ot:5 =92e ~{y_[xOqDv&_jKeXivwϠf:l R [~(@ARZ2X46%*f%_k *zugys-Ӷ4ر$nDxblv3A"d-s^ɥ1*o0)s ̣W6) xOGK'yu;3)ySal5_J NbRF{-SU"hT*U,وǯQb޶⎺k *ݍTXqi;N'f +8njT޴xfhFqShdELN,LMԥh׬&K0ʴp WF`NJ EGaT[ݙ8ZOBHծʰ8 l {O]CIevJwx*E].g&ҫwHțB%H7=cr+|%KYi?Dm}$`YЏЩxC^pB[>; eպOӵ s=HP_uSEI(_uhڰ46>H4~*4i SL7AI[/5?ۮNbBS ?(Gɹ̠l̤f}a[V9Y.oWN֍h~>x~^ߍEg<*0TJ 0VFsL= *9W/̀_+9ՀM6 ys)䴢ƼK xthvMT*n~Ӏh5<8ώx6~uYA:to;6yRkw+auz$gD{>\x-K+>"CC.VOW_|8Uj|I2IXװj;6\M'l5 >(\ figسgus'Y906'c~ - ˯!ʌ%Ew{ƃorp㡎G~ ӡ 7@y[lxhyhcL詛v^&M֍I'5:Xͽ.\k ci, D)/<7F|qvv%qv܍e=i9K77~ 7Y+gB9 YM^A~H˲ސwnx.t.W, շX22_ b\ONXu00=~)0Ķʉ0$QtJ8{sC+B Š,fAKq@@;iy4@OI_Iwt.ag+kKպFR:O ꣀQhL?0?|Jg +$xx}2;hj[^|ZA=jK.Sa)]Z B^;həT a% m9Dyb(DХY ,!0{y:?{]`6~;ݤط"L蚿h8y&9"w_|L}i^]Mz⽤k9BALCli|O\\L C ^ҍ%.iIvB޼ڞn]^>ٖ2"hjG F $GE年i(yg?9 w&@7i!" *\ʼn8Ú.9@q_|l$G&ׯxo}ʜU["#iM= 2y1%ѕ:RIݒ9&bx$zBZ+%SKAHH˨?۳El `ɬ^2Pcs#a?TD'8P函#7K> AJ?H?kx#NN",-IM])C?࠹7BU `4ny^S*0 ޭ=x};>eA A>Pd쇌{ @rD Na╛ kD/-TW'E,#>E 2 <}u}NkTf<:+hq{ NL_p/$5 Z,/vx_`6(5۞L;n(/Z: Ecӗ 0 4W4n,\) axq!fUo` ="# nz/2QI" j7߆" -X_. PQZxƭ?h&.B9+ipI;{m *'oU4E5{ 'O%qs#9Ua#aK B iNpx*P1=9b~#&kUhq˧T-2RZg؈wDz;M47H3ytI6E¥8pwe{s@[PIUx/p8 -}KMװaaxM6S xK{yNq[9v/_iV[Ż!^LL ;2nאNltzlU0Do7MWwtҮ9nj078}_}Sނ]l[BZ8Ցo ^=KV f$]~BІwɧI:~T }-`GYJreaoM_+b ɚ.Ӵ( =fy4ZÑ<ӕG\\ X_ ‘>O]Сv 0q%'Ůk]_#UrdOK|`VdLOa3b` ڏ5)_e>/?yO ~e@U"ݾq;5hoߺ=@j:I8y~'&ɫo)>o|(cd~ 8 ;ٿ̈́N/0h&%Ly@O n}ǝ|E$07> l%iHj%:&pZLַC t$e%.AM9eb#LT߉gwN,%G +u ȇuS%L'mi(~}E:NRWϒfkujhd0R)؇'J:=+ mygN}Rw?ׁyviw{ק~/Y=&GY:_W@=~]N28X@X%,y3g۫ MV'7 g` @ m;>r"&nTv-QO"r^ZT5` @6nkF2lY'>ΫGtlh"kf5f#(u.Ħ# j$T箟ߏ>:}¤%+ o"F05Cmg$eJf;̄C{w54QO~3}`s0Բo>-Nr`$Lu2>\2E(#z9̲L,du:ABZP@w 7)f G 6"5RNi :&^3(qEU=z?aw~aODVLN%6Y0z5 ky@IDAT-8u sC{2軯7Ҁ6+ U }YbL#S6e=~.v,T.M.LL^b>1{ݭ,/2J@&h& )Y,^w%SVWkT^\dK.V'+CI)׻t>)T+&^oIxT!&tQpa={KM+MW5q4P1lC~9Ut\ pُO0^)_Wdͦ/vp7+8\˗\m;aw8ҭwm|s[7O+bXӭ"N֝PQwIS%s@cwxD,fؑ;D@fbftfoˢ&2$gBzww~M~1-Κ@;9`ri~5gySCO 4wYo]P|4T*얡d'fwՄx;?bfx!_>=`"kwAF؀=N0&g9'C,%Γ!13#Z;*y1rHe),6m zoAҰ-Bfk6kK[m ^jܔlV:ydOJ bMtA4Af%~W?oJLP ې2p?Rȟ0j9`?q :pvi_sLuW*zw)-Yt} (E}v/)TGE`\ [N@N~rfeUՉBt*@G|l($\ *2|I4P&3TOlL/v׬xq*: \ˌ @xrR-*AuDyy͢GSF . §QY?w)v!۳y+JL.P DN&\nWx4EY0Qki$h4;$4AgƵsdJ:Wك>h[Mx95d=SB eX6蝐3&V~eCI )<#rY-'8@h! `}HO_mtJ#e)W@7ej"M ɤ.ŅO-t 4ۉ]xa[[mύh?{ @Y{ɑ։nid`SvԔKUg9yzW kGV;TՈ3;F_&s+  (nTRu}jm4H^7[?4F-_`j/? uPgRv2l ߛ hc}y+CKܕW^6wZe w3|cliE{Ν a&`*'o+@'8>\H O}ݳ3_F-e^-.C5C˥;iʼnP&kAK[]0ocTl9;z?#S:G+tizMsǘ? 4E7#S(e`"F_Q`)H` m `{#< d R^0qW-A [p#/äHRuIj͆NbI<H!hy:6\O|hS}7L<:JV)IHS>sO b/s.vn+On<'7?t7r̨u;|>[{c`y8ɊVTG C[ (x_g\}C$$W eԱ^$} @vұ|NNx v~q{ƌK} ~r2AP RwC#|5m2k\;%fDʅmB`c`m f5))N^`P玠)VqQ_\ E1 j-B;D,@*<y/qB2Q.GVBI0H.'.LkIFF :AԁMd^툉g?я~ N?}lN)ʆ+ޫgv/~nYϲg;_4wL~4OK> uи20ވe o@Q~+Tb% 77Q{w^)u#8'}ڃiZ6sK8w;7?GcH`E :N'X ncz%yuxƍ׻ WIވW4س_}@p5F1sOaU!c`#Chlht?­(Z,+]^+I4SFUR]uJdX\W=1 WP?N壗rͅ ?u0-N(Z5n7~Fk@2L SQktߴx\ W҇ |H14?yM\C ˫t\o#':綡w@<?cg?sퟍotO9~s}s3I]m]lvmW_B7s8vb:2X;fp 6" 䣧v}_~Ϸ.Lڥx&IMcv[hj?i[%/"zIM-=ED<. s'BtUGDʍnЄ=7䤢728NnW>ˠ2?]'O2839m)0s|!C|^]h__s906L$B$F .Pr aq+캫kf,K<l-ٳudgը @@7aMe]78P_Rߖ)!}A/{ȯ:^MO`ˆUouC_{vwrC^?n&-\1P@WS̜M,K}󝵗|f1V@i7iAmDM5:Nx :ZaWۡ1';z:dSC]Ȥs :+:EHybv_~lw$.w&'T y<P޺%/# v^]d/cБb-A3ᙚ ;X<-t 'u -zV<UʿՅ2n(_; /7zkHRǩ|G"Z`U|?s |ƯY`FbO= \7F5ZbN4˧y-dP2J4oo1LlAeS<661ӿwYqovc<^Q__>c_'YZ13`>BFGf(]6AsC@=³숒W250c>;ݠ]XyKiϾ9 9.և⚘} *7} ּ Ahzb$*`wt|&>)}alm/6mcǧǟ 6q7/"k+\Lؽ[kJsc` &t@>h塐lT!L ⚲Pq ҟI\ 74]slUZ=z :^s Y!xaNtS&TFnX**B4дGq.i!3I?BIW,\WT-l9uz9Q)>U 3H23ܓ80䠙ރ d&!hS:1I6 J -xli wBC#W74$!'0 9ß|;~Wο#6}#p= m ?_𣃽bb~;k7ڈJ%y~q>Dyj#ۮ e0l"a{'+H rwT!P*y?}݀r0* k:8^cU9m!yrPD!p!a˃84Ήvfa'7 t7$s@oS62mo^wүk ښ6z_qx{Yk ߗ邯5}I={A9{{OC)Mw 𶘆ݙF.PAKiVW-yph +M!xWJG.MX^H:nҰm:{I09J~A!u{~ʷe] qt,{G4n>ӓ0]B(}߿f0Bw&!w]Z7݁zizC5rj$:¤Ǘ%)cCqBv8']2?3~Ec[ʟDTǩJVVt\{ˀ7xXY}d7IH)_V%ޚ@}ӕX XvOTCQ~O~ 8<|x+#{%c6w_؃oO>})|%pǁ%w)A%M=h}⬑ZًL9FdSQ2,6JbdaQ YG Ӏ!v{i\G̔ή7 `9MBOU?X7 !u~207 31^#_=&o{-zU6 7,=j{أ 0?Tώ81'=]?`6ڎ&(~I%I4Z7{?#>c7Sպ?_|ݗ.}wk2$%{SRtSWUl.מc$w )H"`Υ7 = Ϭ[O? l/"чoG=/U!hﳜߝ sAic!_IԾ#/2S`w|@QwDž) |#ص~k*[-3HJdT?"҆I9d_|v>J$^#0prf@BZP6iQ1V30]RDF!U_5`Xh[ڈz'&4qgMSa{c:W1? F<J=oQq2's/FEx_;t1I2]yI)nVQ,/{d6 -tq0 e#}B:0Z3؈Sb_g4_C"|1Yu20pI2p-A߼ne9tH@h=F_.mqյ։I0Hx|&({O#?H 9ЛPڊ]W~%/lLdԺ1̀ aٌMh6OQ/BӖP&T"-?ht\q0.Gz؀}%mTd(biYׇb;KPUwӢmcL:tq\]ǣߟ~{m9˦dCTtMd,>sP2άq㤔[$>'eKܕTa;;Mtǵ* +[+q+`%8;z;q;T8.:_?kYZ V8:?o%H/ !Êݮk߆uo74~>oܛ܊^|nwWJՔ N511>TM;sj'n?c#v{&W 0FgJyʳy*wͤCM;;8i5R]6ꏈ`'_CٯwZޅ~15mvy#NluM4Չ/lj:<#E~Տy";'?`o3u€mw;i&lD`$jlz{@nOӴ2-B:J딫F6̜ \r JQ^4=)C|Dsنf  . qNZ G =.KAT7S~kUt#=Yԛ;+i{]5p{) IY㇩ a.Q'tQ5:a$MrQ\Q t#樂K&; y9$bye7+M QQ_5y9w]+ g}璿y߁WK\2u%.˙_iV|wJyMGJ4tU{6 m:il ˰7I껤oɢN@}DC]'Bܓ1p\w`+ˆkVyk mvH}&Qd_ i[־ |pcJX'joBN76w{9$$01H hY?gw?^{']@o~?{jɏL#]P/5]v_:A'%fuL>(!?^ Iѡ+žU)# OzX&N~qj2Weܫ2-Cqҙ뗰,bl;Gv@|#of~{aϦ % 0N$=e ?E/Î0Mb'.{5y)|h8-|8J:ʳ{ɿM?G]˂{#z_^sh2˕0vYjOHR!:ohEG^rRl K '͇#{L2#5$$YW iӃF׌-De׈3лBEg\33^:.ҧ}oYmiz žNHǹ?BmyӖ0nDwfM$]cXќ> tyT 3HSVVꜫ&ۼ&x=#&Fn!Of%; s {`Xu/kFo}4}aYi}%5w]qӐwM #^q̋qkQN`WC>J-<- |{IwP*Py@zm܁CN)~gqu~?ߎFvX ~] +qw7N`eg[puRCSܻ!CbӁnM_qwDVq+?{_PuVROÁ#[LiFs30uډF$"=f"p>ؔLS ٓcSsewF}u!2% dbcTJօ(gĈ3M^dDj{]ȑ[6Oy%yг ^_,IfXjʷd>QKK~[?QfJ.]9UGIUqE_SC!bu$`N}xB0!ua|KWm+4%?8z1]!\WYM.;9N<&Xf).[]~#bG膯; leNxQ- #+>.;Y3g)N|32輸àƾ|P7bѾ,s]6׎͓μGOόv f['=%2XϺO oU. o/Dej~z9#3#`,yLe})t%ۀG8hS Bc4<7%nЉXq㙸A rثځ4ΰM+f7;;w,r)89+sg?cf#5nG<_Wg=Mb7ܹARy,6ןNnUZ]^\Nߑ?^ʢkY=h aVg%'TIp]8y9B9tq20gK.#6<ٓGezfO1 u=%mUx 4m,!j5w\a,i ػW%$bͬ諐*.\wǚK%ņQ|Ǖiْ0ΖD8og* ؎77Fݍ[.aE1%nH*}OHHp h$.YLߛ4V~]OwO~goa?l;+Ή.Ez׿ tW_W`ǹלw ̣c3.B%~*#LxIG@b}A'wD czTa7F],O't;u-gY.=>cEL3ࡣ"WE5UתP lۤ+5yNT'~lw#J02Ƒ;2| #$o7hѧDZ?".ٟvpck'o^a []]՛7/yULxݐb;%P=gP;S̬J|&8itr8 DbԾa Y H2Xܜ,^TQ({,xdͽN{ ?ٻ?t?VgG B>˦s|e>{N^5&[?N(GpP#HuOI@KpӚ6a 2,C/s B!}ވHA`{ m)pަ%t:j#Mt$\<{ ҈FV!i&4zq+ [8dyku#'lYv| 7-H}p(03A,Ym}81m`go6RLn(waGĪ^KW f*p}}gؑU<p/= oS_}xW}o_ֿj(PV"S'?{||:WҵE: NQ p[(6]0Ό.25dM|DyƜq~̹g/v'l|l'ׯv?|Q} ' bW::. @dzϰKoXU,ĽrT%e$ 0juS:gtI7TΧr93l<L ':ld?CB:-3ԕ]w>3 ?{;oM~Ͼ 7}{@ꗿ7n3c 'qti"x5](zw͟4@ {n&!^&:(oXdQ'SH8όMFoEOSeJyǫlw' ogY:=g?sտC~Bo_}z!7/ #Tmy[H+h\X)F\~K[h4t1=l͜q:N )A}M,R{UCI 4ŪNQg>aG g7O9wo(W4N8K?F/3uN~3oYfX'uf(#Qq5j669(KV~|G`Hu4a҈!p6 J4F5LZts`ܞ*Ϋ|16?b _Kz.A_ɥbM[,R~ؓyĨwlGwqەF'dԋ_|w-}j״ᒟ\пq(e@ɞQ]6mԤ\x2D0qOϾ\Vح>:D,+MIWZ`v`/M.Xb]1RJ?1}&jN͟ >^%@ĿewMu]]E6<]݁AOt탓߉k? T*f3/Ǘ,-?:>~bܤa6{/|K7+NXv;^elDXH3aziCL&dBL)I8EPn A3U=dvaIp`$"o4j5wRqWGKJcf/8)}L[@97%q>FBt#K;a9ѧ=Ǔ<]= (2%¿, 0.[m">UCNfDwܑax>9Ytˏ&Q{[+,La2n"$Wn-u!bDU.83ۅKGw-vΟHYe&.e ID玿׏W|/>Gc|y xWTgע&8Kz\.,g7R7u*\o4;ttOgPNoX]Ke`1-MaO1kx$CGO#Q[X#R2h+FD8d_GXɩfv: h&v$0@IDATm6F c(;.0@. BZVhQNƷpQSj&B yݢsr/wA[d@j$ sU#[VQZLsE`ś~$ Hohе@ )UY ؽLL&u9,qZ^ q, %9s%G!YBFk\I[@N/z/gG^6mi`UorOJP]Ă .ѻOK3>_g|n_g:+k~/$O>[|_9^:E}d À%?x'4ă 0ivA$u ;Q>W e oÒj1"JA0R eK2y͍ e;3 ~9Hh''@ 'rJ"9ӈ+OߥZ3=;J%}oj*7/6R~%e}E&#t Kr߃˰@;$;t_MV5>geNjX^ lRi(Z_wyg?Gn_#ľ1'Zk.3ȟ,gQ_,/}\z̙\yVv*N.Ù(!״ ~V.be %@rvޱTΧdCaezOZtIh\RWۅo^Zɋ Zղ/2Wkw}¶ɇMࠍ@k.%A/zun oH*+6A5_7q 9aF+,O@Ģѐmj{4/ d{:Jc}aF};t7ʪ؇uCGVbC &)dԔd8L:u݅/@,a*a" tLJJ?*C>j8>#m1aia*4.rDuQT @6F3?pxYO#O܇AY2nX> /6މA]ҡ 4ݬt o??X U ,(LGS6W~W6ٜI]*0Y1 |/k~Z?ֲp<_M&qe&䕾z_w?zp @Ahm;k@Io6H]'u!D2OڇY Q^EuZeЌǗQp_f8+)SքG#! DZa>.roE\EL⨚.Wb+{`i~kPW̮飲i8$"cOU,0= #aZrTڻ;e}~$6GɈAF`DlFB9kBj`VL?ᨃ+F>]*ǥf}gZV})9wΓAT!O&LG~2Mg`QF0C|g7 C5=`7oH{eFط҃Dc=[/!2b(EQx,"v7l ]ǫΈA+%Nv_ 4e&KT|dtqxH=D>|~GS~<iŚbDy_^S h_kwVVVX똻{t'sx $4.KpOLHWR/Qy‡Q!?#@ +N?Exf |ꏊ3CQ 7Ǟ1ƒ =~VL+c;qoH3g"u5.#?Sv?@HΉxU909󱞗}_g?'{{ˉaw:эW^8޺WUi~kIW/(WE@g ߝ*4l`ڤV!+^j e`I/aeBaZө!e=yI3〻=;զ!DAdm D Uqv7SJI E} ?3bUsmyqdٓ.ѡ*L3x͡L^_-G?zGoT\R@&ctQ9zn,'l$Gj@c'J bͯϦfᖳɵ6)M#Ah1bql!k{&dWM n0.aJWˊCUoUA\1uQ<:(HL)?e.g@8}&Х¥r׳w~D?v,>O+.sۀ&otӯ5B0}s6{~*JJS!,%I+AۺT4qmϢ/<(]+mQ  W FB E/b:"a]&~ʄ[{P5$؏XYw|?O@2d|C;=aIo[ad<27@?j[/-J;~WؒS/Oɢ0б:Ѣ>y:\u.H?, ]$#$-U39rʚA?x-AIr"XfZL֐b e&~jg'zMXgO;$fs<_zQF|Bܰ.y` TxAAb9/"7D"-sV2դ(cɰi2dS[\`_: D k'M8'3AFU2(N@R÷,j4EP:LEh=)P +S)6`7JD_(;bx Js(ϴx|AFzf{fjի nJVVҸ"д?؆}Q|BJD^p?QlmfDɸumb ܐ5MAf<|fY d?Q؜;O{jN_~9Wpgfقq__?Ѐg|~*o'ӈ3Ҏκ0[" s}6Y4 Gjٕ/+b'>\&6X=;=2+ʆH%EPY3ZǠ-h(?h.6(> LpmLFaD tL)C* jك`RWP\y<ూ1+xM|9w>7 o\H@ :W鐟?s|>,.b(5eD??3~v+>חzk%n{I 6w3Q M^ )SKD rP TڳAb]c.Թ8sCVEi'&mI(^ XvaJIP? i[3&2|[E̦M&aIS8N?Ty[3ي  lOBgB:aJCF&>4l& U[OdΊP*?9g\qeݢ8'|d1#yQ (ɉO&G&[.Ҟ-/vpW=ә3GuJ͛G9'G>+{A{u"Z4iajӝUFO4d:_+U#LA74W3n Z lKރ|rɯe[oyȺٸ?wKW( eB1 ]R7:N?|CM><=wQAJ+T׺hߋ/Ojr{} U*휦1ǖMy-ecŇLqO/L*`aƵ<%5 IyO2$vi+l } >v'uEj˙PzCKkh叉7SQo'=H(9Ud\+h`K> +iu&#|D$,`W8Qq %, iWFKف^ 9T;y9 V+˚tFC$7 a%l? %sG~|o[i/|8d NȷEo/h_ue} t ) z.hkʘ# O~ew}:" j_>wWwZEb%GV Q~TP[ʶi+.zWGIH 9pX( 1y_ o5}1p-YU%@%[/d1zQ#]NFC6H)jd}6"W>Is`=P|WۇP(&%YA_{GN?G9?}=n?\Ҧ$.}?sՋ:'8_=y W&$|c$aŽ(E_2@Nn4L88W%Zխ 酀=Q!9 "$c ?g6 ! df%0FD* }awY4 brL0 -s$CdA·~DR|2y]\+rT\Q~Q.:7puwC}j3Bp"#Q 0jypl܃\tCհM*$2gp,Sp~&g~&M(jJ3㕅qR}U/PɂPPީ>3<| cC*jz, -%I+ǒ{,}U:UKFǗ/wܜj"J? b'H*wx^zC#BqFh=A_ A3 %)Ke (l.ykR\pQ:0hFAG@~|#DЋBO|DtDٖi'CJkiwy}B~*3{vIxEdJ],t8}"-GFt)%!|!!RBhx%o! y -{9-d n}H9RHP~auMhs)`9 +jL`ᇭ+IP&@nOR 2vZD8ҔpcX|nPt{7F>@d ߢKvRZ82`rY]] yg^*8j*:= %Ar(N6ikK4E:\#=ul9P&%u+ q@(8\[[6=Ia3U a}.⣊pTСpy''YwPl 8zp@aGE,ؤ5Y~TPMu,sc胏_ߛTgMInk(`@=?^ }Ggk#ڑx O0aُ?\%lBU?y|J|fh<;@8up)7|LAuO> I!@USMu~36QR(xk?⣋{tAD"qA8,A*?4T K+vjRgLV2#03GpQ?h̐=(N -- hZ? Zk15؆nQל&9 ¤Oޘ8TtM0^;`PǖQ[5x[~_?I>\0+jPRr_g{׫}Y]?Hmz[ig.}W_KDAdy}E@C?%AeI엀d,#&xCFf{ 2`+c/6ڸy$!3Vv̋lFeo##ԊR<ҭ1v1Ӡ5.[g^} Zu7+:C8F={ ̞4Oim|M V& CvGٗ\~MgR):ӷ`Dt TU;E\p y @詋~| Xn[-)ǐ`YK?L#^=YӇIgQtSS)n+]SOGX[?v-vB>ŗDU{G)~DȰ.})";e; Z|!F`t4 ExBpp!N{ww?3~ٿgH|𧛺3`R˲Aw%XQZ_ǯoowOsЏg6M$`L0h/aS~uO_I,֩ ۍ񆧺wdM8G`ŷ|. _rruLSݰ7e2E8dÄjDv( Zn$ZYʥ|cU DeʎGˠ͸l6Q :41v S2aJWJzS2&!m/tK:?޽wbw1,MJR!w(z@ &KC7ynM wU,_kɧxM745o *R4$ XRA 4l1REʦv~@vJfC#l3XZʂ*6XxjtqcmloO!B*dc#> GF8nyWoʠ")fnO@'`C?gߒ'pa@Ӏ)x& &6WV@j[H@` eG΄W/Ae@ם hń:m&9w;=g?A?yO罧ՉU_Z@z.j_;u zZbbn g]6:LL *[r+؆.UKt湝ߊE/a&y@]MPB :Xe(LFMt5 S!W]eoܟ@. [㨇L@@7BˀN9 r P2F秈C<7:4Amʣ0 Z !A`s%=0a_xTi8:Ϳc;a$?IJ$WWbL{N4W!wuoCzDF9^7ZA!'6Y]>Կa$'r/EتH}j.G/Ę-T1z@.1п#sv <=+E>I+Rp0UL)&?Ҙ[z9(=mK7ЅЇ-JY;yD4op'{V4 V~Gt zpe1 @v&}؅ء)<8S~,Z/U=c"5lp4cU^^ %mX]Xf浨,f9ڇ=zҴ3ʮ8|o!xĝ#xi7`.eM8/Dp;X`"RL~ձo5!`{zRVC)<LFtiF豈W?z?Aj`dD;)~g3l־: !o.,%+A1&".c~ ؊)F!ןTfc4<>H5Z3,ѷXc+s8AbkņD׾[!3V< o9# :rs WXSpa#2H J+ dtsr r/'OAk0]G2~`kЗqo%6qp؛OyQ׀ehP~B_`>VLg-aOƷS|S$whʿT ctAw|Z  32A!ÏeltBK< ' (ԾЅIǤ aX ~7gZ‘5Bk!E_4[UQFAl*x邏&:C5~" _.K%"gIe UYkCˌp[ :u>KrQ~q(5 L`)%X(nD3UJ!``1E,?y+ħ!~զ/.5~ZV&g_]>}5k0lcy;ܥݓ0~6+[m @(/8輼M>nkmEg:WlX۞L{[i7>Xc_Ij0+2nǝ&!J/AVV=!l&e@l}D) V*+}?s?Yz?jĻSA}M~_g5IМظNiHVB6; 0)=,eZX}gTw~XV'݉׊Ça_Ivo Ge%u@JE`wGԙ<|"{֮bĀ{Ig =ˎN{D1li"I G+/MKP&N݇a&?v3H7 +Kasm];ti ~$`C9iR UD 3tKghPmTIE8ͥa0S`%‼IeዽBZ:G]Vp/w\E+/$ VhBS9Lny/H|#?C#6h”y'[87@]O񄀉qz~qE}٩:$|>O }gk= -;|c:Ѝ4p x*:gm`"<NAXof5Ǯהٶ鉬/Ey?1RN ěN[{Q!j´)Mc";7iOaeM#M4XY7ރZiu"# XAmk~? RN; ޞڬ32S c.ԞCk/~L;a$ߗh`Upd7FГ>Iq%yJbz @'GiA@@˷TCC>w21u%6AK`_6LwG-dfgpS#PEp5l.Qٲ)0F|Daзvdu~wWS~r`O?w{>[.&3NǂHWkz|OǪL8Vɛ%|[WaG|`c9PV'3 D;d]FM *ّũ.i Um 2?4?ON"a؍[bIK,{_ˎ|{7}/uq+eפ U,qf>r0SEcCjxs jn/͞h$cۯeͫ;z50&9!6LRȹ fzZ^'`8+vYWݎ6,5XW}cvw},x.ʑ>cI D'^yZ5Fq)PXQשLT u vFy^#X/)Qq)ݤHsjU }:"+} (ǀجuS&xGP4i*ny]q^d+!)j)}kOPi晷ti&×んAƗ䛕r t7 Ep`%g|BŭW ԍDt)Vu,+XC 6Q3Pφ'~~}>>H_?,E>toyA9?gCg\,3f sjNo]R4Iu0G.UlZꦈ8 wG\= ->=O{:5zIIh7oat_Bx(,VL}yq@Mk{b|Ҽ&keMy𷽎=<4F*O"g=n9"Od=+(?%I-UޢQ]&}RzvY-<&V¾ӴoWGw|n>pNYͽ)'rdl#W$bq5d0ɣOI]a0%=307FW'U[ҢZtDοtr+[.x`=VCf'd'N,b}|ß͒P|K=,pGzϪ&9-y*M4Cӻ>9 _of O9+j qɼw"DN|lq' r1|UUX,BZLe.˙yv)* rO.:}ߎ;Cu7t0ob7:x.i1;}9 ۬lCL`Bڔ!R" #\$;4"QGVUzO#]i%S&ݿH=PlW`W2KSI֒ x)eDЊ,.D0!DD`DsM)"Pfti +~&B5KC\1@ D'ErOMgⷸ VMK6--ۡm~ aEs|+U?lɊ"7,rcVǻ#w?a7!eSxsݓ S=:$gZW<6.m6̪r7)w;xL)K^%@l*-^duFM-{w lrgLgBh<;W/v|o]|V I*"Zz&{H*r,r4aiAg,"0Gf^>X[{-L0= e6:}^"-?.~_{xΟO?4w_.E,IJ!?=k%~6)Ϋ<S63`J0c idBbz- gl0s__ Y0flЃrK2ȗMe,R W0 LJ h{&9{-t 4lP=#ޫo#T6Я@gaВ |d{eg N}$].vf vh|F$@IDATQ;/a5!=W,' w$_'N5>#C@dGP! g 㠞] K(,.4>T tq&UIZC>Z:?Ҕ 8|t]ᩄ!⬙g2쐳k&]<.RiL/_By}Q]gW9C7r_*uHȠ՛OEcy @$d8t RQkRd}W)% ّSrӀ %"ui:Ÿ-)aAG Z@@bq _"de}wpdwv`;&ҽwW6j~~O9_/7.-_~iRsoo#H⼷§H*p8ۭi<&6C'ZeͫKpr ߾*na AK~ Z!V<;/J9,RK"OǥI>ϬSlb{.4|'S'7t}c?Rh̢ zpm^`|;lx$yE`MoC=coȞ+%c>#LɸߗվO[}$ yd(y@?~{X)찒7rvb~{+]p.sim-3'*U8O 1Ԅą"dBb~럼 [Ȣ㐉.M) TʜNTN55]P+It@sS[2R*wx2QJ`. `]Zo ]1 #eP&"_r6+H!Ek'&^*J?rf%exJ~-лN]!:|wX?{r<~[;nU6]鍒K6rKZx}acd\%$0$a|\)>ndTA.b&27c|0# VK|@ VngRUď\ed"iR; ?~Gf`"B! Bf*)ܜ _eIM2Oð8SV(0G[m;uuҭe aO㻎 1!!(&L;%8Ni},v7oyAKF2p mJpN[.8;i xB2;'ʏ]CM/*1 9;JGDZd_ițԑ_1%8f6G)x: o5`!YgobOZ&Qn(eaqW\ CG[LDos@a!ʻ}}(w :t?իA=B uβoAd?,_~7]=/_yU˜)I9- b0wIMKk-`|C*2a 酴a BurAN}i[e -cӀ/yAZo96%/ȸ o7'Ƒ3Q ` tW> guD{6^@Vxh+,9dgTB$=TwDL p@ZHD8KWq;ix~J\mYрV,c|mBrezY'K Yf?W>=sϟ|}'r[Ցݹ᳒&?Ї+B4:? >t['aRy÷HI'iE6HهOnzb4%MÄ'N|~+ƈts,3X+,_#ȗ:l[>Xy,|Z6ߟQl;S)ìKu לכ8¯}or_<2 $ 5[vrHTMo%joݱ->|>b!2TY"fN<8|dh2v+ӟzl>&1e$Q^nxlS>զ;Or6 ss,Ȕ^Eվ͠o;X؜鴐_X&xKޑ6hitbw|\25#CpcC= oAc.>~AFw7Dm}~:|H0 :7AEiU1l\_<;|#i!LO,<5㥁sֱzjvǎ!A3 b|gww]n]CuٳуVa!rBXvE'NxO'!+k%M;pghf?g\Q(4 >i~;9:!NJ'D^i5}qz9W[$H5aʺ̴@@?̬'2beY0! ႬI{ar|+ѧY34+^EWm:p3oBC]F<'?B9v*2DAQxk2"nC%b0)-OY6?Nl&::B_+KT}r%}07aeNHB1DS}c~x[| G0țjh!V{hKH3DG\Ct~zG:=Ywan;g{G [>p)w`/9FAuqK1V\z_E j>28R`wF2 @Ė /b5[v0JMQ7Z=V_Ф81q@E?{ }BQ+Nfv0C>#6<2jWRn-y-LP̀>zӥPHiFI\TYRPKR =ArWJ^$ r/ibūӧ-џrdzʏ-$Õ̓$~L(V@x71EBr+hAhJb0ɬ`Yz (}]]oMnxU>SQ1PEj:2Keg_qɯiC=GK ?y_5!o;Hs8goҾkٗIO=QOq/&Gb3n0̺(⚡Z8dՁBar͸h!dr k~A` 6 MlPZk"EYL/9D@pQ& V~5)@)A0)?,u|wy/{%'<άl-lJ!/U~Zx_9DSʡ }c~Fx!+' MUN~Җ森Y,E r;.[Gn *o4GzC`2τ]v@4BLzPTfR6wt-¹/yZ:yM\.՗u@3z -K%!wC ?jW \LVOyry #s=+VTƱgg׮|ِf(I8aVCShlB4^gГj\,z3ㆦi͠)"clC;3werr,$]ҹWC<-Xя^v1aOj"gzj{u篝Dc_?g}?ҵO *28 f2N;ۃxsǡx]kO^8.Wlb|Ap3 Lhb6px->$~W 0 8.13O, Z1vP׭f)`T 8ʟl_yodJ/ voPSª6^Xa M=Kl̍ qncD;i6;;?1q#Saqe(Y+O5Db4ꯚZ+w48ցI`s8 Xg gC\wy.+ut#m;Hjk 6bwqe2*Шx8VeN -a[JmCt ]|Ly4>HDKQHlpB?`-C' /m§g|;yί#}Y]/+%CwU?{xZ\K$e%@j"%''Ag&겍,ħچ'x t)f.֦ǠO Fv!Ӂۂ+5QF?m\I+z2UBtmo$li(#iR.xQO$siN zũ7=Ah[`&[ئIZ&OaOeALWvs癰XoL3P6/,{+9{?~n?|?&L fΫ](սo|>Pa7 f(4 nA4VphI 2q  `M0]m Zظ4DDᙞ|KoG h qdD}m#(6 /<ce W1@tPѕMof]Nhf 6Չȱ/͊Hb܈Ĭ+U~:y#Fd Ie:䇰C@87)(!*SubbcdG|,+|}:?deƤ3pع:4J2.G&@[>!w -JU[L|~MJV4 i# RPx噿3h0Q91Bm,DA#@В'.5Rr G?G'hIJʋ~poDI-i"/zۥl'x[e>?4 :ڡf ڈLg=1x:$놉> ]ڲԒ }v<{s2E`yOϒiIh>#>'?|I@e"Pu$`_Yg_玟y}!Jɫ }2.E^$92Do,o2p+=IJaS/~ xh|nK%<8F] /lu=YΫm$69q4 P ;0@54=0S/18H:I#ɯ9!Vt$N` l~4-OWTUʰ,IB 'VtOիEj"/Wx?H"&"؆X&葇 WKZ4>ФCZA+4ws֏0eh?H\v_FClz&Ojy%sJ@&.`ijR`p{˕j 7C6_Vܸo%Bb~v5(Rw, b8z#=3!@'': HBWNK>ө&z' )䤫QOʇm>y0I-ȱzfςI,h0.4z#ʎ</v8BW`"pFaxY(xh1Dc|B1"Qe#-^;^>3{l믇}*i|?9/}6A{Σxʟ,'Jc0t^^1 ӆ%:k^8u^S|0m˛h% 8tI>Z-S{X-tLp:MR&$hsgޔ[$mZxyrP_F:?䩍)hd˷W'N;*,Ru|0#e2AY-O>D`=+ŪR DO;)gUZM'{ IJvuur#u1M/F3ML?A9oRWNuIa&=<n`/׎<%aVqN`d[ 8A/S^>P >}גɛ;̉}Dú *:C\~'$&^ H|wOGy Dp4VfR~a70h`mZW"D~<)Gާ #Iq5ísPBG})1 Ra̙&듉,%8|oM&ǷFSVGF!]bߑVԠ49쬟t~_ג:ou|ӑ*7wp)?D3D kf#ۙMUcǶ4e=@a<:p'Ok^HVNW{JZuOCLJF۴#':CF~WEg9iCJݝ&y @/T¦jPti/BHC&3xX.;(QkX4wkvyڳ^J4* # %NRKs]y(GppvXGb2*v1 JHu`  E}6p+ĉ^)P'xCy/nP؄C_ՀR Rn[uwƤkO;9O}篺t<3$:c'{:tdDL?l}x7ϩa]I3a/2Nqcte%yUVT3v{ES Ef˯$p^#*6 7-o|~q B#>‰6opgAOPцVnK<6LSvmW(dS*&Jy[meNsidc؃1qM͏ ӂ#IJM;vIy|'H;)m5IlآtC9֗"*|St~<]&/?2M8Z<P;|J OBuHԜxW@? p)_] K8A~n%Zx&liCքm]!yE_Hܮ8S<Q#;xDԃiuqg1A,܌c0@f KyT.1#{ -3&fwt^{ >hg/8W+G5SR;tD &G$aWhOfIwݗq~ Nu4'kڞA54mH$jgGD&o"K@7 t\zYEN!ׇcJ +,Rfp/s҆$O:y&_{7P۠K@tnwrk5Ik7@kN4wd{|CD;1h- ?rwPˍ Bz4dzg|*p (\s,B~@<:`*3Y>tTGmrW.ԉrbZIxho#m/;{g+rN67cFo;s|d}&Lxwׇ+_id௳{'[>z7QAˤa`; 8EJH\ܥO痕uֈ`u7270w}fIL"Ƅނ0?J]Cʲ݁< YZ]|/ :^qCȲ mȖiCoax? YK__*AnM V%̡ΘFi3w%f8ׯ ɐ1-Sx+fnKўz8۝@pyswtV#{DG? b#7bFG3DwϴRAQvZɻE.ng y]K|Y(q N'p92FLR@V8ǍA]}=-DoW [&_ B=] 5BWH@ NIvGӦBGn,+/ QD2Hv"KHcZ-bPA<_S,#?GL8k?E:cg{4}觼ܽߢ_FHkSwsk_{>ɒN5?ՖS q 2/mJ$eK(٫7iR~t^\Igei/:/Q7ʍf6$ܒ9a6׹.UрQ:=S5E\̒EZ%0gHq(s*dkU^{XPrʯhQZۗbs/IԛHJ]T1AR !pd˳[ C (ۆ&)G4^Gia\c:d#gY_|Ӳ㟸,*t\fy7=w|]gϘD)oBl(tW"eK3>nd+<8%(e bTܦ%C$&SȦ\gEF)N(:R(F1Z2VxGa=W/ٲӗgKDGpH`xf{;Fa ijm .4a9r5z&#b Ce=$@V{-pJ7a EkNK@6X*q?{bm7k`@ }c<=w>x__pn)ͭu2[EZg.F?iM]X޻,6& t9e2g0/8fiɖư쉯w'<{c!e _"/0.qFǥ;PGyӠESH%rHĂ[ YV %e;)ӥ-H`n1< IJ>σݤ y#-bZ޼Fxyqj`_黻-3=ǤʨŞTy2:j[` 1|}Frw*߮ײCm\D2>G=z'_קu/e~!>9cU: (d>~[x)L[:Sgs$~32ȌoY1}ZL* +2! EDgytÙ.$nsp3nro aDj@xHs2c?г $S{`&by',*H>]w[;ζ| [Xk̤lq~Onu!a *62 ߷O%!4%ćv7П]ޱiV FZ#|4jdbyhvq3c+"vw5-_7n|B&"j3崥=6_7ۭ83Kf:J|A8T{}q/F/ׯi~7ݣX@¤] 6ـOF:런U>ؕO@ݪDJ?a0w ќIx(a6q%4;֖Pծ;rtq´o7! }^ZN~ 7X:D%( 4x5FaI$s5}=eQKxMwOD% A5E9=hH{x$T =V.nVzWxqٿI<{w/ws=i6h8 UΫ0gz,Dd?o4n!/l)ʁ b KȵlIW&s /%}L@$>3eS>f(@qC,S)/5p87Syȍ5 AK9e,_g>as- ZU5R&lp?pf0^1:A^GS-VR%Ю JBI||WE4XB3BBsj_blS : ?br\(vu{Ǐb~Hdi6RmK]e3vXSF6y/3A{Q {fTZrhmtRT9.y6B^0I C%!*i~ToXvqFC˘K`HS\7#TJn9 #^g0eX@`T:ȱf5 #2 =2mqOR@_;K}OO+|kz_~ye^~__~VzRμi [7 F~o->K15 a>姜nElH;ʊ6l4KFyk!X`>dT]-{ٳ*N8 ZvSGP^~EP~v()[K`z1w3TԑbW > ~?sxU_l^_pkpջI0k? SJa~ޖAD'jUAQ`őK܍HhikӨX\~_yƒaaIlOTM;y4R"g~z+8>޼s|9ErG dArKԏxu-4?`IfBU҉'ayhtәSçHHȐZU`6KF`FY#;~fū?Ef e v,Ӳr86G 8GGM~&?g?>llq %`/oޫw_&rd⻤qJ|A7U5Y̯ x?u9q%(pkd6| !(hU^gejwȋ |h{Yk^?\=_T b:Q:*ްi6'4A?Ox !  tГ1j҆ûA qA60h避% }/ޟaٳ>n|߸:5+}a&G; .B_ˬaϭވIԇ՛Rgp>?SȦ}8wZ/i_ӵqаBV-<-?x^qxܩGylQO USjx,C0%SZ^ÕqMwK ve2۾#(aX XzG=C`z*ùިTGiVlwX[`Ӏ ʊP "zê:%vA ֬EӰu٥Si O?ﲻ0Qi• &12סY5>7 mBm~NJ%6&y3tWkh/N!p*SkxQy2No?w",2F% pO]~NsxNN&?~Yf}X&s{apqZ[FSSdA"WLkC7$H'@GOZ ؄f6ވh%aEΥ.Sǐ؁A>EF>a c` MD4bw$&N2i!#?&{яݓſ tʞȥ_$}G:jU`^wͶNb@CTA8#=c&/ [Ғ'g~[ ^=:/_\se5W|o"M%Ĥ0P5si!IדF%X8j N#oVYقLVd0%|7nH_ c0n(KJL1z{)) %pK%D[ +1Nf'8J,;?5M`j=áWlS`P4V87}y_y_;\;a3 O05Ȉ0ҡVhyvV7z~e'E3+JCTm-CY mfΪRkf $y_gĢN[+Vio ve eyL[!1x:~ m"C:H8M¤Qg 9$|9P,od, _:ȯ*#I 1д>#A Nx#,vWZqm_;RWuuBI9}.9#jxF%. @sddX )kTNJM;qL({6|WLۋ 7nީS'r7\ŴlaW2f:aoD#J?ypߒ*gZ`iBT~XVHH0R@J@jN#Y?#0 Ϝ⳽s\,#.tPt[:Kyϖ / [ !]:xOZӥӦe+e!83$ 3`mg3ٓ)Ԅi'&1a@/uU9=a^fYm-zn$.b-M{X\ˊ%98qHE27&$)y/k n BЃ)䀻VHF G" d//}sV`LVgכK/~fB W ]9Gt#1?8{c'g::/ :kGuQggrf?\XGaExʗu2E7\{QDTW&Ԍnt h(O C=t 8˗~NJ˄DˌT5ÂxT2B2 E" ycI.VG~8g{Ǽ~>{`>|}UAm,ԧgO|ek M͝վ)輸>9Ns&Ͽofk.:#Ah 8pOkmd_4~ jWjo7gm ir Y]'[L )mz>M,a.4_U Rxkk-ndƐikoi|}6yL6, Di&pwy :t6Ppuk7fGr["lhC8M b6?ߏ%.x o\WLJ~߲G)w, OXUQ{L(v#dcό-H8cR$L]꣆tR>yS^_Q@^., 3"%iQ̬ ,?hcG*3 R™:|\ZښU1˖\quYw^c,ҧ3x!{'O<{@OG] 3~_]#_/a|#+rKw;߿ }߱93z/T 4ҤB6ْMwᶜYkt猆8ʱ Gx)1ka8 *rٙXxx(o8D`,<;&Uҽ%J:,#o ZOA輓h'2͏im;:GL{g$28vIOjzc+~R6$!aKNsıaQ>X?cKǏekڳ-lkys2Ul oJHrhqapäI4ÌT[|]$M̲ 8W/F˯%OvKgݴ3Eo=̊令+mۆ5?LcxT|GHNՍ`HO+b=t9\JmƧ2ËB:by+if4-%D8Pʨx;ڳ*.WD.Ƶ@T MJo kp00Ѿ5qZt\gq3g`?ْ|; y5xk{ Heen>*wF"3@H$ h*fn1 1yZ@s32'l6_Ӷ}yAKz̕J g|$g 'UJ9\'WS(Q*\W[agcOȧ Gk?{wϞ nf(]x cυT Z;\|~Z:WϩAKVp6\o]Qisݼ732Q%^ [͓qH`=o*5< k^?(́w!۳\M\QX㥬>Dw05uۺ >lI#B=(;_W?,+4?7b԰ٿɚZB)#|o,Ky7pg]&4nUW_2I|d R^pYMyr<{/_1 3foV?i`n,xZ}C~ȽZwǛ'~ʷHfFGxh7~ɑ2-kfd\MHR0 /Z2Pj/:t:( ƪE(wv[oͦ| 6u$Ú.22dѝ|Aww^ԇ6I]k5RD_[}R\4L> i(ZՏJ\+VY$ Pt0bH0bב3b/M8ִK,qM oZѻhV-I ͒*p#l;Kk+j1looUe4d}['D0E+ ~* wԠ;3t)Sg%)Sp~/4¢ɷV'F|5XxC{ ^ .jҤoUmG[e~_7 cq>xw;9W h{DQ|ce_2uJ gUrAAhC>$AoWwPW=?34ܥq*nIC/r{/[/i Ϥ}HzȳH]6a|ZDw L/zT3cÂ}9I([;%M[͚F/1q٠ŒG(1%<ܜj] p GPucVtiCy  ::^Tqi/C#/Ԡq{ Kt8aG7J-z+GĠ`ڌDXL1W2B lgŘ7\ w%4U?g{=G_{oEVd=.x;g/k} tC~t,>tnmuL}TgIӿ y_|{ eqvsY~0f3YR{' )ki^A:R(QUTe{=ZJ:1K΃$fi46hWT:CJ~L<3o~_r W8dO[nY;|y߳deBIx,' #q)R'z\qUC:Y g߃rGP.ݫgaZ}r%Vq+8&Ӕ5h:SFu]4 A]a]ʨOIkqCA&S/ɲRb{g3o4_?rzx]1\Xӕro.=FH)ǻ],ɉ{? }** I# ivP?jk_Jk6u9awP13=U/4moo6?ASk בmhǞS&4?~'W0igXvm [s1w|58p5>C7#ăh G9i 8%4mgrp w~rrWt8*L3?66)J]VU'[Wo;Lx8+O|}9/$]s״ A Ä>Weޅ^)eH>TQ#+$dȉcŠ[*F!;Og?kz3v~q/({BiMMw$z_5W}ɸz94, 8h$unCsDE a3qƋ2e_4cɰwC ?D:],ֿ+ŢC aY]L tuP!H<( ۗKt@):ѽGP/ߘp : UAW0ДOy' "WQzg8񦅍liެZod]< u$)hF9D/d’L(z7 (X +^K Ƈ2"3{@Uf9tt+^Y2 x!V&闷&9yl}Ŝ 82N-~TGDV j`GQ2v{1Q^ɵ\x+̩BV5dl'; zOEVFF h:)Vc3%Ӡf yuxQ(u UFX4Y6ϡmV{ Ҹ 8c%"3ɦ?Џ| %St$52')̛S[9yp:3zH:r5S+ 0};l [{dyV__w7`+.{0~'5ծ֭ho]įC| =ٸ )n{~{gCmuʫ=l>Wl]4¼ξ5׻W^SQɷ+][+}G!t/CCiY)?EQNȸ=cH;mǎP49tmIf\iѵoo.N냘2+Rgɞ$QlK nvX+A A᪟: *(yf摎c֫r({ gZѡ=}*A}eo1 0uutiXC3 cR)]D j8el`Cn |LOA.ɲ D4:2>}\I|`9PpľR?r0wM80ʮXaoe?3*5lOuUO~"̼51=)x3=y|i3\o]YHt㖖1.3=Z w[qF^R~zƯgx%et:; Zdd:82-_E(ʝ?Wڊ>iW (HFބRjL=R-[ ^<ˈAeʎz +!vOR>U%-E^~p;DE57MwN$aJ.ހg{3lV 0F ''H߁?#ex2C,*n2W,y 36]#,bq͙UA{2'vX/*c6]6 X?Ai0yXa C2vCAg,Abf@|϶e4(.3?cyɿ] p|Yn4\sgjF;s>=g+'00p;`` ˠ.~b+d.IȡwU];e3&&6"0@5 Ʀ>haa۽Hf@8ϪYM]^vk;!oNAS^C)31-->!EN-~|.+ҏ}lV>`+ U4wٲFFtȏʗ`LqoAs^&H[|~.ިsHNAgwFKZOu0iK+N%RI]N l I"΃`9ЇWpEJ"@ itҽY$18}WI{_&*N~0bpڃ^2lAqғO]( GvzVNܳ >[®5d'yaW{} 쬕D;;PCbJ؃3)v7OKN4626K]t ˤ' gh &玢w– PJ*.al8֋[~Q0Ci.*+dmBa[<[ě6ܦyꞿWrhyn: %_?Z0~ ݊9òTg-ЦI5ڛjÒokcM09z%NA2,, ?7/ -7 h/n~!.;nY5A>02mb_W&87OKƊe2=J@:TKGd[F#gVZ0mӴm9OhbqZt):;ƭ/XG;ii;@ݚ}Wj/b_Ӱ\iU]WQK75W쇙XVq&j:bEFӍNUA#VFNׯ<_F88B>˩+^R+ '@\qOG쪒^i `khۈ2IҬ[ w Jm<~u[Uބݷ|kx0p1mYbZp3iG\'#> }RO;@7rDNs8~A.aH _U * ,m:Bm& ,L曂h#}~ [V7W//MY{HCI:-6ȪTQwCjksn!`,H\Q+}D" d)j_۱r^Soatꀊ1(8K^OU!\w}ȏݐ &@`Wt4,udh2t`7cj?S=%;~eT|˃у- Af5)[ngz<>MTpÙ,)+bߞ~`4~a43{ۺj}+pFU~[Νv`ρF`SgdqR,D-LC0@!k ^OrL0j0Z7[~i:/޲!Iyddj"mb?>FԻHk4tMFzgͯ\-_AkbmnH MqB!z99{M!Iϩl"g-Nq;_{ɹ]~d3M37=VeT;="^jzt<3{>:pYv9a[ |-W)J.v㛽̈́b7ZKoiܖQb|5Bx=<[Wo;ȷ `^`ڗyL#θ?3`) B$\Ieؙ=!#V#>^Tώ1=iptsOr@P8L<6NIqY%ӷ3%F@ypF8c d2'37@={0)#4\qXJ˄\rnpgvsz[ 5fBWl=M ]X" f<*&z%v'_3̱AC;\m Th,ZLcXs&᷵U!߱Ixi֑t{s9Tw-P쨴&u6 aqt:{[L L^.%"vw֍uOM0[G3}!zoy;hֿ_GcϾq ;.^_] ޯ8r?W;+ ʶecڞ:Vg{gHCVp뭬T>=& =6GAy4I#}t~GNOA:D";4]u)@K9WyoH/?nl/>:9C`ɲ\!Hj !Ӵwy:45վЄ8캇 a'8}msZSO2:iQ*AA]Zr-Gq6g#!DxҾR9u|jc/(hi[,Bj4 <(*^C .&nDfZOLҥc@ ƉCOO?.y5tCVɏƶ&pqI ڡc@\~=8O3o Ojs~N餳MDye;|yNˁ ~yP}P'YƊ+iן6~ˁ`9h!K&'j#2M 3OtYTbWƑ^{'a6e;/ʠeq/CNIVlVIg9)ziR4J,0,oI_Rq*m)Wq:OV8:!k2hu}aL[̀`EO1TaIդf3Gbsk2puC@n8rՍW^[.]~8`"ꯇzt>)[a e:yP9/n ,䐨1[kJK7&W$^$'twh}bŒTbEz?yJ< ߈0G#NـVx1 ނeΕFܭjuE͓ %88_x.y_ }]ʙFr;[/Kg6r:CuK7Ү[ Ftΰ"o}JcUt=WL/,J?;Zު?iHûB~D>FKp)k|۬vSf Ҡet$hֶ;;;>쬮ui7 1tl}%F7aQ ?:X d.:xHt~`$ Igr6Q+T4Jҷ5 ? $Zq/]Dt',Qvt3;3ęԫ'YÁZ{se&t&Mⶱn ~)hgtg4EOdZQu*aFC靶~j+2/Sx["D4>kh? Qufj_Q$Yh{)G<~gDM?ll=QKj{hp8F|LjRbţ *񍛂S{gr<#Ic2og,K:} ='WTa-O݌RG KLep8LؚfC3;9򜽟FTD\[g%Utna#:5?:X  HQ*c.NJ(OeTaFˡ"`7["'TX-JazucSu|h;fLcCwۻ:(#ṇs:ba!@7^V F [p>zO{n>$d;?hNh,yY@K;x <z[IP_b"?l+O3.1rPACLP/)k+;Cc?"8rii'Eʘr\&Fd%$o# C:qGw a%p~ (a GWxZԃΏ5=IYV AvK9#f*1FW*ʫX^U7ߧOåj7hg?n^r5ʌLQ:0InWwO C:zePfq,Q/v g~}V)`<`:VwV'L&7&kh/D[ lG^yh0꥝? %~pAM 1Lx/Yv?$ˮA9 \0 @ '#4*la,xo! DW`{Oڹ~&`JZ}Hdp{ ^Fd-Jh+y^M/q~B1 5Pl=W" 6ˏ+, wKKxGFΔ6 B53 jUgx^t!o"ڞOhAI/ -J+Qkk^ 55y1I%B0-H[~!ړ9%Iuٰl{Fd[ FeD01I|@l]]8|.ZB\apND\0Xk41QHd=tBqH"1^͵t|U -?xmd#Èro<떛ّgZVHӪm+ɒhX P9 dFN$ltz*<.\<;U@o;[۔z?BSg3H٪/m栟ࠟwUdϟKA&$Bma`{Ǘ^+|LcKT3-\CIpȟZയ?i;[C-QY3vR{ ~!]Jߑ64xXA/{!i~ 333a°V 2\ꍇ]F?MxggSxo[wq ΆA ekIr0*<ôCEer^3[ۏ/txwedFx8_z;0+Yx!J)71"暃_4ƎܸHAmOW QJ00PZFU_;|=ݲ[$2*˒/̟25unJK&liL.4 e A/^/n{\iWt H|㯇Πx$lcݲ[J;S>xdʏo;}@:MtN7 vgK^8j,LJ쇡pճQ:(.9,3bšw)Q4V^#/rxLT K޳ \qΆ ~C % - ËgJ P,;+QZ(_+d o eEwZ0ISM^$"jg8 .<=y ei% NIZ0JL{~G>Cg\4kr`@ 2s@@_+៽0[Mtt >/]~jAo#Sǀ3ZC{M|^ x{Uȣ~3x HQL#<^iLDfbcovF{W&gGNf ~޳Si‘e3&—/{{*rr-goDg[db`?]m_[2(`Fa'=+|A;ySR߇3&!Q9HZឯNrFǶ .w&Qf:\<,QP{A4+ë ?O}Zm+af|-%q?Β$ :z񌷎.?Wk? V,Is%HX$HT1ο+"Xڭ42tqih^r(>z[ب/a|F:(L* &bWc,Ówj@F:m4RIS R_c"i†<)o~Hi;凵XF'6pƷuO.xRT |]̈́22<Ȫi:Hx֤}!COm3s@p7ʐm]Ұ$L8xܢڡD_h ҂]2zLspwsA[orSV,l9Ĕd­IN9`p)g4 PsQz3ZGhS@ӟ؄sP"EA9(gt  C25[lR^8@&Պ.On7a*׺` )9q!^s:(>jD(v,e@FxϥXpgJ&fQ}?`e;A>5-@L1)E#:f j$̺jL"5YCLCxI'J^ *:|:Tn((Z?qm|3+y[hq7(!̓ ҝ6 }&~<:SȮ%ٵK82a"c]1V̐k:(Ta+w4*)*ůhvziKs!f!j*!rVt`m}t"xىx:vr9ЬwhoMBXipbqjlOs~Ji r'/ 8yLϷv<?6fʍmb9_`gt|N'M Тqc$ |W6N8pu{5: ,#g O=ɶ̕f`Z]RщvY1\:q(G-iM~5[<o!wg yAb;4-9ɧ4 P$wOvGǟ|z?rߟsC"?P(d)g 5жNكg] P*(9&!m? r+>c7,E@uIHanF5?k$V˨Y56qCPJr#E7B44=÷ŸҬ _ʁjccvƒoF׫M' &cA}${MKCaa /4^s-p q#t{̼ TUFAOTjwjF2d ߠ+l=ObDڛq 3ڵOđ`abەrwyB,Ps9[5Iz*+p|HJH]eb/A\:|%kV!ϵp^?koPV-B઀HPUrH,KMv S`,7cсԙ18(zkvO}ge߂vJ|-Rayӈ^otf-ӿغ49` ~~eAQz6 ^~W~< G.׃O~֘wQ?^ t[ǥSV䇍yX3MtD=qS"z%9}Ga<yLꐄ)V?֙9SġcEւȾP5l%2\B$ՎU\x+,!.vBsI[پ?38 h& b[,v$&L ;Jr:%14fml(XvWd2aا4BRaH$kAlmps֕SL*853te+˘ 6 :K/ԸsfC |>[6yJgS=QFiXzi;l2u5= fAbVIj(,5'x9CO.pҹ":O0Uid1# y'h '1J̻e&]!Sɋ8g6()[#2篒AgAv}Mtd%R8mb%xt*P΅A+vU)v1zjc.}3:3nA2:.œgl!x1f{ Lv@$Xf&?yp >u6)s2ES7w5Gn[9U\u0\:>4AD% J(jě|+WPf M<倌w3 3p2'*,ۨ't?o7uc]qo:/KC폫Whqo_-Pl$J1Em)ܻAD:g@6tzރd^VMѧ!6U -.dBp㨬z|@|!z߻,g]Q)lJ1)nrʳYaRO̓ Kw+?**2fN#MIy$7 ݪ!=5MWқti9ëÎ 7}w xy M~öN˟z3/ ^zkx.H[h=xM78ekTz5 aUWiړ.6O?:itW;n.SkmMDP*[X0W -3x? ڼ R Ct>4$M7.`ΓvdLf71q’;)ȢEf :1 h8c0`I0xZ{/C WTCMw"Y;Bwx#-Us4gKWd޾p]2[xltۗ4s[嚓^u*˯ ?<`غ8&]P#ƸV-;Sp$XڰojLqFڽu*ĜX13dy`/i=W~E pB' GaT`'K^ЈyI'|0`Q(6Y꟏Ry`@ܦqҌ43~{rkS+ݢ[ҝπ\I}kHo+K-dyXh4٨ՑM~J +L B3~Xqf`Iث1O%FnPw]=lx;ኛBf 򢕐|v6J󝟷Z Fgp3ވ AEzD%I21rj`bt]hDO`O8Ҳn_ҡC&x"?[tV2F":?{n1&+SCaɊ o$зM6H4_xd|5 f??"o荄g6CM2qHpH4MA+Y:&a'`G*E-g# 49UHcv}N1t؆ ,8/(8:y9fiө}]983ܚ=R &Vҭn/:@`hid=H퍱5z-zn?~i2m)_*oOց8)?k.q=] FvFYS}'4ĭGE~Y0 %Oî%Q9(d.iJ|иu,ܦ({V}/: xgn%<7?FK:#^;//?woҦ9dzSvfM*sF ITj22cHwCՂG60AF4MTt^:wŻP*TBϦ]EHSTƞ&2)K3_su@\/5kOk CC1 VY;9 &lR\hW~w6X ͭ^^g+'@ދM֤bVsW#Y c eH OĻfR Da>Iy:t ;=7ս~w:yl=kq}:͜52aא%WYc6. 57&mbt;%"1VׂX[S8/# ^c{ }u6ħ@1Pr[+2+J:%S^2 Q"~|p>CyԘ'Pİίr~;|>𒦛wzW3OǼ܉F=W][KP\YXC-][`(8,b-dw+[|viXZ[2ۘJ[#$PMKLBLi乡?sKF?NY":I!+c6#< ŨFQ.#bm@i~ :*/',cHN`^nc}_€c,1W! ?#<)#/2bkGLē>O4쪖Gb듛 x'da&@*cxG^dӒ~6z?>[eer=VIL}ΓɘhFg.>USU[ʟ"{tkM}HyWkb­s?o .y#n?~dq~+D pvpC7 ȜcM39iyDCY,\j\=S&ܚ\\O6~@qpx ֦ q,r*j/1+>~i$_D$j9j2}i/ U tԈvB: u`=M)U܏ŮsVKgM_ "I)dU%p5PQYML8J]3j! #YBľ#]p079̥WU`=ګEUu(̟ i ,^A6̓&cwOӵLQ-jPvG~zd͕%龦ۗ7^ݙN@j,-[-cx,18=Yx9OSW\yL)~nV 2 ۱;/\yRб$}]3.T<L̉DW _b>2@ ol^5",*#oP6ZϒQhmKOL6y)pGL[~A7p$n`%%G: wedzrEH,6iw6'~Vfy;]&o|+W$_`~ް-= [}T$YM]~'Knb<;q|+,}Ir I Џ -:j rՖE̕ +nQL0gkd"ʩ>W~_̬iNZ>N~ ^sx_<:ǒ8WX.7vفph Ԟ*l8m_ϩXNĜurF=oZԘIq +zZ&?krkPPؠMgtw%WHquR!yFs8) cs$YҦCd ufTPz"$x[}K@/G#AlvT4n^QĊޫwd W W~ywFcKUrV+%MͺD}|)O`O*8('RznIkG:-{ӟ`NS՗ vb74A#cgF\*fQ *f |`ąʥ_WM<\jWDk6h 9#\bըvsyXb @>'+Ү42R,x't8 C[\b~:%$!4[Q= +* PW)z'n#0(P]@h;ƅ^t9("Qe_@ݣ<1 TX1ya)%,ڰNP|gp QV?O(V[ֆËx7?ޓi ݂[d o;h8c'h*Cy \|o Wrcu}_ '= 5q K_Yi3 :di=zh=ۓD*EV)'O[ie1J~{TyrXJpMq=<1Q'ANL"qS x:2Xru瘍R n<'/}2&6? Ǟ  ?Q!ĖCX `~CfLqԗ*3XJBK*R7( '*XJАMv@لY*v~J'q08'?g:',H OᏼGOmz*CaO8 f%'-~2 ŵW}AWww/as 'u(]Yo*D/n{Ml y(͉*<z̰ NfWĬqů{޵ MG*_+P&"yA:[yT-$9@oF$|}RZ ]C$ e>W >h A|u -9܃W'A@-TbsIA*NiՇ 2I%/@l,[ae_1WBI,+ͫѠ |jG?|ůG}H+n C+5l*p\%12pn}ծk|3VFU,rqe.h%\4MɤG a  4(V̫U Ŷvd4?&Y_#XS.R}T;FO0V_: 7p8ѶppgrGv5 c7WbNq^q%XxVW ljnq")ڠ A~|U* ~~0HǕ^KfG¸I!1m,\'(C=e#|EJd}eQ޳K&[7vr,nVe -ͭ:+a^׺R]%4l705В(z_ l  Qlzt+5~Zݫd~I:F$ˆ t~I*`m@oUd+dqd}rĂ~K;uS=WيBGFӈ|WH)V3Yd9#'lb7?9a2D_;/x: w2[I zoN?y`*_ǀtJ(H| e溅A6rD>&F)H$' 2#wxABOH.44axyI6Q6 K5(aMeFdnh\I/JtȢl>WϦnHzYA;bBM:vFT^ܺ%;Ɵ[oXF@:hj?;Ae}O~-wVt~%~!$ %|ЮGyx4N6wO𠴠 X2EUwxjdY'{%rC Gk7!68D2q8;d$.13Də)ڝD-O Y:PY$IDnxLAޙ|f #YQi#HQb+tᝓ>8CyZO]Q6NT5Oh]Jkզv @Tb2YcSg=V$.8 bu!T>iXҝ]89٩bB?LX՞A&/^脈}%0o@D' :X~2;dӚ!Ah)cNSlzMΙPAA\FbEe=)U~cb+\6a'?,TTb62-*." =cЙ:xb8Bc | ms"G?_O*)*'?&ib cr9_eX@rGzE Rlʡ@1 vIS#? ~ڟp fEdz<̐vr"VkDp_Y5,:x1Aс{Μ&ElVsmU,9! QftXzʚhaSSe obhIO'&~ŸJd󤢓 }5}K+V:OyB3N" @S&26 8VwYQ[6 '@)JJ)ʊvtX@@"KJ49K\;'1)sMnPn 3]Tu[S.[-{Ϻ'r/ʹts19N<3yx++8uԂ? jXi:4`v~N܎9t 5HԛU%^pohy<6 1hGO;,T XJ㧘ڏ.󆅶2ʩ({_W˴e\7]j3S`>Ⱥ y7]vb|X4ej] *l *h`SNJ;Zޢm;*cvYMD3QE& W$! M7p,E@e@,c2]q{XڒaG(>WՊw>@sꟍ;җP+>8PiG>_JL* O>@ѧyXHW▘KͰ}zN^s2Cޜ Tk°#</뾗=XN U^Ӓ/RAhDlG;orxZ3~EoܝW'}nRżQi8j'|B?tӨ =^EBpi{#.Y"dWxNZ@lHJ>z".[&g@[,<|Hx8 |i%tD;7y&+H3 MRfo24Mm rԴ%X\U0&!rvMt8\~5-=XSo]NRKRֺu[ @o^SP`[:v h$t^'xdv^GZ\">ëxMx^l3nށ<<dOlSD"Ű?̛h({4:䖫9gm 7<O'}uNiH~gә н((Է⌄ M+`6]6 Sɛd@@[|6FRO} *bU ;^nW(pïҠDfdW6ާd[~ $f$cX6,F 8B#=po;S' LHI[ o6T+*no н9/{F[?5 񦎆 XR@_B>M#GG S:'c\I$~ e\89op6|ɷn|Ȣ+pXf݄}2cLzҟ{LV`G=7c&?LV~93r<3lXY~w_ѺdzI`WV~KdHFpz$O:|+~?_)!v/֌Lfx^4"m# x; zļݕ?S"gVYS7BaO>aVvBGY[FH NRY{EPKm-ܤSW/(̎lW7m'"fM+$fRj@NF JYaiާ}*0&`!Ik 1xe8p]}mp9§ ۸nP!iQ[ Q*z \2>/6Q3ѷvml 1pp7[|,RSغakTŃ_mRv}G `s,\McHgs!*%NՈ P iKucuٺg S:ߍ?tY/z@^_`:LJq'Ayඓ?:.:"[HOoUar$k86a ,\V[0e`^k@e?6૵ofp I5\IOg>-cJ0fTt)?z{W|:{בr3݃e@V ) 7?ܡA{j bW8]M$eK<6m;-'7'o-9`bs5[rbvY n \ӻ}¡3NB%˅fhy0bB 8q`DUahO9s%r1 G]\u#R^ 9}F퍬zuSWqۃ,_EўXo>zp% |[vD*,}y[Dx'F8Q cHBXfD)g]u0.>ȷS|MadP§pV=@#YH{®-7B(~*!Y|0ҎT#nGh@?F'!Eg~2Z'yJfl!M| B$y8e zHWRL!6eG׬m"ڱq{{G몾招'?*>~ﺰq &9NRP$ŏ 'OیTAn|THF!$\YɈa˩ At\J/}ɃzR2#E^M]P_9UϴN0^WI @ҟcXit4GI i~x4P  lQ9\JRDzBlG0F{:0y>WvYk b[`-G,š\֡:d!K9`;2AAJ[ pg ( f uNH(~Y(ql."wDl-5"e F:/;BԴ lkyLP ᶯVYexqF&nJݱMrbq= m2W8nH-fP\GL9,N՞oq|-xx!5o+ ``kt9HЇ;zCc\0Yri 'CVS@4h1d;IƬDZE}`SNƦ~#SL _9n_%p;TjP>R(ZbB%@kN2 t[(AہdjTq-)F8ZhۆƘc1> vy!*_c~x3IǤidT>pUn߿k_BPﯵ3d#_$xm[58]\!uC?|6 ̐Ch* ' n,nK@Q#(T@api@JKn`EQ4Q4F넮 E!DXJ=1R+9WYʿ+^*, E)1lf5Tջ]bfqCAOBN1}9ON:$!Ru}U,RݾJ>'>IfcZZ+} 58x:akVӈ'4.i]{eqBNj&/4a6WE“~o~v@ \F!"d6ne1@nS0/`_ ^*K`ORB\-z}>8|#/Aؤ3c14Z.%Y!DJ幑OcVntك㲲PX'P]eoy| =vg(9F`7T# wzdx4 ?Ĉ"+D>P f)v٢L(S/=i*_W2C]螿_SĔ#c>{ 6'b0Oon"0v2BӞ;qLf8d'>M8o eݔvcw|2څց'6]x5;s3آN {r8A:'|)o=?Ӎ^SZg~%4Pɵ^6f+75V[z[Ƨ j<%"n[z,VP~s뾉z%EA.z֡+32f'*4:ѡȷ+Ё~G16 -KyଈPJn70Gx=LQ!O|*O>z"uwߞ;LnH/kmըֻr|FԘHVvIL.D62)kג6e[q7z'0PEQ'q g9z۫c>A c;#aclu=8N,VLR¸eI/![{*%~ F0׍(L1,F%Nmo 6 =<)bXHC>'+GtIʙs2{0Љ>TcVH[ Zx/, |] X¹[&г!:{"- 'W]Fdd`.ݗ9p^ 7  )L)I][^N[p܅R &|[tLJש߶|XCʈRq=G).ީ/w?U{Q߿@'g'/)c{sw*HoLHoh!,֌> X)#`a*{'T j5(Pry: wP3x0|$ |!P\XNnJw\`q?J#WoN޽JN={'8},C6"Jjy4x8A(ojVs?v n  lC@>,2z2t,> Rr \{3o#핯U!f"~`0hB"kb&?4\Dc״ ]{Nޱ YmL1 ȶEZt!@ 6JK㴟Zt`WWJ9KҞ\@*Xc>J+ %ss2=+-R~ k2m=bfcaAc{.K<-u5?CkڹNxXyzs _tXpn~WIbC~H#ϕsM LNNϕgg:{슮?4ay xi-· Ѻ"¿_e\ n6ĄaÙVNĎqE_ `!4(3W<=>sgl$g/鉄 ={}Dqw[~2B@'>%B=y +| 3RNKͧ,_O s[9pcp[,%wc4F]y"Uٛ-0y{R.Sg ܛZP ґ~ JFrL Dʨ{ =l[iG(H&@:>s~#e,fw}\'׊_^ޮ?+R;zXNeՇ߿x7W7W~h4\ksׁ9#2xMѬjt0@CmRA?0C al&ƲO1% ЉQF]1ⶣ#TM"1r?j#F,5wb I7rA@@磻[@?{QX#sL7S-QE\#~H"P)2Ph u_[8ꕶ{ LXn%[d ڲtt%YK&ɩ%6uÙJ^BY?b` eA[{K{F)3JVYv[Xf8nnu Ho4p}7nxx3U'hAŽyԅe,AhK =hM>jCv,UN2fQeTUO*wjsN<蹂%^ad;z=N[[ +FKRމ|==!- ^g1m"@LHB+!|ޣ*qP3,vɈ4ـ\8d'q IV^ ѓ|2DZV/MBfaק[R(OlxL..+uwJ20^tXF!5| Jb?7e`,4NL`,ACZ 2+YZtrц%?={VFsV,:72ɕtщpE~N@ܓͰ> #1!q:/`;:o/[_lq|peG2Z#:+ҨAk*!_I7G"PmYC)pT?L4 P0C %'>iA }-,?US iȭC-c\Ԩ/A~ț]] \[+t}%X)Dʸż0֞`9Y0PY-GQ9,YF!~1V1qo` 1#h]:86ņ#Xp1F@$P*5nۢ#6u봥 h؇*j|SACD^}Ǵ:6vlaw[GrzI%KÐGY F h^?yE'U*ޗ.7#v'لpLtm"`QX^,q@OEJLx!XGUrndU ~*Nx9JiZGk4UnjC"S|0._(oI2su-Ԧ/%wӅ8·,VRjN/t k eئcB o !ΛqG>#L7z(OĈl Koip!+DD?oV.taG p0=Bs_z|Ub&d'Oqvt{+63Kt؁yɺ!ݦ @o!cc`Zzr>='4eƟMA<#@Ќ>O;_ SA`!&䌸yYa[ԕ~.?]MɵR?'RIe;=O UbOVP2[W @WD_  W&faB%< ?mÇ, B&L?Tm[^;2+'vy\c@L\L!:Ǯ3j?xvWGQ~M"b/(X)_,6hL}>eʣpX2{OۣixE{տ~3 zhOqL1ެ{]48f=̳1"ʓaoLtʒ,ʒ F2wf㖯[Tu Du ΝKkTTZ.|ik$C 籇agnٶŢͷKnR+Ȫj2nߤ$ƏoYqI=6EK}&co%g(RZpwQl2D.EZ'f'lt1+)h{lCʢ윫XWG"MÕQ?"4a#m0U.'oҾS7o._jp~'jQV -W{BZ Ӕ/;Ph?/JV7`* MMFk*<א"pL#/^;^-1M?+5Qp;AVi: AsZ Xcq jh̢Vtɿy %M,< )rBfq8VҜj̣%3̃},AFFnwbRGSJ9eA;6ea_tQt4ߣoʇd6 ƼӃrrQB3V'Bk4~q{SwW1=<hj IJd~PhۃATk\-yTf >K#nl,/u;揚xW^?1OSeZoQZ2z V~Je')Ka@q#zvɰ ɅwF0iC b5@'X7'qҍw6MN#2!q+Ѓ}eSr )ٔ& ʷRQ:Bb"W68u`D1:-`CRZ2F_cb' d6A3qt3B:oG,],f=!)!bg!Z.&bHPkx,F&ا"C!(|!B%#RҔ-ΪJ_%g}z@.Hvi {DlMC~g*t;kf}|IR7mOmLȵ"613%e,\_')_k̈́vHi۫rp]Q ~2N,OCۭPO~. Ʉ <(FD2&b s#RZ'K~xARbu @\|JYóOUlރ .ċW= ]GA{Cp,z8 tl |hXxHdȳ&ϻJEH$3cWO_ϛG޸SHb;Lb{MTo,*o: 1G~S0n4a!xebdQ|2/1e|I!G U;msa76U\Y"6"lÒZ􎀅4l@72$XF ;.'f$Rd'#[O{X[OhOd0P ThX#G5ު>%*@h?u$ H7?Q׼A gt 1-%k !_ ƷvwQ 7qH̤uh&QU< W֥?-a%\<4[emQo8R QrGƪxV_ Jpd`|(? "~j/I+Vٴɽ^ )Ab%gsd&۷x~j88GJ O7DK}+H%q+Ć/ To QC{F5S+ߍ+-/IV㾽x}r'*WZVoZN%jm˧DD} 5)D&]3VˆA6e#K^pƏ!Q Y0f?=@ROtUbsO`Z臇@Y`N .XT?oA 0߈:*x,6iry"BgWtJNŲGQulQH+t* jc|4;a#?wtO~o6P]Ahf dC"#Hbϴ;$l% mby.v#}ĸzU0l=$/*ζ_l[Wi #RKmxx8oh,gTH{>&7) e)oH%xQI&$<Ł\q٫'@ Ě^y$-Ǡ͂- LI8i)_{x2 o \M>.LJ@Tz4 f~*S/ǃJO$9 hqʫWmo ?3bB[zo -F :xt{QL$0> \{ s]0%h` njEeX+[p z+2˩RHfp5b&Ol$yY)V%VtoIFV=BtCɛK.16H =Rƒ#U d="^I A 8M'^\+!PZGY:|d“ r sp2. h.9+GQ$?C ]EObޢc ҶXQinLBmD̲̠֗4m$+JxgaItVEEKŘv1tࢗC΅FLkū`} 9InCQԱN2e*mf-y~o`NA=pF^mhOB HeNN1a7=d^p ?Qy-}2Йy%[-Yb~1xnt˘ ܳjO  6yKK(K|j/ {Uar^KyPK@6=D{HO6Bo?5Vۃ#=k1”S [搦 7]Fs+1Wj*'ط251Pzouh5)ԌY`juI×=ULFy +⊪hI\B0?WlIhJS)&m7jesP  Us\Ӑ6l]6χz˟%C@'dMx߼ԽjZKIN$4-͊QztJGR#pid* wBayH&PTbd?[P::)`Fbp=Ǽtke^`W[!>%)6}б1ܣm~Xt㵙kMW,[|^yw~ۢ} 4B{8qHlB*8G9_4F^4C)87b[oVdoڣ0n ΰ14"n5XgT1U^볓Ճ\}T>\||"~&'wqV&'Q  MPic*MLp2QMKC-<97-'yc:<!Zē&?Q_m%L\-T|sk֥[6H]|ck(+v:\d3B?,Z3V,z;ԛG: ^ vGkn-v1US(Mvաvڮɖ"V~+?-U_2H(RC:̏O̡!K,x&f7h?ѽAzd/ye&1Jw#0/(i~\nOXaos2"Zn6?".PʴI[I1݄nCelbS\X&m!FyEbpsoM$}k; ?DQƣ=n fB\}ԏ4+ ^Vt- 9 o= §OxA+A{s,? |Oqs6 2Um#ˏ%* 4R^]~'o^+5=K 5Kwnt\΍0kR i# x{c`TĎʶ4ʿ[}"` H},P!P6- ;,5.&z !3iH1p'A2ӶO!@׮:8ڷ]fx8+hpF[A<"X8.B5h/a6'Bɐ6pOm1Ati({>=A\,GC'f7 4ĨyR#=;xjx&8Tp Æ+ E%F^Q{P&ׁp؄CEՓ7WhڂlWlaPv bߣS6 qXnmQHYדu'tcߟ9w%QCZݞn' zDkhE(/ m0lW |Hc#El g8P9 4t b L -7 S6W.;J@Y!E_>CBq-l%J焯4RD^@lznr +Õ z\AFdtG ]Yn "Cm!6Zn5_!|lNE< Mb}'"*1p ;OPn]!\4!eՊgA\S{S Jf7AepGPO}(vP[sݸs "eN,O Ec.MP'cAѩZ8H!T_ P@u2!O,,zeLC= Ɉ ;t"TXzMzwkR.dy; h 㴏TR41@1jv&X۷6Sd;7MS;! }7oߜ'_?_᫓W%n h5W n4h7N_OhhP95\fV0lL>q:HS4 E;:` ? im|B*K|G08Ƒ0@h3Kz UC|X?L_[=&ވB`G>p OPz6!kdlb= fl Hla_I|541vCv`\ 3`rYf~]ji*XHV͌=huTTn`Mʻ1y̡6%#B@n:A>|@ ^agʣa) δ+µnRLw1 b\`Ppw v`s=#@IDATD أC0Yx:z-O̲,^lZ.D#k”tڟ.xTNl`!xI ě]fm[j^Q|Y 0$tfD2 9KPc` 7Cܧe;JE'U/ZM]=~”J0NTϴD\xHǛTe#$nĩ+z^ўEz~~gNu OQʟGJ~7J׊#Zfoz{0˃=bUwUa@ TTH+?Py A _34%o# fU):}Eb5#5o}}Bfh1Qa/O7;y^ч~2|ys @ރ7jeZŲ~ǑD1eI),>ߙz6J+n?{ޤWni֝"de[lll4na[JK$޷ݒՒm=Kf"H\pNWY"I ʀa>2|rdMFNTiMş׺:ZuTo7#R-$\㬯1yr:$0Nۢ6YFD] 4RэE ܦ< 3g6Gyf]t]olOʐҰ$\V"z/q7Q􊟼I1r1*іZ9>Z^YUbhlyY^<~I7*P:BF&>w.&?ħdM6v7օ[Sd2wkRb-cOY]J> Q͜-y ';oV?At'?=͛^\@ةĿG&n Z38Wa[>PC= ΃yY:wU 5Q` [*HT5q| $d5؟:1h!P t űϤ$=17W1 ~˃onxxϡFƊ<&+b+L>kk &"Nd9|k嫼Q pG;*B]eUgi596='&+PϐѹC\TژBSep𘴣vDit+/?n'7GOLf ]PG&q!OT)=Jn>jт着,UT%:k y$r. $Qfy-LT2 g;9}m5Ar>:Io:2=ޯ7R~XpI|j\T~'AiqBLj;i ~hӇפ ͖ e6D03 /D=p s{wcW|<{úb0~|vp5ߝGc4gA䑧?]=cw$4(:D [Z|@|N8V4^^\'~Ur a̐)&S~x)b DLg]7kںdcOlpĐ@FoT I@( D x0;$e!JVpp0xztgr3' 7؄h{ "T;ɸm$mzg 4fv!5ꃀ?S|~VV Bq5&%Yr9Ty1ywF`2iGEpiUb ߨbQξŒ 쿕E5P&{n޿Ӟ` W|>0gnƯ?||V/&3 ]];(M3N_^qL!(ըۇ"ww%Wb`.TS|je* 5Vwҿw|5ϲ>L+Gy$Fy$& ڝ2},ylv<MVy]$hbW[%Jna jtfV L3v˹!S" kyL }Ѓ S?77%ͨ;R*苺:E(`M ̈Bakx>E['Gs<+{4g5.e*Sk~2;bw<#l (v_ 4wX#NK-R7"K.I(F}eOy A4rD(7A!Q БFB`~G]0~x df=d˘xL]&fi KDӶg8EN;Н;4Skzx&R4Jmh'cwţUĦ+Hg&y&ـl#.5tKw6 [^}ۀη;Z=j~*"S~0Il7)OLFyMK T TH KԝYrD$M7v#%y-ԗGTH})%j NnA7]3Q-'M.q7rz'ef☤}Oqڷ؀yRwoqtO.^XBK: p@^Vz<^ҩMgw ÈP(‘eLB~iMwkKV7RZ+8Quް=_Oޒlrr!wgL]\|7bpvwL>yupy<9uSya?.9@&Gn- Ȫ ۥSA!:`k+$JPNè uYq86Y3>plA,A^|xߞq"iWo~uw|3:<r*x{,2[p {s =YVLIc)\/vp)WLYAPC7[#;{ȡG_7ufqP;oV-Mr@+( K&nQ?P~~?=SeXB|/^tS?]LxyzAS=?Ĉ+KlQ;%vS̄w-V8r eA<,ܣ~B&!WH69/,Lewn< "B!gt{N ?$48Ƅo&jZ%kCP ABzB6jSfQSrPnek3E"m F;蓿Tp3b&>\ q1s^@?r|c&/y)rqwܰ#חLvC@ܡx.X~V|35`,b Ss% >ҁӀ+x( HDC>VS%y3a= ['U䞞K:k~ ;fC^|H`[4ogW8փ;NRI;K&&;yUZt3oyɥlN#E=٘W>T&7vaV8v$"KkيM`=H\q.dh| G؉}ק]+N*l׍~]ƷcyzwSWnX82_![F3n$[dIx_ȬˡO&dS%dT[ n uc!_4ˮm81^XRKDM2b"nFlFKaeip;BVܫD-IChi$UQTi=ɀLƣ0 qσ}8h}9?W*mJeAlF(߾5wh@ c ~;Q⢏gIiK܊b+kgo_vK25J1xա~x )_&,#Y @z 6{ LE+݋qO6p'0]ֈ4a QN`F^ZQI-*^xM]n֚ᗅMӧL;ztC@ 1<#k'K۸UKC8?u,94_Bz]0cCKecKfC3ӸnU*IGSo}& L^sIk8&HD6/i+3q7&\BZ/ qI[/6l& \cK xhELnJT;|[z 34|rLW[HZ8zK)aK _=oZI\UNzo[Y4@.y&s*Ȼ? :^c$eɗO]`Y\QxNe2mdF'"MaJ|ֶ ڃugGE&tdKL y9.ȷ}K-.W.]Cg8E!ow~ ֮ `,;1۶I =^AvRַO22O?gdԢe$)[Bd<屐&~g1\ZPgLzp*=(^Zx"H-{yg55(..顄] X1{⑺ȓ7-|$ip{ivoN1,==1Ѹ7߿?x?e*+sS Nyw n2? *|MWGAw\7I|M64EynMj!܍ZC? [lYE jU:ԕ@lh>:}J=d$paR]!Q"ǂ[ӱ%bH'%jbDKMI&P}.|:͌BJ.LbgF .ArY~Ꞙom~%QNk}w]|Mf3 lRMG4|@эeLh3Or]ށSd$+=&]?ߏr!q /<|uo\!B ~"|槇_^P(V /ygi˒0Bc Ҳv r۞>v҅WwL=$j@o&ɷxoJnj'k2#6[Ǭ3qG|8P;Qk>Ě7xTʎJ]}N~`࢐ `UbFl#nWE˷c:ڃn-Cjm a~i>;ܵNkd?HST-bu+ˆ8f2,!xO2M<2⦈CAZ/u.Kxw_1W:Y@e@CSNB+D%t\Ʉgm*n=&{oI̸ϕM |m>Jjo !1iiC,ށQ x} $GO1 iI8xҞrdS$K WIɬ#wgH:ePcۿ+c-~^3iɲw} (|k~z B KsKw )3W<]>,Vns:'ԎnnC7WPceҪp/]Dt Y&~I>v҅όe%vz;QP-$ۨu,Vא#I\x1!!)44oKTmO3?{U Pv8E],W+4<C}e4~[p (s%N 0 N]:qDװZح^EB6]D$8CJ?: `A+D-FL_ngi%7ᱛfeU"nV}] BeE(Ja!oKBMŃ.}5 kn[ MX7;aX /i<.@[_wǁge @xa2%_[߾>xrz Kɺ|2rKO,ϥΏ7eaUyޥuy ڀ,`,ƗbD{<y&MeP&W=7,@#m#cYP桿GPGY7x˃oLՇoIw|>|9k&))_}uP=V9jI ڙ|Hߊ"9ݳO>).ԑ?5hN7LwP CE0pS8)"K ;x74I^vhz:.H>H^ S8i-JOz3z^KzxI[u8PF,t|1fvn v:IEbȫ~ya/-r&,Btvәr%A˛E5Bĥ֐fv`{0V`봆M:dM8"&%aIʀ'e)W2 5yպj5/ D~28OWqsODo'RYvUd[{ˌpoA @QW>U\Cƿ4jLtH\疀0xÃدX3N;~ࡿ|f=@V|e~kTaClĴ@br5p/ºx3VIH$;tJN];c_4াf6yeG?wPenyK,<_ڻ#f| '#%nu;cGJh]< &i'<󚩣~+fW֑Q :LlON?0moCɢ2- 厢"yzc #1IMe0A}$#4XlmR~2:bAn?7S++vQ;7$qFȝZ43:\룕_,+I.bzfCťG$)ů>Z*%q 쿷)|9r6,N6Fgw˹D3FrJׅk+(/gE!JK9e;Oo([u((+KJ$_y)G䧜r2$vȤދq.S~3\K7K0od`,Llm\r|ČJS)<#vX"o>sbL{ݘO:1U>i̵3, @o/DNUN&v̨\d,FyG(HhQ %>%Jshl#%J/bSHRxsC[yސgtI>*Ys ;²RZr7e#,#]d7v? 4#?u{ab[_AB8GKZeٱ'st v6SD-0arEuCm: >C3M^9OU^u/+ߘp~d4|}Yc_ p^Nx%~:}[/ow(#y&4յGOXn"Dcp\LVE=%;9mz̠K@XСn~̉]E9Yuږw6֮<5݃J2a;kA:,ǀ!}o eޱmOǸ2:1 lP5tu{ꭓG=>H+@A.Tރ_"e>1ZZq'uW;[=YT9Q&TmDt;V2$u,it$Fq1 knb+j: ebT!<0rɄ/JoVC0%CFed_ `V~Xu&=EFXD'ڌ(N|Fe2lf2 DqɖHרoe_[vy7~9k#a]Cu+:4j%|*=u,iH?A' t>#ԯم9E:Y>yd:һ?(~{\:$V gG`?s e5.Gnc#ɖw iG//dw~xjrϰAL#*7l;>852X?)>{ #8?.0Cz. Q#~N h鰥|:$S ü"vм:l7RwmyIԟ@3C"RW^1-;w/km ]#Աv0Fy)-&_ (cB3xD(.Y3XDEDۙa2++"AY~$"ݐwRz::b1kw8 "o$ 컳HBvVSWmz pTTFҭ?7dZsbT=6Y` lnG,=k ?LyCP,y_vH 4Op֐Abt(ah)AȊ~iW~pqL=Fg4_gQh;oδ'ez~ L靿lcNx}vWNΝ{.g}^\9xpr>5_rm0zo2*<5\鿐{ (\OuB޲?ESuگ!uIO$WH1{G9i ?-2gԅ)IbIl]>&Hv&.?>q(;׊h%PIJƃǔi  (7bm2e ĵkY~iX~ 1:8NtYugh" hV;WuLyn)4OeB"O %~`=5j.{Ⱟ%2l>++_XLEE7NLeN`9Õ\P)<2x հ㮟f sC"I[Jr+BDžE\%復liBCu|YIJx_<\ w2L<7< ;dVEn+KJl~.LpXau7vee10ɦPT?iY v· H'~ȃ8|ӾXɉQw-9(VQxX`i%s /Ƿnߜf/yMUQ>F@ +OhV.]7ez;9A~,Ԧؗtz?'[uOP+U _wxuG~7@{,HrmdxL0?čr5*Ge dg0FSտfgQ6;:*áĩvɟ@G#Kִ{0P{G{&U|(>|O҃#?~[P2h蓋PzDŽKW>l7g7Tf8Q&q;`^ vⳇ4ݲ,is)=ī6>;{fe v׃u^ATqEXgr[}qf]*,! J3rڷ>}I? {ʓ J%{B V'.ٓ3%ڟd*'}>Y,ަۿ)GlWG =߿p&fmӸF(9e-s;| uK]_S냇`ڗ|׃@<%l; *[N@~p9y0MHnITexIs[h-ՏNohͭg\G4 Q!SY *S E ezGƫxg%I|"_1z3A_1+S1 z-_T2ogЌ\^"Ž2&¯BstIQJEDBea%T>ָTّ;cebsy^dӾŕ ]߰KrcfjzH~&~$6XaAtq$FBBk"<'Ǡej2f~~NkB _=k~u['>uK&DLbꊥytJl3u?']w*O?%hSS6 nazK pikhK}s@0_ԁW*px}ue")ѻ)Ҕ]q<)A(IÓ u' l/N7_(򊌳/WznTlMٓvblhٙB4;\:6:<+= "8[<ώ%8v*εK#$(RbgyAfk=oE:GquG\Q7x˪B%HN9_%BrDӘw@MщPqRm/k}(te^ [$qq7`ɅK}ƆчP[W"H7r)Bq";1.TScUyE|8T*x6#lGPe2\Rw]GV6$7v }c n֘vo#VC~?%k^~dYN +=Z0~p_|>|/%VRwh]P:϶ =VdTvڤٱoKTNQњzP񫛴<3"YS*)%xTGs%e&ʃW}_]% |vҏH2\Xbt1-cbI lڈ.hE7BCWj1®nMvD|>ҠO}S@2/tՓ _/)W3&^>/a}s}\˂ /a|WW{ 0d' Lj -~d9OƑ/7Qnډ}vZ^T |T,,o[Ga-<џp ĩUv{ߩr\wQ}<_p'n(O.WDNyrYp7H0iF.oA]`+˛@YBC9he@Oif1o$n YҶG/wex>nD݁ LJsˋCy˷5 ې=Gy?@r2=)N'Wb ";Y'$%YT77=S} ggX˩}s!_ 7W ?v{>}aak^f"[٢J []fG~hå-WV H= :&\lxOJFuyȣ=xJlw6eVQBi~K\půWc먈HXZ' (V1?ŵiIO̕ >/e~Y'rJ9\Ө3p^TB2j_x% ||G?aV-{ !pC#LZ^v?0 z >>6ωg |[*0^tg[y Q>`W.h浽 KzC}/^7+Er_r>O@;n?a+2t9ԅ-p}5575FY\'s} (>E2=ڹա]#U| iqGRO.Ȧ~ j2}IZCKU33\bO1(rݐ@H>n8 ,S}{7cئ aH#eAٜDZ0eɏ lsmQʯUEwms 3vM(G[XxO~U_񿇧r+o8|*y"m#nkG겚Eoup;;8w:qKٟ14?l_NR^)/;Ƭ (Ž[~p̔Y+u2dy Zu fO;n]>eݯliA䧽 y']϶qߛZ1v7 .ξ|Gq|3ݮR&mW~J.| sKnU<߶ Px5溊Je_?N?dp/;oxW=}==+wW苉uϊBT \_GyW~ yՅ[,ȠЖ/pMX)O]FëiqMp@IDAT%;t|q>|d.p ;~ӿQGEz7jG!_\R+N\4摹p4WTX{`A8V82`\|)- GD~4 M [|Θpo/a`'->RW#@S G9a~8{G8}:Hy IO}]&RRFlk9 Yޫ;L=gchit^uc`׃Cujy7~|G8%r ׍NxgPϥ'9.z~P:k舻g+nU~o+|N)vđO;UwLO;#qf}TC|:i/Ǵ@rfBB4gxhqOZNRSeE%PuE3E ?)>SW~xkys%`&DB~+Wq6 Mk/=m7X޽Ս^e>:t뺀4^Ο\p6ϣdI~05E3ŰW_Ʃgu'&QlE9@ uBy*ZcxyC>F舴3PaXQ6ΚΜ|'9r@䭸\IMd{Oj^N/YAlmx(P5g駌o+Sf./ {}S0g/hT̷ff@M*ߗy?| ksdqa¹{XUqe+W}NN7> Rӡʾ gz ˯g]ˇR^7.ʴ:^ے7{ Tׄxx51] /GyW~N4SN~"Tʏ-啨ó; :/;~(E<NvՏ{j /3G6{y(7;Z 3<_Mڗ>,]^=x+R^^嫞|l>^qʒt_R^A+kÇ7s_~-r(U-} ? &j{O ;&t~\P\)QBW@|ݍCbbQ3hʏx 7on8}cw+hO^ jԟ?w %]Yg3p}}ծ(@coKG9 TCቿYm.yݣK@!ńB[5cnqJΣaQ<3^`]M2/8ړ?~?$C@VM.>RtrH8:|ըNi`\Ҡ.y<آanu Џ*y Ud{B[:7(>}v<e Kl+?e)|"|wo9dûMP4+?w__}?M<>5;t C&.?.ɫ_`{!)GL?w/y~x}ɏzP_?PGS.acj[| :x@tw+t8AŪCwۍiևƇ;gc>s^<~[Q/՞/>P^C^}X^-o>`'A{{ gX۵<|S Qʋ>LК[B}\hB8!d Uڳ. F}Uڛ_ͻǿ,~=ϨC]}Uk;LӃ_.{ԯ%0$|7L/δ峗bӑ~}3=+oM`{~yM/?"3niSn=aLcBryvc&}{pH~?9U\ W\{tF;g7hqСځ]ΎF?4֚'fDoӅb蝸P~ⲔWnI`[tZg{,ix,k y&uSgV ~g 'g4&v4S-pw ~Oe 1y:5;euU┆s%70| ;;πH^#wB?ig4S`H Kqȇ-,/s„4g.G;LtG8c;tJgmyOX3{]6p8,c[N(1rZǔwW߻9Gl#)F{V4 xZ߫|e{YWY'~ T06`'ʇQsc+ooQB#5Qޒ$Z|j]]8\ ^Xe:SS_S(qS>r^,h7ȥ^۞ߴgpxlR恟Ln@a`ۋ؇Pɯ9R.?p1ArRGys`K%9gZ=qRRΙ$ɾ'a3!t¡W_g?KpMA-3Yq e)nG ѩ7L\?6he`}J*\iㆦ~fOqAө; :T# ~-QM҈BG8%⎱ppi B-Ro$ha +`?桟Jg#%"CpO3֫,Vl5Y{d0"ke oggt2"aqrG t/OeT3t4[euҟ f?fPrRpg-yOgp'?L4L2et"dv2PvIg7?:!(q :q%D"+2/ )d@;2ƹCJ) NУ2.c_qV'Q{oS^ ]p:н?Jg}K>W򝡿OSծFդb}?dR}>qE1Uu w3$>E} ^|Iwα94O.x|Sޥ>S=Q*6M39?LԆ/m?p^U-a'MyI 1 d!ʯ' ʰI Ʊp@oNrm&}<'mESrr7s0e(kET ` [3%-6gJ8aʓBC}Ry7T@[ +ܘp$&2T`G:;cߘN;_fYM|{dQ6yyE {v΂O߼0r*w*S2'-oޜ\J*_!y[ t(uF#~?d?o|qpuQ_-wS*@+sl;#3Py85lG*-< ~Nɯأ|}z^U> |]r*#Coɛ;f=k^ "o^N@;#Ly5?Rv?9L~>Tt ?Rl~o~Ǿeʕ!wl+|"y-/ޝ#|E`˷/J۰?ll~1 ~Csnj|y+oL1502-=5FRV[X¿|3 ;PsꗺK>x [=?n}`k}D./v6E ӟ~6#|.2 N{FBeڵÿ;+{ٝ7gxޘ| ?hs7h'LmJ߫cǁi LR~L6ZR)6&d_L\sT[4ӛ7~%+'|?!а{9螿?÷zaKՈo^~MwwC9םX[Qh`,(/Zx'q9qP2R^E>&B|J&Mȋ;n'I[`I^Xڰx>=R$k_tknC=wl?W'gkh-7G#M'?%F>kB(bԯa6=@.^ihNiz@xCy`X6`eN(w`N@]xcr $%^<3U/hiX8 @PȃV,xg$yiubC?#􉳛λ]KM%ם~!#8As&YzZ"Ht #Ȼz/ ccW@h5bw^+cQ""P7Šwr2:87[ \ :8Usej 2܏/##\ -C]A^V-a"tFC\\*8) Jʋl9_j,ڇ:aF 8 ?9ʩyZvq1{=zM w O NyXw(f55FqI1Î;-Ȍw R-(pB׮l$4uD8s99ӖU6aH&}9Wr:" >Ϲo!5H__}:|Be;L9yCxw}#B,y (_]_u<_A!ܡ 6PLPJ@D[xِA}ŔWqL}[E(`M| :ן؆˒'yDɏ| pNwS^evyUVʏ_Շ(9P.ͱW{la' ?v'/oQejy#yOMPJ#K8XsSG}Xiq(J>t5ι^,/?.{h"`37~ n󣬶G>|`qx"Ԡ<^xX9gQ;/'oWPOb-Kgb$ֆyPesoɆ08aYNefpzt&dIK:$QtO8 o5t]m*=Շ:ȜPNA>.{*?ݔK/Lo* /ԯϑE(',R^%JƄ1A)~w8$2a\ TB_(-jĄ7+ 3_ |UC;Nr=` =)[|Y& ^XJW,՛zo۟!K &_s,>mIfrZ! #4gcS~0l(;?-+JUfGnmܥ{^ٛXADDy.)Q lF9>>.()Z^] S\nwL)~ϾؓO7H&Nol5bg _ϮP7B85b[ -Gi#y:/U*d~|jP wѼ+lSy d66ecO+F{40iTwr|;`;aC~Ncg=<0> ],7*0fm;/~=6.Ost㘶up6pu-w`m N[\SNВQYA;|5B*X1Ѡ/xbͼ Ʒp'ÒO\ٟ c#A#?;}ɎG}3~vQemrTvl-eE?Ԉ{WI[Qlv2|T}'Yuw?3xkm[ꍃ{\6^K/g5shπʦT)d<`|}`'xX-`e"HPxj}]wI'pȀ.|U~ ՑBYSOߩm-6kH`jsNZnhXb%:sO;LIӸXtڻCU2MrGZ NI{p~*E  8PŘn6Zܗ`ITS0+9DlApٳ[+Q!X,\&4 M{\%wQDQxpL0Ҕpٟ(E,?»3 FA1*["ψ=|)xV&,\_-7q(H6G KMy ʃk&l3T%MiFM}3jηasا)8iCL蓢> >Jz D_'Ay-v ae>!jЩqxuwPeG(Y}G55!(FW.bzXmuw~ [f:~^OMi_,Y.O)6݄|;qPPTs'LzRS (Xq9l8`13v]wI=ACSҼ{@fZX{ <"S+Y ,zpdX\?\TުBځ8;jCS\‚|i]&/ Fs@O ]-}`v#rvG72F&G|/\GsN9e;b]b(ۖ\yo$;ˊ?n? I}/MSQU( I/wpB;VcWqZ˂H6sl?]eY6{J~ϛDAo_0Ee{_CR {,ܳ_ -iƊHtR< :ω,=gH{Oskw,x~8^R;W} ӑWtùЀs}f%$]wp$"๺yGh+=|쁂z;}ͷDGOnt#i>I?.l&)V!"5}g?~ӥj~Nm;lfIhq#Asd6]&x|,lf4W,jODxSyhj1f󛨅}xS5}p+uvf|7S6]3 G)0~>UeϋМO9 syyc^{5ݽ`o2GXxy<)г Xclf)0%=HD28/A3ߚ>"b?(y?}~}k<ӟK2ݟ*$g_} d>O"~"{OQƄ Pg$Xwrg`c>~g6r?z]7qlg]!f|L $~Y\?`|Nɚ`/1Q\?5"ƌ-R> }{*Jsw#s]=q;pɿ'F?)˕N >֏ԸϕDO=s |N%,i;>?7w3.7_7 7t[{B#Rs, q7I@i?Zt[3:2"L ~_{,;{h f>}g |g qgs?e~kz͏~Yyy'ۭ%B^o |q9&uw gNcI`TX%o-L&AQ7GMk|.Ϝ2Rs//yL33w;+6~?j>?S|W {¼]gɟGeJ̠vq 5Ġ >ZQ&K̓rgX,lO{lXWJa8JT[@vna*ӵ|^|7t UU ୃxl# ឺIX~9>kk`wߥ+./|!?Y `f$g_k@w`׷]ǀٟfƯj'yҷf2Qa>zf'Ȼ ַ=SٮY5Y5 jGt>Ϙ!?7| w'N؃z[ K #1Qg^Z1§b翠Xy tA-\V]',S1xM}Xy_&}OY=_"sx)/^R#?nV#=}++{VH~~'gY3>ki]Ex뎾y'|"0>Eǥ,z|x兀OdVqGnɓU3H0Y]ylI/| TFп 'k*3²"sFV?ܧU7(V5kF~Ye 1c=-ߟFeJWR ؏vj۾A>;z^ɻW@1yϏ @^U,鏘Htظy*8цYvX4Ty@TGU$X:4XR-@Х?ezJ k';?0w3?G櫡o?Vk\2s21`s}j>~WO' ?Ǧ } @RǹğۓgSnG|/.Ǿ\طzFg>;SJ0yIµ ybφ!}9/q$A@p ?`MIdɲIӳ=ʬnBir+ )ܰw$E,*+#2 w*8^3<<+/3p{rϢ)~Dc0 Wk < )şx{MWtz >t .,+~F,T0HrghÕ~>B}xR?w_g}4R|xmLHd7_$CBt>ZR_ųt7o\JQ  ,UZ={ձC !$T &}0hn:k' Eh7ʂ7PK 1̛Ge.(B',Og%>>A W]qyƭ4yw: 0pFjX5\'߰%X6Ǟp׎\u??^GؑNt{CN';;IgH 7iP[}e%}b3;QIm\VF}Q& K5V āĆ~P%|J!yG < xDŽ'Q~_yG ^B-em֙iT-aG4~K ?~][>$Bd'PPI?!0N4p5 &,u(R"&p~CW9`J#IyzmڹB#Ő&t9] $ڊzl @ڀÄ/)8Rg- >O6YV w+Z6}Ve9J{8rIہh<Ф`lЖStdzBF7f/iV:BF_B:ێзw!E#;{v%R)a&(! MvWS=Ca]{=fӭt'jR !i =,`/]dpBSL?X61#a53 V^64U+|aRM$ֻQ5~oY_ o ]Z!@^'M i#ӋO.tUQSizC(_[/O[|O=[H!7*۩8޿﫯CI;S?  WSw}.}@] Ox#@B"iGtxJiЕ!LC\@_bV}J8:E^U:1eoꭟf$~GN*رWø(Jء|X9iq 2-15PuKLTy.4t9B+e*7LtlcFjbM5 -]4\68#r.Ӣb`vMX[fRsѣ Othkـeveshd2%|dĈ?A8 +]e/>ť*WI\KQIT" E qR{~_;n q_9^wzl)GgkOfeW>hD/T e64}Iq9Q,h %6Cj%MW|TH@@#VRC롿ru)nZW}T; VL_#Wr vs )<,MUG83?/84:`ߋJCu2Oi{%jͰ9 ^٨!l"=)PI*p-g Ԏ`ufEKlNX9L;7߃]#{i^ps-wA-AS CX-Νzἅ-1)gI &SE>뚭1ίDNŘ{D+1뭀1}%?.k sBYLPN'֗QW2 U:6R+.VxT%UVո Q8]5+-{uouL'ᕐcoc=S9U m9pX_|M> 㸗_"I ;q ڏs :9x&Ө6愥6ٳy`W&CZlP%z(i)MfevܢPDt̎t]ws]=HF1. [eUg)ck<D{ DtխCߥ됥C5"\"=E4w wuD9 q~"OtD2ltjqX2]@2k Xc=LaE_3c)?3zm'>נOlZGy6 bY!L/@d6_px Sef5qI@Iqꨕ?)AG҇}J!LbE_QɗL_'Oqfnl16j#MvO`ۑhQ0ttӤO%O6Nf5ބNAW-P18`hI,ʄCo4 j,<#(LB-ؠ($rN+m@u+UWC >cj ='Sơ'GOѭw6@%+F2 A N"NEDC=SkҳOۂЈQ܏u.dcWFV Ę v!rߚ R.5YS].龜Oz]Pnm Ԑ>BǶp~oqd14q:p}Ayb&= ' ɉ(GlǤ|̙CCs?<L ӌ :?SgH|;'cYX:<SBЬ,a/ H[ؒPQ Y(=fYTb` sȠL|T h_;j#!n$ׇ4 }0 6C! JpO]p{̑G5v8Ѽ84z㕓&o~CmaϴewmҌD'_*;Z} x sPBI%l 씙h}OWb?!ƙk|ymV!;gpτDL=_J~.':.7&>Y& `t),D.깤(V<3FW@VtJۈAII"ʮށ5Ce[lyUp". 00l8ح/ Ҷî9exOq!PĆe\e)6$]'ஊStCrax|K,yCXE[H8JD:~%̱d_CT @ Kkqm/7t_ Vd?~^̞@IDATG~Rc_޼U5z<}6"t8W]N~C_*=&<P\ X<nojmuXְh֡"Az5]Lv'=" ^ -mDI0`("4}1U<dϿ=` \-蔦( L-b7o*bL(x~ /1~ڬ75To+9(.0$ ,@%EZ{ti#l%2 25UptW:"}yM\:VÄ5aO mz%鸯όҟ!V B8 `>xn[[&0rx20JA 1rF6ωѐm ^!{@sk1kdrr齤8nC*"sy="+|ѓ"XFnTl)K'{*nSи 8ʇs{+vkrPg)翍QIԗbd NJp}JV\8fkÁԊ~ɏ9?;8a 8xϿ]iנ&Hqx q8}].-SL!× @d= a:lKfEa_H>)/9PEon \l ''پX|A#Siܠ;;6c'qXrg8c؎&1ػ-Gm"]66hgq{-H:8|FN7 rs3KqفX ,W;قur,H]uWzvk\*t{B J yT!/rߨ ]Q!*>+M~]^ynYMagO^yo (~?jĤhsO-޹/q8\v聑C'cI0s&AQyժ 2"&; >w<"WJBސ0g4iбr-,d /[hI%MrYEx0~e=)pPA ValdOE!6V\ nIS\-}}gVV%[:/ 7%"16bd;=kK?6@ Mq#ĕu{ʴF 6 R֘\A:4%nuħ4TO׵_Ư } @*@f?OT a\'~+?ǁsiSWZ_<?ov i*[WJy @x);K .Bњ(HНtCKP ?`Rk4nRu*E¤2IhIjelQ=!1Y<řbqS:io'tۋ'& I!o7Q lƳ< @7s41yvoKG\4[*Dm9˜ mw"SPZ%e0 Jxb->kBкt$+C( Lcթ]\qmlU1\/%tMVUU7k,QbqO!B@/>0 VG{?(ÁoW^_8wc+Y=\1uRDt[%c1huٮZgoN܄=!̽~WL9f l{?WjkFVDJi|ItYXi5^(ОOR EJс:CQ}2zBT]0ج412 uT"c^'NGP#Ŷ9E8dsǙĽ ~VڇoIk;v8{/݋ {;púl-2ta̮I:>)0sߐŌ|()֮pCcGP7&Q=t򀎳m5d`I)BRpNUCYʰ|YL,衚;n' +I-g QeДzLO 3`Z6;X\5[ pS될kh =nw[쀕 )_h6)5 ^܋OsS9psSo ȏPm?w?٦g"]& ׃z4o#>C\@4Z}l|jY!#+]v-ҏju@'[y_?Ǚ+zͯv_N|)ڶbr!*mʘNt'vBEp"VDص' hE6a}JՒۋ=0t@-ѱ`elk@vpTCV'lsRAV]Ql_՜jvB-BA;cVgw_\E{/M=uuJ 8'C8 ̂2h)㍝qIn_5;{*-3t)h%utOT*^gTqA>KN'$4[k4{mʟ_'46!#n ϳCo2NqL<Nx!.\[U҇}e2ds\βjʾ"= %Cτ]A:-[ՖcX[?oe4i*1׎F=_CϮCk{p:O/l")ܸݰPM}<弑y'_1 `J0~:h`_rƧ|$p⡏ & E!=&!6}2â:Ƞ?LڻEn`)!#vN;^Rq.{Ė̴;"&, Rk*~2>el!LZu`x/ wa9!)*_qp[o' ɖc&ΏS=&cqU+_'GX,+ӏfzr+ynE[n[4C^gnPdUW;eAIb ^ cӡ`k?)Ńԝ[~bgSEb^|c{I$v(g0~o혙ݽs#qߟW+}c|x0'ujL$NE|y5wB>:-13Ft8,F"&O8C~6RDƔdUe]SGFSL!"JK_r0?BQZjPM3#eH5) qO=MDK6Sad'㯔YhJvS+~b>>Ty:K}7&ț_|=&x# U)>Ag*%eģhN) ¹8ńa`Yt3&()OS_irO4q߱0ga6*!+RG\u pmz |\UFH0 @FWL1J$!me[Jv3җ_'AT~ G+u˧癷Ux"j$:!p d+p 3kגa_TIkO#ƍ4/!2-8=h)93;[UݢYa0i4*SCf([M\(Q}1lA6੏'8bAZtWl *ЕW3uCμVeq VvF! A&O KPOg! 4( dSz 22:"?&4}[}W.}w%B 1*l_2{(*/g4 #l!r~ '˔Hs,=fvc *>訑eyn" gidJU-2K;} jvV8bZs;&طq9uqs8J{2lq6݋t-d2!Dz9LoahB3uNs }ȑy{x]evt3bMel,zPtʶZ*D;&I!^X5{=A4x]-F 2a̲=>Đ pkٺ% (g{Ӧ8  ͯl:~Xٱj~J/q8~Jƣq Iufu]m{"| B,+pARdԄuV e&-֌K3UGro~Əezr1 CJ B`#qh*2UK8ԉyz'#*\MӪkxU~WN[WJ]nA; o4teF]~t.6h G'I8CADzQ$WtL䖾GFC?K6Їc9/\Ekl ΋(sU߃˲xSUƇ=˄CWV,]gfEV 9cAEpa : M\|&i ,A3bPfhB UG&U1n v0V٭pJRvJ^S=lmjw PgKBR_J#•(r)@j*J`\@<]u5{Q@#]H i1Xu%QLOM}-?cTy4 v>}BqC'qn Wo2g].a5?Vɣ6  B}~{5Vޞ* aT>=?@ p9C;|jGTɨj^>{VGɯѡO郮O¿@?mf',}>/ W?*-'/_O$+F^_]&\t1!j(Wxx)+d{zy {fv9Ʒ(2tD.DSg*ڞxOZ&(ML^rY g97f yoЫ[j7: ~=<6ӪWVRCWAThz3}:!?ewOѧU-oE]sGQ>8K^REݰpG:mȒ:DsE픲(Lמ)dHՔWǜizN;Q4ϑ4/8н;S>&{u+y׿u BzOOr?n =1jetŤ#x6uIhymNCMdp;yN /8*(:g;!.C@rÞ%<r&@aDDټ"6ep-d*= N-gx(Xt Ywi Ͱ#n(ZM}3e^veL5˒j[99j`[_ {rpn?Umoh^7Lm.iShPm٫(H@9.$d|BC]L5& -}ՁY=i|E8Ch+ ?21uM]={=+ c<-S*Tɓv Jb-<=&5‹Mɩ5!TM:*mSp*V}ǪpћbFFICq> %WER SAxG\%bExpr2iW!Hm&Ԏma)"HiX f*iNyo/ HI8)SFOQK]om/2쳾<DUM`2x{Zn;Y=ato0px^2%RyYjg2TQC9he= Gwءk,ಹX~h=YY wa-2bDhHP : ԥbd=Bg~h̀$|ⱙ6KGG6O0/W!Y(- ̮܂m:4ZߑDJ\֦Mc-XUVa,Om׺v[Qu2ӓ׽-  H>6x͂3u!49 FD-!1CewIh65]nwi31:_ι׶&Kj1?N@s6l:4Y//yŏ/* ! NzܱBӤbhًeK(/[;y21^C,mS9 sr2$EP' O$@֧ 1-&  ZO2hځ;ڕ{Y+XHN8<Ά(hI2NkK/m8Y$)f%قQ6-O:PbOS JS`>(HV05 gH}9=ƲD{T*L;rHW5o1`x ^ ,':lnӅC9K/`jźm^zBg+ -#j{ "dL܇?{C"BI]w%hZ[d1{@8.pϟ.d[}nKTL -M 9D& |L,H'Qƕم@LZv(i {"awھɈ&zXƾⴉgl$鈙 M8Sm:Pٍ<)_!ʶwR|b dJa<l)F}[cepSd*5^L-/4۪!ӓ5{|EG3m/P-?w~*9M,%m@SV>*M[񐊀^v 翼PH+\ПW@p҇wlVvd$!"? :x[ LDFвf{EȊ-gZa׬;̑$^8wMvB"a آ]xOhLAqQVS}.S5N Ɛfb>i~u=!drxzwNv3=`Ti!X{礲lӪGTثra,;>;;^yuB紣A¥ow_a.ѝHg~Y9S<5ppl(0  ?O"gIVcW #;?$gfp{ŷPU~%&޷]^ݹSNOޝ_EӧʼV3F*w~_]wk{qߟwy+??KLP[ Gc kX`7^|@K $cr_:?Xny_|VnWi%3P\ʵÁ>>$!|Ȁ]|_I7=ZDžl=g9>iξ_\\Hk?zY_^D\?;:Hӊҽ-:5ox)NM+vAzxx-BY;]`яhI(yzk @ԁt9 Omd?U>Z85Q5аz.\M2AkOw8v^l&&=ִtYJ{pqu=e?ub[FJ~ع_!]#H{c kX;ˋ <,V0X5dƫ[Q4B!1CKU H`h PL0C; *ߝ^;ס7O|'}V巼sͮo=Rׯ_OJL <(T T^}&4,SeQ`4 N^2ic85pO\\`Svpqq>N;krL np7iFOHِ{ꉈ3Aٯ"6`oK^DÄM8߼ݍ˷|׾/˵t=^i| \d繵ӓˋ= ppqqo!NoN\U6$v @+ ͱ3C1qc kX/Yr8VN^/csSˁ1gà/Q=R!)cNċGOPqդEhpŀA;=v _ʿրʡ Wg8=%ӓ7azYvVifxNQh vx? T8#|ٺshЬ83Wc85p\ҕ?6B8X{PH^#&0sۛy [2 0ˎ˩ɺqErʨ֏F=;b:,XI'no+7Bܿ>CH#2+ i`~ |kX8`;SSwtӜ8L~\pCӑ4GSm3f-t!iLkßvAxPN5 V| xkW&.wpy5pc k%j 4->랹HA1ĦJz^`͐ پ,\gw':N/{kNooݜO_ Y/zoN~WWD쏶bGkX8P|Ϧ|b?rw,b,g; qn7BLqkyCW:lWNoޜޜ{58y<8Ml@*>bp~Oo~ x/ 4g*>&5pc kWּx+MLXp&S)3y q"}Dܷ\8?|?` WNiMx&YSps}onnʘ7Wf~M6Zo`J6 Sz_%zm`CTK(OR<,R#:O?H|hrD%8؂DS6$D~&Іs>N/>Ya\Ce$2qe eQ7p%@AJF_$A3%{dwhioS]܇^CHitg d;:;{w$ Wk<zxM!ix$_ͼ!k4$I)c9-Me׹-AKVz<=$U,y(TO;nm/v@#KR7zZOVxi׮yEs|ByTr;TQ4gbj\N-oy)]0 y>1kWox& 3k2`'UeYarҢħdZoCa^(4P"VӘP>,uOڒT+d-c5VE#@`NRBrԟr{e>U}巺x! :DO!K }i>p t8R*VgBN _~wl ~yB9M1xD'aI4W]=QeNxUY~AL| \3C{CG/AOO_NN3RI`Q @A=J;4,sג#@\N@8khrZLT=cș*h?s.vRc e~l)ħ|v><)s/q2@@ u^4p%?Xd Q)ml/D*Xg0  MC^o%-%f_AISϤRY 2'5WS? pWMtX ~,c#prb(YS;q:ؙ* }zFa@l! r}b@Tq^͋>K ![[wOnvvu\hq./0`xǁuԄ=Q3[}\}٦cVV8騯T?KX)V-CdrOlHDwcDlPH4ih"ڙ ioY3kN!9zʂsr26G~R[!N Ct-}j sKtKUHZ}of^k% uH0dkA?B.~MT-_\}#*`pZ2+O}S`Ͻ\X]SzRiw3md#㘹ASPdn { 9vߕHQJ0)Q)_[h<}02Oq,}NYV *sd]Mrd,øg TˎuҠd:\C08+ɟ#'su% j$`9k~Hz[#:>oXau )`[qbd9tqz9BJW*qn(o FL90Yy/b's*|/.q͜q 7AA~ț4'$xS;Lyj4ˋT`ǚ[x%I+O>~M,WD˗SRT!QVw{Uϳٓۂ#(,^i/| }\ H4xv@!|H/tvZ&y7( \"Po0qoŽ5llDoafK\녨|p  t8xAH&$[M6X -pδHO-I.|ЊPbl9\ǖJMWLKtccKH+^[gtrI?uɥOӧj4z[{;|ڋ.xJ(x y0I+/2gwy\\+A 4݃^IX"*k$:;QC^rOu}L 9U"C<폟G׬::GeSzwQqca/(&FRiY"yľyD)=92"6NM@_mi!zt^>zlg#:$FozR;u}wNߝ_<\|wr??^IOTjUgNSq>1NFFG0e2A41^{4}%,G#_yic8Ɠ[\I0OXA"~78,Y Ti do& &=>x,27gLʣp2gӒgѩ d}_'|ПGP4- \LJCmEͻ뵿og{_i_?Ma?|qu=>e2qÇ0QS Nvx@IDATp~{q~߮UP p± @MFhZ䬻.kL&X DP8y%kd=DgL J3L4z9t&YL0DuCG2){=c‚1 <2Bt5M)Ed3S_;V4}ntH)YJUW04{νp[aDbl&p]Ơ;&5mVG$أHѴ$|5uZ"S'G&I+ 4$-R""_d,kH\΄cGP%(Bd%_z qKyNM.`OmDP-k/ʖ\I,Mw B}_e3H"xW4l,{ig|Pɵ3jZdžGb8gC'TZU{XdKgxطL/[^I F?{/Z>lDq2brV\"-!44b9J; jLĸ rJixY8n{<薇NW'r3ŋ<`9X"OccI1I}?Jˇ/|@C$`7zW Ɏxn$[149é9Sˣ,rd&~Ηe, ] !3k1ul tC^cc0 1y b"QÀԹCIZ80o&bY@_Ckrq-O)nS 5aIs5YMVY NZ'5%*͠QG{Lu/U q%>޻XXOx͢ BƛВBJB7bͪ A2ّ)%SquGxA/1'#D,e<EPG*uS;%;֙V v^fCIGr(x%>^!zw1`̯|r-; U+M|J%F,M|M{FwxDOys2+:5K0cgf5|*^=6},(HPl?w9'5}myvSBȋ@}"q ˣOI0U>*eX)kfe,فN:ڟ̽Rl.eCM R'.Wjr83$1zv d> Q-,xW."L7)o/t"4]7{@|^LE t@@^G݊[ue6bcd<okVJW^i9_]Pv>E+~_NNzfSU>Hc-O@/' nPN]6@xėoQ/b'ȩqaUհtrЪs9T嫽U}6RȳR2Wׄx&#dY88'QWM#>OƁƧ?S{cO،ɳC_eg& ^Y)g">Mk}$dO Kۀ3 JQRCuXr).AQKZ?4y̐R(IԌlo3!ڐwPw5胗 LT1p +A^Rݱ.a'D gpitX/Ӊ7W:"cR둡pr-,OW޼@Z‘aA`/SlG~{Nɥ$G'!rZ1[(-4u &4Ntϛz@l~Y೅/z@-T싩E%5J:Rg%Ayd̐1y4mҦdC} 3;*`!x:eNtk.t= OͻϞH['f !⋆7Bɥ^!Hb2Vy]oiSZei5RN6M1B5Pn[|ծ=\ixܲ\_I|Ģ|reE>-PqYw$T ̀@΀EK㊙Ur(U=.>GN5!8}S`:b(dtBFL - yry0_tpqqr̬H?'@F#4[fds~.rūwHT廢9#6h΁AɓExf }!3Uq(Y$O:rfE1&>4ݓ"AKzB&Űt)InJ0煞LPȕYNbKV# d(Rh?yF" 2JÐU XيP>t.&9HGY̬֟'`"޽(wj3ڵcvweiŽʵL}0jbO5dcEZV:N^`)]C| Wz^5AU)S'=E=âv E98d4oh^jK^i UiyLG5ٙ,Yj^~ċM~߫;ik>J~$3A7'ӿj'W&t1YRA|{pJ_>NɃƳ0|&RZ?UU,5}(e=:С LeA>eO1Y5/A* TEr[B'6 $q25\0!)Qx`+!k/&p\F[N/'^A@ާ^iL 0y:bN3 @҄V*Տzp_Y^bApU*!>&Oύr4 -[2~իW'oN5+K(V m!4L.Q:A" ^Зv}Akapfe S6ẙ? ) !Ηfٜ3A*8HMcѳK~ KL!+LBY$vZ|gt(k4)Ν[$L΂$M挚lU7E]Fo 2۰+5-SA~7?iI]xSy ^'ߦ o-dׄz]n!Ԙ3)W{B@9Tb0HD\5\04jـk{&Y%Wv!B'`WB,#'|[[[7_IĹJTiiFZp˫|߅3iB{77zKGx9ޜ&a ǁZ8G&T`B2 _yJ~S,^3U4 MdB_4A{+V@`{U\} ΐ.\FR[W gdHSV{MuDi .r`eTF[;qZ0z֧G^F^(<*Tm mODKMCcڈ(K8Ubđ,Kl@[@QZhx׀褐L l ]c#(4bpqwаR?^t&aCwҡ/=2T }bvXgxgNU?Tӡ:^wL",흶i4914" ]tWl-,YώlmE.@AoSe\j m,1gveIs3y%,=4&|PVbh FHRsk;uE%PT=gl MtLmؓuqOq%zH?H~Q6o Oۯ}+7BkC rYm_ :}0hR$ UJ 0g$"\@p+87}Lc` qJ9dpo~O7O?{z;/|?/dH8f" ٴ^[t}{d_+֓ ;t|wz{w͸~MR;{gV0iI_~N? rgq ΢Q|2k@pKs_!#S8S9EڃC')T0ZSd,S3U".q/ "Jyf^&OX aŤ'Gv~z7k &hDLlO.a#fWL }  @ #usJd2!ϰVE\˱ jҕxMƮ։뀨 H Fv4lj굿+}=W/r;DZܝy3_~zۓ7îono>de͢lꋞ^t '!c@)ȍ*繡v#ߍ&7)h$v4@Q vv8R`(3G:䃿ܠVF!>ģFBC7>| cƛ= Ai[+F3aQbMs>ʡOhAC,a}K~km+{[]{Q!/k?fY8QHhE[qee:짢ʠh۔lO+]X[R.J/!x~UkjTIqECVs)7,쟇Z='o_,|GPbB;<|Up[Upe=^}UlZAӣ-xjl W6E Wg|[ڟ c'O^*X3BRJ0 Pf`ĢOpj? _1-SVML_0@Y鵞ꋉMtx!¹׮+SqfXg.\[wClw95pO"4:ҊuB| h,iywe{쨆_ c+pS9}KrXj$؈L@t~{vzOB>Kx ^ѻ׻<'_hB$&-PEQy+~&@Lb1kX8{W5T_G?бX 4tý{j.|a/Է|ݏ곾ovN7'_G}+9sO^ů/ǯ^鞿և/?h:_Z~ $R}wvOug_Q>)& >>7z([9cv;2ChD38o|uPׯo5x:5pRZK[ z܎> vp [bƒT xO?~xN1?}h5_C{ Ȟp9  +}.oAbo!& wzO+~N%7־=_(g /6iPN_ށFdLS b*w`sI[/콭;2kX8keI3t?7ĠBGKעؙr|׫~wa{0`Y~Oa~Ͽ .Eqnh'Sv?ջhG% wS^l?>62:C 9]S`QL#ćxb vF!x7q#q{2|D$;5pc kj);x6yǏňy|XZxŌ <Vpq_ >lЂpb{k㷼߯?Oh\ zU=N[X+|i@ ;c8ZdGM-8T@x~Z_-7zX}}qvWUËMd^s^?A/}Pc5otk UDIL iDZ5pc kjV"(Ex83N׈4\ƈblqn (ql/~G|\iW[cvI]}1LL%> j^woT;M\~pO?K?P*&xz?^?َdɑ#"תb7!Z r ]"y&OHCa"lꥪ*3#?Y[nU'܏O7_;G(e|i'6¦F "9\#mx-kxE o`nɟ/D~u@\XtٞC?`*MwZ :O"x<Zס{6e0zǞ4oWF[:SqW _0 }\ne++l:kaH뾆7~we ߙ@rѱMr @}Ѿ>ý2 Ft||9S& iߟK>^njjjj#`nRL,gmBJx |}r?;8 o5g=?l1hS7Wqoː؋^ 5,lj?NÃ8Q?ߨ QMh:KbW@ϧv;sd`gZgԏp"%8\n']23w VGoD zE@xq>}Qýο裀8g8)hw`\rjjji=!vk5'Ϟ8dTq߆> ,fD{_h`79>_9=jYe`˝:W8v]6oTR| ~Ygp<gfѱ>sYg W%Th#p8j=+#O&2Lp`'F[cNx(,/|'m9*8 ⯡jjjj`T˘?ǐq%Oσ⼕|UXd,.־7pi&yw?&I3?j=ߤ|WO'}'4xi`OrL~ή:D`}ioW u_JnLkx_.kr4aHkd{xM GA7F-y ?y8j Nb\xgo~ѯ~})2{r8 VA +߶11᭭{/MOl QËsI>6okgg|? 3:{H>L g1kV`sOxA Stj.^H4ACOīް姮XV1m9(BrQu"Ԣ=MX-AA k[?m`GF(D/&h:=nG?mޅh#"#"o1bI?+XI$ߐW<+ukQ~V(W yL`ײ-&1(CB#i3&L榴%r.S&YClv@xp1gTƔo bgg`] $ĺNށzr;tw4d36gG>3C_<΁GV`X6ؔwaW uh@O?1[ÞS :8@5ۗ'aә|3{!Jޙ0as@_LJ 5y+0a`qj7q`c /t ;vS9zń3mD.Fe%ՍFh p d@܍tsÚ&^\ncq\ 4DvYs,z$T&M S B $15Q,s v W"DE220cCl \xz%va<23q_!5g1khiI#+u,rΉYTMG V#*3Jy$Qt0A@V'8#izvOxKqjv 8>LP1[,5}[㲱gKh%.W>76JV dCp`:i?2@,}HID1F}f?S@Ƙ/=r Y9v6EOn `)^/+pxϟË PC@pnлt d0M3Ӏ di888@4 Yh4&]l$t#Q,Y&M> U rϓB4KZ?9 '#6#Z^Co@B'g0W5L!Dt&*PcK  `| Qg^#!ի(e9hGSUu>h[2 +n~]RBn_SW(6%X-@]m&=4SC#/ADq`ؒhI=bl8F mpFc`[kJ@ =@d4˙2:Ct0ny) a+Էrqb|(!D!ey&CW+fklK vJS6Tmfb,I3)/S[LǶ4ɃǗ6zE@_:2~U:3{ð#G@yegC5 f9.?~{ߛp>1TJlǺ+p^yå|ӔF"-_`+U?F`~?K&PјѰHԠ'ga J wH¼&"9梬 aMn2$II#0/\(He-s yIT(k5. \U%Z%^9RHX@?Z ΐ.Ys80d҅j0fԟ;SA[&)yK 0j0Ζ'5{V18P+#=1O:_Mg)#jϦ?pWg]4YΉ~W",eqF*_Lbe<=NNY]:(j Ge)^S{ g`ܨ,x` īb@#;̀hr tfl48v;'ڤ-8P3?A8C\9\G x\zG uŕr]UoVk;’l%b}ϱ/TCH9[e8BDWl,I z&8hvIѦ_"moO4HB|ȅO!OyK>3= Ө_L*k6RS&5+k?"7!dHf@9v7GEtݿhWXmd#͎(LYBW W> E@uɸO|.L+XLAg6`4_PRrz?n0镞|$]S)hwC!Qu\~Gr>Qx})'#p.n@SQF9y4QHSHn<2&g_zgAk>'#Mg l=|jj`r/duihSXmN4+a>)Bo7[7+z5SK}hkJ, 0߀ o~+ q=D`M$ó9w_y OШ_xL63xYTo<ǝesu:xAO>x- _M-2/ϐLoi{ ?-NKli6y}!x.m|/{fL h7 r^h@B(cy"NIW 2_t 6H@p`vկ>mnfi➁Ɵ0āMhV7B-kw$3ey}aiƅtǏYo Hi+^=X{ XT3,€|͏x1_υ@JP8ALAy-h@Q8'z7g|25]÷(<փHkd$=/8JjӚϨYV~ !?ڒ #{/ Fc?Ԛi  /RLeɇ#A֮j^D%)6Q! bi@=gFZ}VLgFz ll4{(9c]> }6FT{90FS X%|Y'"~*UơLn{;HhS裏>Qy,R/UUfh&F-mJ p*c 414 Qp,M:=KDljh?vفjP5P5P5pk5@?<}M@ \M~@, :a#ahk|)3Rd4%Ls0v땾:( l:i^S*B10~KxMӍt/v{'O~΂8n`1t3{P6a )%j!7 N)a{Ljw*yyuc#*ѧMu^667//3iS jjjh.N9G"X%8[D=٤L_d;W}ꋣ'+@^S`}M~tPe3OֵA]:tUNa= g/:/_N7fz:.^Jd#&?*͌gAya9H|$$uTbxa$8X!-vbv5 oP5P5P5p[5\0`sxf 9g #xC8dkUBk>7DhB|x_hLC(#hETqPgМvw6R%nyW7n:YgIin]D|Þ=?2G. -̟яti).c/ 8 +N92TPK2:y%XmV T  Ч%S{qnM_R>H?tR SV ].5'y=߲'@F# jv> Gvc+jF |\x8h rAO Ucio J܃Yg.A5T T T  ea.%[rb*@}lĕLOiˁ02褁w1\A 3O濽T6^w[cW˿6O .nr9* 5îwpOL'F3`XJ,|\ zSM.(ذ$[5P5P5p4d5JqGM{c!{/_g"%_f9/L2/ٛ> 䯪:ф?=OsHjjvIvdMS]cPmƇӊdpq4fd}#oFe;$f@3jqLH. >*jjZ4橈ǐy_3t8Y]q&=v3~X4ƾ1zvrY>l3)~7}U}ur&hB- k TFDF{8.S0$v$BnN6IDAT`ǁ $$|;<\+B땓ѰgoQS5P5P54Ұyt>"4d;X&L+l+r Eg?AJ+f? RmG9GO<:MiXޞ5oʎ'iC=9?pw4~$m?ǺpܡG0GCrgsKp:zd9:9n js`&Fn` O2M?zÞBț3Р,/)S~GYDġD=oO[ޢ5dh&ŋ6NNOO74~]5c9gۺ_ȑ"M% 8q䯘LuŔ>P.oڗ^x5lgZzzFPfԿfvο\G4_3#BT/f?ikěI@PFqNvǺ? d|0dek{, l(VfTƚP (Ŀe-<ߪ[wm!ZJBo;X?]՛:oF+xSD8}ٙa3(Sns;w8'"?93#؟(G7~nd^2utB hr@ UUUwYN-ɏ%Ul 2V .J#|KY?uM}-ܞw'Nu񓣳ߥڹJEM_:ד7q=ZnC}g?٣`%ÿYdVq6. 3_Sz-%m'/B-jjZ4o+%Úڛ} ȳ/+v8 7'?goݮhώgdxvOS"ߓQÿ'GMWч ,+G,x1PZ m4RثW`'PgP5+Mw58їǣx;8uGOw/VX6~36d{{{{yvvv~G 1;kFy(mU٫ޱ`ծپX1p_hTF:N}'sMkJx"8QZ T Vz.9XӧO91f>Hh?硲wWu~O[BE' d UUUUoZiͺ:zřvkD?O騯/4](P9;ߢطNK,{4C|ķ0@84X>ASG2394~cplpn魂=9 C4(ǀ}jOS l ϛd6@H@8MBt*LBMZsUA3Ƒ'Hb$&K"⒬jjk@ 䕃:z"'i= OBZ)8_kPHsC͚j?h7ߋiwϪ͆:Ex`iM_sHk:nM' 5D<1odo^\8Țq{7e!869`<|qg/ȯZPb`ll'A5[eq o~lq M?DHͿԥ/:w[ I Sft BNˣGitzg8uѨw)\yKP4אajF4}2r~$"`^-љv h6d鋳17ռ3lj_ߔt (_{&A\Dp饐 KTB@Հ[\EJgaD[#[\Si]ZOu TYigx:jm_Sݑ;4?SyFQCN+N'[ƭa`Eip\8A@;cB(~So 07I"NؙtFӞ?5A)m*?7x'b!cȣeVUUKܵ _6:h: fG#= <[\tfΏiD:CMA;0/:4vk4״3mj D3ի|ÓɉFl1WFKlw9 q ᢴd 0ߏ!7v!x6|>)<h20_8;xOp) dC]]-ʲĒdfiqԃ`;^r|W T T Ӏ2d0=h@7-f4;dcn5вr:2;՚ 8 5yy)/!7ΗJs+]prEx~@FphyyZ\iBt0՟X֏G9|I'm:i04;J /2@tp1=-yrXx(zكy٤i0@.ҹ@`n8'N0OJ%znP8]OPnNWX5P5P5pY o8Isgz Kn~dp<`ΟӉi%[(;2)5]s}txBN'E"?h4h( yJi8ş8o^E]w;iE 841ͼ7N><~}YVe)q@e`?UUUoPyJܗXEfcl ti_t N]q9/4wXP 2ӠCdRt|.t~Op)5A~grjjj i}Ky7gqyi@yx~nb >˳ rs)7C/O#&/:s:q7a: u 4yBh<FVsߖ i偳Rč7W`C O~8҄REy )u~?dLF4#ix1n:?u~p\MWE<9=Óh^UUUՀYi}w}q\f࡙\.WN#"u[4Z_uO|!e{״Jyx[6_UUUW@nɻ.m[- *;e˱|W)e:k|Wr/ǍkVb=`S <oCx0;nxQF8wk}0.i ys\,Fb~pr|V T T T ӆ44'7ZfKڪ4Uy9 Xǔ7xhl4ܸfhbjjjVh 7T4o[PIPW{,yxr:uӎBxg=5]5P5P5p5#uEˮU T T T T T \?R2yiIENDB`zeal-0.7.1/src/app/resources/zeal.ico000066400000000000000000001361541462517734600175130ustar00rootroot00000000000000 V[V@@ (B[  ԝ | hPNG  IHDR>a IDATxwU{NBwUuWPRK X pFC/D Œ!&HdP{tV箜n8|M~m[%{5L:{]3g^ϼy=z3ߟN>> !+軁vGșe|+}8D<ʒ55mZ АzeiUCkP֐B2m]Ҡ؏"1$t0MEFm-Е##Hfڒ} F!@E;CZ F)CFfئڿNX(bBkrB1ZjCY2yIy޾i7} Px 3M>Aqe9*^qce9:)7e[>2&r)ose;^uAљG-A6U7y,B G:2'f䚏;^Um3t7KM~lg6GVqur|CN'>}{?_ːU]1]qӚq>ϟGNm;m&ԇʢ':| Z̹֘c+R 3_R]whaf\{͸vq[Pfq㸫oϛQc܂򨼯a&?vlc7ߌ=tc9];Ds6v =Y?D{Yc&sCGn%gГ9zsǞ́kq[8PT;L0kWqocĹ,l?tSoOwO L,1eRf\S^:~*oCUY9rf6%@tIHvzףLMU7l]ȍS*˿ܑW|W[oVY6,Zi2:B$2EG1bۿ`iM u9FLe;mH1J΍ra5m,rѕ 15&.VQՖ(0Eo3qgI}@i'D;O<>'2TSWP&O!`& 9C" ,f6E)HG mE/|U/; i~$t*ML2k1дm4eO3j=}?_~u@Th'DN}ĕ<(m%@S&n ~{rR/ƢjCln&-fZ%l@~Tڢo.AkB"8bL[IKV5zڐT P"G֐+6ą'np qd!mơ]"]$!x@;v9Ga"vV 9q"?w_GV-w;3l\<[Ę1Fc&ƄʊYFCU9!HJ&Sc\8_0`H ^ص iZS",9FCʘʂҤ1œ Џ3qq ADL HUZ|B[-La &bRX ,;l[G/Om%!n}ZRʤ48H |a1R"u#P<#a1L!c"刮-)gLS`5LCOU8\0ZJ@ ZX!A#!F)r|=BeӉ1zDe^,aً`5%|}i+lDCj ,Y!rmӮ_sϝ(BN0 SL 1JѴ5#1f{jV$\C] wkB7/-еvdSpXd`n)k+H3kmMuME e5*f|7X!K ;'-Ftk6o4H=ʀq VMs[T-9Fbܝ6^CԖmgWՁ (giaZ^Aw _NO?vM>G@)8zbL4UFJIE8k UcY-{E~_][Ly'.FEqE6Z7;O~ܭR$YT `4vFkLRƐw`bҖ=) QZ"W7IH҂wk iLdf%z'2X(.4,z 1#E1;)[r@PŭR2J6ql4)57a6뮻<B!_Or(p}`-{U_@+x=ݏ#hj(.T MGQJwuk@k,X$z)լYe('Ka!DңKhqn v#)+EN8H}"e -!Fr hgQ3l/I>HC&-UJP_,3."]@+ R6} ^9e$ƴ.{_E!,;9h!gx(E\~#%(͸ RDn}& r G!^jL*B'OTq(Wb][@^TCb?^{3nm]tJ嗵 *wz );TNđzmҖ< $v/)S)bu]=v;wc@dPdș0!rf)f*gCy7\+I[pp BAƢ)R( ͫP3A9K~3 ^5"ERP`JUvWaʠ ճ+*6Ͽ|˟嵺<vNk1TN㴆 Non%ňےKNJmGUF$臧BYk9SZ#a*fV@Nhڊ!ha +͝'/B;w}﹡BkEXyĉV94YՀn eK9bD휰lZJ/m H`áWn_.05 ; ^x K$"q*%71'TR Sϸ;e̻ /zl -JDCBkAn8q@+cQF fZF#W,"ݣA Pʵ |ZykHNg,LpaH:Yy FK^H>JR3ƂR;M>0eXTLD@ĕ7;}s6.\-Ρ~cFO׹A{O{9Ko`ux?w{yբgt-_**d Q!HYfTN Ov}͏>ijoAwy)V9%pS]pP68~ 4(sₚZ8尧῭ 9(J:VB <Ä10~Ե_hPgGp{1^~3r݀k EV 5q#n̞Z> Iލ!:!6!`&;ڐb.8 |K3F ;o %xa2 rVozm~Ox~piBN,)m-Ƶ6QXrrL>_ #%ZTA$RU7kQ(R'x~[tVPSKKhu,@"ML}ٴcf#5"ՕyVA6&Hk+d] -SEN#vAO kψ' 󃇔m2J9rEB7۵Qʧg*rLmr;Eo} W N~ϯFk7ѓ}$lcf`LJ&n׹3Z% /lK1(%`FpBcA)+dy^S6ksYq 8 JbVBNu#w{ܷ ^<݀ȭCL|Ci H,eileHF/)cLkb@Okt֊lT)ti@\j>9P_u7S_yhн\AXV({֦V+@e {\%Ҏc(E k<{w? dNb+PTYҐ-bNJmhTQ 8C=a [{wcî+ &=yI}ΧxnRDT8JdC9JlaGTm:KY)ٸ(;=a5/"IDC+*`vۋ .|TX-ؿo2ֹDc-1Z&67^M8Sh]Ą_9/򳳸YC_`cF㗣P!%*̼rP)Qr~hdA촦XTx*2JQ>ƈ[[ ^OXZp}| TAeXac$ ;q创TmŸiˡ4 6P/!bF)a;g91nI%7ɦm\EL':[յr&`aFRXXCR0QMCaB(%UA.,<ˑj)O+A{("p7(pho0%xHIU9ggq'>m?2IfP&ě-D+inȃ^gdbM nE\y)>BN}/F!7VhhN^F[HՓ3jI[+Tj8zˁJᬡo +ßC+QUT0tk0MM\ dW!,zI=Z*CXeHW<tD%M`&5Z c5 [~ IDAT1Rp_o{2<;ĻYb/r EXr'H̰¶RyhM{T2rXrC 0JV'QrΣ e!~V,{̞˻?wcH!i,: AV[YBE <0/jbgל<" ))k4) @YJ+Pǣؼe k]Cj8x U " Xkke/y,zπ4"I|u*8 () %W(0~kF|AL#)0$O;ŴD9BԂ/y Vb~Mv.Jn.$|&*N8|cۀ>"O2bLEvͥTK>*->' c'5F$yKwttmC1WԆq!\E*iԆ-yW?O𯽍:1.C& K+J70 <+]Z,ZjD1eak"'y& m#1^RVȾTi1cLn -̞lEL ?x1*q5TKw2:?RH滯-=bĬR+EӊN*?@VmlIJ03%}5*ZJz4`!l2Z[q Ɩ5QK!~Q0 TCфLZi C?dVٴƛ ⁓b Qru -I{\(L_q*V9l ^`T[ dY?3L{}/;w1n̝"XMc>ak+E+fCTRØxiQۊ;Ҵ*Iv"LeČ"n~kFcv+ ~q(զDxRlH;!`53m8zZXb"+Nn9 O"W#NREUJ(!@Ch# "ERҝl"F;nD cµ5G~<{ ߇ ^GdQ$ YX'.9HA8 4]V2o!r U[-[5o 6:M Id4NH#hxZ`x^0Zv.]+xHԥq;πŖ":[YRi1(PꛘY=Ǖ/"-nx>ůuUaCt\1${iڻw ݲ'DUY3$bqp/e BdЭ%;1^--\F@O0H 61e3"\vn{RyV3?yOF4~I k*I22ۯDT)hQ/n8))DLjuZ;62}mE1a&KS".d.1S7HG^!uVAkirci+L1laD!meZ+؜7>qҙ|u2 }zcqĻjqF|0.ivtںU/3r?|-W7=龖?]t'õ9l#.$L"?b'JEO312+ɰ SB:vEYb m rGhg5X|KXJR L,/cnܴE80\ \XO "ȅC)>N*B1K߀9te⃧8̇xx։N+' 맴%"$f/\€EFf7ʍ}ӣǟtOg>&تo/֪ZehSHhybH́x7Is2˘5k QP\ؙΡ%dd #nO-vrʌ; {~d V`Y-#:52&.DHS ם!6G7"^8EG@_nj"F$^5zVbD/5xXĘ%w_˵O67rOOtnK[[o(ck A7V0 ݀rG %ec7ʨ\hVXL3*erHؙ+l|]_9oz4ak@oɣ=@b{䫸߁cqhA2 D skNԹ12Ԩ@X6W2(;U+FqR7bw6,jʰhJt N|9$d2[ōO"u=?C,>sL m%۵tS9gPU Ay 3zF$$Ed\]Fm {;wRQPL Ǖ`",1R'DOH D[sA-sxi4!L;ŷ8g軁~  TZd3S JWxV+=T(h-9T;% q>#WܷnO,[#/SHv %Q'>@H.BCoh-ԺtZޣR"vA6GM{;exeʘɌ=J9dz`1CLcxiҕ J ah1jsh9hvPH-s1,q޷=0/*l4[Cd,)i bؒb! Z+ܟOgFy-\kH-of a8pjg'K艣90#DX2V@^}d(Đ[způw̤q71֑L")CSזA1H׌2!,|⬭[_=w|t$9@i>`gN6J3;MF(p;mk^>p78l8T>ΠVp)ܛRõ?G3h CE61  #D}Myv>QMdPr+mWQv5 e2i11+v.)"U̞֬r6=xƐ˧-Xu[ci=c`#,>A-¡N'[b CS B*YTBJ>Q_jw]v~=|u_O\m덳4k )9u7"ҚH¤QI%% s☄Sƈ212%*c!Q@ʀ}+8+?-}"~ѭh$:q3RH1Jd)m!f"4JYF1)TNj&cNkU~x |Cs@+)fG5et'~Q6 o{.ٴf>_iyeZ!"+Ĭ"(ib:T%4jUJ8F)C'Z O0o6;yMBzVH5oeҙDڐȸPQM\oCt4=Qdƭz΢5 Zab\xI} y^B ] ))#{\ EJ)!o>ɔiƲx>*F?%g3W))R4+_l$$#Ǹ_O}0G~UGH^VjwCpj1vՓ|q.#֥@(S)R?RMD20.GIl3ȡ4jaXR|jb,ς-5lѰ>N|"^v.{^?9b4m3a|/z|cR7&9 IGFƄm "N\j{~0@EVkLn|6q4>pT˾k_] N.VſI'%|{ˡl¯Yn^'1M"6ruLZ;/DGF"6@2yc{N+wZZ˛7ί'hR LGuq>?~Nѕ ȋ`I!gD d>`LMXBJ3ѴO䦄T''Tme<(O1xQo..OxSwku.}꓌_zNLI ZIi-49dX-2pD#6|lb*E13&E6i[2'X*9w'.;!sx23U6JaNШsyՓKW +m3#˂Ӵe+3ŒUW)BfTdIJN)#xzlQIV? CݷD)<闄J6j`sC0y1/c3oz4?ڠ|ֿU֞E(EA`'vh<ͨM2f%!jD泹OOdȼ;1Y#.|Lkf=ڤI:QBrH'| h[buÏ=|DA*aerE&ju-,^ C'`f3fmg%;'ET]ٔJSݨPVaqB @6j&4aN.ginT3m-卒b`uHE-D!q)6fsB3WҋL&qIzYRb _PjMˤBXρV(hGcy|F]?'t>PT^EMn jjfS(jeYcg ,,]'-Qmjx:g[k%]ӕ3t{a5 bnd״n2:F0vZ2"ЦZ'V5F#LU l&7S]PSNS&?HYl&^zJFdo׳QJF%cz|&laFy -\NWQ13M|ҝ-j>JES^3BT-{}uMZ?ߟF\:o^Kf)$%TL;j$6Sm&N؉e\7JlIqxa'Ԭ-Z|+=1B;np2F Q:PءڗNIk)>UߨgE]4* $iʺo]N'4x7ZE[!qf80c扟@/+醻L B(^]PCt0O$1Fx?`ts"t5KM"GЖuR* P5vK(znS2Hz{m*C, $}sQVR{TTJB6On, Lq)eM9CZaeN4}-OI 7^*$(QFh ]NX]tu#в+betMQXqlЮK1!?$rZuLR)')oTiKμIDAT@D&nй#z]J\/c(>SG~|3,Z,wHdFi:2-Y\VJ5¡ZKu*[jЩRhzijhYMvu'`FٳJ\!Kunք?`=>D\a]"h*Z%Pb-B!)+Ac%ЃG:H6fWD'zT **74[+s5s0Y3ƪ"ޕx/ʠ>Mf"˜h#]t]'<PPT6+U-AbIP| LpQ"[*D)IM&I>ءEFXEQpjZtnZ3ިNp U7 +NhRR%i"tۛM%v`SPDV67zS ,6[ި;)&[Ә ) ݸÇ;\ǏO(շk4z]?DHQ8"u#ӫ꺡*kF㊮ EV6 j*q&KH˒IIIP'kmтR"`vhq=  fI=ⲣhImdw}+h" 6SRM^az.Dk0N4[ BXIgS*_,zSd]덖>*Q-:?'VI /Ҹas8*~#sGh/o~Ѹf4V8evD* +UP.;)53t )R.[`|k.5Eڱ0oHk: `P0L[XVTI=+4]+jݑ({N$gmiJIݪT:VYU| i-;-,UFk/YAbD%8RRӈoD U˟Ԗ2*>?H}eٯs#oxf\+. ]E-pJȟEѶ @ES-YBU;u9V ڧ`^p9oUٚyͰRI)>+8)5c|F yg· ÊW6 :)|eJc&_9NwIUuӥZ\fc3 _i,/) _i,Il+YÕV||xNa Lu^?yy络WQ^yf+79};u>^BŬgystZae|2:tit-Ơ^!Cds8^YOƾiܞa, a= e8A|G SK3az=mxe.=Uyt.r^3ϑY@%~Y}\=? \;B,Xbc11K~Yt1@i82'}g˱CX~{0EE\ן-.{}|9lɃo}̠J^½MCtŇ'<㏓-$_>3X9l^<~ܵ/iU ܾ 8z@2CxZD"R!Fƛ32l>AzK_;O!3;r /N~0*8R[\~3\Dgvf.݇T9xf8?]_|la?HlQhP3x˨Ͽ],Hb{oay,šØh Eh~]`sBϿagɝkd{cF9tQ?@"jUy|W^ZriJAJ6*|ѽ![񁹂zoޕ8CgM| JTXYط\VXB}~->X +WM'P2cEE17gt{ESq=(lQښc Z-5Q!UZO3t864z6D+뭞F$Y$=Hu'/#t1%Ӵbc\3lZxgx!ɋ|]lN O%) VRj1pp"k[vƷq~w ~OZviw9|7I2gɬ8ˆs,7⭦cP=:҆_]`;}U|/'|=Pӄg3g4+JfY>\ x~bԾ sY6~KL:BiΚ@]2K[S Ge Μ9MYcX>={J)I9ygJٵ1nJ \ɉܨns[_{[g`>'8} "@Ӷ4MC9.%Fbh <\B&pDTMfC޶q1#s\EuZQLJ*~zj JO;q%Fl @M-"` b&L~Ad\,: :Tbc 9o5];$]ΰaΞ9<#/r,W}44:g"ؐ:yG':[S[g-l$vh wB&rm}4 63Vv񘦩)uPeYQ75R!F܅l@^<(u޽xG(PC<'/M:-LqܲD g#@R;yL&OC8ZcA:qF|]}cKGYYYin(>C;v sss C>.paŘΟJO;l6Ppϰ[EJVL`[?OFf2x r+@O0"'$ʚSVhslfy,(@m#G9z8Y!EQy$ IӋvz:&#_pc?`$a 5NڤӜgLIQey~>rΥN߉@崓r.|WPґ\eg>g8Sz8ki#.F+~iiP\˂ׯn1mSvpM),`ԏDw~ qY%seYR9f:yQWlYGg:I7jz=wb2}7cS=\t&rOD'l d&3­;nܛvW) Ka/F`{si0wZ 5Хu{h {[Wo:,#fOI=ɨ7og>ی!C bɋodlJ^d|[tw ܅OhyԃL'ViO<1;]wiy.:JSѦ$TݬڬoGD% upa~9>ι{>o7qYn3p9CZКH[t"XIHZdB`<;۾={2 }x着 A1qBU /kγ'Z `t o7{1y3F*zijYvsΊ@btEVV]{usJ>|6F p`$6VfۮeOJO+4Ktw(^}UⅦZ K.bmKm^k J%11zyMUB. +tmGFctD&lu;F<xۻ%| 2ڶk[ jJk|296Rkܫ/2?W7- `}},X#K/r$nD,X5M4hpR0+a:eS8x1(fz8NPVN@ v[noBE$P,%c&@iWNocaO`/9hˇ9~ehs'?s0i4i`hLyR0Z xsb1k }3ύ ֿW>Ο_iZկA2s?{ FD1iQ}|+yx3 C9%~y'w `}>׮_Ci͇?wq;mu[Bf{LXmD}ʋ,:ÜbϱewW *nֈ#=s{ke0ߟ еDkE+accCZq%NSNs ,8ì f߱es|BݮW'СC+o.]>A^xyy>C?ٻc~fLIO|"h}W#֞< |u4[ዟ1o\cǎ[gYƩg>O|Go~?~ 9MWW!Py"g.MY;7eoj1KUZՊL)2ȝavq{Z 0}oskyC{보tpȵkoε8cE8 ALE=z3~>p[>¸l;/=O(> 5Y1af8C(t\N:VX[\6{ Ĺse{衇;y\~%^? zTZaůlƻ9]SN}\ke6x˶Sl?7_k'9~Wgm7 ,,,1qk:hvՍ:*n9-޼D'aԚQa׮g6ƟpGɟ,}`g(#Cܩ_6NX PG(|{5=wE4\| 7&uN8Yz80 oFpK -> l7B˻'%F-Y`![Wo1wˎV⶟=~oIENDB`(@ B#')++,,,,,,,,,,,,,,,,,,,,,,,,,,+*(%"$,27:<>>???????????@@@@@@@@@???>>=;951,& %/8@FJMOOPPPPPQQQQQQQQQQQQQQQQQQQPOLIE@:2*"!,7AJQVY[\\]]]]]]]]]]]]]]]]]]]]]]]\[YVRLD<3) 1!!!&&&''' $H ~ { pv4'###&&&&&&&&&&&&-/A t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t t u v w z ~ x]$$$&&&&&&)))=<;@??05J,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,B,C,C-C-D.F0I3L*C#g'''&&&&&&322876#(@:E{;E|;F};F};F~>&&&&&&LKKrqqrqs-"%(,887&&&&&&988dbbsrqxvv{yy|zz}}~}}||{zy~}}~~}|{rpq- "/25>==&&&&&&&&&22279LCKDLEMGNHOIPJQJQJRKSLSLTMTMUMUNUNTNTNTNTNTNTNTNTNTNTNUNUNUNUMUMUMTLTKSKSJQHPDK?F}OStfglnmmqooljl-""!%,14EEE655&&&&&&%%%2:w\]dbab,""!%").'/46;@CEFDBB?>='A$7qJifIifQna{e~Ggd~Gfd~_zQmd}Cbd}QmYue~Hgc}Lj^xe~Gfc}Hgd~_yRod~Ddd~Ro[weLjeD]@S-$JKa}}*""!&!(,&-2)27*28%,2!/%K%H,~%G*n&Q%G-$F.%G&P)m$F-$F)l(Y$F,}$F+v&P%F,$E*{$F&P(l$G.%G)n'Z%G,~%G.z1r;XRq6H%mls(""!%!)-'/3*27*28$+1#68787476664437445747677466555437557857780*o0HiWj+5&"""&!(,&,0*27*38$+1#5([767867878:75686444555546877:755866576777.(R)L5M!!$!(+%-0*27*27$+0!:89<;9;9:;:7;87:79767:89;8:;:7:9888:98:=;9=:8)@\r""& ',&.2)16+59$+0#: ;9<<:>89;<8;8::88:879:8;79:<9;9:;:9<:9<<:=3(SCa"!$ '+%-1*27*27%,2"<)[>;=<;:<;:;:;;;:;:8=8;<99;9:::<<;:<;:>;==;<>1]u!!& '+&/3)17*28$+1#>>=>>><=<<;;<<::;:=<;===:;<;:;<=;;<<>>=>?>=>>'ILe"! %#(&.2)17*17$+1#@+]?>><?=?=><><==<;=<=;3RNhj=><?>??>=@?@+^B]"! %&%*%) '* %? AA>@>?@?>@>>??=OkxBa@>?@??A??A1sYo""#$)42:VUXMMO'',"?CA?@?@@?>B`Olw\uPl@<?<>@?>??=A'J'K?BA?A?BBA'I8V""!%$*/8<@?BF25='*/ $D+]ABB?Pl@A=?A?>@>>?@BAA@A?AAkB@@BAAACD6Yq"" %!(-%,0*28+38%,2!D B?CD@(K^xBb?<?C@@@=@B>?AA?BAB@=@CbBB@CDBBB(IE`"! $!(,&.3*28*27$,1$DGEECABAA(N^yCCB@CBB@@@@ACC@DDCED7XFEDBDDCD3tUn"! % ',&.3)17*37%,2!EGADEDECCDFQoDCBB>ABCDAACEDCEDBFE*OFADEGHEE+UZu"!!%!',&.2*27+48%,2#H HIFEGGGFFC+P)MDCEECCECFEFDDFGEGCEDGHFGIII*J=["! $!(-$*/*38*28$+1! I.^ J GFHE IGEFGFF`zSqDGDDFCGDDFFFGFGFH GFI G G IG!K H :]v#! # '+&/3)17*28%,1"!K"L"L!K!L H!IG H I H HG H I,Q IG IFGEGHG HG!I!J G G J!I!K!K!J"L!I!L J!L*J?`" $!(,%-2)27+59%-2!#M 0_#K$M"K#L#L#L"J"J"I"J H!I!G!H!I|/T!H"J"I"J!H!G!I!I H"J"I"J"I"K"I"K"J$M#L$N#N$O#M!;`z" %"*-&/3)16+39$+1##L $L$L$M#K"J#L#M#L$M#N"K"I"J#L"I!HHiVs"H!H"I"K"K#L#M"K#J#K$M"I"J"K#K#K$L$M$L$M$M$O +K\x"!% (,%-2*28+38$+1"%N%P$M%M%N$N%O"J$N#K#K"H$M"J$M"I#L#K"I#L#K"I#M"J"J"H$M#J%N#J$N$N#L$N$L%M$O%O%O#M%P!.UId! $ $!',%-1*38*17%-2!&O%O%Q&Q%M%N%N&Q%N%M%M%P$N$M#L#K#L#K$L#K$MWv>c$L$M#L%O$M%N$L$L$M#M$N%N&P&Q$O&O%O&S%O#>>>==;961-&  '.5;?ACCDDDDCCA?<71*#  T++*'''$R a a a a b b b b b b b b b b b c c f j r o?$ 0//v'''221322:@]CODPDPEQEQFRFRFRFRFRFRFRFRFRFRFSERFTHU0D$9 ~ .--100PON~||}(?,~ &&&KIImt,A &&&lji)@ )))DCCxxvyyy+> !444)((ZZXussutt]h #"%555&&&=<<<@eELHOJQLRMTNUOVPVPWPWPWPWPWPWPVOVNULRGN`c~rrrlkkz  #" %):<=8884:N"?"?"?"?"?"?"?"?"?"?"?"?"?"?"@"?"?"?"?=+OQe #"!%'.3,4:)]7576877688676878687=&L!;;<:;::;=;:<:;:;<=;;3O  "%(/4+38-f>=>>=><<=><>>=><<>>=>>,L!'/3+38.hA@AAA@A@AAPmyk@AAAAA"A  #"'@@C#&*=BA6WQoQmllQmQoQmRoBABAC"B""%05905;.dDF8[*ODECDDDFFD{mDEEE$D""%(04,49/iGGGHGhTsGFFFGGHHHa}GGGH$F"!%'.3,491j!L!K!L!J!J!J!K!J!J!J!J!J!J!J!J!L!K!K!K'I!%(/4+382j$O$O$N$N$N$N$NXv1X$M$N$N$M$N$L$O$O$N$N$O)K"%'.3+382j'P'Q'P'P'P'O'O&O3YYy&P'O'P'O%O'O'O'R'Q'Q+M" $(/3,49 4k)T)R)S)R)S)R)R(Q(Q(St)S)R)R)Q)Q)S)T)T)S.O "'),#$F+V+W+U+U*U*UCh*U*T*U*VPsPs*T+T+T+V+W+V+U/Q"#';=?*.3!1_.Y.X.Y.Y.X-Y.W.Y.Y.Y.Z.Z1T####""%'.3+39"5l1[1[1Z1\1\1\1[0[0Z0[0[0[0\1\1\1[1\1Z1[1\1\1]9Z"!%(/3+38$6m2^3_3^3_3_3]3]3^3_3]3^3]3_3_3^3^3]3_3]3_3^3`TmMPS"%(.3,49$8m5a5a6`6a6a6b6b6b6a6`6`6`6a6a6b6b6b6a5a5a6a6b|49<(.3,39$:n8f7d7e7e8d8d8d8c8c8e8d8e8e8d8d8e7c7c7e8f8fgu(0 ` +SqHqHqHqHqHqHqHqHqHqHqHqHqHrGrGm#&&&q'''/...~.~.~.~.~.~.~.~.~.~.~.~./0.% ](+,866VUUQ_ 1,38y~:K-48x~'.3[bgz5ELVYpcfhijmnporpsqsqsqsqspsormogiadsrq$+019=4>>>>@>>=;;;%K#*/#*/=N7BC$F(J,K*L*J)L*K+K*J(HD?@F $!&)%> D'K0Q8WVon[s*MECB$*/,.5(,2(@%IVqj=[2S-Q.Q5VIe2T#JFD$*/$*/$+/AU%C0TJhw@]1T)N'O+P4V[uKi(N IJ$+0$+/#+/+I$J)R3W>_s=^3X,S(Q+R-T,S&P#M!L$+/$+/.K&O)R.V6[AaUpSn<^1X-S*S)R&Q%P%P$+/$+0>U(H/X5]=`CdKhRlAc7\/W*V(T(T(U#$&+!$(6N/V7^oz~On7]0Y,X,W,X$+/&,2)/43R2Y:bPqv{:a2^/[/\/\#*.$*/#*/BZ.P7b=eAgBhBgCgCgChChAg=d7b3_2`2_1]-9@#*/#*/?]4^6d8e9d9c9f9e9d9e9e9d8c6d5c5b5cKj3 ',/$)-:S9g9i9f9h9h9g9h9h9i9g9h9g9h9g;fSvϪ  (  @q+//////---------./!2\af454UTS.?;AFLSW=DI*Qnrv"!'+)I(M/O3R9WKep|Fb,M G!Ikqs&,148A/OLjut~4V%KHswz!'/4>Q4WXsNhGcA`?]a{Nl*P#Mgnp&,1'.21T0X9^EdWsCc9]5[1Y+T(Rpsv!&.22V2[9`CeMks?b5\/X,X+WFbr"$)25;AU6_v7_1[/[/[Ifwv%,1%,1;`8c=f@hBhDiDiBi>g9d6b4a4bQr/+49&-1=c9f:h #pragma code_page(65001) IDI_ICON1 ICON DISCARDABLE "resources/zeal.ico" #define VER_COMPANYNAME_STR "${PROJECT_COMPANY_NAME}" #define VER_FILEDESCRIPTION_STR "${CMAKE_PROJECT_DESCRIPTION}" #define VER_FILEVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH} #define VER_FILEVERSION_STR "${PROJECT_VERSION}" #define VER_INTERNALNAME_STR "${CMAKE_PROJECT_NAME}" #define VER_LEGALCOPYRIGHT_STR "${PROJECT_COPYRIGHT}" #define VER_LEGALTRADEMARKS1_STR "" #define VER_LEGALTRADEMARKS2_STR "" #define VER_ORIGINALFILENAME_STR "${PROJECT_EXECUTABLE_NAME}" #define VER_PRODUCTNAME_STR "${CMAKE_PROJECT_NAME}" #define VER_PRODUCTVERSION ${PROJECT_VERSION_MAJOR},${PROJECT_VERSION_MINOR},${PROJECT_VERSION_PATCH} #define VER_PRODUCTVERSION_STR "${PROJECT_VERSION}" #ifndef DEBUG #define VER_DEBUG 0 #else #define VER_DEBUG VS_FF_DEBUG #endif VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_PRODUCTVERSION FILEFLAGSMASK VS_FFI_FILEFLAGSMASK //TODO: Set file flags. //FILEFLAGS (VER_PRIVATEBUILD|VER_PRERELEASE|VER_DEBUG) FILEFLAGS 0 FILEOS VOS__WINDOWS32 FILETYPE VFT_DLL FILESUBTYPE VFT2_UNKNOWN BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", VER_COMPANYNAME_STR VALUE "FileDescription", VER_FILEDESCRIPTION_STR VALUE "FileVersion", VER_FILEVERSION_STR VALUE "InternalName", VER_INTERNALNAME_STR VALUE "LegalCopyright", VER_LEGALCOPYRIGHT_STR VALUE "LegalTrademarks1", VER_LEGALTRADEMARKS1_STR VALUE "LegalTrademarks2", VER_LEGALTRADEMARKS2_STR VALUE "OriginalFilename", VER_ORIGINALFILENAME_STR VALUE "ProductName", VER_PRODUCTNAME_STR VALUE "ProductVersion", VER_PRODUCTVERSION_STR END END BLOCK "VarFileInfo" BEGIN VALUE "Translation", 0x0409, 1200 END END zeal-0.7.1/src/app/zeal.qrc000066400000000000000000000513111462517734600155030ustar00rootroot00000000000000 resources/zeal.ico resources/browser/assets/css/darkmode.css resources/browser/assets/css/fa-brands.min.css resources/browser/assets/css/fa-solid.min.css resources/browser/assets/css/fontawesome.min.css resources/browser/assets/css/highlight.css resources/browser/assets/css/welcome.min.css resources/browser/assets/webfonts/fa-brands-400.woff2 resources/browser/assets/webfonts/fa-solid-900.woff2 resources/browser/404.html resources/browser/welcome.html resources/icons/type/Abbreviation.png resources/icons/type/Abbreviation@2x.png resources/icons/type/Alias.png resources/icons/type/Alias@2x.png resources/icons/type/Annotation.png resources/icons/type/Annotation@2x.png resources/icons/type/Attribute.png resources/icons/type/Attribute@2x.png resources/icons/type/Axiom.png resources/icons/type/Axiom@2x.png resources/icons/type/Binding.png resources/icons/type/Binding@2x.png resources/icons/type/Block.png resources/icons/type/Block@2x.png resources/icons/type/Bookmark.png resources/icons/type/Bookmark@2x.png resources/icons/type/Builtin.png resources/icons/type/Builtin@2x.png resources/icons/type/Callback.png resources/icons/type/Callback@2x.png resources/icons/type/Category.png resources/icons/type/Category@2x.png resources/icons/type/Class.png resources/icons/type/Class@2x.png resources/icons/type/Collection.png resources/icons/type/Collection@2x.png resources/icons/type/Column.png resources/icons/type/Column@2x.png resources/icons/type/Command.png resources/icons/type/Command@2x.png resources/icons/type/Component.png resources/icons/type/Component@2x.png resources/icons/type/Constant.png resources/icons/type/Constant@2x.png resources/icons/type/Constructor.png resources/icons/type/Constructor@2x.png resources/icons/type/Conversion.png resources/icons/type/Conversion@2x.png resources/icons/type/Data Source.png resources/icons/type/Data Source@2x.png resources/icons/type/Database.png resources/icons/type/Database@2x.png resources/icons/type/Decorator.png resources/icons/type/Decorator@2x.png resources/icons/type/Define.png resources/icons/type/Define@2x.png resources/icons/type/Delegate.png resources/icons/type/Delegate@2x.png resources/icons/type/Device.png resources/icons/type/Device@2x.png resources/icons/type/Diagram.png resources/icons/type/Diagram@2x.png resources/icons/type/Directive.png resources/icons/type/Directive@2x.png resources/icons/type/Element.png resources/icons/type/Element@2x.png resources/icons/type/Entry.png resources/icons/type/Entry@2x.png resources/icons/type/Enumeration.png resources/icons/type/Enumeration@2x.png resources/icons/type/Environment.png resources/icons/type/Environment@2x.png resources/icons/type/Error.png resources/icons/type/Error@2x.png resources/icons/type/Event.png resources/icons/type/Event@2x.png resources/icons/type/Exception.png resources/icons/type/Exception@2x.png resources/icons/type/Expression.png resources/icons/type/Expression@2x.png resources/icons/type/Extension.png resources/icons/type/Extension@2x.png resources/icons/type/Field.png resources/icons/type/Field@2x.png resources/icons/type/File.png resources/icons/type/File@2x.png resources/icons/type/Filter.png resources/icons/type/Filter@2x.png resources/icons/type/Flag.png resources/icons/type/Flag@2x.png resources/icons/type/Foreign Key.png resources/icons/type/Foreign Key@2x.png resources/icons/type/Framework.png resources/icons/type/Framework@2x.png resources/icons/type/Function.png resources/icons/type/Function@2x.png resources/icons/type/Global.png resources/icons/type/Global@2x.png resources/icons/type/Glossary.png resources/icons/type/Glossary@2x.png resources/icons/type/Guide.png resources/icons/type/Guide@2x.png resources/icons/type/Handler.png resources/icons/type/Handler@2x.png resources/icons/type/Helper.png resources/icons/type/Helper@2x.png resources/icons/type/Hook.png resources/icons/type/Hook@2x.png resources/icons/type/Index.png resources/icons/type/Index@2x.png resources/icons/type/Indirection.png resources/icons/type/Indirection@2x.png resources/icons/type/Inductive.png resources/icons/type/Inductive@2x.png resources/icons/type/Instance.png resources/icons/type/Instance@2x.png resources/icons/type/Instruction.png resources/icons/type/Instruction@2x.png resources/icons/type/Interface.png resources/icons/type/Interface@2x.png resources/icons/type/Iterator.png resources/icons/type/Iterator@2x.png resources/icons/type/Keyword.png resources/icons/type/Keyword@2x.png resources/icons/type/Kind.png resources/icons/type/Kind@2x.png resources/icons/type/Lemma.png resources/icons/type/Lemma@2x.png resources/icons/type/Library.png resources/icons/type/Library@2x.png resources/icons/type/Literal.png resources/icons/type/Literal@2x.png resources/icons/type/Macro.png resources/icons/type/Macro@2x.png resources/icons/type/Member.png resources/icons/type/Member@2x.png resources/icons/type/Message.png resources/icons/type/Message@2x.png resources/icons/type/Method.png resources/icons/type/Method@2x.png resources/icons/type/Mixin.png resources/icons/type/Mixin@2x.png resources/icons/type/Modifier.png resources/icons/type/Modifier@2x.png resources/icons/type/Module.png resources/icons/type/Module@2x.png resources/icons/type/Namespace.png resources/icons/type/Namespace@2x.png resources/icons/type/NewSnippet.png resources/icons/type/NewSnippet@2x.png resources/icons/type/Node.png resources/icons/type/Node@2x.png resources/icons/type/Notation.png resources/icons/type/Notation@2x.png resources/icons/type/Object.png resources/icons/type/Object@2x.png resources/icons/type/Operator.png resources/icons/type/Operator@2x.png resources/icons/type/Option.png resources/icons/type/Option@2x.png resources/icons/type/Package.png resources/icons/type/Package@2x.png resources/icons/type/Parameter.png resources/icons/type/Parameter@2x.png resources/icons/type/Pattern.png resources/icons/type/Pattern@2x.png resources/icons/type/Pipe.png resources/icons/type/Pipe@2x.png resources/icons/type/Plugin.png resources/icons/type/Plugin@2x.png resources/icons/type/Procedure.png resources/icons/type/Procedure@2x.png resources/icons/type/Projection.png resources/icons/type/Projection@2x.png resources/icons/type/Property.png resources/icons/type/Property@2x.png resources/icons/type/Protocol.png resources/icons/type/Protocol@2x.png resources/icons/type/Provider.png resources/icons/type/Provider@2x.png resources/icons/type/Provisioner.png resources/icons/type/Provisioner@2x.png resources/icons/type/Query.png resources/icons/type/Query@2x.png resources/icons/type/Reference.png resources/icons/type/Reference@2x.png resources/icons/type/Register.png resources/icons/type/Register@2x.png resources/icons/type/Record.png resources/icons/type/Record@2x.png resources/icons/type/Relationship.png resources/icons/type/Relationship@2x.png resources/icons/type/Report.png resources/icons/type/Report@2x.png resources/icons/type/Request.png resources/icons/type/Request@2x.png resources/icons/type/Resource.png resources/icons/type/Resource@2x.png resources/icons/type/Role.png resources/icons/type/Role@2x.png resources/icons/type/Sample.png resources/icons/type/Sample@2x.png resources/icons/type/Schema.png resources/icons/type/Schema@2x.png resources/icons/type/Script.png resources/icons/type/Script@2x.png resources/icons/type/Section.png resources/icons/type/Section@2x.png resources/icons/type/Sender.png resources/icons/type/Sender@2x.png resources/icons/type/Service.png resources/icons/type/Service@2x.png resources/icons/type/Setting.png resources/icons/type/Setting@2x.png resources/icons/type/Shortcut.png resources/icons/type/Shortcut@2x.png resources/icons/type/Signature.png resources/icons/type/Signature@2x.png resources/icons/type/Special Form.png resources/icons/type/Special Form@2x.png resources/icons/type/State.png resources/icons/type/State@2x.png resources/icons/type/Statement.png resources/icons/type/Statement@2x.png resources/icons/type/Structure.png resources/icons/type/Structure@2x.png resources/icons/type/Style.png resources/icons/type/Style@2x.png resources/icons/type/Syntax.png resources/icons/type/Syntax@2x.png resources/icons/type/Subroutine.png resources/icons/type/Subroutine@2x.png resources/icons/type/Table.png resources/icons/type/Table@2x.png resources/icons/type/Tactic.png resources/icons/type/Tactic@2x.png resources/icons/type/Tag.png resources/icons/type/Tag@2x.png resources/icons/type/Template.png resources/icons/type/Template@2x.png resources/icons/type/Test.png resources/icons/type/Test@2x.png resources/icons/type/Trait.png resources/icons/type/Trait@2x.png resources/icons/type/Trigger.png resources/icons/type/Trigger@2x.png resources/icons/type/Type.png resources/icons/type/Type@2x.png resources/icons/type/Union.png resources/icons/type/Union@2x.png resources/icons/type/Unknown.png resources/icons/type/Unknown@2x.png resources/icons/type/Value.png resources/icons/type/Value@2x.png resources/icons/type/Variable.png resources/icons/type/Variable@2x.png resources/icons/type/Variant.png resources/icons/type/Variant@2x.png resources/icons/type/View.png resources/icons/type/View@2x.png resources/icons/type/Widget.png resources/icons/type/Widget@2x.png resources/icons/type/Word.png resources/icons/type/Word@2x.png resources/icons/logo/64x64.png resources/icons/logo/128x128.png resources/icons/logo/icon.png resources/icons/logo/icon@2x.png zeal-0.7.1/src/libs/000077500000000000000000000000001462517734600142115ustar00rootroot00000000000000zeal-0.7.1/src/libs/CMakeLists.txt000066400000000000000000000002221462517734600167450ustar00rootroot00000000000000add_subdirectory(browser) add_subdirectory(core) add_subdirectory(registry) add_subdirectory(sidebar) add_subdirectory(ui) add_subdirectory(util) zeal-0.7.1/src/libs/browser/000077500000000000000000000000001462517734600156745ustar00rootroot00000000000000zeal-0.7.1/src/libs/browser/CMakeLists.txt000066400000000000000000000006051462517734600204350ustar00rootroot00000000000000add_library(Browser STATIC searchtoolbar.cpp settings.cpp urlrequestinterceptor.cpp webbridge.cpp webcontrol.cpp webpage.cpp webview.cpp ) target_link_libraries(Browser) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebChannel WebEngineWidgets REQUIRED) target_link_libraries(Browser Qt${QT_VERSION_MAJOR}::WebChannel Qt${QT_VERSION_MAJOR}::WebEngineWidgets) zeal-0.7.1/src/libs/browser/searchtoolbar.cpp000066400000000000000000000146031462517734600212340ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2018 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchtoolbar.h" #include #include #include #include #include #include #include #include #include using namespace Zeal::Browser; SearchToolBar::SearchToolBar(QWebEngineView *webView, QWidget *parent) : QWidget(parent) , m_webView(webView) { auto layout = new QHBoxLayout(this); layout->setContentsMargins(4, 4, 4, 4); layout->setSpacing(4); m_lineEdit = new QLineEdit(); m_lineEdit->installEventFilter(this); m_lineEdit->setPlaceholderText(tr("Find in page")); m_lineEdit->setMaximumWidth(200); connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::findNext); connect(m_lineEdit, &QLineEdit::textChanged, this, &SearchToolBar::updateHighlight); layout->addWidget(m_lineEdit); m_findPreviousButton = new QToolButton(); m_findPreviousButton->setAutoRaise(true); m_findPreviousButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack)); m_findPreviousButton->setToolTip(tr("Previous result")); connect(m_findPreviousButton, &QToolButton::clicked, this, &SearchToolBar::findPrevious); layout->addWidget(m_findPreviousButton); // A workaround for QAbstractButton lacking support for multiple shortcuts. auto action = new QAction(m_findPreviousButton); action->setShortcuts(QKeySequence::FindPrevious); connect(action, &QAction::triggered, this, [this]() { m_findPreviousButton->animateClick(); }); addAction(action); m_findNextButton = new QToolButton(); m_findNextButton->setAutoRaise(true); m_findNextButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward)); m_findNextButton->setToolTip(tr("Next result")); connect(m_findNextButton, &QToolButton::clicked, this, &SearchToolBar::findNext); layout->addWidget(m_findNextButton); action = new QAction(m_findNextButton); action->setShortcuts(QKeySequence::FindNext); connect(action, &QAction::triggered, this, [this]() { m_findNextButton->animateClick(); }); addAction(action); m_highlightAllButton = new QToolButton(); m_highlightAllButton->setAutoRaise(true); m_highlightAllButton->setCheckable(true); m_highlightAllButton->setText(tr("High&light All")); connect(m_highlightAllButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight); layout->addWidget(m_highlightAllButton); m_matchCaseButton = new QToolButton(); m_matchCaseButton->setAutoRaise(true); m_matchCaseButton->setCheckable(true); m_matchCaseButton->setText(tr("Mat&ch Case")); connect(m_matchCaseButton, &QToolButton::toggled, this, &SearchToolBar::updateHighlight); layout->addWidget(m_matchCaseButton); layout->addStretch(); auto closeButton = new QToolButton(); closeButton->setAutoRaise(true); closeButton->setIcon(qApp->style()->standardIcon(QStyle::SP_TitleBarCloseButton)); closeButton->setToolTip(tr("Close find bar")); connect(closeButton, &QToolButton::clicked, this, &QWidget::hide); layout->addWidget(closeButton); setLayout(layout); setMaximumHeight(sizeHint().height()); setMinimumWidth(sizeHint().width()); } void SearchToolBar::setText(const QString &text) { m_lineEdit->setText(text); } void SearchToolBar::activate() { show(); m_lineEdit->selectAll(); m_lineEdit->setFocus(); } bool SearchToolBar::eventFilter(QObject *object, QEvent *event) { if (object == m_lineEdit && event->type() == QEvent::KeyPress) { auto keyEvent = static_cast(event); switch (keyEvent->key()) { case Qt::Key_Enter: case Qt::Key_Return: if (keyEvent->modifiers().testFlag(Qt::ControlModifier)) { m_highlightAllButton->toggle(); } else if (keyEvent->modifiers().testFlag(Qt::ShiftModifier)) { findPrevious(); } else { findNext(); } return true; case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: QCoreApplication::sendEvent(m_webView->focusProxy(), event); return true; default: break; } } return QWidget::eventFilter(object, event); } void SearchToolBar::hideEvent(QHideEvent *event) { hideHighlight(); m_webView->setFocus(); QWidget::hideEvent(event); } void SearchToolBar::showEvent(QShowEvent *event) { activate(); QWidget::showEvent(event); } void SearchToolBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hide(); } } void SearchToolBar::findNext() { if (!isVisible()) { return; } QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); m_webView->findText(m_lineEdit->text(), ff); } void SearchToolBar::findPrevious() { if (!isVisible()) { return; } QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); ff.setFlag(QWebEnginePage::FindBackward); m_webView->findText(m_lineEdit->text(), ff); } void SearchToolBar::hideHighlight() { m_webView->findText(QString()); } void SearchToolBar::updateHighlight() { hideHighlight(); if (m_highlightAllButton->isChecked()) { QWebEnginePage::FindFlags ff; ff.setFlag(QWebEnginePage::FindCaseSensitively, m_matchCaseButton->isChecked()); m_webView->findText(m_lineEdit->text(), ff); } } zeal-0.7.1/src/libs/browser/searchtoolbar.h000066400000000000000000000037521462517734600207040ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2018 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_SEARCHTOOLBAR_H #define ZEAL_BROWSER_SEARCHTOOLBAR_H #include class QLineEdit; class QToolButton; class QWebEngineView; namespace Zeal { namespace Browser { class SearchToolBar final : public QWidget { Q_OBJECT Q_DISABLE_COPY(SearchToolBar) public: explicit SearchToolBar(QWebEngineView *webView, QWidget *parent = nullptr); void setText(const QString &text); void activate(); bool eventFilter(QObject *object, QEvent *event) override; protected: void hideEvent(QHideEvent *event) override; void showEvent(QShowEvent *event) override; void keyPressEvent(QKeyEvent *event) override; private: void findNext(); void findPrevious(); void hideHighlight(); void updateHighlight(); QLineEdit *m_lineEdit = nullptr; QToolButton *m_findNextButton = nullptr; QToolButton *m_findPreviousButton = nullptr; QToolButton *m_highlightAllButton = nullptr; QToolButton *m_matchCaseButton = nullptr; QWebEngineView *m_webView = nullptr; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_SEARCHTOOLBAR_H zeal-0.7.1/src/libs/browser/settings.cpp000066400000000000000000000101231462517734600202350ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "settings.h" #include "urlrequestinterceptor.h" #include #include #include #include #include #include namespace { constexpr char DarkModeCssUrl[] = "qrc:///browser/assets/css/darkmode.css"; constexpr char HighlightOnNavigateCssUrl[] = "qrc:///browser/assets/css/highlight.css"; } using namespace Zeal; using namespace Zeal::Browser; QWebEngineProfile *Settings::m_webProfile = nullptr; Settings::Settings(Core::Settings *appSettings, QObject *parent) : QObject(parent) , m_appSettings(appSettings) { Q_ASSERT(!m_webProfile); // Create a new off-the-record profile. m_webProfile = new QWebEngineProfile(this); // Setup URL interceptor. #if QT_VERSION >= QT_VERSION_CHECK(5, 13, 0) m_webProfile->setUrlRequestInterceptor(new UrlRequestInterceptor(this)); #else m_webProfile->setRequestInterceptor(new UrlRequestInterceptor(this)); #endif // Listen to settings changes. connect(m_appSettings, &Core::Settings::updated, this, &Settings::applySettings); applySettings(); } void Settings::applySettings() { m_webProfile->settings()->setAttribute(QWebEngineSettings::ScrollAnimatorEnabled, m_appSettings->isSmoothScrollingEnabled); // Apply custom CSS. // TODO: Apply to all open pages. m_webProfile->scripts()->clear(); // Remove all scripts first. // Qt 5.14+ uses native Chromium dark mode. #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) const bool enableDarkMode = m_appSettings->contentAppearance == Core::Settings::ContentAppearance::Dark || (m_appSettings->contentAppearance == Core::Settings::ContentAppearance::Automatic && m_appSettings->colorScheme() == Core::Settings::ColorScheme::Dark); if (enableDarkMode) { setCustomStyleSheet(QStringLiteral("_zeal_darkstylesheet"), DarkModeCssUrl); } #endif if (m_appSettings->isHighlightOnNavigateEnabled) { setCustomStyleSheet(QStringLiteral("_zeal_highlightstylesheet"), HighlightOnNavigateCssUrl); } if (QFileInfo::exists(m_appSettings->customCssFile)) { setCustomStyleSheet(QStringLiteral("_zeal_userstylesheet"), m_appSettings->customCssFile); } } QWebEngineProfile *Settings::defaultProfile() { Q_ASSERT(m_webProfile); return m_webProfile; } void Settings::setCustomStyleSheet(const QString &name, const QString &cssUrl) { QString cssInjectCode = QLatin1String("(function() {" "let head = document.getElementsByTagName('head')[0];" "if (!head) { console.error('Cannot set custom stylesheet.'); return; }" "let link = document.createElement('link');" "link.rel = 'stylesheet';" "link.type = 'text/css';" "link.href = '%1';" "link.media = 'all';" "head.appendChild(link);" "})()"); QWebEngineScript script; script.setName(name); script.setSourceCode(cssInjectCode.arg(cssUrl)); script.setInjectionPoint(QWebEngineScript::DocumentReady); script.setRunsOnSubFrames(true); m_webProfile->scripts()->insert(script); } zeal-0.7.1/src/libs/browser/settings.h000066400000000000000000000031041462517734600177030ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_SETTINGS_H #define ZEAL_BROWSER_SETTINGS_H #include class QWebEngineProfile; namespace Zeal { namespace Core { class Settings; } namespace Browser { class Settings final : public QObject { Q_OBJECT Q_DISABLE_COPY(Settings) public: explicit Settings(Core::Settings *appSettings, QObject *parent = nullptr); static QWebEngineProfile *defaultProfile(); private slots: void applySettings(); private: void setCustomStyleSheet(const QString &name, const QString &cssUrl); Core::Settings *m_appSettings = nullptr; static QWebEngineProfile *m_webProfile; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_SETTINGS_H zeal-0.7.1/src/libs/browser/urlrequestinterceptor.cpp000066400000000000000000000051611462517734600230750ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Copyright (C) 2019 Kay Gawlik ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "urlrequestinterceptor.h" #include #include using namespace Zeal::Browser; static Q_LOGGING_CATEGORY(log, "zeal.browser.urlrequestinterceptor") UrlRequestInterceptor::UrlRequestInterceptor(QObject *parent) : QWebEngineUrlRequestInterceptor(parent) { } void UrlRequestInterceptor::interceptRequest(QWebEngineUrlRequestInfo &info) { const QUrl requestUrl = info.requestUrl(); const QUrl firstPartyUrl = info.firstPartyUrl(); #if QT_VERSION < QT_VERSION_CHECK(5, 12, 0) // Workaround for https://github.com/zealdocs/zeal/issues/1376. Fixed in Qt 5.12.0. if (!firstPartyUrl.isValid() && requestUrl.scheme() == QLatin1String("blob")) { return; } #endif // Block invalid requests. if (!requestUrl.isValid() || !firstPartyUrl.isValid()) { blockRequest(info); return; } // Direct links are controlled in the WebPage if (info.resourceType() == QWebEngineUrlRequestInfo::ResourceTypeMainFrame) { return; } bool isFirstPartyUrlLocal = Core::NetworkAccessManager::isLocalUrl(firstPartyUrl); bool isRequestUrlLocal = Core::NetworkAccessManager::isLocalUrl(requestUrl); // Allow local resources on local pages and external resources on external pages. if (isFirstPartyUrlLocal == isRequestUrlLocal) { return; } blockRequest(info); } void UrlRequestInterceptor::blockRequest(QWebEngineUrlRequestInfo &info) { qCDebug(log, "Blocked request: %s '%s' (resource_type=%d, navigation_type=%d).", info.requestMethod().data(), qPrintable(info.requestUrl().toString()), info.resourceType(), info.navigationType()); info.block(true); } zeal-0.7.1/src/libs/browser/urlrequestinterceptor.h000066400000000000000000000027711462517734600225460ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Copyright (C) 2019 Kay Gawlik ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H #define ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H #include namespace Zeal { namespace Browser { class UrlRequestInterceptor final : public QWebEngineUrlRequestInterceptor { Q_OBJECT Q_DISABLE_COPY(UrlRequestInterceptor) public: UrlRequestInterceptor(QObject *parent = nullptr); void interceptRequest(QWebEngineUrlRequestInfo &info) override; private: void blockRequest(QWebEngineUrlRequestInfo &info); }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_URLREQUESTINTERCEPTOR_H zeal-0.7.1/src/libs/browser/webbridge.cpp000066400000000000000000000026171462517734600203400ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2018 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "webbridge.h" #include #include #include using namespace Zeal::Browser; WebBridge::WebBridge(QObject *parent) : QObject(parent) { } void WebBridge::openShortUrl(const QString &key) { QDesktopServices::openUrl(QUrl(QStringLiteral("https://go.zealdocs.org/l/") + key)); } void WebBridge::triggerAction(const QString &action) { emit actionTriggered(action); } QString WebBridge::appVersion() const { return Core::Application::versionString(); } zeal-0.7.1/src/libs/browser/webbridge.h000066400000000000000000000030071462517734600177770ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2018 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_WEBBRIDGE_H #define ZEAL_BROWSER_WEBBRIDGE_H #include namespace Zeal { namespace Browser { class WebBridge final : public QObject { Q_OBJECT Q_DISABLE_COPY(WebBridge) Q_PROPERTY(QString AppVersion READ appVersion CONSTANT) public: explicit WebBridge(QObject *parent = nullptr); signals: void actionTriggered(const QString &action); public slots: Q_INVOKABLE void openShortUrl(const QString &key); Q_INVOKABLE void triggerAction(const QString &action); private: QString appVersion() const; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBBRIDGE_H zeal-0.7.1/src/libs/browser/webcontrol.cpp000066400000000000000000000103331462517734600205560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "webcontrol.h" #include "searchtoolbar.h" #include "webview.h" #include #include #include #include #include #include #include #include using namespace Zeal::Browser; WebControl::WebControl(QWidget *parent) : QWidget(parent) { auto layout = new QVBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); m_webView = new WebView(); setFocusProxy(m_webView); connect(m_webView->page(), &QWebEnginePage::linkHovered, this, [this](const QString &link) { if (Core::NetworkAccessManager::isLocalUrl(QUrl(link))) { return; } m_webView->setToolTip(link); }); connect(m_webView, &QWebEngineView::titleChanged, this, &WebControl::titleChanged); connect(m_webView, &QWebEngineView::urlChanged, this, &WebControl::urlChanged); layout->addWidget(m_webView); setLayout(layout); } void WebControl::focus() { m_webView->setFocus(); } int WebControl::zoomLevel() const { return m_webView->zoomLevel(); } void WebControl::setZoomLevel(int level) { m_webView->setZoomLevel(level); } void WebControl::zoomIn() { m_webView->zoomIn(); } void WebControl::zoomOut() { m_webView->zoomOut(); } void WebControl::resetZoom() { m_webView->resetZoom(); } void WebControl::setJavaScriptEnabled(bool enabled) { m_webView->page()->settings()->setAttribute(QWebEngineSettings::JavascriptEnabled, enabled); } void WebControl::setWebBridgeObject(const QString &name, QObject *object) { QWebEnginePage *page = m_webView->page(); QWebChannel *channel = new QWebChannel(page); channel->registerObject(name, object); page->setWebChannel(channel); } void WebControl::load(const QUrl &url) { m_webView->load(url); } void WebControl::activateSearchBar() { if (m_searchToolBar == nullptr) { m_searchToolBar = new SearchToolBar(m_webView); layout()->addWidget(m_searchToolBar); } if (m_webView->hasSelection()) { const QString selectedText = m_webView->selectedText().simplified(); if (!selectedText.isEmpty()) { m_searchToolBar->setText(selectedText); } } m_searchToolBar->activate(); } void WebControl::back() { m_webView->back(); } void WebControl::forward() { m_webView->forward(); } bool WebControl::canGoBack() const { return m_webView->history()->canGoBack(); } bool WebControl::canGoForward() const { return m_webView->history()->canGoForward(); } QString WebControl::title() const { return m_webView->title(); } QUrl WebControl::url() const { return m_webView->url(); } QWebEngineHistory *WebControl::history() const { return m_webView->history(); } void WebControl::restoreHistory(const QByteArray &array) { QDataStream stream(array); stream >> *m_webView->history(); } QByteArray WebControl::saveHistory() const { QByteArray array; QDataStream stream(&array, QIODevice::WriteOnly); stream << *m_webView->history(); return array; } void WebControl::keyPressEvent(QKeyEvent *event) { switch (event->key()) { case Qt::Key_Slash: activateSearchBar(); break; default: event->ignore(); break; } } zeal-0.7.1/src/libs/browser/webcontrol.h000066400000000000000000000043061462517734600202260ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_WEBCONTROL_H #define ZEAL_BROWSER_WEBCONTROL_H #include class QWebEngineHistory; namespace Zeal { namespace Browser { class SearchToolBar; class WebView; class WebControl final : public QWidget { Q_OBJECT Q_DISABLE_COPY(WebControl) public: explicit WebControl(QWidget *parent = nullptr); void focus(); void load(const QUrl &url); bool canGoBack() const; bool canGoForward() const; QString title() const; QUrl url() const; QWebEngineHistory *history() const; void restoreHistory(const QByteArray &array); QByteArray saveHistory() const; int zoomLevel() const; void setZoomLevel(int level); void setJavaScriptEnabled(bool enabled); void setWebBridgeObject(const QString &name, QObject *object); signals: void titleChanged(const QString &title); void urlChanged(const QUrl &url); public slots: void activateSearchBar(); void back(); void forward(); void zoomIn(); void zoomOut(); void resetZoom(); protected: void keyPressEvent(QKeyEvent *event) override; private: friend class WebView; WebView *m_webView = nullptr; SearchToolBar *m_searchToolBar = nullptr; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBCONTROL_H zeal-0.7.1/src/libs/browser/webpage.cpp000066400000000000000000000103721462517734600200150ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Copyright (C) 2019 Kay Gawlik ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "webpage.h" #include "settings.h" #include #include #include #include #include #include #include #include #include using namespace Zeal::Browser; static Q_LOGGING_CATEGORY(log, "zeal.browser.webpage") WebPage::WebPage(QObject *parent) : QWebEnginePage(Settings::defaultProfile(), parent) { } bool WebPage::acceptNavigationRequest(const QUrl &requestUrl, QWebEnginePage::NavigationType type, bool isMainFrame) { Q_UNUSED(type) // Local elements are always allowed. if (Core::NetworkAccessManager::isLocalUrl(requestUrl)) { return true; } // Allow external resources if already on an external page. const QUrl pageUrl = url(); if (pageUrl.isValid() && !Core::NetworkAccessManager::isLocalUrl(pageUrl)) { return true; } // Block external elements on local pages. if (!isMainFrame) { qCDebug(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } auto appSettings = Core::Application::instance()->settings(); // TODO: [C++20] using enum Core::Settings::ExternalLinkPolicy; typedef Core::Settings::ExternalLinkPolicy ExternalLinkPolicy; switch (appSettings->externalLinkPolicy) { case ExternalLinkPolicy::Open: return true; case ExternalLinkPolicy::Ask: { QMessageBox mb; mb.setIcon(QMessageBox::Question); mb.setText(tr("How do you want to open the external link?
URL: %1") .arg(requestUrl.toString())); QCheckBox *checkBox = new QCheckBox("Do ¬ ask again"); mb.setCheckBox(checkBox); QPushButton *openInBrowserButton = mb.addButton(tr("Open in &Desktop Browser"), QMessageBox::ActionRole); QPushButton *openInZealButton = mb.addButton(tr("Open in &Zeal"), QMessageBox::ActionRole); mb.addButton(QMessageBox::Cancel); mb.setDefaultButton(openInBrowserButton); if (mb.exec() == QMessageBox::Cancel) { qCDebug(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } if (mb.clickedButton() == openInZealButton) { if (checkBox->isChecked()) { appSettings->externalLinkPolicy = ExternalLinkPolicy::Open; appSettings->save(); } return true; } if (mb.clickedButton() == openInBrowserButton) { if (checkBox->isChecked()) { appSettings->externalLinkPolicy = ExternalLinkPolicy::OpenInSystemBrowser; appSettings->save(); } QDesktopServices::openUrl(requestUrl); return false; } break; } case ExternalLinkPolicy::OpenInSystemBrowser: QDesktopServices::openUrl(requestUrl); return false; } // This code should not be reachable so log a warning. qCWarning(log, "Blocked request to '%s'.", qPrintable(requestUrl.toString())); return false; } void WebPage::javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceId) { Q_UNUSED(level) Q_UNUSED(message) Q_UNUSED(lineNumber) Q_UNUSED(sourceId) } zeal-0.7.1/src/libs/browser/webpage.h000066400000000000000000000030401462517734600174540ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Copyright (C) 2019 Kay Gawlik ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_WEBPAGE_H #define ZEAL_BROWSER_WEBPAGE_H #include namespace Zeal { namespace Browser { class WebPage final : public QWebEnginePage { Q_OBJECT Q_DISABLE_COPY(WebPage) public: explicit WebPage(QObject *parent = nullptr); protected: bool acceptNavigationRequest(const QUrl &requestUrl, NavigationType type, bool isMainFrame) override; void javaScriptConsoleMessage(QWebEnginePage::JavaScriptConsoleMessageLevel level, const QString &message, int lineNumber, const QString &sourceId) override; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBPAGE_H zeal-0.7.1/src/libs/browser/webview.cpp000066400000000000000000000156751462517734600200660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "webview.h" #include "webcontrol.h" #include "webpage.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif using namespace Zeal::Browser; WebView::WebView(QWidget *parent) : QWebEngineView(parent) { setPage(new WebPage(this)); setZoomLevel(defaultZoomLevel()); settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false); QApplication::instance()->installEventFilter(this); } int WebView::zoomLevel() const { return m_zoomLevel; } void WebView::setZoomLevel(int level) { if (level == m_zoomLevel) { return; } level = qMax(0, level); level = qMin(level, availableZoomLevels().size() - 1); m_zoomLevel = level; // Scale the webview relative to the DPI of the screen. const double dpiZoomFactor = logicalDpiY() / 96.0; setZoomFactor(availableZoomLevels().at(level) / 100.0 * dpiZoomFactor); emit zoomLevelChanged(); } const QVector &WebView::availableZoomLevels() { static const QVector zoomLevels = {30, 40, 50, 67, 80, 90, 100, 110, 120, 133, 150, 170, 200, 220, 233, 250, 270, 285, 300}; return zoomLevels; } int WebView::defaultZoomLevel() { static const int level = availableZoomLevels().indexOf(100); return level; } void WebView::zoomIn() { setZoomLevel(m_zoomLevel + 1); } void WebView::zoomOut() { setZoomLevel(m_zoomLevel - 1); } void WebView::resetZoom() { setZoomLevel(defaultZoomLevel()); } QWebEngineView *WebView::createWindow(QWebEnginePage::WebWindowType type) { Q_UNUSED(type) return Core::Application::instance()->mainWindow()->createTab()->webControl()->m_webView; } void WebView::contextMenuEvent(QContextMenuEvent *event) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const QWebEngineContextMenuData& contextData = page()->contextMenuData(); if (!contextData.isValid()) { QWebEngineView::contextMenuEvent(event); return; } #else QWebEngineContextMenuRequest *contextMenuRequest = lastContextMenuRequest(); if (contextMenuRequest == nullptr) { QWebEngineView::contextMenuEvent(event); return; } #endif event->accept(); if (m_contextMenu) { m_contextMenu->deleteLater(); } m_contextMenu = new QMenu(this); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) QUrl linkUrl = contextData.linkUrl(); #else QUrl linkUrl = contextMenuRequest->linkUrl(); #endif if (linkUrl.isValid()) { const QString scheme = linkUrl.scheme(); if (scheme != QLatin1String("javascript")) { m_contextMenu->addAction(tr("Open Link in New Tab"), this, [this]() { triggerPageAction(QWebEnginePage::WebAction::OpenLinkInNewWindow); }); } if (scheme != QLatin1String("qrc")) { if (scheme != QLatin1String("javascript")) { m_contextMenu->addAction(tr("Open Link in Desktop Browser"), this, [linkUrl]() { QDesktopServices::openUrl(linkUrl); }); } m_contextMenu->addAction(pageAction(QWebEnginePage::CopyLinkToClipboard)); } } #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const QString selectedText = contextData.selectedText(); #else const QString selectedText = contextMenuRequest->selectedText(); #endif if (!selectedText.isEmpty()) { if (!m_contextMenu->isEmpty()) { m_contextMenu->addSeparator(); } m_contextMenu->addAction(pageAction(QWebEnginePage::Copy)); } if (!linkUrl.isValid() && url().scheme() != QLatin1String("qrc")) { if (!m_contextMenu->isEmpty()) { m_contextMenu->addSeparator(); } m_contextMenu->addAction(pageAction(QWebEnginePage::Back)); m_contextMenu->addAction(pageAction(QWebEnginePage::Forward)); m_contextMenu->addSeparator(); m_contextMenu->addAction(tr("Open Page in Desktop Browser"), this, [this]() { QDesktopServices::openUrl(url()); }); } if (m_contextMenu->isEmpty()) { return; } m_contextMenu->popup(event->globalPos()); } bool WebView::handleMousePressEvent(QMouseEvent *event) { switch (event->button()) { case Qt::BackButton: back(); event->accept(); return true; case Qt::ForwardButton: forward(); event->accept(); return true; default: break; } return false; } bool WebView::handleWheelEvent(QWheelEvent *event) { if (event->modifiers() & Qt::ControlModifier) { const QPoint angleDelta = event->angleDelta(); int delta = qAbs(angleDelta.x()) > qAbs(angleDelta.y()) ? angleDelta.x() : angleDelta.y(); const int direction = delta > 0 ? 1 : -1; int levelDelta = 0; while (delta * direction >= 120) { levelDelta += direction; delta -= 120 * direction; } setZoomLevel(m_zoomLevel + levelDelta); event->accept(); return true; } return false; } bool WebView::eventFilter(QObject *watched, QEvent *event) { if (watched->parent() == this) { switch (event->type()) { case QEvent::MouseButtonPress: if (handleMousePressEvent(static_cast(event))) { return true; } break; case QEvent::Wheel: if (handleWheelEvent(static_cast(event))) { return true; } break; default: break; } } return QWebEngineView::eventFilter(watched, event); } zeal-0.7.1/src/libs/browser/webview.h000066400000000000000000000036741462517734600175270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_BROWSER_WEBVIEW_H #define ZEAL_BROWSER_WEBVIEW_H #include namespace Zeal { namespace Browser { class WebView final : public QWebEngineView { Q_OBJECT Q_DISABLE_COPY(WebView) public: explicit WebView(QWidget *parent = nullptr); int zoomLevel() const; void setZoomLevel(int level); bool eventFilter(QObject *watched, QEvent *event) override; static const QVector &availableZoomLevels(); static int defaultZoomLevel(); public slots: void zoomIn(); void zoomOut(); void resetZoom(); signals: void zoomLevelChanged(); protected: QWebEngineView *createWindow(QWebEnginePage::WebWindowType type) override; void contextMenuEvent(QContextMenuEvent *event) override; private: bool handleMousePressEvent(QMouseEvent *event); bool handleWheelEvent(QWheelEvent *event); QMenu *m_contextMenu = nullptr; QUrl m_clickedLink; int m_zoomLevel = 0; }; } // namespace Browser } // namespace Zeal #endif // ZEAL_BROWSER_WEBVIEW_H zeal-0.7.1/src/libs/core/000077500000000000000000000000001462517734600151415ustar00rootroot00000000000000zeal-0.7.1/src/libs/core/CMakeLists.txt000066400000000000000000000032211462517734600176770ustar00rootroot00000000000000add_library(Core STATIC application.cpp applicationsingleton.cpp extractor.cpp filemanager.cpp httpserver.cpp networkaccessmanager.cpp settings.cpp # Show headers without .cpp in Qt Creator. httplib.h ) # Configure cpp-httplib. add_definitions(-DCPPHTTPLIB_USE_POLL) target_link_libraries(Core Registry Ui) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Network WebEngineCore Widgets REQUIRED) target_link_libraries(Core Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::WebEngineCore Qt${QT_VERSION_MAJOR}::Widgets) if(QT_VERSION_MAJOR EQUAL 6) find_package(Qt6 COMPONENTS WebEngineCore Widgets REQUIRED) target_link_libraries(Core Qt6::WebEngineCore) else() find_package(Qt5 COMPONENTS WebEngine Widgets REQUIRED) target_link_libraries(Core Qt5::WebEngine) endif() find_package(LibArchive QUIET) if(NOT LibArchive_FOUND) find_path(LibArchive_INCLUDE_DIRS archive.h PATHS /opt/homebrew/opt/libarchive/include /usr/local/opt/libarchive/include REQUIRED ) find_library(LibArchive_LIBRARIES NAMES archive libarchive PATHS /opt/homebrew/opt/libarchive/lib /usr/local/opt/libarchive/lib REQUIRED NO_DEFAULT_PATH ) endif() if((CMAKE_VERSION VERSION_GREATER_EQUAL 3.17.0) AND (TARGET LibArchive::LibArchive)) target_link_libraries(Core LibArchive::LibArchive) else() include_directories(${LibArchive_INCLUDE_DIRS}) target_link_libraries(Core ${LibArchive_LIBRARIES}) endif() # Required by cpp-httplib. if(NOT WIN32) set(THREADS_PREFER_PTHREAD_FLAG ON) find_package(Threads REQUIRED) target_link_libraries(Core Threads::Threads) endif() zeal-0.7.1/src/libs/core/application.cpp000066400000000000000000000222461462517734600201560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "application.h" #include "extractor.h" #include "filemanager.h" #include "httpserver.h" #include "networkaccessmanager.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::Core; namespace { constexpr char ReleasesApiUrl[] = "https://api.zealdocs.org/v1/releases"; } // namespace Application *Application::m_instance = nullptr; Application::Application(QObject *parent) : QObject(parent) { // Ensure only one instance of Application Q_ASSERT(!m_instance); m_instance = this; m_settings = new Settings(this); m_networkManager = new NetworkAccessManager(this); m_fileManager = new FileManager(this); m_httpServer = new HttpServer(this); connect(m_networkManager, &QNetworkAccessManager::sslErrors, this, [this](QNetworkReply *reply, const QList &errors) { if (m_settings->isIgnoreSslErrorsEnabled) { reply->ignoreSslErrors(); } }); // Extractor setup m_extractorThread = new QThread(this); m_extractor = new Extractor(); m_extractor->moveToThread(m_extractorThread); m_extractorThread->start(); connect(m_extractor, &Extractor::completed, this, &Application::extractionCompleted); connect(m_extractor, &Extractor::error, this, &Application::extractionError); connect(m_extractor, &Extractor::progress, this, &Application::extractionProgress); m_docsetRegistry = new Registry::DocsetRegistry(); connect(m_settings, &Settings::updated, this, &Application::applySettings); applySettings(); m_mainWindow = new WidgetUi::MainWindow(this); if (m_settings->startMinimized) { if (m_settings->showSystrayIcon && m_settings->minimizeToSystray) return; m_mainWindow->showMinimized(); } else { m_mainWindow->show(); } } Application::~Application() { m_extractorThread->quit(); m_extractorThread->wait(); delete m_extractor; delete m_mainWindow; delete m_docsetRegistry; } /*! * \internal * \brief Returns a pointer to the Core::Application instance. * \return A pointer or \c nullptr, if no instance has been created. */ Application *Application::instance() { return m_instance; } WidgetUi::MainWindow *Application::mainWindow() const { return m_mainWindow; } QNetworkAccessManager *Application::networkManager() const { return m_networkManager; } Settings *Application::settings() const { return m_settings; } Registry::DocsetRegistry *Application::docsetRegistry() { return m_docsetRegistry; } FileManager *Application::fileManager() const { return m_fileManager; } HttpServer *Application::httpServer() const { return m_httpServer; } QString Application::cacheLocation() { #ifndef PORTABLE_BUILD return QStandardPaths::writableLocation(QStandardPaths::CacheLocation); #else return QCoreApplication::applicationDirPath() + QLatin1String("/cache"); #endif } QString Application::configLocation() { #ifndef PORTABLE_BUILD // TODO: Replace 'Zeal/Zeal' with 'zeal'. return QStandardPaths::writableLocation(QStandardPaths::AppConfigLocation); #else return QCoreApplication::applicationDirPath() + QLatin1String("/config"); #endif } QVersionNumber Application::version() { static const auto vn = QVersionNumber::fromString(QCoreApplication::applicationVersion()); return vn; } QString Application::versionString() { static const auto v = QStringLiteral("v%1").arg(QCoreApplication::applicationVersion()); return v; } void Application::executeQuery(const Registry::SearchQuery &query, bool preventActivation) { m_mainWindow->search(query); if (preventActivation) return; m_mainWindow->bringToFront(); } void Application::extract(const QString &filePath, const QString &destination, const QString &root) { QMetaObject::invokeMethod(m_extractor, "extract", Qt::QueuedConnection, Q_ARG(QString, filePath), Q_ARG(QString, destination), Q_ARG(QString, root)); } QNetworkReply *Application::download(const QUrl &url) { static const QString ua = userAgent(); static const QByteArray uaJson = userAgentJson().toUtf8(); QNetworkRequest request(url); request.setHeader(QNetworkRequest::UserAgentHeader, ua); if (url.host().endsWith(QLatin1String(".zealdocs.org", Qt::CaseInsensitive))) request.setRawHeader("X-Zeal-User-Agent", uaJson); return m_networkManager->get(request); } /*! \internal Performs a check whether a new Zeal version is available. Setting \a quiet to true supresses error and "you are using the latest version" message boxes. */ void Application::checkForUpdates(bool quiet) { QNetworkReply *reply = download(QUrl(ReleasesApiUrl)); connect(reply, &QNetworkReply::finished, this, [this, quiet]() { QScopedPointer reply( qobject_cast(sender())); if (reply->error() != QNetworkReply::NoError) { if (!quiet) emit updateCheckError(reply->errorString()); return; } QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(reply->readAll(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { if (!quiet) emit updateCheckError(jsonError.errorString()); return; } const QJsonObject versionInfo = jsonDoc.array().first().toObject(); // Latest is the first. const auto latestVersion = QVersionNumber::fromString(versionInfo[QLatin1String("version")].toString()); if (latestVersion > version()) { emit updateCheckDone(latestVersion.toString()); } else if (!quiet) { emit updateCheckDone(); } }); } void Application::applySettings() { m_docsetRegistry->setStoragePath(m_settings->docsetPath); m_docsetRegistry->setFuzzySearchEnabled(m_settings->isFuzzySearchEnabled); // HTTP Proxy Settings switch (m_settings->proxyType) { case Settings::ProxyType::None: QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy); break; case Settings::ProxyType::System: QNetworkProxyFactory::setUseSystemConfiguration(true); break; case Settings::ProxyType::Http: case Settings::ProxyType::Socks5: { const QNetworkProxy::ProxyType type = m_settings->proxyType == Settings::ProxyType::Socks5 ? QNetworkProxy::Socks5Proxy : QNetworkProxy::HttpProxy; QNetworkProxy proxy(type, m_settings->proxyHost, m_settings->proxyPort); if (m_settings->proxyAuthenticate) { proxy.setUser(m_settings->proxyUserName); proxy.setPassword(m_settings->proxyPassword); } QNetworkProxy::setApplicationProxy(proxy); break; } } // Force NM to pick up changes. m_networkManager->clearAccessCache(); } QString Application::userAgent() { return QStringLiteral("Zeal/%1").arg(QCoreApplication::applicationVersion()); } QString Application::userAgentJson() const { QJsonObject app = { {QStringLiteral("version"), QCoreApplication::applicationVersion()}, {QStringLiteral("qt_version"), qVersion()}, {QStringLiteral("install_id"), m_settings->installId} }; QJsonObject os = { {QStringLiteral("arch"), QSysInfo::currentCpuArchitecture()}, {QStringLiteral("name"), QSysInfo::prettyProductName()}, {QStringLiteral("product_type"), QSysInfo::productType()}, {QStringLiteral("product_version"), QSysInfo::productVersion()}, {QStringLiteral("kernel_type"), QSysInfo::kernelType()}, {QStringLiteral("kernel_version"), QSysInfo::kernelVersion()}, {QStringLiteral("locale"), QLocale::system().name()} }; QJsonObject ua = { {QStringLiteral("app"), app}, {QStringLiteral("os"), os} }; return QString::fromUtf8(QJsonDocument(ua).toJson(QJsonDocument::Compact)); } zeal-0.7.1/src/libs/core/application.h000066400000000000000000000062041462517734600176170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_APPLICATION_H #define ZEAL_CORE_APPLICATION_H #include #include class QNetworkAccessManager; class QNetworkReply; class QThread; namespace Zeal { namespace Registry { class DocsetRegistry; class SearchQuery; } // namespace Registry namespace WidgetUi { class MainWindow; } // namespace WidgetUi namespace Core { class Extractor; class FileManager; class HttpServer; class Settings; class Application final : public QObject { Q_OBJECT Q_DISABLE_COPY(Application) public: explicit Application(QObject *parent = nullptr); ~Application() override; static Application *instance(); WidgetUi::MainWindow *mainWindow() const; QNetworkAccessManager *networkManager() const; Settings *settings() const; Registry::DocsetRegistry *docsetRegistry(); FileManager *fileManager() const; HttpServer *httpServer() const; static QString cacheLocation(); static QString configLocation(); static QVersionNumber version(); static QString versionString(); public slots: void executeQuery(const Registry::SearchQuery &query, bool preventActivation); void extract(const QString &filePath, const QString &destination, const QString &root = QString()); QNetworkReply *download(const QUrl &url); void checkForUpdates(bool quiet = false); signals: void extractionCompleted(const QString &filePath); void extractionError(const QString &filePath, const QString &errorString); void extractionProgress(const QString &filePath, qint64 extracted, qint64 total); void updateCheckDone(const QString &version = QString()); void updateCheckError(const QString &message); private slots: void applySettings(); private: static inline QString userAgent(); QString userAgentJson() const; static Application *m_instance; Settings *m_settings = nullptr; QNetworkAccessManager *m_networkManager = nullptr; FileManager *m_fileManager = nullptr; HttpServer *m_httpServer = nullptr; QThread *m_extractorThread = nullptr; Extractor *m_extractor = nullptr; Registry::DocsetRegistry *m_docsetRegistry = nullptr; WidgetUi::MainWindow *m_mainWindow = nullptr; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_APPLICATION_H zeal-0.7.1/src/libs/core/applicationsingleton.cpp000066400000000000000000000120231462517734600220710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2017 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "applicationsingleton.h" #include #include #include #include #include #include #include #include using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.applicationsingleton") struct SharedData { qint64 primaryPid; }; ApplicationSingleton::ApplicationSingleton(QObject *parent) : QObject(parent) { if (QCoreApplication::instance() == nullptr) { qFatal("QCoreApplication (or derived type) must be created before ApplicationSingleton."); } m_id = computeId(); qCDebug(log, "Singleton ID: %s", qPrintable(m_id)); m_sharedMemory = new QSharedMemory(m_id, this); m_isPrimary = m_sharedMemory->create(sizeof(SharedData)); if (m_isPrimary) { setupPrimary(); return; } #ifdef Q_OS_UNIX // Verify it's not a segment that survived an application crash. m_sharedMemory->attach(); m_sharedMemory->detach(); m_isPrimary = m_sharedMemory->create(sizeof(SharedData)); if (m_isPrimary) { setupPrimary(); return; } #endif if (!m_sharedMemory->attach(QSharedMemory::ReadOnly)) { qCWarning(log) << "Cannot attach to the shared memory segment:" << m_sharedMemory->errorString(); return; } setupSecondary(); } bool ApplicationSingleton::isPrimary() const { return m_isPrimary; } bool ApplicationSingleton::isSecondary() const { return !m_isPrimary; } qint64 ApplicationSingleton::primaryPid() const { return m_primaryPid; } bool ApplicationSingleton::sendMessage(QByteArray &data, int timeout) { // No support for primary to secondary communication. if (m_isPrimary) { return false; } QScopedPointer socket(new QLocalSocket); socket->connectToServer(m_id); if (!socket->waitForConnected(timeout)) { qCWarning(log) << "Cannot connect to the local service:" << socket->errorString(); return false; } socket->write(data); socket->flush(); // Required for Linux. return socket->waitForBytesWritten(timeout); } void ApplicationSingleton::setupPrimary() { m_primaryPid = QCoreApplication::applicationPid(); qCInfo(log, "Starting as a primary instance. (PID: %lld)", m_primaryPid); m_sharedMemory->lock(); auto sd = static_cast(m_sharedMemory->data()); sd->primaryPid = m_primaryPid; m_sharedMemory->unlock(); QLocalServer::removeServer(m_id); m_localServer = new QLocalServer(this); m_localServer->setSocketOptions(QLocalServer::UserAccessOption); connect(m_localServer, &QLocalServer::newConnection, this, [this] { QLocalSocket *socket = m_localServer->nextPendingConnection(); connect(socket, &QLocalSocket::readyRead, this, [this, socket] { QByteArray data = socket->readAll(); emit messageReceived(data); socket->deleteLater(); }); }); if (!m_localServer->listen(m_id)) { qCWarning(log) << "Cannot start the local service:" << m_localServer->errorString(); return; } } void ApplicationSingleton::setupSecondary() { m_sharedMemory->lock(); auto sd = static_cast(m_sharedMemory->data()); m_primaryPid = sd->primaryPid; m_sharedMemory->unlock(); qCInfo(log, "Starting as a secondary instance. (Primary PID: %lld)", m_primaryPid); } QString ApplicationSingleton::computeId() { // Make sure the result can be used as a name for the local socket. static const QByteArray::Base64Options base64Options = QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals; QCryptographicHash hash(QCryptographicHash::Sha256); hash.addData(QCoreApplication::applicationName().toUtf8()); hash.addData(QCoreApplication::organizationName().toUtf8()); hash.addData(QCoreApplication::organizationDomain().toUtf8()); // Support multi-user setup. hash.addData(QDir::homePath().toUtf8()); return QString::fromLatin1(hash.result().toBase64(base64Options)); } zeal-0.7.1/src/libs/core/applicationsingleton.h000066400000000000000000000034331462517734600215430ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2017 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_APPLICATIONSINGLETON_H #define ZEAL_CORE_APPLICATIONSINGLETON_H #include class QLocalServer; class QSharedMemory; namespace Zeal { namespace Core { class ApplicationSingleton final : public QObject { Q_OBJECT Q_DISABLE_COPY(ApplicationSingleton) public: explicit ApplicationSingleton(QObject *parent = nullptr); bool isPrimary() const; bool isSecondary() const; qint64 primaryPid() const; bool sendMessage(QByteArray &data, int timeout = 500); signals: void messageReceived(const QByteArray &data); private: void setupPrimary(); void setupSecondary(); static QString computeId(); QString m_id; bool m_isPrimary = false; qint64 m_primaryPid = 0; QSharedMemory *m_sharedMemory = nullptr; QLocalServer *m_localServer = nullptr; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_APPLICATIONSINGLETON_H zeal-0.7.1/src/libs/core/extractor.cpp000066400000000000000000000104251462517734600176620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "extractor.h" #include #include #include #include using namespace Zeal::Core; Extractor::Extractor(QObject *parent) : QObject(parent) { } void Extractor::extract(const QString &sourceFile, const QString &destination, const QString &root) { ExtractInfo info = { archive_read_new(), // archiveHandle sourceFile, // filePath QFileInfo(sourceFile).size(), // totalBytes 0 // extractedBytes }; archive_read_support_filter_all(info.archiveHandle); archive_read_support_format_all(info.archiveHandle); int r = archive_read_open_filename(info.archiveHandle, qPrintable(sourceFile), 10240); if (r) { emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle))); return; } QDir destinationDir(destination); if (!root.isEmpty()) { destinationDir.setPath(destinationDir.filePath(root)); } // Destination directory must be created before any other files. destinationDir.mkpath(QLatin1String(".")); // TODO: Do not strip root directory in archive if it equals to 'root' archive_entry *entry; while (archive_read_next_header(info.archiveHandle, &entry) == ARCHIVE_OK) { #ifndef Q_OS_WIN32 QString pathname = QString::fromUtf8(archive_entry_pathname(entry)); #else // TODO: Remove once https://github.com/libarchive/libarchive/issues/587 is resolved. QString pathname = QString::fromWCharArray(archive_entry_pathname_w(entry)); #endif if (!root.isEmpty()) { pathname.remove(0, pathname.indexOf(QLatin1String("/")) + 1); } const QString filePath = destinationDir.absoluteFilePath(pathname); const auto filetype = archive_entry_filetype(entry); if (filetype == S_IFDIR) { QDir().mkpath(QFileInfo(filePath).absolutePath()); continue; } if (filetype != S_IFREG) { qWarning("Unsupported filetype %d for %s!", filetype, qPrintable(pathname)); continue; } QScopedPointer file(new QFile(filePath)); if (!file->open(QIODevice::WriteOnly)) { qWarning("Cannot open file for writing: %s", qPrintable(pathname)); continue; } const void *buffer; size_t size; std::int64_t offset; for (;;) { int rc = archive_read_data_block(info.archiveHandle, &buffer, &size, &offset); if (rc != ARCHIVE_OK) { if (rc == ARCHIVE_EOF) { break; } qWarning("Cannot read from archive: %s", archive_error_string(info.archiveHandle)); emit error(sourceFile, QString::fromLocal8Bit(archive_error_string(info.archiveHandle))); return; } file->write(static_cast(buffer), size); } emitProgress(info); } emit completed(sourceFile); archive_read_free(info.archiveHandle); } void Extractor::emitProgress(ExtractInfo &info) { const qint64 extractedBytes = archive_filter_bytes(info.archiveHandle, -1); if (extractedBytes == info.extractedBytes) { return; } info.extractedBytes = extractedBytes; emit progress(info.filePath, extractedBytes, info.totalBytes); } zeal-0.7.1/src/libs/core/extractor.h000066400000000000000000000034241462517734600173300ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_EXTRACTOR_H #define ZEAL_CORE_EXTRACTOR_H #include struct archive; namespace Zeal { namespace Core { class Extractor final : public QObject { Q_OBJECT Q_DISABLE_COPY(Extractor) public: explicit Extractor(QObject *parent = nullptr); public slots: void extract(const QString &sourceFile, const QString &destination, const QString &root = QString()); signals: void error(const QString &filePath, const QString &message); void completed(const QString &filePath); void progress(const QString &filePath, qint64 extracted, qint64 total); private: struct ExtractInfo { archive *archiveHandle; QString filePath; qint64 totalBytes; qint64 extractedBytes; }; void emitProgress(ExtractInfo &info); }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_EXTRACTOR_H zeal-0.7.1/src/libs/core/filemanager.cpp000066400000000000000000000045261462517734600201260ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2017 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "filemanager.h" #include "application.h" #include #include #include #include #include #include #include using namespace Zeal::Core; static Q_LOGGING_CATEGORY(log, "zeal.core.filemanager") FileManager::FileManager(QObject *parent) : QObject(parent) { // Ensure that cache location exists. // TODO: Check for errors. QDir().mkpath(Application::cacheLocation()); } bool FileManager::removeRecursively(const QString &path) { qCDebug(log, "Removing '%s'...", qPrintable(path)); if (!QFileInfo(path).isDir()) { qCWarning(log, "'%s' is not a directory.", qPrintable(path)); return false; } const QString deletePath = QStringLiteral("%1.%2.deleteme") .arg(path, QString::number(QDateTime::currentMSecsSinceEpoch())); if (!QDir().rename(path, deletePath)) { qCWarning(log, "Failed to rename '%s' to '%s'.", qPrintable(path), qPrintable(deletePath)); return false; } qCDebug(log, "Renamed '%s' to '%s'.", qPrintable(path), qPrintable(deletePath)); std::future f = std::async(std::launch::async, [deletePath](){ return QDir(deletePath).removeRecursively(); }); f.wait(); if (!f.get()) { qCWarning(log, "Failed to remove '%s'.", qPrintable(deletePath)); } else { qCDebug(log, "Removed '%s'.", qPrintable(deletePath)); } return true; } zeal-0.7.1/src/libs/core/filemanager.h000066400000000000000000000024241462517734600175660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2017 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_FILEMANAGER_H #define ZEAL_CORE_FILEMANAGER_H #include namespace Zeal { namespace Core { class FileManager final : public QObject { Q_OBJECT Q_DISABLE_COPY(FileManager) public: explicit FileManager(QObject *parent = nullptr); bool removeRecursively(const QString &path); }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_FILEMANAGER_H zeal-0.7.1/src/libs/core/httplib.h000066400000000000000000011144031462517734600167640ustar00rootroot00000000000000// // httplib.h // // Copyright (c) 2023 Yuji Hirose. All rights reserved. // MIT License // #ifndef CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_HTTPLIB_H #define CPPHTTPLIB_VERSION "0.13.3" /* * Configuration */ #ifndef CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND #define CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_KEEPALIVE_MAX_COUNT #define CPPHTTPLIB_KEEPALIVE_MAX_COUNT 5 #endif #ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND #define CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND 300 #endif #ifndef CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND #define CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_READ_TIMEOUT_SECOND #define CPPHTTPLIB_READ_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_READ_TIMEOUT_USECOND #define CPPHTTPLIB_READ_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_WRITE_TIMEOUT_SECOND #define CPPHTTPLIB_WRITE_TIMEOUT_SECOND 5 #endif #ifndef CPPHTTPLIB_WRITE_TIMEOUT_USECOND #define CPPHTTPLIB_WRITE_TIMEOUT_USECOND 0 #endif #ifndef CPPHTTPLIB_IDLE_INTERVAL_SECOND #define CPPHTTPLIB_IDLE_INTERVAL_SECOND 0 #endif #ifndef CPPHTTPLIB_IDLE_INTERVAL_USECOND #ifdef _WIN32 #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 10000 #else #define CPPHTTPLIB_IDLE_INTERVAL_USECOND 0 #endif #endif #ifndef CPPHTTPLIB_REQUEST_URI_MAX_LENGTH #define CPPHTTPLIB_REQUEST_URI_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_HEADER_MAX_LENGTH #define CPPHTTPLIB_HEADER_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_REDIRECT_MAX_COUNT #define CPPHTTPLIB_REDIRECT_MAX_COUNT 20 #endif #ifndef CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT #define CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT 1024 #endif #ifndef CPPHTTPLIB_PAYLOAD_MAX_LENGTH #define CPPHTTPLIB_PAYLOAD_MAX_LENGTH ((std::numeric_limits::max)()) #endif #ifndef CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH #define CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH 8192 #endif #ifndef CPPHTTPLIB_TCP_NODELAY #define CPPHTTPLIB_TCP_NODELAY false #endif #ifndef CPPHTTPLIB_RECV_BUFSIZ #define CPPHTTPLIB_RECV_BUFSIZ size_t(4096u) #endif #ifndef CPPHTTPLIB_COMPRESSION_BUFSIZ #define CPPHTTPLIB_COMPRESSION_BUFSIZ size_t(16384u) #endif #ifndef CPPHTTPLIB_THREAD_POOL_COUNT #define CPPHTTPLIB_THREAD_POOL_COUNT \ ((std::max)(8u, std::thread::hardware_concurrency() > 0 \ ? std::thread::hardware_concurrency() - 1 \ : 0)) #endif #ifndef CPPHTTPLIB_RECV_FLAGS #define CPPHTTPLIB_RECV_FLAGS 0 #endif #ifndef CPPHTTPLIB_SEND_FLAGS #define CPPHTTPLIB_SEND_FLAGS 0 #endif #ifndef CPPHTTPLIB_LISTEN_BACKLOG #define CPPHTTPLIB_LISTEN_BACKLOG 5 #endif /* * Headers */ #ifdef _WIN32 #ifndef _CRT_SECURE_NO_WARNINGS #define _CRT_SECURE_NO_WARNINGS #endif //_CRT_SECURE_NO_WARNINGS #ifndef _CRT_NONSTDC_NO_DEPRECATE #define _CRT_NONSTDC_NO_DEPRECATE #endif //_CRT_NONSTDC_NO_DEPRECATE #if defined(_MSC_VER) #if _MSC_VER < 1900 #error Sorry, Visual Studio versions prior to 2015 are not supported #endif #pragma comment(lib, "ws2_32.lib") #ifdef _WIN64 using ssize_t = __int64; #else using ssize_t = long; #endif #endif // _MSC_VER #ifndef S_ISREG #define S_ISREG(m) (((m)&S_IFREG) == S_IFREG) #endif // S_ISREG #ifndef S_ISDIR #define S_ISDIR(m) (((m)&S_IFDIR) == S_IFDIR) #endif // S_ISDIR #ifndef NOMINMAX #define NOMINMAX #endif // NOMINMAX #include #include #include #ifndef WSA_FLAG_NO_HANDLE_INHERIT #define WSA_FLAG_NO_HANDLE_INHERIT 0x80 #endif #ifndef strcasecmp #define strcasecmp _stricmp #endif // strcasecmp using socket_t = SOCKET; #ifdef CPPHTTPLIB_USE_POLL #define poll(fds, nfds, timeout) WSAPoll(fds, nfds, timeout) #endif #else // not _WIN32 #include #if !defined(_AIX) && !defined(__MVS__) #include #endif #ifdef __MVS__ #include #ifndef NI_MAXHOST #define NI_MAXHOST 1025 #endif #endif #include #include #include #ifdef __linux__ #include #endif #include #ifdef CPPHTTPLIB_USE_POLL #include #endif #include #include #include #include #include #include #include using socket_t = int; #ifndef INVALID_SOCKET #define INVALID_SOCKET (-1) #endif #endif //_WIN32 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 #include // these are defined in wincrypt.h and it breaks compilation if BoringSSL is // used #undef X509_NAME #undef X509_CERT_PAIR #undef X509_EXTENSIONS #undef PKCS7_SIGNER_INFO #ifdef _MSC_VER #pragma comment(lib, "crypt32.lib") #pragma comment(lib, "cryptui.lib") #endif #elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) #include #if TARGET_OS_OSX #include #include #endif // TARGET_OS_OSX #endif // _WIN32 #include #include #include #include #if defined(_WIN32) && defined(OPENSSL_USE_APPLINK) #include #endif #include #include #if OPENSSL_VERSION_NUMBER < 0x1010100fL #error Sorry, OpenSSL versions prior to 1.1.1 are not supported #elif OPENSSL_VERSION_NUMBER < 0x30000000L #define SSL_get1_peer_certificate SSL_get_peer_certificate #endif #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT #include #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT #include #include #endif /* * Declaration */ namespace httplib { namespace detail { /* * Backport std::make_unique from C++14. * * NOTE: This code came up with the following stackoverflow post: * https://stackoverflow.com/questions/10149840/c-arrays-and-make-unique * */ template typename std::enable_if::value, std::unique_ptr>::type make_unique(Args &&...args) { return std::unique_ptr(new T(std::forward(args)...)); } template typename std::enable_if::value, std::unique_ptr>::type make_unique(std::size_t n) { typedef typename std::remove_extent::type RT; return std::unique_ptr(new RT[n]); } struct ci { bool operator()(const std::string &s1, const std::string &s2) const { return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), [](unsigned char c1, unsigned char c2) { return ::tolower(c1) < ::tolower(c2); }); } }; // This is based on // "http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2014/n4189". struct scope_exit { explicit scope_exit(std::function &&f) : exit_function(std::move(f)), execute_on_destruction{true} {} scope_exit(scope_exit &&rhs) : exit_function(std::move(rhs.exit_function)), execute_on_destruction{rhs.execute_on_destruction} { rhs.release(); } ~scope_exit() { if (execute_on_destruction) { this->exit_function(); } } void release() { this->execute_on_destruction = false; } private: scope_exit(const scope_exit &) = delete; void operator=(const scope_exit &) = delete; scope_exit &operator=(scope_exit &&) = delete; std::function exit_function; bool execute_on_destruction; }; } // namespace detail using Headers = std::multimap; using Params = std::multimap; using Match = std::smatch; using Progress = std::function; struct Response; using ResponseHandler = std::function; struct MultipartFormData { std::string name; std::string content; std::string filename; std::string content_type; }; using MultipartFormDataItems = std::vector; using MultipartFormDataMap = std::multimap; class DataSink { public: DataSink() : os(&sb_), sb_(*this) {} DataSink(const DataSink &) = delete; DataSink &operator=(const DataSink &) = delete; DataSink(DataSink &&) = delete; DataSink &operator=(DataSink &&) = delete; std::function write; std::function done; std::function done_with_trailer; std::ostream os; private: class data_sink_streambuf : public std::streambuf { public: explicit data_sink_streambuf(DataSink &sink) : sink_(sink) {} protected: std::streamsize xsputn(const char *s, std::streamsize n) { sink_.write(s, static_cast(n)); return n; } private: DataSink &sink_; }; data_sink_streambuf sb_; }; using ContentProvider = std::function; using ContentProviderWithoutLength = std::function; using ContentProviderResourceReleaser = std::function; struct MultipartFormDataProvider { std::string name; ContentProviderWithoutLength provider; std::string filename; std::string content_type; }; using MultipartFormDataProviderItems = std::vector; using ContentReceiverWithProgress = std::function; using ContentReceiver = std::function; using MultipartContentHeader = std::function; class ContentReader { public: using Reader = std::function; using MultipartReader = std::function; ContentReader(Reader reader, MultipartReader multipart_reader) : reader_(std::move(reader)), multipart_reader_(std::move(multipart_reader)) {} bool operator()(MultipartContentHeader header, ContentReceiver receiver) const { return multipart_reader_(std::move(header), std::move(receiver)); } bool operator()(ContentReceiver receiver) const { return reader_(std::move(receiver)); } Reader reader_; MultipartReader multipart_reader_; }; using Range = std::pair; using Ranges = std::vector; struct Request { std::string method; std::string path; Headers headers; std::string body; std::string remote_addr; int remote_port = -1; std::string local_addr; int local_port = -1; // for server std::string version; std::string target; Params params; MultipartFormDataMap files; Ranges ranges; Match matches; std::unordered_map path_params; // for client ResponseHandler response_handler; ContentReceiverWithProgress content_receiver; Progress progress; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT const SSL *ssl = nullptr; #endif bool has_header(const std::string &key) const; std::string get_header_value(const std::string &key, size_t id = 0) const; template T get_header_value(const std::string &key, size_t id = 0) const; size_t get_header_value_count(const std::string &key) const; void set_header(const std::string &key, const std::string &val); bool has_param(const std::string &key) const; std::string get_param_value(const std::string &key, size_t id = 0) const; size_t get_param_value_count(const std::string &key) const; bool is_multipart_form_data() const; bool has_file(const std::string &key) const; MultipartFormData get_file_value(const std::string &key) const; std::vector get_file_values(const std::string &key) const; // private members... size_t redirect_count_ = CPPHTTPLIB_REDIRECT_MAX_COUNT; size_t content_length_ = 0; ContentProvider content_provider_; bool is_chunked_content_provider_ = false; size_t authorization_count_ = 0; }; struct Response { std::string version; int status = -1; std::string reason; Headers headers; std::string body; std::string location; // Redirect location bool has_header(const std::string &key) const; std::string get_header_value(const std::string &key, size_t id = 0) const; template T get_header_value(const std::string &key, size_t id = 0) const; size_t get_header_value_count(const std::string &key) const; void set_header(const std::string &key, const std::string &val); void set_redirect(const std::string &url, int status = 302); void set_content(const char *s, size_t n, const std::string &content_type); void set_content(const std::string &s, const std::string &content_type); void set_content_provider( size_t length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser = nullptr); void set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr); void set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser = nullptr); Response() = default; Response(const Response &) = default; Response &operator=(const Response &) = default; Response(Response &&) = default; Response &operator=(Response &&) = default; ~Response() { if (content_provider_resource_releaser_) { content_provider_resource_releaser_(content_provider_success_); } } // private members... size_t content_length_ = 0; ContentProvider content_provider_; ContentProviderResourceReleaser content_provider_resource_releaser_; bool is_chunked_content_provider_ = false; bool content_provider_success_ = false; }; class Stream { public: virtual ~Stream() = default; virtual bool is_readable() const = 0; virtual bool is_writable() const = 0; virtual ssize_t read(char *ptr, size_t size) = 0; virtual ssize_t write(const char *ptr, size_t size) = 0; virtual void get_remote_ip_and_port(std::string &ip, int &port) const = 0; virtual void get_local_ip_and_port(std::string &ip, int &port) const = 0; virtual socket_t socket() const = 0; template ssize_t write_format(const char *fmt, const Args &...args); ssize_t write(const char *ptr); ssize_t write(const std::string &s); }; class TaskQueue { public: TaskQueue() = default; virtual ~TaskQueue() = default; virtual void enqueue(std::function fn) = 0; virtual void shutdown() = 0; virtual void on_idle() {} }; class ThreadPool : public TaskQueue { public: explicit ThreadPool(size_t n) : shutdown_(false) { while (n) { threads_.emplace_back(worker(*this)); n--; } } ThreadPool(const ThreadPool &) = delete; ~ThreadPool() override = default; void enqueue(std::function fn) override { { std::unique_lock lock(mutex_); jobs_.push_back(std::move(fn)); } cond_.notify_one(); } void shutdown() override { // Stop all worker threads... { std::unique_lock lock(mutex_); shutdown_ = true; } cond_.notify_all(); // Join... for (auto &t : threads_) { t.join(); } } private: struct worker { explicit worker(ThreadPool &pool) : pool_(pool) {} void operator()() { for (;;) { std::function fn; { std::unique_lock lock(pool_.mutex_); pool_.cond_.wait( lock, [&] { return !pool_.jobs_.empty() || pool_.shutdown_; }); if (pool_.shutdown_ && pool_.jobs_.empty()) { break; } fn = std::move(pool_.jobs_.front()); pool_.jobs_.pop_front(); } assert(true == static_cast(fn)); fn(); } } ThreadPool &pool_; }; friend struct worker; std::vector threads_; std::list> jobs_; bool shutdown_; std::condition_variable cond_; std::mutex mutex_; }; using Logger = std::function; using SocketOptions = std::function; void default_socket_options(socket_t sock); const char *status_message(int status); namespace detail { class MatcherBase { public: virtual ~MatcherBase() = default; // Match request path and populate its matches and virtual bool match(Request &request) const = 0; }; /** * Captures parameters in request path and stores them in Request::path_params * * Capture name is a substring of a pattern from : to /. * The rest of the pattern is matched agains the request path directly * Parameters are captured starting from the next character after * the end of the last matched static pattern fragment until the next /. * * Example pattern: * "/path/fragments/:capture/more/fragments/:second_capture" * Static fragments: * "/path/fragments/", "more/fragments/" * * Given the following request path: * "/path/fragments/:1/more/fragments/:2" * the resulting capture will be * {{"capture", "1"}, {"second_capture", "2"}} */ class PathParamsMatcher : public MatcherBase { public: PathParamsMatcher(const std::string &pattern); bool match(Request &request) const override; private: static constexpr char marker = ':'; // Treat segment separators as the end of path parameter capture // Does not need to handle query parameters as they are parsed before path // matching static constexpr char separator = '/'; // Contains static path fragments to match against, excluding the '/' after // path params // Fragments are separated by path params std::vector static_fragments_; // Stores the names of the path parameters to be used as keys in the // Request::path_params map std::vector param_names_; }; /** * Performs std::regex_match on request path * and stores the result in Request::matches * * Note that regex match is performed directly on the whole request. * This means that wildcard patterns may match multiple path segments with /: * "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end". */ class RegexMatcher : public MatcherBase { public: RegexMatcher(const std::string &pattern) : regex_(pattern) {} bool match(Request &request) const override; private: std::regex regex_; }; } // namespace detail class Server { public: using Handler = std::function; using ExceptionHandler = std::function; enum class HandlerResponse { Handled, Unhandled, }; using HandlerWithResponse = std::function; using HandlerWithContentReader = std::function; using Expect100ContinueHandler = std::function; Server(); virtual ~Server(); virtual bool is_valid() const; Server &Get(const std::string &pattern, Handler handler); Server &Post(const std::string &pattern, Handler handler); Server &Post(const std::string &pattern, HandlerWithContentReader handler); Server &Put(const std::string &pattern, Handler handler); Server &Put(const std::string &pattern, HandlerWithContentReader handler); Server &Patch(const std::string &pattern, Handler handler); Server &Patch(const std::string &pattern, HandlerWithContentReader handler); Server &Delete(const std::string &pattern, Handler handler); Server &Delete(const std::string &pattern, HandlerWithContentReader handler); Server &Options(const std::string &pattern, Handler handler); bool set_base_dir(const std::string &dir, const std::string &mount_point = std::string()); bool set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers = Headers()); bool remove_mount_point(const std::string &mount_point); Server &set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime); Server &set_default_file_mimetype(const std::string &mime); Server &set_file_request_handler(Handler handler); Server &set_error_handler(HandlerWithResponse handler); Server &set_error_handler(Handler handler); Server &set_exception_handler(ExceptionHandler handler); Server &set_pre_routing_handler(HandlerWithResponse handler); Server &set_post_routing_handler(Handler handler); Server &set_expect_100_continue_handler(Expect100ContinueHandler handler); Server &set_logger(Logger logger); Server &set_address_family(int family); Server &set_tcp_nodelay(bool on); Server &set_socket_options(SocketOptions socket_options); Server &set_default_headers(Headers headers); Server &set_keep_alive_max_count(size_t count); Server &set_keep_alive_timeout(time_t sec); Server &set_read_timeout(time_t sec, time_t usec = 0); template Server &set_read_timeout(const std::chrono::duration &duration); Server &set_write_timeout(time_t sec, time_t usec = 0); template Server &set_write_timeout(const std::chrono::duration &duration); Server &set_idle_interval(time_t sec, time_t usec = 0); template Server &set_idle_interval(const std::chrono::duration &duration); Server &set_payload_max_length(size_t length); bool bind_to_port(const std::string &host, int port, int socket_flags = 0); int bind_to_any_port(const std::string &host, int socket_flags = 0); bool listen_after_bind(); bool listen(const std::string &host, int port, int socket_flags = 0); bool is_running() const; void wait_until_ready() const; void stop(); std::function new_task_queue; protected: bool process_request(Stream &strm, bool close_connection, bool &connection_closed, const std::function &setup_request); std::atomic svr_sock_{INVALID_SOCKET}; size_t keep_alive_max_count_ = CPPHTTPLIB_KEEPALIVE_MAX_COUNT; time_t keep_alive_timeout_sec_ = CPPHTTPLIB_KEEPALIVE_TIMEOUT_SECOND; time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND; time_t idle_interval_sec_ = CPPHTTPLIB_IDLE_INTERVAL_SECOND; time_t idle_interval_usec_ = CPPHTTPLIB_IDLE_INTERVAL_USECOND; size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH; private: using Handlers = std::vector, Handler>>; using HandlersForContentReader = std::vector, HandlerWithContentReader>>; static std::unique_ptr make_matcher(const std::string &pattern); socket_t create_server_socket(const std::string &host, int port, int socket_flags, SocketOptions socket_options) const; int bind_internal(const std::string &host, int port, int socket_flags); bool listen_internal(); bool routing(Request &req, Response &res, Stream &strm); bool handle_file_request(const Request &req, Response &res, bool head = false); bool dispatch_request(Request &req, Response &res, const Handlers &handlers); bool dispatch_request_for_content_reader(Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers); bool parse_request_line(const char *s, Request &req); void apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary); bool write_response(Stream &strm, bool close_connection, const Request &req, Response &res); bool write_response_with_content(Stream &strm, bool close_connection, const Request &req, Response &res); bool write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, bool need_apply_ranges); bool write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type); bool read_content(Stream &strm, Request &req, Response &res); bool read_content_with_content_receiver(Stream &strm, Request &req, Response &res, ContentReceiver receiver, MultipartContentHeader multipart_header, ContentReceiver multipart_receiver); bool read_content_core(Stream &strm, Request &req, Response &res, ContentReceiver receiver, MultipartContentHeader multipart_header, ContentReceiver multipart_receiver); virtual bool process_and_close_socket(socket_t sock); std::atomic is_running_{false}; std::atomic done_{false}; struct MountPointEntry { std::string mount_point; std::string base_dir; Headers headers; }; std::vector base_dirs_; std::map file_extension_and_mimetype_map_; std::string default_file_mimetype_ = "application/octet-stream"; Handler file_request_handler_; Handlers get_handlers_; Handlers post_handlers_; HandlersForContentReader post_handlers_for_content_reader_; Handlers put_handlers_; HandlersForContentReader put_handlers_for_content_reader_; Handlers patch_handlers_; HandlersForContentReader patch_handlers_for_content_reader_; Handlers delete_handlers_; HandlersForContentReader delete_handlers_for_content_reader_; Handlers options_handlers_; HandlerWithResponse error_handler_; ExceptionHandler exception_handler_; HandlerWithResponse pre_routing_handler_; Handler post_routing_handler_; Expect100ContinueHandler expect_100_continue_handler_; Logger logger_; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; SocketOptions socket_options_ = default_socket_options; Headers default_headers_; }; enum class Error { Success = 0, Unknown, Connection, BindIPAddress, Read, Write, ExceedRedirectCount, Canceled, SSLConnection, SSLLoadingCerts, SSLServerVerification, UnsupportedMultipartBoundaryChars, Compression, ConnectionTimeout, ProxyConnection, // For internal use only SSLPeerCouldBeClosed_, }; std::string to_string(const Error error); std::ostream &operator<<(std::ostream &os, const Error &obj); class Result { public: Result() = default; Result(std::unique_ptr &&res, Error err, Headers &&request_headers = Headers{}) : res_(std::move(res)), err_(err), request_headers_(std::move(request_headers)) {} // Response operator bool() const { return res_ != nullptr; } bool operator==(std::nullptr_t) const { return res_ == nullptr; } bool operator!=(std::nullptr_t) const { return res_ != nullptr; } const Response &value() const { return *res_; } Response &value() { return *res_; } const Response &operator*() const { return *res_; } Response &operator*() { return *res_; } const Response *operator->() const { return res_.get(); } Response *operator->() { return res_.get(); } // Error Error error() const { return err_; } // Request Headers bool has_request_header(const std::string &key) const; std::string get_request_header_value(const std::string &key, size_t id = 0) const; template T get_request_header_value(const std::string &key, size_t id = 0) const; size_t get_request_header_value_count(const std::string &key) const; private: std::unique_ptr res_; Error err_ = Error::Unknown; Headers request_headers_; }; class ClientImpl { public: explicit ClientImpl(const std::string &host); explicit ClientImpl(const std::string &host, int port); explicit ClientImpl(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path); virtual ~ClientImpl(); virtual bool is_valid() const; Result Get(const std::string &path); Result Get(const std::string &path, const Headers &headers); Result Get(const std::string &path, Progress progress); Result Get(const std::string &path, const Headers &headers, Progress progress); Result Get(const std::string &path, ContentReceiver content_receiver); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver); Result Get(const std::string &path, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, Progress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, Progress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress = nullptr); Result Head(const std::string &path); Result Head(const std::string &path, const Headers &headers); Result Post(const std::string &path); Result Post(const std::string &path, const Headers &headers); Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Post(const std::string &path, const std::string &body, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Post(const std::string &path, const Params ¶ms); Result Post(const std::string &path, const Headers &headers, const Params ¶ms); Result Post(const std::string &path, const MultipartFormDataItems &items); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items); Result Put(const std::string &path); Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Put(const std::string &path, const std::string &body, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); Result Put(const std::string &path, const MultipartFormDataItems &items); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Patch(const std::string &path, const std::string &body, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Delete(const std::string &path); Result Delete(const std::string &path, const Headers &headers); Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Delete(const std::string &path, const std::string &body, const std::string &content_type); Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Options(const std::string &path); Result Options(const std::string &path, const Headers &headers); bool send(Request &req, Response &res, Error &error); Result send(const Request &req); void stop(); std::string host() const; int port() const; size_t is_socket_open() const; socket_t socket() const; void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); void set_address_family(int family); void set_tcp_nodelay(bool on); void set_socket_options(SocketOptions socket_options); void set_connection_timeout(time_t sec, time_t usec = 0); template void set_connection_timeout(const std::chrono::duration &duration); void set_read_timeout(time_t sec, time_t usec = 0); template void set_read_timeout(const std::chrono::duration &duration); void set_write_timeout(time_t sec, time_t usec = 0); template void set_write_timeout(const std::chrono::duration &duration); void set_basic_auth(const std::string &username, const std::string &password); void set_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_digest_auth(const std::string &username, const std::string &password); #endif void set_keep_alive(bool on); void set_follow_location(bool on); void set_url_encode(bool on); void set_compress(bool on); void set_decompress(bool on); void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, const std::string &password); void set_proxy_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_proxy_digest_auth(const std::string &username, const std::string &password); #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(X509_STORE *ca_cert_store); X509_STORE *create_ca_cert_store(const char *ca_cert, std::size_t size); #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void enable_server_certificate_verification(bool enabled); #endif void set_logger(Logger logger); protected: struct Socket { socket_t sock = INVALID_SOCKET; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT SSL *ssl = nullptr; #endif bool is_open() const { return sock != INVALID_SOCKET; } }; virtual bool create_and_connect_socket(Socket &socket, Error &error); // All of: // shutdown_ssl // shutdown_socket // close_socket // should ONLY be called when socket_mutex_ is locked. // Also, shutdown_ssl and close_socket should also NOT be called concurrently // with a DIFFERENT thread sending requests using that socket. virtual void shutdown_ssl(Socket &socket, bool shutdown_gracefully); void shutdown_socket(Socket &socket); void close_socket(Socket &socket); bool process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error); bool write_content_with_provider(Stream &strm, const Request &req, Error &error); void copy_settings(const ClientImpl &rhs); // Socket endpoint information const std::string host_; const int port_; const std::string host_and_port_; // Current open socket Socket socket_; mutable std::mutex socket_mutex_; std::recursive_mutex request_mutex_; // These are all protected under socket_mutex size_t socket_requests_in_flight_ = 0; std::thread::id socket_requests_are_from_thread_ = std::thread::id(); bool socket_should_be_closed_when_request_is_done_ = false; // Hostname-IP map std::map addr_map_; // Default headers Headers default_headers_; // Settings std::string client_cert_path_; std::string client_key_path_; time_t connection_timeout_sec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_SECOND; time_t connection_timeout_usec_ = CPPHTTPLIB_CONNECTION_TIMEOUT_USECOND; time_t read_timeout_sec_ = CPPHTTPLIB_READ_TIMEOUT_SECOND; time_t read_timeout_usec_ = CPPHTTPLIB_READ_TIMEOUT_USECOND; time_t write_timeout_sec_ = CPPHTTPLIB_WRITE_TIMEOUT_SECOND; time_t write_timeout_usec_ = CPPHTTPLIB_WRITE_TIMEOUT_USECOND; std::string basic_auth_username_; std::string basic_auth_password_; std::string bearer_token_auth_token_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT std::string digest_auth_username_; std::string digest_auth_password_; #endif bool keep_alive_ = false; bool follow_location_ = false; bool url_encode_ = true; int address_family_ = AF_UNSPEC; bool tcp_nodelay_ = CPPHTTPLIB_TCP_NODELAY; SocketOptions socket_options_ = nullptr; bool compress_ = false; bool decompress_ = true; std::string interface_; std::string proxy_host_; int proxy_port_ = -1; std::string proxy_basic_auth_username_; std::string proxy_basic_auth_password_; std::string proxy_bearer_token_auth_token_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT std::string proxy_digest_auth_username_; std::string proxy_digest_auth_password_; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT std::string ca_cert_file_path_; std::string ca_cert_dir_path_; X509_STORE *ca_cert_store_ = nullptr; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT bool server_certificate_verification_ = true; #endif Logger logger_; private: bool send_(Request &req, Response &res, Error &error); Result send_(Request &&req); socket_t create_client_socket(Error &error) const; bool read_response_line(Stream &strm, const Request &req, Response &res); bool write_request(Stream &strm, Request &req, bool close_connection, Error &error); bool redirect(Request &req, Response &res, Error &error); bool handle_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error); std::unique_ptr send_with_content_provider( Request &req, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, Error &error); Result send_with_content_provider( const std::string &method, const std::string &path, const Headers &headers, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type); ContentProviderWithoutLength get_multipart_content_provider( const std::string &boundary, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items); std::string adjust_host_string(const std::string &host) const; virtual bool process_socket(const Socket &socket, std::function callback); virtual bool is_ssl() const; }; class Client { public: // Universal interface explicit Client(const std::string &scheme_host_port); explicit Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path); // HTTP only interface explicit Client(const std::string &host, int port); explicit Client(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path); Client(Client &&) = default; ~Client(); bool is_valid() const; Result Get(const std::string &path); Result Get(const std::string &path, const Headers &headers); Result Get(const std::string &path, Progress progress); Result Get(const std::string &path, const Headers &headers, Progress progress); Result Get(const std::string &path, ContentReceiver content_receiver); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver); Result Get(const std::string &path, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver); Result Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, Progress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, Progress progress = nullptr); Result Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress = nullptr); Result Head(const std::string &path); Result Head(const std::string &path, const Headers &headers); Result Post(const std::string &path); Result Post(const std::string &path, const Headers &headers); Result Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Post(const std::string &path, const std::string &body, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Post(const std::string &path, const Params ¶ms); Result Post(const std::string &path, const Headers &headers, const Params ¶ms); Result Post(const std::string &path, const MultipartFormDataItems &items); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary); Result Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items); Result Put(const std::string &path); Result Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Put(const std::string &path, const std::string &body, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Put(const std::string &path, const Params ¶ms); Result Put(const std::string &path, const Headers &headers, const Params ¶ms); Result Put(const std::string &path, const MultipartFormDataItems &items); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary); Result Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items); Result Patch(const std::string &path); Result Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Patch(const std::string &path, const std::string &body, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type); Result Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type); Result Delete(const std::string &path); Result Delete(const std::string &path, const Headers &headers); Result Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type); Result Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type); Result Delete(const std::string &path, const std::string &body, const std::string &content_type); Result Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type); Result Options(const std::string &path); Result Options(const std::string &path, const Headers &headers); bool send(Request &req, Response &res, Error &error); Result send(const Request &req); void stop(); std::string host() const; int port() const; size_t is_socket_open() const; socket_t socket() const; void set_hostname_addr_map(std::map addr_map); void set_default_headers(Headers headers); void set_address_family(int family); void set_tcp_nodelay(bool on); void set_socket_options(SocketOptions socket_options); void set_connection_timeout(time_t sec, time_t usec = 0); template void set_connection_timeout(const std::chrono::duration &duration); void set_read_timeout(time_t sec, time_t usec = 0); template void set_read_timeout(const std::chrono::duration &duration); void set_write_timeout(time_t sec, time_t usec = 0); template void set_write_timeout(const std::chrono::duration &duration); void set_basic_auth(const std::string &username, const std::string &password); void set_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_digest_auth(const std::string &username, const std::string &password); #endif void set_keep_alive(bool on); void set_follow_location(bool on); void set_url_encode(bool on); void set_compress(bool on); void set_decompress(bool on); void set_interface(const std::string &intf); void set_proxy(const std::string &host, int port); void set_proxy_basic_auth(const std::string &username, const std::string &password); void set_proxy_bearer_token_auth(const std::string &token); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_proxy_digest_auth(const std::string &username, const std::string &password); #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void enable_server_certificate_verification(bool enabled); #endif void set_logger(Logger logger); // SSL #ifdef CPPHTTPLIB_OPENSSL_SUPPORT void set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path = std::string()); void set_ca_cert_store(X509_STORE *ca_cert_store); void load_ca_cert_store(const char *ca_cert, std::size_t size); long get_openssl_verify_result() const; SSL_CTX *ssl_context() const; #endif private: std::unique_ptr cli_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT bool is_ssl_ = false; #endif }; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT class SSLServer : public Server { public: SSLServer(const char *cert_path, const char *private_key_path, const char *client_ca_cert_file_path = nullptr, const char *client_ca_cert_dir_path = nullptr, const char *private_key_password = nullptr); SSLServer(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store = nullptr); SSLServer( const std::function &setup_ssl_ctx_callback); ~SSLServer() override; bool is_valid() const override; SSL_CTX *ssl_context() const; private: bool process_and_close_socket(socket_t sock) override; SSL_CTX *ctx_; std::mutex ctx_mutex_; }; class SSLClient : public ClientImpl { public: explicit SSLClient(const std::string &host); explicit SSLClient(const std::string &host, int port); explicit SSLClient(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path); explicit SSLClient(const std::string &host, int port, X509 *client_cert, EVP_PKEY *client_key); ~SSLClient() override; bool is_valid() const override; void set_ca_cert_store(X509_STORE *ca_cert_store); void load_ca_cert_store(const char *ca_cert, std::size_t size); long get_openssl_verify_result() const; SSL_CTX *ssl_context() const; private: bool create_and_connect_socket(Socket &socket, Error &error) override; void shutdown_ssl(Socket &socket, bool shutdown_gracefully) override; void shutdown_ssl_impl(Socket &socket, bool shutdown_socket); bool process_socket(const Socket &socket, std::function callback) override; bool is_ssl() const override; bool connect_with_proxy(Socket &sock, Response &res, bool &success, Error &error); bool initialize_ssl(Socket &socket, Error &error); bool load_certs(); bool verify_host(X509 *server_cert) const; bool verify_host_with_subject_alt_name(X509 *server_cert) const; bool verify_host_with_common_name(X509 *server_cert) const; bool check_host_name(const char *pattern, size_t pattern_len) const; SSL_CTX *ctx_; std::mutex ctx_mutex_; std::once_flag initialize_cert_; std::vector host_components_; long verify_result_ = 0; friend class ClientImpl; }; #endif /* * Implementation of template methods. */ namespace detail { template inline void duration_to_sec_and_usec(const T &duration, U callback) { auto sec = std::chrono::duration_cast(duration).count(); auto usec = std::chrono::duration_cast( duration - std::chrono::seconds(sec)) .count(); callback(static_cast(sec), static_cast(usec)); } template inline T get_header_value(const Headers & /*headers*/, const std::string & /*key*/, size_t /*id*/ = 0, uint64_t /*def*/ = 0) {} template <> inline uint64_t get_header_value(const Headers &headers, const std::string &key, size_t id, uint64_t def) { auto rng = headers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return std::strtoull(it->second.data(), nullptr, 10); } return def; } } // namespace detail template inline T Request::get_header_value(const std::string &key, size_t id) const { return detail::get_header_value(headers, key, id, 0); } template inline T Response::get_header_value(const std::string &key, size_t id) const { return detail::get_header_value(headers, key, id, 0); } template inline ssize_t Stream::write_format(const char *fmt, const Args &...args) { const auto bufsiz = 2048; std::array buf{}; auto sn = snprintf(buf.data(), buf.size() - 1, fmt, args...); if (sn <= 0) { return sn; } auto n = static_cast(sn); if (n >= buf.size() - 1) { std::vector glowable_buf(buf.size()); while (n >= glowable_buf.size() - 1) { glowable_buf.resize(glowable_buf.size() * 2); n = static_cast( snprintf(&glowable_buf[0], glowable_buf.size() - 1, fmt, args...)); } return write(&glowable_buf[0], n); } else { return write(buf.data(), n); } } inline void default_socket_options(socket_t sock) { int yes = 1; #ifdef _WIN32 setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), sizeof(yes)); setsockopt(sock, SOL_SOCKET, SO_EXCLUSIVEADDRUSE, reinterpret_cast(&yes), sizeof(yes)); #else #ifdef SO_REUSEPORT setsockopt(sock, SOL_SOCKET, SO_REUSEPORT, reinterpret_cast(&yes), sizeof(yes)); #else setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&yes), sizeof(yes)); #endif #endif } inline const char *status_message(int status) { switch (status) { case 100: return "Continue"; case 101: return "Switching Protocol"; case 102: return "Processing"; case 103: return "Early Hints"; case 200: return "OK"; case 201: return "Created"; case 202: return "Accepted"; case 203: return "Non-Authoritative Information"; case 204: return "No Content"; case 205: return "Reset Content"; case 206: return "Partial Content"; case 207: return "Multi-Status"; case 208: return "Already Reported"; case 226: return "IM Used"; case 300: return "Multiple Choice"; case 301: return "Moved Permanently"; case 302: return "Found"; case 303: return "See Other"; case 304: return "Not Modified"; case 305: return "Use Proxy"; case 306: return "unused"; case 307: return "Temporary Redirect"; case 308: return "Permanent Redirect"; case 400: return "Bad Request"; case 401: return "Unauthorized"; case 402: return "Payment Required"; case 403: return "Forbidden"; case 404: return "Not Found"; case 405: return "Method Not Allowed"; case 406: return "Not Acceptable"; case 407: return "Proxy Authentication Required"; case 408: return "Request Timeout"; case 409: return "Conflict"; case 410: return "Gone"; case 411: return "Length Required"; case 412: return "Precondition Failed"; case 413: return "Payload Too Large"; case 414: return "URI Too Long"; case 415: return "Unsupported Media Type"; case 416: return "Range Not Satisfiable"; case 417: return "Expectation Failed"; case 418: return "I'm a teapot"; case 421: return "Misdirected Request"; case 422: return "Unprocessable Entity"; case 423: return "Locked"; case 424: return "Failed Dependency"; case 425: return "Too Early"; case 426: return "Upgrade Required"; case 428: return "Precondition Required"; case 429: return "Too Many Requests"; case 431: return "Request Header Fields Too Large"; case 451: return "Unavailable For Legal Reasons"; case 501: return "Not Implemented"; case 502: return "Bad Gateway"; case 503: return "Service Unavailable"; case 504: return "Gateway Timeout"; case 505: return "HTTP Version Not Supported"; case 506: return "Variant Also Negotiates"; case 507: return "Insufficient Storage"; case 508: return "Loop Detected"; case 510: return "Not Extended"; case 511: return "Network Authentication Required"; default: case 500: return "Internal Server Error"; } } template inline Server & Server::set_read_timeout(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); return *this; } template inline Server & Server::set_write_timeout(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); return *this; } template inline Server & Server::set_idle_interval(const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_idle_interval(sec, usec); }); return *this; } inline std::string to_string(const Error error) { switch (error) { case Error::Success: return "Success (no error)"; case Error::Connection: return "Could not establish connection"; case Error::BindIPAddress: return "Failed to bind IP address"; case Error::Read: return "Failed to read connection"; case Error::Write: return "Failed to write connection"; case Error::ExceedRedirectCount: return "Maximum redirect count exceeded"; case Error::Canceled: return "Connection handling canceled"; case Error::SSLConnection: return "SSL connection failed"; case Error::SSLLoadingCerts: return "SSL certificate loading failed"; case Error::SSLServerVerification: return "SSL server verification failed"; case Error::UnsupportedMultipartBoundaryChars: return "Unsupported HTTP multipart boundary characters"; case Error::Compression: return "Compression failed"; case Error::ConnectionTimeout: return "Connection timed out"; case Error::ProxyConnection: return "Proxy connection failed"; case Error::Unknown: return "Unknown"; default: break; } return "Invalid"; } inline std::ostream &operator<<(std::ostream &os, const Error &obj) { os << to_string(obj); os << " (" << static_cast::type>(obj) << ')'; return os; } template inline T Result::get_request_header_value(const std::string &key, size_t id) const { return detail::get_header_value(request_headers_, key, id, 0); } template inline void ClientImpl::set_connection_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec(duration, [&](time_t sec, time_t usec) { set_connection_timeout(sec, usec); }); } template inline void ClientImpl::set_read_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_read_timeout(sec, usec); }); } template inline void ClientImpl::set_write_timeout( const std::chrono::duration &duration) { detail::duration_to_sec_and_usec( duration, [&](time_t sec, time_t usec) { set_write_timeout(sec, usec); }); } template inline void Client::set_connection_timeout( const std::chrono::duration &duration) { cli_->set_connection_timeout(duration); } template inline void Client::set_read_timeout(const std::chrono::duration &duration) { cli_->set_read_timeout(duration); } template inline void Client::set_write_timeout(const std::chrono::duration &duration) { cli_->set_write_timeout(duration); } /* * Forward declarations and types that will be part of the .h file if split into * .h + .cc. */ std::string hosted_at(const std::string &hostname); void hosted_at(const std::string &hostname, std::vector &addrs); std::string append_query_params(const std::string &path, const Params ¶ms); std::pair make_range_header(Ranges ranges); std::pair make_basic_authentication_header(const std::string &username, const std::string &password, bool is_proxy = false); namespace detail { std::string encode_query_param(const std::string &value); std::string decode_url(const std::string &s, bool convert_plus_to_space); void read_file(const std::string &path, std::string &out); std::string trim_copy(const std::string &s); void split(const char *b, const char *e, char d, std::function fn); bool process_client_socket(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, std::function callback); socket_t create_client_socket( const std::string &host, const std::string &ip, int port, int address_family, bool tcp_nodelay, SocketOptions socket_options, time_t connection_timeout_sec, time_t connection_timeout_usec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, const std::string &intf, Error &error); const char *get_header_value(const Headers &headers, const std::string &key, size_t id = 0, const char *def = nullptr); std::string params_to_query_str(const Params ¶ms); void parse_query_text(const std::string &s, Params ¶ms); bool parse_multipart_boundary(const std::string &content_type, std::string &boundary); bool parse_range_header(const std::string &s, Ranges &ranges); int close_socket(socket_t sock); ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags); ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags); enum class EncodingType { None = 0, Gzip, Brotli }; EncodingType encoding_type(const Request &req, const Response &res); class BufferStream : public Stream { public: BufferStream() = default; ~BufferStream() override = default; bool is_readable() const override; bool is_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; const std::string &get_buffer() const; private: std::string buffer; size_t position = 0; }; class compressor { public: virtual ~compressor() = default; typedef std::function Callback; virtual bool compress(const char *data, size_t data_length, bool last, Callback callback) = 0; }; class decompressor { public: virtual ~decompressor() = default; virtual bool is_valid() const = 0; typedef std::function Callback; virtual bool decompress(const char *data, size_t data_length, Callback callback) = 0; }; class nocompressor : public compressor { public: virtual ~nocompressor() = default; bool compress(const char *data, size_t data_length, bool /*last*/, Callback callback) override; }; #ifdef CPPHTTPLIB_ZLIB_SUPPORT class gzip_compressor : public compressor { public: gzip_compressor(); ~gzip_compressor(); bool compress(const char *data, size_t data_length, bool last, Callback callback) override; private: bool is_valid_ = false; z_stream strm_; }; class gzip_decompressor : public decompressor { public: gzip_decompressor(); ~gzip_decompressor(); bool is_valid() const override; bool decompress(const char *data, size_t data_length, Callback callback) override; private: bool is_valid_ = false; z_stream strm_; }; #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT class brotli_compressor : public compressor { public: brotli_compressor(); ~brotli_compressor(); bool compress(const char *data, size_t data_length, bool last, Callback callback) override; private: BrotliEncoderState *state_ = nullptr; }; class brotli_decompressor : public decompressor { public: brotli_decompressor(); ~brotli_decompressor(); bool is_valid() const override; bool decompress(const char *data, size_t data_length, Callback callback) override; private: BrotliDecoderResult decoder_r; BrotliDecoderState *decoder_s = nullptr; }; #endif // NOTE: until the read size reaches `fixed_buffer_size`, use `fixed_buffer` // to store data. The call can set memory on stack for performance. class stream_line_reader { public: stream_line_reader(Stream &strm, char *fixed_buffer, size_t fixed_buffer_size); const char *ptr() const; size_t size() const; bool end_with_crlf() const; bool getline(); private: void append(char c); Stream &strm_; char *fixed_buffer_; const size_t fixed_buffer_size_; size_t fixed_buffer_used_size_ = 0; std::string glowable_buffer_; }; class mmap { public: mmap(const char *path); ~mmap(); bool open(const char *path); void close(); bool is_open() const; size_t size() const; const char *data() const; private: #if defined(_WIN32) HANDLE hFile_; HANDLE hMapping_; #else int fd_; #endif size_t size_; void *addr_; }; } // namespace detail // ---------------------------------------------------------------------------- /* * Implementation that will be part of the .cc file if split into .h + .cc. */ namespace detail { inline bool is_hex(char c, int &v) { if (0x20 <= c && isdigit(c)) { v = c - '0'; return true; } else if ('A' <= c && c <= 'F') { v = c - 'A' + 10; return true; } else if ('a' <= c && c <= 'f') { v = c - 'a' + 10; return true; } return false; } inline bool from_hex_to_i(const std::string &s, size_t i, size_t cnt, int &val) { if (i >= s.size()) { return false; } val = 0; for (; cnt; i++, cnt--) { if (!s[i]) { return false; } auto v = 0; if (is_hex(s[i], v)) { val = val * 16 + v; } else { return false; } } return true; } inline std::string from_i_to_hex(size_t n) { static const auto charset = "0123456789abcdef"; std::string ret; do { ret = charset[n & 15] + ret; n >>= 4; } while (n > 0); return ret; } inline size_t to_utf8(int code, char *buff) { if (code < 0x0080) { buff[0] = (code & 0x7F); return 1; } else if (code < 0x0800) { buff[0] = static_cast(0xC0 | ((code >> 6) & 0x1F)); buff[1] = static_cast(0x80 | (code & 0x3F)); return 2; } else if (code < 0xD800) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0xE000) { // D800 - DFFF is invalid... return 0; } else if (code < 0x10000) { buff[0] = static_cast(0xE0 | ((code >> 12) & 0xF)); buff[1] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[2] = static_cast(0x80 | (code & 0x3F)); return 3; } else if (code < 0x110000) { buff[0] = static_cast(0xF0 | ((code >> 18) & 0x7)); buff[1] = static_cast(0x80 | ((code >> 12) & 0x3F)); buff[2] = static_cast(0x80 | ((code >> 6) & 0x3F)); buff[3] = static_cast(0x80 | (code & 0x3F)); return 4; } // NOTREACHED return 0; } // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/180947/base64-decode-snippet-in-c inline std::string base64_encode(const std::string &in) { static const auto lookup = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; std::string out; out.reserve(in.size()); auto val = 0; auto valb = -6; for (auto c : in) { val = (val << 8) + static_cast(c); valb += 8; while (valb >= 0) { out.push_back(lookup[(val >> valb) & 0x3F]); valb -= 6; } } if (valb > -6) { out.push_back(lookup[((val << 8) >> (valb + 8)) & 0x3F]); } while (out.size() % 4) { out.push_back('='); } return out; } inline bool is_file(const std::string &path) { #ifdef _WIN32 return _access_s(path.c_str(), 0) == 0; #else struct stat st; return stat(path.c_str(), &st) >= 0 && S_ISREG(st.st_mode); #endif } inline bool is_dir(const std::string &path) { struct stat st; return stat(path.c_str(), &st) >= 0 && S_ISDIR(st.st_mode); } inline bool is_valid_path(const std::string &path) { size_t level = 0; size_t i = 0; // Skip slash while (i < path.size() && path[i] == '/') { i++; } while (i < path.size()) { // Read component auto beg = i; while (i < path.size() && path[i] != '/') { i++; } auto len = i - beg; assert(len > 0); if (!path.compare(beg, len, ".")) { ; } else if (!path.compare(beg, len, "..")) { if (level == 0) { return false; } level--; } else { level++; } // Skip slash while (i < path.size() && path[i] == '/') { i++; } } return true; } inline std::string encode_query_param(const std::string &value) { std::ostringstream escaped; escaped.fill('0'); escaped << std::hex; for (auto c : value) { if (std::isalnum(static_cast(c)) || c == '-' || c == '_' || c == '.' || c == '!' || c == '~' || c == '*' || c == '\'' || c == '(' || c == ')') { escaped << c; } else { escaped << std::uppercase; escaped << '%' << std::setw(2) << static_cast(static_cast(c)); escaped << std::nouppercase; } } return escaped.str(); } inline std::string encode_url(const std::string &s) { std::string result; result.reserve(s.size()); for (size_t i = 0; s[i]; i++) { switch (s[i]) { case ' ': result += "%20"; break; case '+': result += "%2B"; break; case '\r': result += "%0D"; break; case '\n': result += "%0A"; break; case '\'': result += "%27"; break; case ',': result += "%2C"; break; // case ':': result += "%3A"; break; // ok? probably... case ';': result += "%3B"; break; default: auto c = static_cast(s[i]); if (c >= 0x80) { result += '%'; char hex[4]; auto len = snprintf(hex, sizeof(hex) - 1, "%02X", c); assert(len == 2); result.append(hex, static_cast(len)); } else { result += s[i]; } break; } } return result; } inline std::string decode_url(const std::string &s, bool convert_plus_to_space) { std::string result; for (size_t i = 0; i < s.size(); i++) { if (s[i] == '%' && i + 1 < s.size()) { if (s[i + 1] == 'u') { auto val = 0; if (from_hex_to_i(s, i + 2, 4, val)) { // 4 digits Unicode codes char buff[4]; size_t len = to_utf8(val, buff); if (len > 0) { result.append(buff, len); } i += 5; // 'u0000' } else { result += s[i]; } } else { auto val = 0; if (from_hex_to_i(s, i + 1, 2, val)) { // 2 digits hex codes result += static_cast(val); i += 2; // '00' } else { result += s[i]; } } } else if (convert_plus_to_space && s[i] == '+') { result += ' '; } else { result += s[i]; } } return result; } inline void read_file(const std::string &path, std::string &out) { std::ifstream fs(path, std::ios_base::binary); fs.seekg(0, std::ios_base::end); auto size = fs.tellg(); fs.seekg(0); out.resize(static_cast(size)); fs.read(&out[0], static_cast(size)); } inline std::string file_extension(const std::string &path) { std::smatch m; static auto re = std::regex("\\.([a-zA-Z0-9]+)$"); if (std::regex_search(path, m, re)) { return m[1].str(); } return std::string(); } inline bool is_space_or_tab(char c) { return c == ' ' || c == '\t'; } inline std::pair trim(const char *b, const char *e, size_t left, size_t right) { while (b + left < e && is_space_or_tab(b[left])) { left++; } while (right > 0 && is_space_or_tab(b[right - 1])) { right--; } return std::make_pair(left, right); } inline std::string trim_copy(const std::string &s) { auto r = trim(s.data(), s.data() + s.size(), 0, s.size()); return s.substr(r.first, r.second - r.first); } inline std::string trim_double_quotes_copy(const std::string &s) { if (s.length() >= 2 && s.front() == '"' && s.back() == '"') { return s.substr(1, s.size() - 2); } return s; } inline void split(const char *b, const char *e, char d, std::function fn) { size_t i = 0; size_t beg = 0; while (e ? (b + i < e) : (b[i] != '\0')) { if (b[i] == d) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } beg = i + 1; } i++; } if (i) { auto r = trim(b, e, beg, i); if (r.first < r.second) { fn(&b[r.first], &b[r.second]); } } } inline stream_line_reader::stream_line_reader(Stream &strm, char *fixed_buffer, size_t fixed_buffer_size) : strm_(strm), fixed_buffer_(fixed_buffer), fixed_buffer_size_(fixed_buffer_size) {} inline const char *stream_line_reader::ptr() const { if (glowable_buffer_.empty()) { return fixed_buffer_; } else { return glowable_buffer_.data(); } } inline size_t stream_line_reader::size() const { if (glowable_buffer_.empty()) { return fixed_buffer_used_size_; } else { return glowable_buffer_.size(); } } inline bool stream_line_reader::end_with_crlf() const { auto end = ptr() + size(); return size() >= 2 && end[-2] == '\r' && end[-1] == '\n'; } inline bool stream_line_reader::getline() { fixed_buffer_used_size_ = 0; glowable_buffer_.clear(); for (size_t i = 0;; i++) { char byte; auto n = strm_.read(&byte, 1); if (n < 0) { return false; } else if (n == 0) { if (i == 0) { return false; } else { break; } } append(byte); if (byte == '\n') { break; } } return true; } inline void stream_line_reader::append(char c) { if (fixed_buffer_used_size_ < fixed_buffer_size_ - 1) { fixed_buffer_[fixed_buffer_used_size_++] = c; fixed_buffer_[fixed_buffer_used_size_] = '\0'; } else { if (glowable_buffer_.empty()) { assert(fixed_buffer_[fixed_buffer_used_size_] == '\0'); glowable_buffer_.assign(fixed_buffer_, fixed_buffer_used_size_); } glowable_buffer_ += c; } } inline mmap::mmap(const char *path) #if defined(_WIN32) : hFile_(NULL), hMapping_(NULL) #else : fd_(-1) #endif , size_(0), addr_(nullptr) { if (!open(path)) { std::runtime_error(""); } } inline mmap::~mmap() { close(); } inline bool mmap::open(const char *path) { close(); #if defined(_WIN32) hFile_ = ::CreateFileA(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile_ == INVALID_HANDLE_VALUE) { return false; } size_ = ::GetFileSize(hFile_, NULL); hMapping_ = ::CreateFileMapping(hFile_, NULL, PAGE_READONLY, 0, 0, NULL); if (hMapping_ == NULL) { close(); return false; } addr_ = ::MapViewOfFile(hMapping_, FILE_MAP_READ, 0, 0, 0); #else fd_ = ::open(path, O_RDONLY); if (fd_ == -1) { return false; } struct stat sb; if (fstat(fd_, &sb) == -1) { close(); return false; } size_ = static_cast(sb.st_size); addr_ = ::mmap(NULL, size_, PROT_READ, MAP_PRIVATE, fd_, 0); #endif if (addr_ == nullptr) { close(); return false; } return true; } inline bool mmap::is_open() const { return addr_ != nullptr; } inline size_t mmap::size() const { return size_; } inline const char *mmap::data() const { return (const char *)addr_; } inline void mmap::close() { #if defined(_WIN32) if (addr_) { ::UnmapViewOfFile(addr_); addr_ = nullptr; } if (hMapping_) { ::CloseHandle(hMapping_); hMapping_ = NULL; } if (hFile_ != INVALID_HANDLE_VALUE) { ::CloseHandle(hFile_); hFile_ = INVALID_HANDLE_VALUE; } #else if (addr_ != nullptr) { munmap(addr_, size_); addr_ = nullptr; } if (fd_ != -1) { ::close(fd_); fd_ = -1; } #endif size_ = 0; } inline int close_socket(socket_t sock) { #ifdef _WIN32 return closesocket(sock); #else return close(sock); #endif } template inline ssize_t handle_EINTR(T fn) { ssize_t res = 0; while (true) { res = fn(); if (res < 0 && errno == EINTR) { continue; } break; } return res; } inline ssize_t read_socket(socket_t sock, void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return recv(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } inline ssize_t send_socket(socket_t sock, const void *ptr, size_t size, int flags) { return handle_EINTR([&]() { return send(sock, #ifdef _WIN32 static_cast(ptr), static_cast(size), #else ptr, size, #endif flags); }); } inline ssize_t select_read(socket_t sock, time_t sec, time_t usec) { #ifdef CPPHTTPLIB_USE_POLL struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN; auto timeout = static_cast(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); #else #ifndef _WIN32 if (sock >= FD_SETSIZE) { return 1; } #endif fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds); timeval tv; tv.tv_sec = static_cast(sec); tv.tv_usec = static_cast(usec); return handle_EINTR([&]() { return select(static_cast(sock + 1), &fds, nullptr, nullptr, &tv); }); #endif } inline ssize_t select_write(socket_t sock, time_t sec, time_t usec) { #ifdef CPPHTTPLIB_USE_POLL struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLOUT; auto timeout = static_cast(sec * 1000 + usec / 1000); return handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); #else #ifndef _WIN32 if (sock >= FD_SETSIZE) { return 1; } #endif fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds); timeval tv; tv.tv_sec = static_cast(sec); tv.tv_usec = static_cast(usec); return handle_EINTR([&]() { return select(static_cast(sock + 1), nullptr, &fds, nullptr, &tv); }); #endif } inline Error wait_until_socket_is_ready(socket_t sock, time_t sec, time_t usec) { #ifdef CPPHTTPLIB_USE_POLL struct pollfd pfd_read; pfd_read.fd = sock; pfd_read.events = POLLIN | POLLOUT; auto timeout = static_cast(sec * 1000 + usec / 1000); auto poll_res = handle_EINTR([&]() { return poll(&pfd_read, 1, timeout); }); if (poll_res == 0) { return Error::ConnectionTimeout; } if (poll_res > 0 && pfd_read.revents & (POLLIN | POLLOUT)) { auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); auto successful = res >= 0 && !error; return successful ? Error::Success : Error::Connection; } return Error::Connection; #else #ifndef _WIN32 if (sock >= FD_SETSIZE) { return Error::Connection; } #endif fd_set fdsr; FD_ZERO(&fdsr); FD_SET(sock, &fdsr); auto fdsw = fdsr; auto fdse = fdsr; timeval tv; tv.tv_sec = static_cast(sec); tv.tv_usec = static_cast(usec); auto ret = handle_EINTR([&]() { return select(static_cast(sock + 1), &fdsr, &fdsw, &fdse, &tv); }); if (ret == 0) { return Error::ConnectionTimeout; } if (ret > 0 && (FD_ISSET(sock, &fdsr) || FD_ISSET(sock, &fdsw))) { auto error = 0; socklen_t len = sizeof(error); auto res = getsockopt(sock, SOL_SOCKET, SO_ERROR, reinterpret_cast(&error), &len); auto successful = res >= 0 && !error; return successful ? Error::Success : Error::Connection; } return Error::Connection; #endif } inline bool is_socket_alive(socket_t sock) { const auto val = detail::select_read(sock, 0, 0); if (val == 0) { return true; } else if (val < 0 && errno == EBADF) { return false; } char buf[1]; return detail::read_socket(sock, &buf[0], sizeof(buf), MSG_PEEK) > 0; } class SocketStream : public Stream { public: SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec); ~SocketStream() override; bool is_readable() const override; bool is_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; private: socket_t sock_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; std::vector read_buff_; size_t read_buff_off_ = 0; size_t read_buff_content_size_ = 0; static const size_t read_buff_size_ = 1024 * 4; }; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT class SSLSocketStream : public Stream { public: SSLSocketStream(socket_t sock, SSL *ssl, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec); ~SSLSocketStream() override; bool is_readable() const override; bool is_writable() const override; ssize_t read(char *ptr, size_t size) override; ssize_t write(const char *ptr, size_t size) override; void get_remote_ip_and_port(std::string &ip, int &port) const override; void get_local_ip_and_port(std::string &ip, int &port) const override; socket_t socket() const override; private: socket_t sock_; SSL *ssl_; time_t read_timeout_sec_; time_t read_timeout_usec_; time_t write_timeout_sec_; time_t write_timeout_usec_; }; #endif inline bool keep_alive(socket_t sock, time_t keep_alive_timeout_sec) { using namespace std::chrono; auto start = steady_clock::now(); while (true) { auto val = select_read(sock, 0, 10000); if (val < 0) { return false; } else if (val == 0) { auto current = steady_clock::now(); auto duration = duration_cast(current - start); auto timeout = keep_alive_timeout_sec * 1000; if (duration.count() > timeout) { return false; } std::this_thread::sleep_for(std::chrono::milliseconds(1)); } else { return true; } } } template inline bool process_server_socket_core(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, T callback) { assert(keep_alive_max_count > 0); auto ret = false; auto count = keep_alive_max_count; while (svr_sock != INVALID_SOCKET && count > 0 && keep_alive(sock, keep_alive_timeout_sec)) { auto close_connection = count == 1; auto connection_closed = false; ret = callback(close_connection, connection_closed); if (!ret || connection_closed) { break; } count--; } return ret; } template inline bool process_server_socket(const std::atomic &svr_sock, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } inline bool process_client_socket(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, std::function callback) { SocketStream strm(sock, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm); } inline int shutdown_socket(socket_t sock) { #ifdef _WIN32 return shutdown(sock, SD_BOTH); #else return shutdown(sock, SHUT_RDWR); #endif } template socket_t create_socket(const std::string &host, const std::string &ip, int port, int address_family, int socket_flags, bool tcp_nodelay, SocketOptions socket_options, BindOrConnect bind_or_connect) { // Get address info const char *node = nullptr; struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (!ip.empty()) { node = ip.c_str(); // Ask getaddrinfo to convert IP in c-string to address hints.ai_family = AF_UNSPEC; hints.ai_flags = AI_NUMERICHOST; } else { if (!host.empty()) { node = host.c_str(); } hints.ai_family = address_family; hints.ai_flags = socket_flags; } #ifndef _WIN32 if (hints.ai_family == AF_UNIX) { const auto addrlen = host.length(); if (addrlen > sizeof(sockaddr_un::sun_path)) return INVALID_SOCKET; auto sock = socket(hints.ai_family, hints.ai_socktype, hints.ai_protocol); if (sock != INVALID_SOCKET) { sockaddr_un addr{}; addr.sun_family = AF_UNIX; std::copy(host.begin(), host.end(), addr.sun_path); hints.ai_addr = reinterpret_cast(&addr); hints.ai_addrlen = static_cast( sizeof(addr) - sizeof(addr.sun_path) + addrlen); fcntl(sock, F_SETFD, FD_CLOEXEC); if (socket_options) { socket_options(sock); } if (!bind_or_connect(sock, hints)) { close_socket(sock); sock = INVALID_SOCKET; } } return sock; } #endif auto service = std::to_string(port); if (getaddrinfo(node, service.c_str(), &hints, &result)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return INVALID_SOCKET; } for (auto rp = result; rp; rp = rp->ai_next) { // Create a socket #ifdef _WIN32 auto sock = WSASocketW(rp->ai_family, rp->ai_socktype, rp->ai_protocol, nullptr, 0, WSA_FLAG_NO_HANDLE_INHERIT | WSA_FLAG_OVERLAPPED); /** * Since the WSA_FLAG_NO_HANDLE_INHERIT is only supported on Windows 7 SP1 * and above the socket creation fails on older Windows Systems. * * Let's try to create a socket the old way in this case. * * Reference: * https://docs.microsoft.com/en-us/windows/win32/api/winsock2/nf-winsock2-wsasocketa * * WSA_FLAG_NO_HANDLE_INHERIT: * This flag is supported on Windows 7 with SP1, Windows Server 2008 R2 with * SP1, and later * */ if (sock == INVALID_SOCKET) { sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); } #else auto sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol); #endif if (sock == INVALID_SOCKET) { continue; } #ifndef _WIN32 if (fcntl(sock, F_SETFD, FD_CLOEXEC) == -1) { close_socket(sock); continue; } #endif if (tcp_nodelay) { auto yes = 1; #ifdef _WIN32 setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), sizeof(yes)); #else setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, reinterpret_cast(&yes), sizeof(yes)); #endif } if (socket_options) { socket_options(sock); } if (rp->ai_family == AF_INET6) { auto no = 0; #ifdef _WIN32 setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&no), sizeof(no)); #else setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&no), sizeof(no)); #endif } // bind or connect if (bind_or_connect(sock, *rp)) { freeaddrinfo(result); return sock; } close_socket(sock); } freeaddrinfo(result); return INVALID_SOCKET; } inline void set_nonblocking(socket_t sock, bool nonblocking) { #ifdef _WIN32 auto flags = nonblocking ? 1UL : 0UL; ioctlsocket(sock, FIONBIO, &flags); #else auto flags = fcntl(sock, F_GETFL, 0); fcntl(sock, F_SETFL, nonblocking ? (flags | O_NONBLOCK) : (flags & (~O_NONBLOCK))); #endif } inline bool is_connection_error() { #ifdef _WIN32 return WSAGetLastError() != WSAEWOULDBLOCK; #else return errno != EINPROGRESS; #endif } inline bool bind_ip_address(socket_t sock, const std::string &host) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (getaddrinfo(host.c_str(), "0", &hints, &result)) { return false; } auto ret = false; for (auto rp = result; rp; rp = rp->ai_next) { const auto &ai = *rp; if (!::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { ret = true; break; } } freeaddrinfo(result); return ret; } #if !defined _WIN32 && !defined ANDROID && !defined _AIX && !defined __MVS__ #define USE_IF2IP #endif #ifdef USE_IF2IP inline std::string if2ip(int address_family, const std::string &ifn) { struct ifaddrs *ifap; getifaddrs(&ifap); std::string addr_candidate; for (auto ifa = ifap; ifa; ifa = ifa->ifa_next) { if (ifa->ifa_addr && ifn == ifa->ifa_name && (AF_UNSPEC == address_family || ifa->ifa_addr->sa_family == address_family)) { if (ifa->ifa_addr->sa_family == AF_INET) { auto sa = reinterpret_cast(ifa->ifa_addr); char buf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &sa->sin_addr, buf, INET_ADDRSTRLEN)) { freeifaddrs(ifap); return std::string(buf, INET_ADDRSTRLEN); } } else if (ifa->ifa_addr->sa_family == AF_INET6) { auto sa = reinterpret_cast(ifa->ifa_addr); if (!IN6_IS_ADDR_LINKLOCAL(&sa->sin6_addr)) { char buf[INET6_ADDRSTRLEN] = {}; if (inet_ntop(AF_INET6, &sa->sin6_addr, buf, INET6_ADDRSTRLEN)) { // equivalent to mac's IN6_IS_ADDR_UNIQUE_LOCAL auto s6_addr_head = sa->sin6_addr.s6_addr[0]; if (s6_addr_head == 0xfc || s6_addr_head == 0xfd) { addr_candidate = std::string(buf, INET6_ADDRSTRLEN); } else { freeifaddrs(ifap); return std::string(buf, INET6_ADDRSTRLEN); } } } } } } freeifaddrs(ifap); return addr_candidate; } #endif inline socket_t create_client_socket( const std::string &host, const std::string &ip, int port, int address_family, bool tcp_nodelay, SocketOptions socket_options, time_t connection_timeout_sec, time_t connection_timeout_usec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, const std::string &intf, Error &error) { auto sock = create_socket( host, ip, port, address_family, 0, tcp_nodelay, std::move(socket_options), [&](socket_t sock2, struct addrinfo &ai) -> bool { if (!intf.empty()) { #ifdef USE_IF2IP auto ip_from_if = if2ip(address_family, intf); if (ip_from_if.empty()) { ip_from_if = intf; } if (!bind_ip_address(sock2, ip_from_if.c_str())) { error = Error::BindIPAddress; return false; } #endif } set_nonblocking(sock2, true); auto ret = ::connect(sock2, ai.ai_addr, static_cast(ai.ai_addrlen)); if (ret < 0) { if (is_connection_error()) { error = Error::Connection; return false; } error = wait_until_socket_is_ready(sock2, connection_timeout_sec, connection_timeout_usec); if (error != Error::Success) { return false; } } set_nonblocking(sock2, false); { #ifdef _WIN32 auto timeout = static_cast(read_timeout_sec * 1000 + read_timeout_usec / 1000); setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(read_timeout_sec); tv.tv_usec = static_cast(read_timeout_usec); setsockopt(sock2, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv), sizeof(tv)); #endif } { #ifdef _WIN32 auto timeout = static_cast(write_timeout_sec * 1000 + write_timeout_usec / 1000); setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(write_timeout_sec); tv.tv_usec = static_cast(write_timeout_usec); setsockopt(sock2, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&tv), sizeof(tv)); #endif } error = Error::Success; return true; }); if (sock != INVALID_SOCKET) { error = Error::Success; } else { if (error == Error::Success) { error = Error::Connection; } } return sock; } inline bool get_ip_and_port(const struct sockaddr_storage &addr, socklen_t addr_len, std::string &ip, int &port) { if (addr.ss_family == AF_INET) { port = ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { port = ntohs(reinterpret_cast(&addr)->sin6_port); } else { return false; } std::array ipstr{}; if (getnameinfo(reinterpret_cast(&addr), addr_len, ipstr.data(), static_cast(ipstr.size()), nullptr, 0, NI_NUMERICHOST)) { return false; } ip = ipstr.data(); return true; } inline void get_local_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getsockname(sock, reinterpret_cast(&addr), &addr_len)) { get_ip_and_port(addr, addr_len, ip, port); } } inline void get_remote_ip_and_port(socket_t sock, std::string &ip, int &port) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (!getpeername(sock, reinterpret_cast(&addr), &addr_len)) { #ifndef _WIN32 if (addr.ss_family == AF_UNIX) { #if defined(__linux__) struct ucred ucred; socklen_t len = sizeof(ucred); if (getsockopt(sock, SOL_SOCKET, SO_PEERCRED, &ucred, &len) == 0) { port = ucred.pid; } #elif defined(SOL_LOCAL) && defined(SO_PEERPID) // __APPLE__ pid_t pid; socklen_t len = sizeof(pid); if (getsockopt(sock, SOL_LOCAL, SO_PEERPID, &pid, &len) == 0) { port = pid; } #endif return; } #endif get_ip_and_port(addr, addr_len, ip, port); } } inline constexpr unsigned int str2tag_core(const char *s, size_t l, unsigned int h) { return (l == 0) ? h : str2tag_core( s + 1, l - 1, // Unsets the 6 high bits of h, therefore no overflow happens (((std::numeric_limits::max)() >> 6) & h * 33) ^ static_cast(*s)); } inline unsigned int str2tag(const std::string &s) { return str2tag_core(s.data(), s.size(), 0); } namespace udl { inline constexpr unsigned int operator"" _t(const char *s, size_t l) { return str2tag_core(s, l, 0); } } // namespace udl inline std::string find_content_type(const std::string &path, const std::map &user_data, const std::string &default_content_type) { auto ext = file_extension(path); auto it = user_data.find(ext); if (it != user_data.end()) { return it->second.c_str(); } using udl::operator""_t; switch (str2tag(ext)) { default: return default_content_type; case "css"_t: return "text/css"; case "csv"_t: return "text/csv"; case "htm"_t: case "html"_t: return "text/html"; case "js"_t: case "mjs"_t: return "text/javascript"; case "txt"_t: return "text/plain"; case "vtt"_t: return "text/vtt"; case "apng"_t: return "image/apng"; case "avif"_t: return "image/avif"; case "bmp"_t: return "image/bmp"; case "gif"_t: return "image/gif"; case "png"_t: return "image/png"; case "svg"_t: return "image/svg+xml"; case "webp"_t: return "image/webp"; case "ico"_t: return "image/x-icon"; case "tif"_t: return "image/tiff"; case "tiff"_t: return "image/tiff"; case "jpg"_t: case "jpeg"_t: return "image/jpeg"; case "mp4"_t: return "video/mp4"; case "mpeg"_t: return "video/mpeg"; case "webm"_t: return "video/webm"; case "mp3"_t: return "audio/mp3"; case "mpga"_t: return "audio/mpeg"; case "weba"_t: return "audio/webm"; case "wav"_t: return "audio/wave"; case "otf"_t: return "font/otf"; case "ttf"_t: return "font/ttf"; case "woff"_t: return "font/woff"; case "woff2"_t: return "font/woff2"; case "7z"_t: return "application/x-7z-compressed"; case "atom"_t: return "application/atom+xml"; case "pdf"_t: return "application/pdf"; case "json"_t: return "application/json"; case "rss"_t: return "application/rss+xml"; case "tar"_t: return "application/x-tar"; case "xht"_t: case "xhtml"_t: return "application/xhtml+xml"; case "xslt"_t: return "application/xslt+xml"; case "xml"_t: return "application/xml"; case "gz"_t: return "application/gzip"; case "zip"_t: return "application/zip"; case "wasm"_t: return "application/wasm"; } } inline bool can_compress_content_type(const std::string &content_type) { using udl::operator""_t; auto tag = str2tag(content_type); switch (tag) { case "image/svg+xml"_t: case "application/javascript"_t: case "application/json"_t: case "application/xml"_t: case "application/protobuf"_t: case "application/xhtml+xml"_t: return true; default: return !content_type.rfind("text/", 0) && tag != "text/event-stream"_t; } } inline EncodingType encoding_type(const Request &req, const Response &res) { auto ret = detail::can_compress_content_type(res.get_header_value("Content-Type")); if (!ret) { return EncodingType::None; } const auto &s = req.get_header_value("Accept-Encoding"); (void)(s); #ifdef CPPHTTPLIB_BROTLI_SUPPORT // TODO: 'Accept-Encoding' has br, not br;q=0 ret = s.find("br") != std::string::npos; if (ret) { return EncodingType::Brotli; } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT // TODO: 'Accept-Encoding' has gzip, not gzip;q=0 ret = s.find("gzip") != std::string::npos; if (ret) { return EncodingType::Gzip; } #endif return EncodingType::None; } inline bool nocompressor::compress(const char *data, size_t data_length, bool /*last*/, Callback callback) { if (!data_length) { return true; } return callback(data, data_length); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT inline gzip_compressor::gzip_compressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; is_valid_ = deflateInit2(&strm_, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 31, 8, Z_DEFAULT_STRATEGY) == Z_OK; } inline gzip_compressor::~gzip_compressor() { deflateEnd(&strm_); } inline bool gzip_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { assert(is_valid_); do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; auto flush = (last && data_length == 0) ? Z_FINISH : Z_NO_FLUSH; auto ret = Z_OK; std::array buff{}; do { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = deflate(&strm_, flush); if (ret == Z_STREAM_ERROR) { return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } while (strm_.avail_out == 0); assert((flush == Z_FINISH && ret == Z_STREAM_END) || (flush == Z_NO_FLUSH && ret == Z_OK)); assert(strm_.avail_in == 0); } while (data_length > 0); return true; } inline gzip_decompressor::gzip_decompressor() { std::memset(&strm_, 0, sizeof(strm_)); strm_.zalloc = Z_NULL; strm_.zfree = Z_NULL; strm_.opaque = Z_NULL; // 15 is the value of wbits, which should be at the maximum possible value // to ensure that any gzip stream can be decoded. The offset of 32 specifies // that the stream type should be automatically detected either gzip or // deflate. is_valid_ = inflateInit2(&strm_, 32 + 15) == Z_OK; } inline gzip_decompressor::~gzip_decompressor() { inflateEnd(&strm_); } inline bool gzip_decompressor::is_valid() const { return is_valid_; } inline bool gzip_decompressor::decompress(const char *data, size_t data_length, Callback callback) { assert(is_valid_); auto ret = Z_OK; do { constexpr size_t max_avail_in = (std::numeric_limits::max)(); strm_.avail_in = static_cast( (std::min)(data_length, max_avail_in)); strm_.next_in = const_cast(reinterpret_cast(data)); data_length -= strm_.avail_in; data += strm_.avail_in; std::array buff{}; while (strm_.avail_in > 0 && ret == Z_OK) { strm_.avail_out = static_cast(buff.size()); strm_.next_out = reinterpret_cast(buff.data()); ret = inflate(&strm_, Z_NO_FLUSH); assert(ret != Z_STREAM_ERROR); switch (ret) { case Z_NEED_DICT: case Z_DATA_ERROR: case Z_MEM_ERROR: inflateEnd(&strm_); return false; } if (!callback(buff.data(), buff.size() - strm_.avail_out)) { return false; } } if (ret != Z_OK && ret != Z_STREAM_END) return false; } while (data_length > 0); return true; } #endif #ifdef CPPHTTPLIB_BROTLI_SUPPORT inline brotli_compressor::brotli_compressor() { state_ = BrotliEncoderCreateInstance(nullptr, nullptr, nullptr); } inline brotli_compressor::~brotli_compressor() { BrotliEncoderDestroyInstance(state_); } inline bool brotli_compressor::compress(const char *data, size_t data_length, bool last, Callback callback) { std::array buff{}; auto operation = last ? BROTLI_OPERATION_FINISH : BROTLI_OPERATION_PROCESS; auto available_in = data_length; auto next_in = reinterpret_cast(data); for (;;) { if (last) { if (BrotliEncoderIsFinished(state_)) { break; } } else { if (!available_in) { break; } } auto available_out = buff.size(); auto next_out = buff.data(); if (!BrotliEncoderCompressStream(state_, operation, &available_in, &next_in, &available_out, &next_out, nullptr)) { return false; } auto output_bytes = buff.size() - available_out; if (output_bytes) { callback(reinterpret_cast(buff.data()), output_bytes); } } return true; } inline brotli_decompressor::brotli_decompressor() { decoder_s = BrotliDecoderCreateInstance(0, 0, 0); decoder_r = decoder_s ? BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT : BROTLI_DECODER_RESULT_ERROR; } inline brotli_decompressor::~brotli_decompressor() { if (decoder_s) { BrotliDecoderDestroyInstance(decoder_s); } } inline bool brotli_decompressor::is_valid() const { return decoder_s; } inline bool brotli_decompressor::decompress(const char *data, size_t data_length, Callback callback) { if (decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_ERROR) { return 0; } auto next_in = reinterpret_cast(data); size_t avail_in = data_length; size_t total_out; decoder_r = BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT; std::array buff{}; while (decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_OUTPUT) { char *next_out = buff.data(); size_t avail_out = buff.size(); decoder_r = BrotliDecoderDecompressStream( decoder_s, &avail_in, &next_in, &avail_out, reinterpret_cast(&next_out), &total_out); if (decoder_r == BROTLI_DECODER_RESULT_ERROR) { return false; } if (!callback(buff.data(), buff.size() - avail_out)) { return false; } } return decoder_r == BROTLI_DECODER_RESULT_SUCCESS || decoder_r == BROTLI_DECODER_RESULT_NEEDS_MORE_INPUT; } #endif inline bool has_header(const Headers &headers, const std::string &key) { return headers.find(key) != headers.end(); } inline const char *get_header_value(const Headers &headers, const std::string &key, size_t id, const char *def) { auto rng = headers.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second.c_str(); } return def; } inline bool compare_case_ignore(const std::string &a, const std::string &b) { if (a.size() != b.size()) { return false; } for (size_t i = 0; i < b.size(); i++) { if (::tolower(a[i]) != ::tolower(b[i])) { return false; } } return true; } template inline bool parse_header(const char *beg, const char *end, T fn) { // Skip trailing spaces and tabs. while (beg < end && is_space_or_tab(end[-1])) { end--; } auto p = beg; while (p < end && *p != ':') { p++; } if (p == end) { return false; } auto key_end = p; if (*p++ != ':') { return false; } while (p < end && is_space_or_tab(*p)) { p++; } if (p < end) { auto key = std::string(beg, key_end); auto val = compare_case_ignore(key, "Location") ? std::string(p, end) : decode_url(std::string(p, end), false); fn(std::move(key), std::move(val)); return true; } return false; } inline bool read_headers(Stream &strm, Headers &headers) { const auto bufsiz = 2048; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); for (;;) { if (!line_reader.getline()) { return false; } // Check if the line ends with CRLF. auto line_terminator_len = 2; if (line_reader.end_with_crlf()) { // Blank line indicates end of headers. if (line_reader.size() == 2) { break; } #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR } else { // Blank line indicates end of headers. if (line_reader.size() == 1) { break; } line_terminator_len = 1; } #else } else { continue; // Skip invalid line. } #endif if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } // Exclude line terminator auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; parse_header(line_reader.ptr(), end, [&](std::string &&key, std::string &&val) { headers.emplace(std::move(key), std::move(val)); }); } return true; } inline bool read_content_with_length(Stream &strm, uint64_t len, Progress progress, ContentReceiverWithProgress out) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; uint64_t r = 0; while (r < len) { auto read_len = static_cast(len - r); auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); if (n <= 0) { return false; } if (!out(buf, static_cast(n), r, len)) { return false; } r += static_cast(n); if (progress) { if (!progress(r, len)) { return false; } } } return true; } inline void skip_content_with_length(Stream &strm, uint64_t len) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; uint64_t r = 0; while (r < len) { auto read_len = static_cast(len - r); auto n = strm.read(buf, (std::min)(read_len, CPPHTTPLIB_RECV_BUFSIZ)); if (n <= 0) { return; } r += static_cast(n); } } inline bool read_content_without_length(Stream &strm, ContentReceiverWithProgress out) { char buf[CPPHTTPLIB_RECV_BUFSIZ]; uint64_t r = 0; for (;;) { auto n = strm.read(buf, CPPHTTPLIB_RECV_BUFSIZ); if (n < 0) { return false; } else if (n == 0) { return true; } if (!out(buf, static_cast(n), r, 0)) { return false; } r += static_cast(n); } return true; } template inline bool read_content_chunked(Stream &strm, T &x, ContentReceiverWithProgress out) { const auto bufsiz = 16; char buf[bufsiz]; stream_line_reader line_reader(strm, buf, bufsiz); if (!line_reader.getline()) { return false; } unsigned long chunk_len; while (true) { char *end_ptr; chunk_len = std::strtoul(line_reader.ptr(), &end_ptr, 16); if (end_ptr == line_reader.ptr()) { return false; } if (chunk_len == ULONG_MAX) { return false; } if (chunk_len == 0) { break; } if (!read_content_with_length(strm, chunk_len, nullptr, out)) { return false; } if (!line_reader.getline()) { return false; } if (strcmp(line_reader.ptr(), "\r\n")) { return false; } if (!line_reader.getline()) { return false; } } assert(chunk_len == 0); // Trailer if (!line_reader.getline()) { return false; } while (strcmp(line_reader.ptr(), "\r\n")) { if (line_reader.size() > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } // Exclude line terminator constexpr auto line_terminator_len = 2; auto end = line_reader.ptr() + line_reader.size() - line_terminator_len; parse_header(line_reader.ptr(), end, [&](std::string &&key, std::string &&val) { x.headers.emplace(std::move(key), std::move(val)); }); if (!line_reader.getline()) { return false; } } return true; } inline bool is_chunked_transfer_encoding(const Headers &headers) { return !strcasecmp(get_header_value(headers, "Transfer-Encoding", 0, ""), "chunked"); } template bool prepare_content_receiver(T &x, int &status, ContentReceiverWithProgress receiver, bool decompress, U callback) { if (decompress) { std::string encoding = x.get_header_value("Content-Encoding"); std::unique_ptr decompressor; if (encoding == "gzip" || encoding == "deflate") { #ifdef CPPHTTPLIB_ZLIB_SUPPORT decompressor = detail::make_unique(); #else status = 415; return false; #endif } else if (encoding.find("br") != std::string::npos) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT decompressor = detail::make_unique(); #else status = 415; return false; #endif } if (decompressor) { if (decompressor->is_valid()) { ContentReceiverWithProgress out = [&](const char *buf, size_t n, uint64_t off, uint64_t len) { return decompressor->decompress(buf, n, [&](const char *buf2, size_t n2) { return receiver(buf2, n2, off, len); }); }; return callback(std::move(out)); } else { status = 500; return false; } } } ContentReceiverWithProgress out = [&](const char *buf, size_t n, uint64_t off, uint64_t len) { return receiver(buf, n, off, len); }; return callback(std::move(out)); } template bool read_content(Stream &strm, T &x, size_t payload_max_length, int &status, Progress progress, ContentReceiverWithProgress receiver, bool decompress) { return prepare_content_receiver( x, status, std::move(receiver), decompress, [&](const ContentReceiverWithProgress &out) { auto ret = true; auto exceed_payload_max_length = false; if (is_chunked_transfer_encoding(x.headers)) { ret = read_content_chunked(strm, x, out); } else if (!has_header(x.headers, "Content-Length")) { ret = read_content_without_length(strm, out); } else { auto len = get_header_value(x.headers, "Content-Length"); if (len > payload_max_length) { exceed_payload_max_length = true; skip_content_with_length(strm, len); ret = false; } else if (len > 0) { ret = read_content_with_length(strm, len, std::move(progress), out); } } if (!ret) { status = exceed_payload_max_length ? 413 : 400; } return ret; }); } // namespace detail inline ssize_t write_headers(Stream &strm, const Headers &headers) { ssize_t write_len = 0; for (const auto &x : headers) { auto len = strm.write_format("%s: %s\r\n", x.first.c_str(), x.second.c_str()); if (len < 0) { return len; } write_len += len; } auto len = strm.write("\r\n"); if (len < 0) { return len; } write_len += len; return write_len; } inline bool write_data(Stream &strm, const char *d, size_t l) { size_t offset = 0; while (offset < l) { auto length = strm.write(d + offset, l - offset); if (length < 0) { return false; } offset += static_cast(length); } return true; } template inline bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, T is_shutting_down, Error &error) { size_t end_offset = offset + length; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { if (strm.is_writable() && write_data(strm, d, l)) { offset += l; } else { ok = false; } } return ok; }; while (offset < end_offset && !is_shutting_down()) { if (!strm.is_writable()) { error = Error::Write; return false; } else if (!content_provider(offset, end_offset - offset, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } error = Error::Success; return true; } template inline bool write_content(Stream &strm, const ContentProvider &content_provider, size_t offset, size_t length, const T &is_shutting_down) { auto error = Error::Success; return write_content(strm, content_provider, offset, length, is_shutting_down, error); } template inline bool write_content_without_length(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { offset += l; if (!strm.is_writable() || !write_data(strm, d, l)) { ok = false; } } return ok; }; data_sink.done = [&](void) { data_available = false; }; while (data_available && !is_shutting_down()) { if (!strm.is_writable()) { return false; } else if (!content_provider(offset, 0, data_sink)) { return false; } else if (!ok) { return false; } } return true; } template inline bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor, Error &error) { size_t offset = 0; auto data_available = true; auto ok = true; DataSink data_sink; data_sink.write = [&](const char *d, size_t l) -> bool { if (ok) { data_available = l > 0; offset += l; std::string payload; if (compressor.compress(d, l, false, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!strm.is_writable() || !write_data(strm, chunk.data(), chunk.size())) { ok = false; } } } else { ok = false; } } return ok; }; auto done_with_trailer = [&](const Headers *trailer) { if (!ok) { return; } data_available = false; std::string payload; if (!compressor.compress(nullptr, 0, true, [&](const char *data, size_t data_len) { payload.append(data, data_len); return true; })) { ok = false; return; } if (!payload.empty()) { // Emit chunked response header and footer for each chunk auto chunk = from_i_to_hex(payload.size()) + "\r\n" + payload + "\r\n"; if (!strm.is_writable() || !write_data(strm, chunk.data(), chunk.size())) { ok = false; return; } } static const std::string done_marker("0\r\n"); if (!write_data(strm, done_marker.data(), done_marker.size())) { ok = false; } // Trailer if (trailer) { for (const auto &kv : *trailer) { std::string field_line = kv.first + ": " + kv.second + "\r\n"; if (!write_data(strm, field_line.data(), field_line.size())) { ok = false; } } } static const std::string crlf("\r\n"); if (!write_data(strm, crlf.data(), crlf.size())) { ok = false; } }; data_sink.done = [&](void) { done_with_trailer(nullptr); }; data_sink.done_with_trailer = [&](const Headers &trailer) { done_with_trailer(&trailer); }; while (data_available && !is_shutting_down()) { if (!strm.is_writable()) { error = Error::Write; return false; } else if (!content_provider(offset, 0, data_sink)) { error = Error::Canceled; return false; } else if (!ok) { error = Error::Write; return false; } } error = Error::Success; return true; } template inline bool write_content_chunked(Stream &strm, const ContentProvider &content_provider, const T &is_shutting_down, U &compressor) { auto error = Error::Success; return write_content_chunked(strm, content_provider, is_shutting_down, compressor, error); } template inline bool redirect(T &cli, Request &req, Response &res, const std::string &path, const std::string &location, Error &error) { Request new_req = req; new_req.path = path; new_req.redirect_count_ -= 1; if (res.status == 303 && (req.method != "GET" && req.method != "HEAD")) { new_req.method = "GET"; new_req.body.clear(); new_req.headers.clear(); } Response new_res; auto ret = cli.send(new_req, new_res, error); if (ret) { req = new_req; res = new_res; if (res.location.empty()) res.location = location; } return ret; } inline std::string params_to_query_str(const Params ¶ms) { std::string query; for (auto it = params.begin(); it != params.end(); ++it) { if (it != params.begin()) { query += "&"; } query += it->first; query += "="; query += encode_query_param(it->second); } return query; } inline void parse_query_text(const std::string &s, Params ¶ms) { std::set cache; split(s.data(), s.data() + s.size(), '&', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(kv); std::string key; std::string val; split(b, e, '=', [&](const char *b2, const char *e2) { if (key.empty()) { key.assign(b2, e2); } else { val.assign(b2, e2); } }); if (!key.empty()) { params.emplace(decode_url(key, true), decode_url(val, true)); } }); } inline bool parse_multipart_boundary(const std::string &content_type, std::string &boundary) { auto boundary_keyword = "boundary="; auto pos = content_type.find(boundary_keyword); if (pos == std::string::npos) { return false; } auto end = content_type.find(';', pos); auto beg = pos + strlen(boundary_keyword); boundary = trim_double_quotes_copy(content_type.substr(beg, end - beg)); return !boundary.empty(); } inline void parse_disposition_params(const std::string &s, Params ¶ms) { std::set cache; split(s.data(), s.data() + s.size(), ';', [&](const char *b, const char *e) { std::string kv(b, e); if (cache.find(kv) != cache.end()) { return; } cache.insert(kv); std::string key; std::string val; split(b, e, '=', [&](const char *b2, const char *e2) { if (key.empty()) { key.assign(b2, e2); } else { val.assign(b2, e2); } }); if (!key.empty()) { params.emplace(trim_double_quotes_copy((key)), trim_double_quotes_copy((val))); } }); } #ifdef CPPHTTPLIB_NO_EXCEPTIONS inline bool parse_range_header(const std::string &s, Ranges &ranges) { #else inline bool parse_range_header(const std::string &s, Ranges &ranges) try { #endif static auto re_first_range = std::regex(R"(bytes=(\d*-\d*(?:,\s*\d*-\d*)*))"); std::smatch m; if (std::regex_match(s, m, re_first_range)) { auto pos = static_cast(m.position(1)); auto len = static_cast(m.length(1)); auto all_valid_ranges = true; split(&s[pos], &s[pos + len], ',', [&](const char *b, const char *e) { if (!all_valid_ranges) return; static auto re_another_range = std::regex(R"(\s*(\d*)-(\d*))"); std::cmatch cm; if (std::regex_match(b, e, cm, re_another_range)) { ssize_t first = -1; if (!cm.str(1).empty()) { first = static_cast(std::stoll(cm.str(1))); } ssize_t last = -1; if (!cm.str(2).empty()) { last = static_cast(std::stoll(cm.str(2))); } if (first != -1 && last != -1 && first > last) { all_valid_ranges = false; return; } ranges.emplace_back(std::make_pair(first, last)); } }); return all_valid_ranges; } return false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS } #else } catch (...) { return false; } #endif class MultipartFormDataParser { public: MultipartFormDataParser() = default; void set_boundary(std::string &&boundary) { boundary_ = boundary; dash_boundary_crlf_ = dash_ + boundary_ + crlf_; crlf_dash_boundary_ = crlf_ + dash_ + boundary_; } bool is_valid() const { return is_valid_; } bool parse(const char *buf, size_t n, const ContentReceiver &content_callback, const MultipartContentHeader &header_callback) { buf_append(buf, n); while (buf_size() > 0) { switch (state_) { case 0: { // Initial boundary buf_erase(buf_find(dash_boundary_crlf_)); if (dash_boundary_crlf_.size() > buf_size()) { return true; } if (!buf_start_with(dash_boundary_crlf_)) { return false; } buf_erase(dash_boundary_crlf_.size()); state_ = 1; break; } case 1: { // New entry clear_file_info(); state_ = 2; break; } case 2: { // Headers auto pos = buf_find(crlf_); if (pos > CPPHTTPLIB_HEADER_MAX_LENGTH) { return false; } while (pos < buf_size()) { // Empty line if (pos == 0) { if (!header_callback(file_)) { is_valid_ = false; return false; } buf_erase(crlf_.size()); state_ = 3; break; } static const std::string header_name = "content-type:"; const auto header = buf_head(pos); if (start_with_case_ignore(header, header_name)) { file_.content_type = trim_copy(header.substr(header_name.size())); } else { static const std::regex re_content_disposition( R"~(^Content-Disposition:\s*form-data;\s*(.*)$)~", std::regex_constants::icase); std::smatch m; if (std::regex_match(header, m, re_content_disposition)) { Params params; parse_disposition_params(m[1], params); auto it = params.find("name"); if (it != params.end()) { file_.name = it->second; } else { is_valid_ = false; return false; } it = params.find("filename"); if (it != params.end()) { file_.filename = it->second; } it = params.find("filename*"); if (it != params.end()) { // Only allow UTF-8 enconnding... static const std::regex re_rfc5987_encoding( R"~(^UTF-8''(.+?)$)~", std::regex_constants::icase); std::smatch m2; if (std::regex_match(it->second, m2, re_rfc5987_encoding)) { file_.filename = decode_url(m2[1], false); // override... } else { is_valid_ = false; return false; } } } else { is_valid_ = false; return false; } } buf_erase(pos + crlf_.size()); pos = buf_find(crlf_); } if (state_ != 3) { return true; } break; } case 3: { // Body if (crlf_dash_boundary_.size() > buf_size()) { return true; } auto pos = buf_find(crlf_dash_boundary_); if (pos < buf_size()) { if (!content_callback(buf_data(), pos)) { is_valid_ = false; return false; } buf_erase(pos + crlf_dash_boundary_.size()); state_ = 4; } else { auto len = buf_size() - crlf_dash_boundary_.size(); if (len > 0) { if (!content_callback(buf_data(), len)) { is_valid_ = false; return false; } buf_erase(len); } return true; } break; } case 4: { // Boundary if (crlf_.size() > buf_size()) { return true; } if (buf_start_with(crlf_)) { buf_erase(crlf_.size()); state_ = 1; } else { if (dash_.size() > buf_size()) { return true; } if (buf_start_with(dash_)) { buf_erase(dash_.size()); is_valid_ = true; buf_erase(buf_size()); // Remove epilogue } else { return true; } } break; } } } return true; } private: void clear_file_info() { file_.name.clear(); file_.filename.clear(); file_.content_type.clear(); } bool start_with_case_ignore(const std::string &a, const std::string &b) const { if (a.size() < b.size()) { return false; } for (size_t i = 0; i < b.size(); i++) { if (::tolower(a[i]) != ::tolower(b[i])) { return false; } } return true; } const std::string dash_ = "--"; const std::string crlf_ = "\r\n"; std::string boundary_; std::string dash_boundary_crlf_; std::string crlf_dash_boundary_; size_t state_ = 0; bool is_valid_ = false; MultipartFormData file_; // Buffer bool start_with(const std::string &a, size_t spos, size_t epos, const std::string &b) const { if (epos - spos < b.size()) { return false; } for (size_t i = 0; i < b.size(); i++) { if (a[i + spos] != b[i]) { return false; } } return true; } size_t buf_size() const { return buf_epos_ - buf_spos_; } const char *buf_data() const { return &buf_[buf_spos_]; } std::string buf_head(size_t l) const { return buf_.substr(buf_spos_, l); } bool buf_start_with(const std::string &s) const { return start_with(buf_, buf_spos_, buf_epos_, s); } size_t buf_find(const std::string &s) const { auto c = s.front(); size_t off = buf_spos_; while (off < buf_epos_) { auto pos = off; while (true) { if (pos == buf_epos_) { return buf_size(); } if (buf_[pos] == c) { break; } pos++; } auto remaining_size = buf_epos_ - pos; if (s.size() > remaining_size) { return buf_size(); } if (start_with(buf_, pos, buf_epos_, s)) { return pos - buf_spos_; } off = pos + 1; } return buf_size(); } void buf_append(const char *data, size_t n) { auto remaining_size = buf_size(); if (remaining_size > 0 && buf_spos_ > 0) { for (size_t i = 0; i < remaining_size; i++) { buf_[i] = buf_[buf_spos_ + i]; } } buf_spos_ = 0; buf_epos_ = remaining_size; if (remaining_size + n > buf_.size()) { buf_.resize(remaining_size + n); } for (size_t i = 0; i < n; i++) { buf_[buf_epos_ + i] = data[i]; } buf_epos_ += n; } void buf_erase(size_t size) { buf_spos_ += size; } std::string buf_; size_t buf_spos_ = 0; size_t buf_epos_ = 0; }; inline std::string to_lower(const char *beg, const char *end) { std::string out; auto it = beg; while (it != end) { out += static_cast(::tolower(*it)); it++; } return out; } inline std::string make_multipart_data_boundary() { static const char data[] = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // std::random_device might actually be deterministic on some // platforms, but due to lack of support in the c++ standard library, // doing better requires either some ugly hacks or breaking portability. std::random_device seed_gen; // Request 128 bits of entropy for initialization std::seed_seq seed_sequence{seed_gen(), seed_gen(), seed_gen(), seed_gen()}; std::mt19937 engine(seed_sequence); std::string result = "--cpp-httplib-multipart-data-"; for (auto i = 0; i < 16; i++) { result += data[engine() % (sizeof(data) - 1)]; } return result; } inline bool is_multipart_boundary_chars_valid(const std::string &boundary) { auto valid = true; for (size_t i = 0; i < boundary.size(); i++) { auto c = boundary[i]; if (!std::isalnum(c) && c != '-' && c != '_') { valid = false; break; } } return valid; } template inline std::string serialize_multipart_formdata_item_begin(const T &item, const std::string &boundary) { std::string body = "--" + boundary + "\r\n"; body += "Content-Disposition: form-data; name=\"" + item.name + "\""; if (!item.filename.empty()) { body += "; filename=\"" + item.filename + "\""; } body += "\r\n"; if (!item.content_type.empty()) { body += "Content-Type: " + item.content_type + "\r\n"; } body += "\r\n"; return body; } inline std::string serialize_multipart_formdata_item_end() { return "\r\n"; } inline std::string serialize_multipart_formdata_finish(const std::string &boundary) { return "--" + boundary + "--\r\n"; } inline std::string serialize_multipart_formdata_get_content_type(const std::string &boundary) { return "multipart/form-data; boundary=" + boundary; } inline std::string serialize_multipart_formdata(const MultipartFormDataItems &items, const std::string &boundary, bool finish = true) { std::string body; for (const auto &item : items) { body += serialize_multipart_formdata_item_begin(item, boundary); body += item.content + serialize_multipart_formdata_item_end(); } if (finish) body += serialize_multipart_formdata_finish(boundary); return body; } inline std::pair get_range_offset_and_length(const Request &req, size_t content_length, size_t index) { auto r = req.ranges[index]; if (r.first == -1 && r.second == -1) { return std::make_pair(0, content_length); } auto slen = static_cast(content_length); if (r.first == -1) { r.first = (std::max)(static_cast(0), slen - r.second); r.second = slen - 1; } if (r.second == -1) { r.second = slen - 1; } return std::make_pair(r.first, static_cast(r.second - r.first) + 1); } inline std::string make_content_range_header_field(const std::pair &range, size_t content_length) { std::string field = "bytes "; if (range.first != -1) { field += std::to_string(range.first); } field += "-"; if (range.second != -1) { field += std::to_string(range.second); } field += "/"; field += std::to_string(content_length); return field; } template bool process_multipart_ranges_data(const Request &req, Response &res, const std::string &boundary, const std::string &content_type, SToken stoken, CToken ctoken, Content content) { for (size_t i = 0; i < req.ranges.size(); i++) { ctoken("--"); stoken(boundary); ctoken("\r\n"); if (!content_type.empty()) { ctoken("Content-Type: "); stoken(content_type); ctoken("\r\n"); } ctoken("Content-Range: "); const auto &range = req.ranges[i]; stoken(make_content_range_header_field(range, res.content_length_)); ctoken("\r\n"); ctoken("\r\n"); auto offsets = get_range_offset_and_length(req, res.content_length_, i); auto offset = offsets.first; auto length = offsets.second; if (!content(offset, length)) { return false; } ctoken("\r\n"); } ctoken("--"); stoken(boundary); ctoken("--"); return true; } inline bool make_multipart_ranges_data(const Request &req, Response &res, const std::string &boundary, const std::string &content_type, std::string &data) { return process_multipart_ranges_data( req, res, boundary, content_type, [&](const std::string &token) { data += token; }, [&](const std::string &token) { data += token; }, [&](size_t offset, size_t length) { if (offset < res.body.size()) { data += res.body.substr(offset, length); return true; } return false; }); } inline size_t get_multipart_ranges_data_length(const Request &req, Response &res, const std::string &boundary, const std::string &content_type) { size_t data_length = 0; process_multipart_ranges_data( req, res, boundary, content_type, [&](const std::string &token) { data_length += token.size(); }, [&](const std::string &token) { data_length += token.size(); }, [&](size_t /*offset*/, size_t length) { data_length += length; return true; }); return data_length; } template inline bool write_multipart_ranges_data(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type, const T &is_shutting_down) { return process_multipart_ranges_data( req, res, boundary, content_type, [&](const std::string &token) { strm.write(token); }, [&](const std::string &token) { strm.write(token); }, [&](size_t offset, size_t length) { return write_content(strm, res.content_provider_, offset, length, is_shutting_down); }); } inline std::pair get_range_offset_and_length(const Request &req, const Response &res, size_t index) { auto r = req.ranges[index]; if (r.second == -1) { r.second = static_cast(res.content_length_) - 1; } return std::make_pair(r.first, r.second - r.first + 1); } inline bool expect_content(const Request &req) { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH" || req.method == "PRI" || req.method == "DELETE") { return true; } // TODO: check if Content-Length is set return false; } inline bool has_crlf(const std::string &s) { auto p = s.c_str(); while (*p) { if (*p == '\r' || *p == '\n') { return true; } p++; } return false; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline std::string message_digest(const std::string &s, const EVP_MD *algo) { auto context = std::unique_ptr( EVP_MD_CTX_new(), EVP_MD_CTX_free); unsigned int hash_length = 0; unsigned char hash[EVP_MAX_MD_SIZE]; EVP_DigestInit_ex(context.get(), algo, nullptr); EVP_DigestUpdate(context.get(), s.c_str(), s.size()); EVP_DigestFinal_ex(context.get(), hash, &hash_length); std::stringstream ss; for (auto i = 0u; i < hash_length; ++i) { ss << std::hex << std::setw(2) << std::setfill('0') << static_cast(hash[i]); } return ss.str(); } inline std::string MD5(const std::string &s) { return message_digest(s, EVP_md5()); } inline std::string SHA_256(const std::string &s) { return message_digest(s, EVP_sha256()); } inline std::string SHA_512(const std::string &s) { return message_digest(s, EVP_sha512()); } #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 // NOTE: This code came up with the following stackoverflow post: // https://stackoverflow.com/questions/9507184/can-openssl-on-windows-use-the-system-certificate-store inline bool load_system_certs_on_windows(X509_STORE *store) { auto hStore = CertOpenSystemStoreW((HCRYPTPROV_LEGACY)NULL, L"ROOT"); if (!hStore) { return false; } auto result = false; PCCERT_CONTEXT pContext = NULL; while ((pContext = CertEnumCertificatesInStore(hStore, pContext)) != nullptr) { auto encoded_cert = static_cast(pContext->pbCertEncoded); auto x509 = d2i_X509(NULL, &encoded_cert, pContext->cbCertEncoded); if (x509) { X509_STORE_add_cert(store, x509); X509_free(x509); result = true; } } CertFreeCertificateContext(pContext); CertCloseStore(hStore, 0); return result; } #elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) #if TARGET_OS_OSX template using CFObjectPtr = std::unique_ptr::type, void (*)(CFTypeRef)>; inline void cf_object_ptr_deleter(CFTypeRef obj) { if (obj) { CFRelease(obj); } } inline bool retrieve_certs_from_keychain(CFObjectPtr &certs) { CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecReturnRef}; CFTypeRef values[] = {kSecClassCertificate, kSecMatchLimitAll, kCFBooleanTrue}; CFObjectPtr query( CFDictionaryCreate(nullptr, reinterpret_cast(keys), values, sizeof(keys) / sizeof(keys[0]), &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks), cf_object_ptr_deleter); if (!query) { return false; } CFTypeRef security_items = nullptr; if (SecItemCopyMatching(query.get(), &security_items) != errSecSuccess || CFArrayGetTypeID() != CFGetTypeID(security_items)) { return false; } certs.reset(reinterpret_cast(security_items)); return true; } inline bool retrieve_root_certs_from_keychain(CFObjectPtr &certs) { CFArrayRef root_security_items = nullptr; if (SecTrustCopyAnchorCertificates(&root_security_items) != errSecSuccess) { return false; } certs.reset(root_security_items); return true; } inline bool add_certs_to_x509_store(CFArrayRef certs, X509_STORE *store) { auto result = false; for (auto i = 0; i < CFArrayGetCount(certs); ++i) { const auto cert = reinterpret_cast( CFArrayGetValueAtIndex(certs, i)); if (SecCertificateGetTypeID() != CFGetTypeID(cert)) { continue; } CFDataRef cert_data = nullptr; if (SecItemExport(cert, kSecFormatX509Cert, 0, nullptr, &cert_data) != errSecSuccess) { continue; } CFObjectPtr cert_data_ptr(cert_data, cf_object_ptr_deleter); auto encoded_cert = static_cast( CFDataGetBytePtr(cert_data_ptr.get())); auto x509 = d2i_X509(NULL, &encoded_cert, CFDataGetLength(cert_data_ptr.get())); if (x509) { X509_STORE_add_cert(store, x509); X509_free(x509); result = true; } } return result; } inline bool load_system_certs_on_macos(X509_STORE *store) { auto result = false; CFObjectPtr certs(nullptr, cf_object_ptr_deleter); if (retrieve_certs_from_keychain(certs) && certs) { result = add_certs_to_x509_store(certs.get(), store); } if (retrieve_root_certs_from_keychain(certs) && certs) { result = add_certs_to_x509_store(certs.get(), store) || result; } return result; } #endif // TARGET_OS_OSX #endif // _WIN32 #endif // CPPHTTPLIB_OPENSSL_SUPPORT #ifdef _WIN32 class WSInit { public: WSInit() { WSADATA wsaData; if (WSAStartup(0x0002, &wsaData) == 0) is_valid_ = true; } ~WSInit() { if (is_valid_) WSACleanup(); } bool is_valid_ = false; }; static WSInit wsinit_; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline std::pair make_digest_authentication_header( const Request &req, const std::map &auth, size_t cnonce_count, const std::string &cnonce, const std::string &username, const std::string &password, bool is_proxy = false) { std::string nc; { std::stringstream ss; ss << std::setfill('0') << std::setw(8) << std::hex << cnonce_count; nc = ss.str(); } std::string qop; if (auth.find("qop") != auth.end()) { qop = auth.at("qop"); if (qop.find("auth-int") != std::string::npos) { qop = "auth-int"; } else if (qop.find("auth") != std::string::npos) { qop = "auth"; } else { qop.clear(); } } std::string algo = "MD5"; if (auth.find("algorithm") != auth.end()) { algo = auth.at("algorithm"); } std::string response; { auto H = algo == "SHA-256" ? detail::SHA_256 : algo == "SHA-512" ? detail::SHA_512 : detail::MD5; auto A1 = username + ":" + auth.at("realm") + ":" + password; auto A2 = req.method + ":" + req.path; if (qop == "auth-int") { A2 += ":" + H(req.body); } if (qop.empty()) { response = H(H(A1) + ":" + auth.at("nonce") + ":" + H(A2)); } else { response = H(H(A1) + ":" + auth.at("nonce") + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2)); } } auto opaque = (auth.find("opaque") != auth.end()) ? auth.at("opaque") : ""; auto field = "Digest username=\"" + username + "\", realm=\"" + auth.at("realm") + "\", nonce=\"" + auth.at("nonce") + "\", uri=\"" + req.path + "\", algorithm=" + algo + (qop.empty() ? ", response=\"" : ", qop=" + qop + ", nc=" + nc + ", cnonce=\"" + cnonce + "\", response=\"") + response + "\"" + (opaque.empty() ? "" : ", opaque=\"" + opaque + "\""); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, field); } #endif inline bool parse_www_authenticate(const Response &res, std::map &auth, bool is_proxy) { auto auth_key = is_proxy ? "Proxy-Authenticate" : "WWW-Authenticate"; if (res.has_header(auth_key)) { static auto re = std::regex(R"~((?:(?:,\s*)?(.+?)=(?:"(.*?)"|([^,]*))))~"); auto s = res.get_header_value(auth_key); auto pos = s.find(' '); if (pos != std::string::npos) { auto type = s.substr(0, pos); if (type == "Basic") { return false; } else if (type == "Digest") { s = s.substr(pos + 1); auto beg = std::sregex_iterator(s.begin(), s.end(), re); for (auto i = beg; i != std::sregex_iterator(); ++i) { auto m = *i; auto key = s.substr(static_cast(m.position(1)), static_cast(m.length(1))); auto val = m.length(2) > 0 ? s.substr(static_cast(m.position(2)), static_cast(m.length(2))) : s.substr(static_cast(m.position(3)), static_cast(m.length(3))); auth[key] = val; } return true; } } } return false; } // https://stackoverflow.com/questions/440133/how-do-i-create-a-random-alpha-numeric-string-in-c/440240#answer-440240 inline std::string random_string(size_t length) { auto randchar = []() -> char { const char charset[] = "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "abcdefghijklmnopqrstuvwxyz"; const size_t max_index = (sizeof(charset) - 1); return charset[static_cast(std::rand()) % max_index]; }; std::string str(length, 0); std::generate_n(str.begin(), length, randchar); return str; } class ContentProviderAdapter { public: explicit ContentProviderAdapter( ContentProviderWithoutLength &&content_provider) : content_provider_(content_provider) {} bool operator()(size_t offset, size_t, DataSink &sink) { return content_provider_(offset, sink); } private: ContentProviderWithoutLength content_provider_; }; } // namespace detail inline std::string hosted_at(const std::string &hostname) { std::vector addrs; hosted_at(hostname, addrs); if (addrs.empty()) { return std::string(); } return addrs[0]; } inline void hosted_at(const std::string &hostname, std::vector &addrs) { struct addrinfo hints; struct addrinfo *result; memset(&hints, 0, sizeof(struct addrinfo)); hints.ai_family = AF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = 0; if (getaddrinfo(hostname.c_str(), nullptr, &hints, &result)) { #if defined __linux__ && !defined __ANDROID__ res_init(); #endif return; } for (auto rp = result; rp; rp = rp->ai_next) { const auto &addr = *reinterpret_cast(rp->ai_addr); std::string ip; auto dummy = -1; if (detail::get_ip_and_port(addr, sizeof(struct sockaddr_storage), ip, dummy)) { addrs.push_back(ip); } } freeaddrinfo(result); } inline std::string append_query_params(const std::string &path, const Params ¶ms) { std::string path_with_query = path; const static std::regex re("[^?]+\\?.*"); auto delm = std::regex_match(path, re) ? '&' : '?'; path_with_query += delm + detail::params_to_query_str(params); return path_with_query; } // Header utilities inline std::pair make_range_header(Ranges ranges) { std::string field = "bytes="; auto i = 0; for (auto r : ranges) { if (i != 0) { field += ", "; } if (r.first != -1) { field += std::to_string(r.first); } field += '-'; if (r.second != -1) { field += std::to_string(r.second); } i++; } return std::make_pair("Range", std::move(field)); } inline std::pair make_basic_authentication_header(const std::string &username, const std::string &password, bool is_proxy) { auto field = "Basic " + detail::base64_encode(username + ":" + password); auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } inline std::pair make_bearer_token_authentication_header(const std::string &token, bool is_proxy = false) { auto field = "Bearer " + token; auto key = is_proxy ? "Proxy-Authorization" : "Authorization"; return std::make_pair(key, std::move(field)); } // Request implementation inline bool Request::has_header(const std::string &key) const { return detail::has_header(headers, key); } inline std::string Request::get_header_value(const std::string &key, size_t id) const { return detail::get_header_value(headers, key, id, ""); } inline size_t Request::get_header_value_count(const std::string &key) const { auto r = headers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline void Request::set_header(const std::string &key, const std::string &val) { if (!detail::has_crlf(key) && !detail::has_crlf(val)) { headers.emplace(key, val); } } inline bool Request::has_param(const std::string &key) const { return params.find(key) != params.end(); } inline std::string Request::get_param_value(const std::string &key, size_t id) const { auto rng = params.equal_range(key); auto it = rng.first; std::advance(it, static_cast(id)); if (it != rng.second) { return it->second; } return std::string(); } inline size_t Request::get_param_value_count(const std::string &key) const { auto r = params.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline bool Request::is_multipart_form_data() const { const auto &content_type = get_header_value("Content-Type"); return !content_type.rfind("multipart/form-data", 0); } inline bool Request::has_file(const std::string &key) const { return files.find(key) != files.end(); } inline MultipartFormData Request::get_file_value(const std::string &key) const { auto it = files.find(key); if (it != files.end()) { return it->second; } return MultipartFormData(); } inline std::vector Request::get_file_values(const std::string &key) const { std::vector values; auto rng = files.equal_range(key); for (auto it = rng.first; it != rng.second; it++) { values.push_back(it->second); } return values; } // Response implementation inline bool Response::has_header(const std::string &key) const { return headers.find(key) != headers.end(); } inline std::string Response::get_header_value(const std::string &key, size_t id) const { return detail::get_header_value(headers, key, id, ""); } inline size_t Response::get_header_value_count(const std::string &key) const { auto r = headers.equal_range(key); return static_cast(std::distance(r.first, r.second)); } inline void Response::set_header(const std::string &key, const std::string &val) { if (!detail::has_crlf(key) && !detail::has_crlf(val)) { headers.emplace(key, val); } } inline void Response::set_redirect(const std::string &url, int stat) { if (!detail::has_crlf(url)) { set_header("Location", url); if (300 <= stat && stat < 400) { this->status = stat; } else { this->status = 302; } } } inline void Response::set_content(const char *s, size_t n, const std::string &content_type) { body.assign(s, n); auto rng = headers.equal_range("Content-Type"); headers.erase(rng.first, rng.second); set_header("Content-Type", content_type); } inline void Response::set_content(const std::string &s, const std::string &content_type) { set_content(s.data(), s.size(), content_type); } inline void Response::set_content_provider( size_t in_length, const std::string &content_type, ContentProvider provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = in_length; if (in_length > 0) { content_provider_ = std::move(provider); } content_provider_resource_releaser_ = resource_releaser; is_chunked_content_provider_ = false; } inline void Response::set_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = resource_releaser; is_chunked_content_provider_ = false; } inline void Response::set_chunked_content_provider( const std::string &content_type, ContentProviderWithoutLength provider, ContentProviderResourceReleaser resource_releaser) { set_header("Content-Type", content_type); content_length_ = 0; content_provider_ = detail::ContentProviderAdapter(std::move(provider)); content_provider_resource_releaser_ = resource_releaser; is_chunked_content_provider_ = true; } // Result implementation inline bool Result::has_request_header(const std::string &key) const { return request_headers_.find(key) != request_headers_.end(); } inline std::string Result::get_request_header_value(const std::string &key, size_t id) const { return detail::get_header_value(request_headers_, key, id, ""); } inline size_t Result::get_request_header_value_count(const std::string &key) const { auto r = request_headers_.equal_range(key); return static_cast(std::distance(r.first, r.second)); } // Stream implementation inline ssize_t Stream::write(const char *ptr) { return write(ptr, strlen(ptr)); } inline ssize_t Stream::write(const std::string &s) { return write(s.data(), s.size()); } namespace detail { // Socket stream implementation inline SocketStream::SocketStream(socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec) : sock_(sock), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec), read_buff_(read_buff_size_, 0) {} inline SocketStream::~SocketStream() {} inline bool SocketStream::is_readable() const { return select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } inline bool SocketStream::is_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && is_socket_alive(sock_); } inline ssize_t SocketStream::read(char *ptr, size_t size) { #ifdef _WIN32 size = (std::min)(size, static_cast((std::numeric_limits::max)())); #else size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif if (read_buff_off_ < read_buff_content_size_) { auto remaining_size = read_buff_content_size_ - read_buff_off_; if (size <= remaining_size) { memcpy(ptr, read_buff_.data() + read_buff_off_, size); read_buff_off_ += size; return static_cast(size); } else { memcpy(ptr, read_buff_.data() + read_buff_off_, remaining_size); read_buff_off_ += remaining_size; return static_cast(remaining_size); } } if (!is_readable()) { return -1; } read_buff_off_ = 0; read_buff_content_size_ = 0; if (size < read_buff_size_) { auto n = read_socket(sock_, read_buff_.data(), read_buff_size_, CPPHTTPLIB_RECV_FLAGS); if (n <= 0) { return n; } else if (n <= static_cast(size)) { memcpy(ptr, read_buff_.data(), static_cast(n)); return n; } else { memcpy(ptr, read_buff_.data(), size); read_buff_off_ = size; read_buff_content_size_ = static_cast(n); return static_cast(size); } } else { return read_socket(sock_, ptr, size, CPPHTTPLIB_RECV_FLAGS); } } inline ssize_t SocketStream::write(const char *ptr, size_t size) { if (!is_writable()) { return -1; } #if defined(_WIN32) && !defined(_WIN64) size = (std::min)(size, static_cast((std::numeric_limits::max)())); #endif return send_socket(sock_, ptr, size, CPPHTTPLIB_SEND_FLAGS); } inline void SocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { return detail::get_remote_ip_and_port(sock_, ip, port); } inline void SocketStream::get_local_ip_and_port(std::string &ip, int &port) const { return detail::get_local_ip_and_port(sock_, ip, port); } inline socket_t SocketStream::socket() const { return sock_; } // Buffer stream implementation inline bool BufferStream::is_readable() const { return true; } inline bool BufferStream::is_writable() const { return true; } inline ssize_t BufferStream::read(char *ptr, size_t size) { #if defined(_MSC_VER) && _MSC_VER < 1910 auto len_read = buffer._Copy_s(ptr, size, size, position); #else auto len_read = buffer.copy(ptr, size, position); #endif position += static_cast(len_read); return static_cast(len_read); } inline ssize_t BufferStream::write(const char *ptr, size_t size) { buffer.append(ptr, size); return static_cast(size); } inline void BufferStream::get_remote_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} inline void BufferStream::get_local_ip_and_port(std::string & /*ip*/, int & /*port*/) const {} inline socket_t BufferStream::socket() const { return 0; } inline const std::string &BufferStream::get_buffer() const { return buffer; } inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) { // One past the last ending position of a path param substring std::size_t last_param_end = 0; #ifndef CPPHTTPLIB_NO_EXCEPTIONS // Needed to ensure that parameter names are unique during matcher // construction // If exceptions are disabled, only last duplicate path // parameter will be set std::unordered_set param_name_set; #endif while (true) { const auto marker_pos = pattern.find(marker, last_param_end); if (marker_pos == std::string::npos) { break; } static_fragments_.push_back( pattern.substr(last_param_end, marker_pos - last_param_end)); const auto param_name_start = marker_pos + 1; auto sep_pos = pattern.find(separator, param_name_start); if (sep_pos == std::string::npos) { sep_pos = pattern.length(); } auto param_name = pattern.substr(param_name_start, sep_pos - param_name_start); #ifndef CPPHTTPLIB_NO_EXCEPTIONS if (param_name_set.find(param_name) != param_name_set.cend()) { std::string msg = "Encountered path parameter '" + param_name + "' multiple times in route pattern '" + pattern + "'."; throw std::invalid_argument(msg); } #endif param_names_.push_back(std::move(param_name)); last_param_end = sep_pos + 1; } if (last_param_end < pattern.length()) { static_fragments_.push_back(pattern.substr(last_param_end)); } } inline bool PathParamsMatcher::match(Request &request) const { request.matches = std::smatch(); request.path_params.clear(); request.path_params.reserve(param_names_.size()); // One past the position at which the path matched the pattern last time std::size_t starting_pos = 0; for (size_t i = 0; i < static_fragments_.size(); ++i) { const auto &fragment = static_fragments_[i]; if (starting_pos + fragment.length() > request.path.length()) { return false; } // Avoid unnecessary allocation by using strncmp instead of substr + // comparison if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(), fragment.length()) != 0) { return false; } starting_pos += fragment.length(); // Should only happen when we have a static fragment after a param // Example: '/users/:id/subscriptions' // The 'subscriptions' fragment here does not have a corresponding param if (i >= param_names_.size()) { continue; } auto sep_pos = request.path.find(separator, starting_pos); if (sep_pos == std::string::npos) { sep_pos = request.path.length(); } const auto ¶m_name = param_names_[i]; request.path_params.emplace( param_name, request.path.substr(starting_pos, sep_pos - starting_pos)); // Mark everythin up to '/' as matched starting_pos = sep_pos + 1; } // Returns false if the path is longer than the pattern return starting_pos >= request.path.length(); } inline bool RegexMatcher::match(Request &request) const { request.path_params.clear(); return std::regex_match(request.path, request.matches, regex_); } } // namespace detail // HTTP server implementation inline Server::Server() : new_task_queue( [] { return new ThreadPool(CPPHTTPLIB_THREAD_POOL_COUNT); }) { #ifndef _WIN32 signal(SIGPIPE, SIG_IGN); #endif } inline Server::~Server() {} inline std::unique_ptr Server::make_matcher(const std::string &pattern) { if (pattern.find("/:") != std::string::npos) { return detail::make_unique(pattern); } else { return detail::make_unique(pattern); } } inline Server &Server::Get(const std::string &pattern, Handler handler) { get_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Post(const std::string &pattern, Handler handler) { post_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Post(const std::string &pattern, HandlerWithContentReader handler) { post_handlers_for_content_reader_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Put(const std::string &pattern, Handler handler) { put_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Put(const std::string &pattern, HandlerWithContentReader handler) { put_handlers_for_content_reader_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Patch(const std::string &pattern, Handler handler) { patch_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Patch(const std::string &pattern, HandlerWithContentReader handler) { patch_handlers_for_content_reader_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Delete(const std::string &pattern, Handler handler) { delete_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Delete(const std::string &pattern, HandlerWithContentReader handler) { delete_handlers_for_content_reader_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline Server &Server::Options(const std::string &pattern, Handler handler) { options_handlers_.push_back( std::make_pair(make_matcher(pattern), std::move(handler))); return *this; } inline bool Server::set_base_dir(const std::string &dir, const std::string &mount_point) { return set_mount_point(mount_point, dir); } inline bool Server::set_mount_point(const std::string &mount_point, const std::string &dir, Headers headers) { if (detail::is_dir(dir)) { std::string mnt = !mount_point.empty() ? mount_point : "/"; if (!mnt.empty() && mnt[0] == '/') { base_dirs_.push_back({mnt, dir, std::move(headers)}); return true; } } return false; } inline bool Server::remove_mount_point(const std::string &mount_point) { for (auto it = base_dirs_.begin(); it != base_dirs_.end(); ++it) { if (it->mount_point == mount_point) { base_dirs_.erase(it); return true; } } return false; } inline Server & Server::set_file_extension_and_mimetype_mapping(const std::string &ext, const std::string &mime) { file_extension_and_mimetype_map_[ext] = mime; return *this; } inline Server &Server::set_default_file_mimetype(const std::string &mime) { default_file_mimetype_ = mime; return *this; } inline Server &Server::set_file_request_handler(Handler handler) { file_request_handler_ = std::move(handler); return *this; } inline Server &Server::set_error_handler(HandlerWithResponse handler) { error_handler_ = std::move(handler); return *this; } inline Server &Server::set_error_handler(Handler handler) { error_handler_ = [handler](const Request &req, Response &res) { handler(req, res); return HandlerResponse::Handled; }; return *this; } inline Server &Server::set_exception_handler(ExceptionHandler handler) { exception_handler_ = std::move(handler); return *this; } inline Server &Server::set_pre_routing_handler(HandlerWithResponse handler) { pre_routing_handler_ = std::move(handler); return *this; } inline Server &Server::set_post_routing_handler(Handler handler) { post_routing_handler_ = std::move(handler); return *this; } inline Server &Server::set_logger(Logger logger) { logger_ = std::move(logger); return *this; } inline Server & Server::set_expect_100_continue_handler(Expect100ContinueHandler handler) { expect_100_continue_handler_ = std::move(handler); return *this; } inline Server &Server::set_address_family(int family) { address_family_ = family; return *this; } inline Server &Server::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; return *this; } inline Server &Server::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); return *this; } inline Server &Server::set_default_headers(Headers headers) { default_headers_ = std::move(headers); return *this; } inline Server &Server::set_keep_alive_max_count(size_t count) { keep_alive_max_count_ = count; return *this; } inline Server &Server::set_keep_alive_timeout(time_t sec) { keep_alive_timeout_sec_ = sec; return *this; } inline Server &Server::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; return *this; } inline Server &Server::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; return *this; } inline Server &Server::set_idle_interval(time_t sec, time_t usec) { idle_interval_sec_ = sec; idle_interval_usec_ = usec; return *this; } inline Server &Server::set_payload_max_length(size_t length) { payload_max_length_ = length; return *this; } inline bool Server::bind_to_port(const std::string &host, int port, int socket_flags) { if (bind_internal(host, port, socket_flags) < 0) return false; return true; } inline int Server::bind_to_any_port(const std::string &host, int socket_flags) { return bind_internal(host, 0, socket_flags); } inline bool Server::listen_after_bind() { auto se = detail::scope_exit([&]() { done_ = true; }); return listen_internal(); } inline bool Server::listen(const std::string &host, int port, int socket_flags) { auto se = detail::scope_exit([&]() { done_ = true; }); return bind_to_port(host, port, socket_flags) && listen_internal(); } inline bool Server::is_running() const { return is_running_; } inline void Server::wait_until_ready() const { while (!is_running() && !done_) { std::this_thread::sleep_for(std::chrono::milliseconds{1}); } } inline void Server::stop() { if (is_running_) { assert(svr_sock_ != INVALID_SOCKET); std::atomic sock(svr_sock_.exchange(INVALID_SOCKET)); detail::shutdown_socket(sock); detail::close_socket(sock); } } inline bool Server::parse_request_line(const char *s, Request &req) { auto len = strlen(s); if (len < 2 || s[len - 2] != '\r' || s[len - 1] != '\n') { return false; } len -= 2; { size_t count = 0; detail::split(s, s + len, ' ', [&](const char *b, const char *e) { switch (count) { case 0: req.method = std::string(b, e); break; case 1: req.target = std::string(b, e); break; case 2: req.version = std::string(b, e); break; default: break; } count++; }); if (count != 3) { return false; } } static const std::set methods{ "GET", "HEAD", "POST", "PUT", "DELETE", "CONNECT", "OPTIONS", "TRACE", "PATCH", "PRI"}; if (methods.find(req.method) == methods.end()) { return false; } if (req.version != "HTTP/1.1" && req.version != "HTTP/1.0") { return false; } { // Skip URL fragment for (size_t i = 0; i < req.target.size(); i++) { if (req.target[i] == '#') { req.target.erase(i); break; } } size_t count = 0; detail::split(req.target.data(), req.target.data() + req.target.size(), '?', [&](const char *b, const char *e) { switch (count) { case 0: req.path = detail::decode_url(std::string(b, e), false); break; case 1: { if (e - b > 0) { detail::parse_query_text(std::string(b, e), req.params); } break; } default: break; } count++; }); if (count > 2) { return false; } } return true; } inline bool Server::write_response(Stream &strm, bool close_connection, const Request &req, Response &res) { return write_response_core(strm, close_connection, req, res, false); } inline bool Server::write_response_with_content(Stream &strm, bool close_connection, const Request &req, Response &res) { return write_response_core(strm, close_connection, req, res, true); } inline bool Server::write_response_core(Stream &strm, bool close_connection, const Request &req, Response &res, bool need_apply_ranges) { assert(res.status != -1); if (400 <= res.status && error_handler_ && error_handler_(req, res) == HandlerResponse::Handled) { need_apply_ranges = true; } std::string content_type; std::string boundary; if (need_apply_ranges) { apply_ranges(req, res, content_type, boundary); } // Prepare additional headers if (close_connection || req.get_header_value("Connection") == "close") { res.set_header("Connection", "close"); } else { std::stringstream ss; ss << "timeout=" << keep_alive_timeout_sec_ << ", max=" << keep_alive_max_count_; res.set_header("Keep-Alive", ss.str()); } if (!res.has_header("Content-Type") && (!res.body.empty() || res.content_length_ > 0 || res.content_provider_)) { res.set_header("Content-Type", "text/plain"); } if (!res.has_header("Content-Length") && res.body.empty() && !res.content_length_ && !res.content_provider_) { res.set_header("Content-Length", "0"); } if (!res.has_header("Accept-Ranges") && req.method == "HEAD") { res.set_header("Accept-Ranges", "bytes"); } if (post_routing_handler_) { post_routing_handler_(req, res); } // Response line and headers { detail::BufferStream bstrm; if (!bstrm.write_format("HTTP/1.1 %d %s\r\n", res.status, status_message(res.status))) { return false; } if (!detail::write_headers(bstrm, res.headers)) { return false; } // Flush buffer auto &data = bstrm.get_buffer(); detail::write_data(strm, data.data(), data.size()); } // Body auto ret = true; if (req.method != "HEAD") { if (!res.body.empty()) { if (!detail::write_data(strm, res.body.data(), res.body.size())) { ret = false; } } else if (res.content_provider_) { if (write_content_with_provider(strm, req, res, boundary, content_type)) { res.content_provider_success_ = true; } else { res.content_provider_success_ = false; ret = false; } } } // Log if (logger_) { logger_(req, res); } return ret; } inline bool Server::write_content_with_provider(Stream &strm, const Request &req, Response &res, const std::string &boundary, const std::string &content_type) { auto is_shutting_down = [this]() { return this->svr_sock_ == INVALID_SOCKET; }; if (res.content_length_ > 0) { if (req.ranges.empty()) { return detail::write_content(strm, res.content_provider_, 0, res.content_length_, is_shutting_down); } else if (req.ranges.size() == 1) { auto offsets = detail::get_range_offset_and_length(req, res.content_length_, 0); auto offset = offsets.first; auto length = offsets.second; return detail::write_content(strm, res.content_provider_, offset, length, is_shutting_down); } else { return detail::write_multipart_ranges_data( strm, req, res, boundary, content_type, is_shutting_down); } } else { if (res.is_chunked_content_provider_) { auto type = detail::encoding_type(req, res); std::unique_ptr compressor; if (type == detail::EncodingType::Gzip) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT compressor = detail::make_unique(); #endif } else if (type == detail::EncodingType::Brotli) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT compressor = detail::make_unique(); #endif } else { compressor = detail::make_unique(); } assert(compressor != nullptr); return detail::write_content_chunked(strm, res.content_provider_, is_shutting_down, *compressor); } else { return detail::write_content_without_length(strm, res.content_provider_, is_shutting_down); } } } inline bool Server::read_content(Stream &strm, Request &req, Response &res) { MultipartFormDataMap::iterator cur; auto file_count = 0; if (read_content_core( strm, req, res, // Regular [&](const char *buf, size_t n) { if (req.body.size() + n > req.body.max_size()) { return false; } req.body.append(buf, n); return true; }, // Multipart [&](const MultipartFormData &file) { if (file_count++ == CPPHTTPLIB_MULTIPART_FORM_DATA_FILE_MAX_COUNT) { return false; } cur = req.files.emplace(file.name, file); return true; }, [&](const char *buf, size_t n) { auto &content = cur->second.content; if (content.size() + n > content.max_size()) { return false; } content.append(buf, n); return true; })) { const auto &content_type = req.get_header_value("Content-Type"); if (!content_type.find("application/x-www-form-urlencoded")) { if (req.body.size() > CPPHTTPLIB_FORM_URL_ENCODED_PAYLOAD_MAX_LENGTH) { res.status = 413; // NOTE: should be 414? return false; } detail::parse_query_text(req.body, req.params); } return true; } return false; } inline bool Server::read_content_with_content_receiver( Stream &strm, Request &req, Response &res, ContentReceiver receiver, MultipartContentHeader multipart_header, ContentReceiver multipart_receiver) { return read_content_core(strm, req, res, std::move(receiver), std::move(multipart_header), std::move(multipart_receiver)); } inline bool Server::read_content_core(Stream &strm, Request &req, Response &res, ContentReceiver receiver, MultipartContentHeader multipart_header, ContentReceiver multipart_receiver) { detail::MultipartFormDataParser multipart_form_data_parser; ContentReceiverWithProgress out; if (req.is_multipart_form_data()) { const auto &content_type = req.get_header_value("Content-Type"); std::string boundary; if (!detail::parse_multipart_boundary(content_type, boundary)) { res.status = 400; return false; } multipart_form_data_parser.set_boundary(std::move(boundary)); out = [&](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) { /* For debug size_t pos = 0; while (pos < n) { auto read_size = (std::min)(1, n - pos); auto ret = multipart_form_data_parser.parse( buf + pos, read_size, multipart_receiver, multipart_header); if (!ret) { return false; } pos += read_size; } return true; */ return multipart_form_data_parser.parse(buf, n, multipart_receiver, multipart_header); }; } else { out = [receiver](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) { return receiver(buf, n); }; } if (req.method == "DELETE" && !req.has_header("Content-Length")) { return true; } if (!detail::read_content(strm, req, payload_max_length_, res.status, nullptr, out, true)) { return false; } if (req.is_multipart_form_data()) { if (!multipart_form_data_parser.is_valid()) { res.status = 400; return false; } } return true; } inline bool Server::handle_file_request(const Request &req, Response &res, bool head) { for (const auto &entry : base_dirs_) { // Prefix match if (!req.path.compare(0, entry.mount_point.size(), entry.mount_point)) { std::string sub_path = "/" + req.path.substr(entry.mount_point.size()); if (detail::is_valid_path(sub_path)) { auto path = entry.base_dir + sub_path; if (path.back() == '/') { path += "index.html"; } if (detail::is_file(path)) { for (const auto &kv : entry.headers) { res.set_header(kv.first.c_str(), kv.second); } auto mm = std::make_shared(path.c_str()); if (!mm->is_open()) { return false; } res.set_content_provider( mm->size(), detail::find_content_type(path, file_extension_and_mimetype_map_, default_file_mimetype_), [mm](size_t offset, size_t length, DataSink &sink) -> bool { sink.write(mm->data() + offset, length); return true; }); if (!head && file_request_handler_) { file_request_handler_(req, res); } return true; } } } } return false; } inline socket_t Server::create_server_socket(const std::string &host, int port, int socket_flags, SocketOptions socket_options) const { return detail::create_socket( host, std::string(), port, address_family_, socket_flags, tcp_nodelay_, std::move(socket_options), [](socket_t sock, struct addrinfo &ai) -> bool { if (::bind(sock, ai.ai_addr, static_cast(ai.ai_addrlen))) { return false; } if (::listen(sock, CPPHTTPLIB_LISTEN_BACKLOG)) { return false; } return true; }); } inline int Server::bind_internal(const std::string &host, int port, int socket_flags) { if (!is_valid()) { return -1; } svr_sock_ = create_server_socket(host, port, socket_flags, socket_options_); if (svr_sock_ == INVALID_SOCKET) { return -1; } if (port == 0) { struct sockaddr_storage addr; socklen_t addr_len = sizeof(addr); if (getsockname(svr_sock_, reinterpret_cast(&addr), &addr_len) == -1) { return -1; } if (addr.ss_family == AF_INET) { return ntohs(reinterpret_cast(&addr)->sin_port); } else if (addr.ss_family == AF_INET6) { return ntohs(reinterpret_cast(&addr)->sin6_port); } else { return -1; } } else { return port; } } inline bool Server::listen_internal() { auto ret = true; is_running_ = true; auto se = detail::scope_exit([&]() { is_running_ = false; }); { std::unique_ptr task_queue(new_task_queue()); while (svr_sock_ != INVALID_SOCKET) { #ifndef _WIN32 if (idle_interval_sec_ > 0 || idle_interval_usec_ > 0) { #endif auto val = detail::select_read(svr_sock_, idle_interval_sec_, idle_interval_usec_); if (val == 0) { // Timeout task_queue->on_idle(); continue; } #ifndef _WIN32 } #endif socket_t sock = accept(svr_sock_, nullptr, nullptr); if (sock == INVALID_SOCKET) { if (errno == EMFILE) { // The per-process limit of open file descriptors has been reached. // Try to accept new connections after a short sleep. std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } else if (errno == EINTR || errno == EAGAIN) { continue; } if (svr_sock_ != INVALID_SOCKET) { detail::close_socket(svr_sock_); ret = false; } else { ; // The server socket was closed by user. } break; } { #ifdef _WIN32 auto timeout = static_cast(read_timeout_sec_ * 1000 + read_timeout_usec_ / 1000); setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(read_timeout_sec_); tv.tv_usec = static_cast(read_timeout_usec_); setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, reinterpret_cast(&tv), sizeof(tv)); #endif } { #ifdef _WIN32 auto timeout = static_cast(write_timeout_sec_ * 1000 + write_timeout_usec_ / 1000); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&timeout), sizeof(timeout)); #else timeval tv; tv.tv_sec = static_cast(write_timeout_sec_); tv.tv_usec = static_cast(write_timeout_usec_); setsockopt(sock, SOL_SOCKET, SO_SNDTIMEO, reinterpret_cast(&tv), sizeof(tv)); #endif } task_queue->enqueue([this, sock]() { process_and_close_socket(sock); }); } task_queue->shutdown(); } return ret; } inline bool Server::routing(Request &req, Response &res, Stream &strm) { if (pre_routing_handler_ && pre_routing_handler_(req, res) == HandlerResponse::Handled) { return true; } // File handler auto is_head_request = req.method == "HEAD"; if ((req.method == "GET" || is_head_request) && handle_file_request(req, res, is_head_request)) { return true; } if (detail::expect_content(req)) { // Content reader handler { ContentReader reader( [&](ContentReceiver receiver) { return read_content_with_content_receiver( strm, req, res, std::move(receiver), nullptr, nullptr); }, [&](MultipartContentHeader header, ContentReceiver receiver) { return read_content_with_content_receiver(strm, req, res, nullptr, std::move(header), std::move(receiver)); }); if (req.method == "POST") { if (dispatch_request_for_content_reader( req, res, std::move(reader), post_handlers_for_content_reader_)) { return true; } } else if (req.method == "PUT") { if (dispatch_request_for_content_reader( req, res, std::move(reader), put_handlers_for_content_reader_)) { return true; } } else if (req.method == "PATCH") { if (dispatch_request_for_content_reader( req, res, std::move(reader), patch_handlers_for_content_reader_)) { return true; } } else if (req.method == "DELETE") { if (dispatch_request_for_content_reader( req, res, std::move(reader), delete_handlers_for_content_reader_)) { return true; } } } // Read content into `req.body` if (!read_content(strm, req, res)) { return false; } } // Regular handler if (req.method == "GET" || req.method == "HEAD") { return dispatch_request(req, res, get_handlers_); } else if (req.method == "POST") { return dispatch_request(req, res, post_handlers_); } else if (req.method == "PUT") { return dispatch_request(req, res, put_handlers_); } else if (req.method == "DELETE") { return dispatch_request(req, res, delete_handlers_); } else if (req.method == "OPTIONS") { return dispatch_request(req, res, options_handlers_); } else if (req.method == "PATCH") { return dispatch_request(req, res, patch_handlers_); } res.status = 400; return false; } inline bool Server::dispatch_request(Request &req, Response &res, const Handlers &handlers) { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { handler(req, res); return true; } } return false; } inline void Server::apply_ranges(const Request &req, Response &res, std::string &content_type, std::string &boundary) { if (req.ranges.size() > 1) { boundary = detail::make_multipart_data_boundary(); auto it = res.headers.find("Content-Type"); if (it != res.headers.end()) { content_type = it->second; res.headers.erase(it); } res.set_header("Content-Type", "multipart/byteranges; boundary=" + boundary); } auto type = detail::encoding_type(req, res); if (res.body.empty()) { if (res.content_length_ > 0) { size_t length = 0; if (req.ranges.empty()) { length = res.content_length_; } else if (req.ranges.size() == 1) { auto offsets = detail::get_range_offset_and_length(req, res.content_length_, 0); length = offsets.second; auto content_range = detail::make_content_range_header_field( req.ranges[0], res.content_length_); res.set_header("Content-Range", content_range); } else { length = detail::get_multipart_ranges_data_length(req, res, boundary, content_type); } res.set_header("Content-Length", std::to_string(length)); } else { if (res.content_provider_) { if (res.is_chunked_content_provider_) { res.set_header("Transfer-Encoding", "chunked"); if (type == detail::EncodingType::Gzip) { res.set_header("Content-Encoding", "gzip"); } else if (type == detail::EncodingType::Brotli) { res.set_header("Content-Encoding", "br"); } } } } } else { if (req.ranges.empty()) { ; } else if (req.ranges.size() == 1) { auto content_range = detail::make_content_range_header_field( req.ranges[0], res.body.size()); res.set_header("Content-Range", content_range); auto offsets = detail::get_range_offset_and_length(req, res.body.size(), 0); auto offset = offsets.first; auto length = offsets.second; if (offset < res.body.size()) { res.body = res.body.substr(offset, length); } else { res.body.clear(); res.status = 416; } } else { std::string data; if (detail::make_multipart_ranges_data(req, res, boundary, content_type, data)) { res.body.swap(data); } else { res.body.clear(); res.status = 416; } } if (type != detail::EncodingType::None) { std::unique_ptr compressor; std::string content_encoding; if (type == detail::EncodingType::Gzip) { #ifdef CPPHTTPLIB_ZLIB_SUPPORT compressor = detail::make_unique(); content_encoding = "gzip"; #endif } else if (type == detail::EncodingType::Brotli) { #ifdef CPPHTTPLIB_BROTLI_SUPPORT compressor = detail::make_unique(); content_encoding = "br"; #endif } if (compressor) { std::string compressed; if (compressor->compress(res.body.data(), res.body.size(), true, [&](const char *data, size_t data_len) { compressed.append(data, data_len); return true; })) { res.body.swap(compressed); res.set_header("Content-Encoding", content_encoding); } } } auto length = std::to_string(res.body.size()); res.set_header("Content-Length", length); } } inline bool Server::dispatch_request_for_content_reader( Request &req, Response &res, ContentReader content_reader, const HandlersForContentReader &handlers) { for (const auto &x : handlers) { const auto &matcher = x.first; const auto &handler = x.second; if (matcher->match(req)) { handler(req, res, content_reader); return true; } } return false; } inline bool Server::process_request(Stream &strm, bool close_connection, bool &connection_closed, const std::function &setup_request) { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); // Connection has been closed on client if (!line_reader.getline()) { return false; } Request req; Response res; res.version = "HTTP/1.1"; res.headers = default_headers_; #ifdef _WIN32 // TODO: Increase FD_SETSIZE statically (libzmq), dynamically (MySQL). #else #ifndef CPPHTTPLIB_USE_POLL // Socket file descriptor exceeded FD_SETSIZE... if (strm.socket() >= FD_SETSIZE) { Headers dummy; detail::read_headers(strm, dummy); res.status = 500; return write_response(strm, close_connection, req, res); } #endif #endif // Check if the request URI doesn't exceed the limit if (line_reader.size() > CPPHTTPLIB_REQUEST_URI_MAX_LENGTH) { Headers dummy; detail::read_headers(strm, dummy); res.status = 414; return write_response(strm, close_connection, req, res); } // Request line and headers if (!parse_request_line(line_reader.ptr(), req) || !detail::read_headers(strm, req.headers)) { res.status = 400; return write_response(strm, close_connection, req, res); } if (req.get_header_value("Connection") == "close") { connection_closed = true; } if (req.version == "HTTP/1.0" && req.get_header_value("Connection") != "Keep-Alive") { connection_closed = true; } strm.get_remote_ip_and_port(req.remote_addr, req.remote_port); req.set_header("REMOTE_ADDR", req.remote_addr); req.set_header("REMOTE_PORT", std::to_string(req.remote_port)); strm.get_local_ip_and_port(req.local_addr, req.local_port); req.set_header("LOCAL_ADDR", req.local_addr); req.set_header("LOCAL_PORT", std::to_string(req.local_port)); if (req.has_header("Range")) { const auto &range_header_value = req.get_header_value("Range"); if (!detail::parse_range_header(range_header_value, req.ranges)) { res.status = 416; return write_response(strm, close_connection, req, res); } } if (setup_request) { setup_request(req); } if (req.get_header_value("Expect") == "100-continue") { auto status = 100; if (expect_100_continue_handler_) { status = expect_100_continue_handler_(req, res); } switch (status) { case 100: case 417: strm.write_format("HTTP/1.1 %d %s\r\n\r\n", status, status_message(status)); break; default: return write_response(strm, close_connection, req, res); } } // Rounting auto routed = false; #ifdef CPPHTTPLIB_NO_EXCEPTIONS routed = routing(req, res, strm); #else try { routed = routing(req, res, strm); } catch (std::exception &e) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = 500; std::string val; auto s = e.what(); for (size_t i = 0; s[i]; i++) { switch (s[i]) { case '\r': val += "\\r"; break; case '\n': val += "\\n"; break; default: val += s[i]; break; } } res.set_header("EXCEPTION_WHAT", val); } } catch (...) { if (exception_handler_) { auto ep = std::current_exception(); exception_handler_(req, res, ep); routed = true; } else { res.status = 500; res.set_header("EXCEPTION_WHAT", "UNKNOWN"); } } #endif if (routed) { if (res.status == -1) { res.status = req.ranges.empty() ? 200 : 206; } return write_response_with_content(strm, close_connection, req, res); } else { if (res.status == -1) { res.status = 404; } return write_response(strm, close_connection, req, res); } } inline bool Server::is_valid() const { return true; } inline bool Server::process_and_close_socket(socket_t sock) { auto ret = detail::process_server_socket( svr_sock_, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [this](Stream &strm, bool close_connection, bool &connection_closed) { return process_request(strm, close_connection, connection_closed, nullptr); }); detail::shutdown_socket(sock); detail::close_socket(sock); return ret; } // HTTP client implementation inline ClientImpl::ClientImpl(const std::string &host) : ClientImpl(host, 80, std::string(), std::string()) {} inline ClientImpl::ClientImpl(const std::string &host, int port) : ClientImpl(host, port, std::string(), std::string()) {} inline ClientImpl::ClientImpl(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : host_(host), port_(port), host_and_port_(adjust_host_string(host) + ":" + std::to_string(port)), client_cert_path_(client_cert_path), client_key_path_(client_key_path) {} inline ClientImpl::~ClientImpl() { std::lock_guard guard(socket_mutex_); shutdown_socket(socket_); close_socket(socket_); } inline bool ClientImpl::is_valid() const { return true; } inline void ClientImpl::copy_settings(const ClientImpl &rhs) { client_cert_path_ = rhs.client_cert_path_; client_key_path_ = rhs.client_key_path_; connection_timeout_sec_ = rhs.connection_timeout_sec_; read_timeout_sec_ = rhs.read_timeout_sec_; read_timeout_usec_ = rhs.read_timeout_usec_; write_timeout_sec_ = rhs.write_timeout_sec_; write_timeout_usec_ = rhs.write_timeout_usec_; basic_auth_username_ = rhs.basic_auth_username_; basic_auth_password_ = rhs.basic_auth_password_; bearer_token_auth_token_ = rhs.bearer_token_auth_token_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT digest_auth_username_ = rhs.digest_auth_username_; digest_auth_password_ = rhs.digest_auth_password_; #endif keep_alive_ = rhs.keep_alive_; follow_location_ = rhs.follow_location_; url_encode_ = rhs.url_encode_; address_family_ = rhs.address_family_; tcp_nodelay_ = rhs.tcp_nodelay_; socket_options_ = rhs.socket_options_; compress_ = rhs.compress_; decompress_ = rhs.decompress_; interface_ = rhs.interface_; proxy_host_ = rhs.proxy_host_; proxy_port_ = rhs.proxy_port_; proxy_basic_auth_username_ = rhs.proxy_basic_auth_username_; proxy_basic_auth_password_ = rhs.proxy_basic_auth_password_; proxy_bearer_token_auth_token_ = rhs.proxy_bearer_token_auth_token_; #ifdef CPPHTTPLIB_OPENSSL_SUPPORT proxy_digest_auth_username_ = rhs.proxy_digest_auth_username_; proxy_digest_auth_password_ = rhs.proxy_digest_auth_password_; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT ca_cert_file_path_ = rhs.ca_cert_file_path_; ca_cert_dir_path_ = rhs.ca_cert_dir_path_; ca_cert_store_ = rhs.ca_cert_store_; #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT server_certificate_verification_ = rhs.server_certificate_verification_; #endif logger_ = rhs.logger_; } inline socket_t ClientImpl::create_client_socket(Error &error) const { if (!proxy_host_.empty() && proxy_port_ != -1) { return detail::create_client_socket( proxy_host_, std::string(), proxy_port_, address_family_, tcp_nodelay_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); } // Check is custom IP specified for host_ std::string ip; auto it = addr_map_.find(host_); if (it != addr_map_.end()) ip = it->second; return detail::create_client_socket( host_, ip, port_, address_family_, tcp_nodelay_, socket_options_, connection_timeout_sec_, connection_timeout_usec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, interface_, error); } inline bool ClientImpl::create_and_connect_socket(Socket &socket, Error &error) { auto sock = create_client_socket(error); if (sock == INVALID_SOCKET) { return false; } socket.sock = sock; return true; } inline void ClientImpl::shutdown_ssl(Socket & /*socket*/, bool /*shutdown_gracefully*/) { // If there are any requests in flight from threads other than us, then it's // a thread-unsafe race because individual ssl* objects are not thread-safe. assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); } inline void ClientImpl::shutdown_socket(Socket &socket) { if (socket.sock == INVALID_SOCKET) { return; } detail::shutdown_socket(socket.sock); } inline void ClientImpl::close_socket(Socket &socket) { // If there are requests in flight in another thread, usually closing // the socket will be fine and they will simply receive an error when // using the closed socket, but it is still a bug since rarely the OS // may reassign the socket id to be used for a new socket, and then // suddenly they will be operating on a live socket that is different // than the one they intended! assert(socket_requests_in_flight_ == 0 || socket_requests_are_from_thread_ == std::this_thread::get_id()); // It is also a bug if this happens while SSL is still active #ifdef CPPHTTPLIB_OPENSSL_SUPPORT assert(socket.ssl == nullptr); #endif if (socket.sock == INVALID_SOCKET) { return; } detail::close_socket(socket.sock); socket.sock = INVALID_SOCKET; } inline bool ClientImpl::read_response_line(Stream &strm, const Request &req, Response &res) { std::array buf{}; detail::stream_line_reader line_reader(strm, buf.data(), buf.size()); if (!line_reader.getline()) { return false; } #ifdef CPPHTTPLIB_ALLOW_LF_AS_LINE_TERMINATOR const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r\n"); #else const static std::regex re("(HTTP/1\\.[01]) (\\d{3})(?: (.*?))?\r?\n"); #endif std::cmatch m; if (!std::regex_match(line_reader.ptr(), m, re)) { return req.method == "CONNECT"; } res.version = std::string(m[1]); res.status = std::stoi(std::string(m[2])); res.reason = std::string(m[3]); // Ignore '100 Continue' while (res.status == 100) { if (!line_reader.getline()) { return false; } // CRLF if (!line_reader.getline()) { return false; } // next response line if (!std::regex_match(line_reader.ptr(), m, re)) { return false; } res.version = std::string(m[1]); res.status = std::stoi(std::string(m[2])); res.reason = std::string(m[3]); } return true; } inline bool ClientImpl::send(Request &req, Response &res, Error &error) { std::lock_guard request_mutex_guard(request_mutex_); auto ret = send_(req, res, error); if (error == Error::SSLPeerCouldBeClosed_) { assert(!ret); ret = send_(req, res, error); } return ret; } inline bool ClientImpl::send_(Request &req, Response &res, Error &error) { { std::lock_guard guard(socket_mutex_); // Set this to false immediately - if it ever gets set to true by the end of // the request, we know another thread instructed us to close the socket. socket_should_be_closed_when_request_is_done_ = false; auto is_alive = false; if (socket_.is_open()) { is_alive = detail::is_socket_alive(socket_.sock); if (!is_alive) { // Attempt to avoid sigpipe by shutting down nongracefully if it seems // like the other side has already closed the connection Also, there // cannot be any requests in flight from other threads since we locked // request_mutex_, so safe to close everything immediately const bool shutdown_gracefully = false; shutdown_ssl(socket_, shutdown_gracefully); shutdown_socket(socket_); close_socket(socket_); } } if (!is_alive) { if (!create_and_connect_socket(socket_, error)) { return false; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT // TODO: refactoring if (is_ssl()) { auto &scli = static_cast(*this); if (!proxy_host_.empty() && proxy_port_ != -1) { auto success = false; if (!scli.connect_with_proxy(socket_, res, success, error)) { return success; } } if (!scli.initialize_ssl(socket_, error)) { return false; } } #endif } // Mark the current socket as being in use so that it cannot be closed by // anyone else while this request is ongoing, even though we will be // releasing the mutex. if (socket_requests_in_flight_ > 1) { assert(socket_requests_are_from_thread_ == std::this_thread::get_id()); } socket_requests_in_flight_ += 1; socket_requests_are_from_thread_ = std::this_thread::get_id(); } for (const auto &header : default_headers_) { if (req.headers.find(header.first) == req.headers.end()) { req.headers.insert(header); } } auto ret = false; auto close_connection = !keep_alive_; auto se = detail::scope_exit([&]() { // Briefly lock mutex in order to mark that a request is no longer ongoing std::lock_guard guard(socket_mutex_); socket_requests_in_flight_ -= 1; if (socket_requests_in_flight_ <= 0) { assert(socket_requests_in_flight_ == 0); socket_requests_are_from_thread_ = std::thread::id(); } if (socket_should_be_closed_when_request_is_done_ || close_connection || !ret) { shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } }); ret = process_socket(socket_, [&](Stream &strm) { return handle_request(strm, req, res, close_connection, error); }); if (!ret) { if (error == Error::Success) { error = Error::Unknown; } } return ret; } inline Result ClientImpl::send(const Request &req) { auto req2 = req; return send_(std::move(req2)); } inline Result ClientImpl::send_(Request &&req) { auto res = detail::make_unique(); auto error = Error::Success; auto ret = send(req, *res, error); return Result{ret ? std::move(res) : nullptr, error, std::move(req.headers)}; } inline bool ClientImpl::handle_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { if (req.path.empty()) { error = Error::Connection; return false; } auto req_save = req; bool ret; if (!is_ssl() && !proxy_host_.empty() && proxy_port_ != -1) { auto req2 = req; req2.path = "http://" + host_and_port_ + req.path; ret = process_request(strm, req2, res, close_connection, error); req = req2; req.path = req_save.path; } else { ret = process_request(strm, req, res, close_connection, error); } if (!ret) { return false; } if (res.get_header_value("Connection") == "close" || (res.version == "HTTP/1.0" && res.reason != "Connection established")) { // TODO this requires a not-entirely-obvious chain of calls to be correct // for this to be safe. // This is safe to call because handle_request is only called by send_ // which locks the request mutex during the process. It would be a bug // to call it from a different thread since it's a thread-safety issue // to do these things to the socket if another thread is using the socket. std::lock_guard guard(socket_mutex_); shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } if (300 < res.status && res.status < 400 && follow_location_) { req = req_save; ret = redirect(req, res, error); } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT if ((res.status == 401 || res.status == 407) && req.authorization_count_ < 5) { auto is_proxy = res.status == 407; const auto &username = is_proxy ? proxy_digest_auth_username_ : digest_auth_username_; const auto &password = is_proxy ? proxy_digest_auth_password_ : digest_auth_password_; if (!username.empty() && !password.empty()) { std::map auth; if (detail::parse_www_authenticate(res, auth, is_proxy)) { Request new_req = req; new_req.authorization_count_ += 1; new_req.headers.erase(is_proxy ? "Proxy-Authorization" : "Authorization"); new_req.headers.insert(detail::make_digest_authentication_header( req, auth, new_req.authorization_count_, detail::random_string(10), username, password, is_proxy)); Response new_res; ret = send(new_req, new_res, error); if (ret) { res = new_res; } } } } #endif return ret; } inline bool ClientImpl::redirect(Request &req, Response &res, Error &error) { if (req.redirect_count_ == 0) { error = Error::ExceedRedirectCount; return false; } auto location = res.get_header_value("location"); if (location.empty()) { return false; } const static std::regex re( R"((?:(https?):)?(?://(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)?([^?#]*)(\?[^#]*)?(?:#.*)?)"); std::smatch m; if (!std::regex_match(location, m, re)) { return false; } auto scheme = is_ssl() ? "https" : "http"; auto next_scheme = m[1].str(); auto next_host = m[2].str(); if (next_host.empty()) { next_host = m[3].str(); } auto port_str = m[4].str(); auto next_path = m[5].str(); auto next_query = m[6].str(); auto next_port = port_; if (!port_str.empty()) { next_port = std::stoi(port_str); } else if (!next_scheme.empty()) { next_port = next_scheme == "https" ? 443 : 80; } if (next_scheme.empty()) { next_scheme = scheme; } if (next_host.empty()) { next_host = host_; } if (next_path.empty()) { next_path = "/"; } auto path = detail::decode_url(next_path, true) + next_query; if (next_scheme == scheme && next_host == host_ && next_port == port_) { return detail::redirect(*this, req, res, path, location, error); } else { if (next_scheme == "https") { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT SSLClient cli(next_host.c_str(), next_port); cli.copy_settings(*this); if (ca_cert_store_) { cli.set_ca_cert_store(ca_cert_store_); } return detail::redirect(cli, req, res, path, location, error); #else return false; #endif } else { ClientImpl cli(next_host.c_str(), next_port); cli.copy_settings(*this); return detail::redirect(cli, req, res, path, location, error); } } } inline bool ClientImpl::write_content_with_provider(Stream &strm, const Request &req, Error &error) { auto is_shutting_down = []() { return false; }; if (req.is_chunked_content_provider_) { // TODO: Brotli support std::unique_ptr compressor; #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_) { compressor = detail::make_unique(); } else #endif { compressor = detail::make_unique(); } return detail::write_content_chunked(strm, req.content_provider_, is_shutting_down, *compressor, error); } else { return detail::write_content(strm, req.content_provider_, 0, req.content_length_, is_shutting_down, error); } } inline bool ClientImpl::write_request(Stream &strm, Request &req, bool close_connection, Error &error) { // Prepare additional headers if (close_connection) { if (!req.has_header("Connection")) { req.set_header("Connection", "close"); } } if (!req.has_header("Host")) { if (is_ssl()) { if (port_ == 443) { req.set_header("Host", host_); } else { req.set_header("Host", host_and_port_); } } else { if (port_ == 80) { req.set_header("Host", host_); } else { req.set_header("Host", host_and_port_); } } } if (!req.has_header("Accept")) { req.set_header("Accept", "*/*"); } #ifndef CPPHTTPLIB_NO_DEFAULT_USER_AGENT if (!req.has_header("User-Agent")) { auto agent = std::string("cpp-httplib/") + CPPHTTPLIB_VERSION; req.set_header("User-Agent", agent); } #endif if (req.body.empty()) { if (req.content_provider_) { if (!req.is_chunked_content_provider_) { if (!req.has_header("Content-Length")) { auto length = std::to_string(req.content_length_); req.set_header("Content-Length", length); } } } else { if (req.method == "POST" || req.method == "PUT" || req.method == "PATCH") { req.set_header("Content-Length", "0"); } } } else { if (!req.has_header("Content-Type")) { req.set_header("Content-Type", "text/plain"); } if (!req.has_header("Content-Length")) { auto length = std::to_string(req.body.size()); req.set_header("Content-Length", length); } } if (!basic_auth_password_.empty() || !basic_auth_username_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_basic_authentication_header( basic_auth_username_, basic_auth_password_, false)); } } if (!proxy_basic_auth_username_.empty() && !proxy_basic_auth_password_.empty()) { if (!req.has_header("Proxy-Authorization")) { req.headers.insert(make_basic_authentication_header( proxy_basic_auth_username_, proxy_basic_auth_password_, true)); } } if (!bearer_token_auth_token_.empty()) { if (!req.has_header("Authorization")) { req.headers.insert(make_bearer_token_authentication_header( bearer_token_auth_token_, false)); } } if (!proxy_bearer_token_auth_token_.empty()) { if (!req.has_header("Proxy-Authorization")) { req.headers.insert(make_bearer_token_authentication_header( proxy_bearer_token_auth_token_, true)); } } // Request line and headers { detail::BufferStream bstrm; const auto &path = url_encode_ ? detail::encode_url(req.path) : req.path; bstrm.write_format("%s %s HTTP/1.1\r\n", req.method.c_str(), path.c_str()); detail::write_headers(bstrm, req.headers); // Flush buffer auto &data = bstrm.get_buffer(); if (!detail::write_data(strm, data.data(), data.size())) { error = Error::Write; return false; } } // Body if (req.body.empty()) { return write_content_with_provider(strm, req, error); } if (!detail::write_data(strm, req.body.data(), req.body.size())) { error = Error::Write; return false; } return true; } inline std::unique_ptr ClientImpl::send_with_content_provider( Request &req, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type, Error &error) { if (!content_type.empty()) { req.set_header("Content-Type", content_type); } #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_) { req.set_header("Content-Encoding", "gzip"); } #endif #ifdef CPPHTTPLIB_ZLIB_SUPPORT if (compress_ && !content_provider_without_length) { // TODO: Brotli support detail::gzip_compressor compressor; if (content_provider) { auto ok = true; size_t offset = 0; DataSink data_sink; data_sink.write = [&](const char *data, size_t data_len) -> bool { if (ok) { auto last = offset + data_len == content_length; auto ret = compressor.compress( data, data_len, last, [&](const char *compressed_data, size_t compressed_data_len) { req.body.append(compressed_data, compressed_data_len); return true; }); if (ret) { offset += data_len; } else { ok = false; } } return ok; }; while (ok && offset < content_length) { if (!content_provider(offset, content_length - offset, data_sink)) { error = Error::Canceled; return nullptr; } } } else { if (!compressor.compress(body, content_length, true, [&](const char *data, size_t data_len) { req.body.append(data, data_len); return true; })) { error = Error::Compression; return nullptr; } } } else #endif { if (content_provider) { req.content_length_ = content_length; req.content_provider_ = std::move(content_provider); req.is_chunked_content_provider_ = false; } else if (content_provider_without_length) { req.content_length_ = 0; req.content_provider_ = detail::ContentProviderAdapter( std::move(content_provider_without_length)); req.is_chunked_content_provider_ = true; req.set_header("Transfer-Encoding", "chunked"); } else { req.body.assign(body, content_length); } } auto res = detail::make_unique(); return send(req, *res, error) ? std::move(res) : nullptr; } inline Result ClientImpl::send_with_content_provider( const std::string &method, const std::string &path, const Headers &headers, const char *body, size_t content_length, ContentProvider content_provider, ContentProviderWithoutLength content_provider_without_length, const std::string &content_type) { Request req; req.method = method; req.headers = headers; req.path = path; auto error = Error::Success; auto res = send_with_content_provider( req, body, content_length, std::move(content_provider), std::move(content_provider_without_length), content_type, error); return Result{std::move(res), error, std::move(req.headers)}; } inline std::string ClientImpl::adjust_host_string(const std::string &host) const { if (host.find(':') != std::string::npos) { return "[" + host + "]"; } return host; } inline bool ClientImpl::process_request(Stream &strm, Request &req, Response &res, bool close_connection, Error &error) { // Send request if (!write_request(strm, req, close_connection, error)) { return false; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT if (is_ssl()) { auto is_proxy_enabled = !proxy_host_.empty() && proxy_port_ != -1; if (!is_proxy_enabled) { char buf[1]; if (SSL_peek(socket_.ssl, buf, 1) == 0 && SSL_get_error(socket_.ssl, 0) == SSL_ERROR_ZERO_RETURN) { error = Error::SSLPeerCouldBeClosed_; return false; } } } #endif // Receive response and headers if (!read_response_line(strm, req, res) || !detail::read_headers(strm, res.headers)) { error = Error::Read; return false; } // Body if ((res.status != 204) && req.method != "HEAD" && req.method != "CONNECT") { auto redirect = 300 < res.status && res.status < 400 && follow_location_; if (req.response_handler && !redirect) { if (!req.response_handler(res)) { error = Error::Canceled; return false; } } auto out = req.content_receiver ? static_cast( [&](const char *buf, size_t n, uint64_t off, uint64_t len) { if (redirect) { return true; } auto ret = req.content_receiver(buf, n, off, len); if (!ret) { error = Error::Canceled; } return ret; }) : static_cast( [&](const char *buf, size_t n, uint64_t /*off*/, uint64_t /*len*/) { if (res.body.size() + n > res.body.max_size()) { return false; } res.body.append(buf, n); return true; }); auto progress = [&](uint64_t current, uint64_t total) { if (!req.progress || redirect) { return true; } auto ret = req.progress(current, total); if (!ret) { error = Error::Canceled; } return ret; }; int dummy_status; if (!detail::read_content(strm, res, (std::numeric_limits::max)(), dummy_status, std::move(progress), std::move(out), decompress_)) { if (error != Error::Canceled) { error = Error::Read; } return false; } } // Log if (logger_) { logger_(req, res); } return true; } inline ContentProviderWithoutLength ClientImpl::get_multipart_content_provider( const std::string &boundary, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items) { size_t cur_item = 0, cur_start = 0; // cur_item and cur_start are copied to within the std::function and maintain // state between successive calls return [&, cur_item, cur_start](size_t offset, DataSink &sink) mutable -> bool { if (!offset && items.size()) { sink.os << detail::serialize_multipart_formdata(items, boundary, false); return true; } else if (cur_item < provider_items.size()) { if (!cur_start) { const auto &begin = detail::serialize_multipart_formdata_item_begin( provider_items[cur_item], boundary); offset += begin.size(); cur_start = offset; sink.os << begin; } DataSink cur_sink; auto has_data = true; cur_sink.write = sink.write; cur_sink.done = [&]() { has_data = false; }; if (!provider_items[cur_item].provider(offset - cur_start, cur_sink)) return false; if (!has_data) { sink.os << detail::serialize_multipart_formdata_item_end(); cur_item++; cur_start = 0; } return true; } else { sink.os << detail::serialize_multipart_formdata_finish(boundary); sink.done(); return true; } }; } inline bool ClientImpl::process_socket(const Socket &socket, std::function callback) { return detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, std::move(callback)); } inline bool ClientImpl::is_ssl() const { return false; } inline Result ClientImpl::Get(const std::string &path) { return Get(path, Headers(), Progress()); } inline Result ClientImpl::Get(const std::string &path, Progress progress) { return Get(path, Headers(), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers) { return Get(path, headers, Progress()); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, Progress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.progress = std::move(progress); return send_(std::move(req)); } inline Result ClientImpl::Get(const std::string &path, ContentReceiver content_receiver) { return Get(path, Headers(), nullptr, std::move(content_receiver), nullptr); } inline Result ClientImpl::Get(const std::string &path, ContentReceiver content_receiver, Progress progress) { return Get(path, Headers(), nullptr, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver) { return Get(path, headers, nullptr, std::move(content_receiver), nullptr); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, Progress progress) { return Get(path, headers, nullptr, std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver) { return Get(path, Headers(), std::move(response_handler), std::move(content_receiver), nullptr); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver) { return Get(path, headers, std::move(response_handler), std::move(content_receiver), nullptr); } inline Result ClientImpl::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { return Get(path, Headers(), std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result ClientImpl::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { Request req; req.method = "GET"; req.path = path; req.headers = headers; req.response_handler = std::move(response_handler); req.content_receiver = [content_receiver](const char *data, size_t data_length, uint64_t /*offset*/, uint64_t /*total_length*/) { return content_receiver(data, data_length); }; req.progress = std::move(progress); return send_(std::move(req)); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, Progress progress) { if (params.empty()) { return Get(path, headers); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query.c_str(), headers, progress); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, Progress progress) { return Get(path, params, headers, nullptr, content_receiver, progress); } inline Result ClientImpl::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { if (params.empty()) { return Get(path, headers, response_handler, content_receiver, progress); } std::string path_with_query = append_query_params(path, params); return Get(path_with_query.c_str(), headers, response_handler, content_receiver, progress); } inline Result ClientImpl::Head(const std::string &path) { return Head(path, Headers()); } inline Result ClientImpl::Head(const std::string &path, const Headers &headers) { Request req; req.method = "HEAD"; req.headers = headers; req.path = path; return send_(std::move(req)); } inline Result ClientImpl::Post(const std::string &path) { return Post(path, std::string(), std::string()); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers) { return Post(path, headers, nullptr, 0, std::string()); } inline Result ClientImpl::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return Post(path, Headers(), body, content_length, content_type); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return send_with_content_provider("POST", path, headers, body, content_length, nullptr, nullptr, content_type); } inline Result ClientImpl::Post(const std::string &path, const std::string &body, const std::string &content_type) { return Post(path, Headers(), body, content_type); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return send_with_content_provider("POST", path, headers, body.data(), body.size(), nullptr, nullptr, content_type); } inline Result ClientImpl::Post(const std::string &path, const Params ¶ms) { return Post(path, Headers(), params); } inline Result ClientImpl::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return Post(path, Headers(), content_length, std::move(content_provider), content_type); } inline Result ClientImpl::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return Post(path, Headers(), std::move(content_provider), content_type); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return send_with_content_provider("POST", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return send_with_content_provider("POST", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Post(path, headers, query, "application/x-www-form-urlencoded"); } inline Result ClientImpl::Post(const std::string &path, const MultipartFormDataItems &items) { return Post(path, Headers(), items); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Post(path, headers, body, content_type.c_str()); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Post(path, headers, body, content_type.c_str()); } inline Result ClientImpl::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider( "POST", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type); } inline Result ClientImpl::Put(const std::string &path) { return Put(path, std::string(), std::string()); } inline Result ClientImpl::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return Put(path, Headers(), body, content_length, content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return send_with_content_provider("PUT", path, headers, body, content_length, nullptr, nullptr, content_type); } inline Result ClientImpl::Put(const std::string &path, const std::string &body, const std::string &content_type) { return Put(path, Headers(), body, content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return send_with_content_provider("PUT", path, headers, body.data(), body.size(), nullptr, nullptr, content_type); } inline Result ClientImpl::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return Put(path, Headers(), content_length, std::move(content_provider), content_type); } inline Result ClientImpl::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return Put(path, Headers(), std::move(content_provider), content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return send_with_content_provider("PUT", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return send_with_content_provider("PUT", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type); } inline Result ClientImpl::Put(const std::string &path, const Params ¶ms) { return Put(path, Headers(), params); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const Params ¶ms) { auto query = detail::params_to_query_str(params); return Put(path, headers, query, "application/x-www-form-urlencoded"); } inline Result ClientImpl::Put(const std::string &path, const MultipartFormDataItems &items) { return Put(path, Headers(), items); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Put(path, headers, body, content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary) { if (!detail::is_multipart_boundary_chars_valid(boundary)) { return Result{nullptr, Error::UnsupportedMultipartBoundaryChars}; } const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); const auto &body = detail::serialize_multipart_formdata(items, boundary); return Put(path, headers, body, content_type); } inline Result ClientImpl::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items) { const auto &boundary = detail::make_multipart_data_boundary(); const auto &content_type = detail::serialize_multipart_formdata_get_content_type(boundary); return send_with_content_provider( "PUT", path, headers, nullptr, 0, nullptr, get_multipart_content_provider(boundary, items, provider_items), content_type); } inline Result ClientImpl::Patch(const std::string &path) { return Patch(path, std::string(), std::string()); } inline Result ClientImpl::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return Patch(path, Headers(), body, content_length, content_type); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return send_with_content_provider("PATCH", path, headers, body, content_length, nullptr, nullptr, content_type); } inline Result ClientImpl::Patch(const std::string &path, const std::string &body, const std::string &content_type) { return Patch(path, Headers(), body, content_type); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return send_with_content_provider("PATCH", path, headers, body.data(), body.size(), nullptr, nullptr, content_type); } inline Result ClientImpl::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return Patch(path, Headers(), content_length, std::move(content_provider), content_type); } inline Result ClientImpl::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return Patch(path, Headers(), std::move(content_provider), content_type); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return send_with_content_provider("PATCH", path, headers, nullptr, content_length, std::move(content_provider), nullptr, content_type); } inline Result ClientImpl::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return send_with_content_provider("PATCH", path, headers, nullptr, 0, nullptr, std::move(content_provider), content_type); } inline Result ClientImpl::Delete(const std::string &path) { return Delete(path, Headers(), std::string(), std::string()); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers) { return Delete(path, headers, std::string(), std::string()); } inline Result ClientImpl::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return Delete(path, Headers(), body, content_length, content_type); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { Request req; req.method = "DELETE"; req.headers = headers; req.path = path; if (!content_type.empty()) { req.set_header("Content-Type", content_type); } req.body.assign(body, content_length); return send_(std::move(req)); } inline Result ClientImpl::Delete(const std::string &path, const std::string &body, const std::string &content_type) { return Delete(path, Headers(), body.data(), body.size(), content_type); } inline Result ClientImpl::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return Delete(path, headers, body.data(), body.size(), content_type); } inline Result ClientImpl::Options(const std::string &path) { return Options(path, Headers()); } inline Result ClientImpl::Options(const std::string &path, const Headers &headers) { Request req; req.method = "OPTIONS"; req.headers = headers; req.path = path; return send_(std::move(req)); } inline void ClientImpl::stop() { std::lock_guard guard(socket_mutex_); // If there is anything ongoing right now, the ONLY thread-safe thing we can // do is to shutdown_socket, so that threads using this socket suddenly // discover they can't read/write any more and error out. Everything else // (closing the socket, shutting ssl down) is unsafe because these actions are // not thread-safe. if (socket_requests_in_flight_ > 0) { shutdown_socket(socket_); // Aside from that, we set a flag for the socket to be closed when we're // done. socket_should_be_closed_when_request_is_done_ = true; return; } // Otherwise, still holding the mutex, we can shut everything down ourselves shutdown_ssl(socket_, true); shutdown_socket(socket_); close_socket(socket_); } inline std::string ClientImpl::host() const { return host_; } inline int ClientImpl::port() const { return port_; } inline size_t ClientImpl::is_socket_open() const { std::lock_guard guard(socket_mutex_); return socket_.is_open(); } inline socket_t ClientImpl::socket() const { return socket_.sock; } inline void ClientImpl::set_connection_timeout(time_t sec, time_t usec) { connection_timeout_sec_ = sec; connection_timeout_usec_ = usec; } inline void ClientImpl::set_read_timeout(time_t sec, time_t usec) { read_timeout_sec_ = sec; read_timeout_usec_ = usec; } inline void ClientImpl::set_write_timeout(time_t sec, time_t usec) { write_timeout_sec_ = sec; write_timeout_usec_ = usec; } inline void ClientImpl::set_basic_auth(const std::string &username, const std::string &password) { basic_auth_username_ = username; basic_auth_password_ = password; } inline void ClientImpl::set_bearer_token_auth(const std::string &token) { bearer_token_auth_token_ = token; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void ClientImpl::set_digest_auth(const std::string &username, const std::string &password) { digest_auth_username_ = username; digest_auth_password_ = password; } #endif inline void ClientImpl::set_keep_alive(bool on) { keep_alive_ = on; } inline void ClientImpl::set_follow_location(bool on) { follow_location_ = on; } inline void ClientImpl::set_url_encode(bool on) { url_encode_ = on; } inline void ClientImpl::set_hostname_addr_map(std::map addr_map) { addr_map_ = std::move(addr_map); } inline void ClientImpl::set_default_headers(Headers headers) { default_headers_ = std::move(headers); } inline void ClientImpl::set_address_family(int family) { address_family_ = family; } inline void ClientImpl::set_tcp_nodelay(bool on) { tcp_nodelay_ = on; } inline void ClientImpl::set_socket_options(SocketOptions socket_options) { socket_options_ = std::move(socket_options); } inline void ClientImpl::set_compress(bool on) { compress_ = on; } inline void ClientImpl::set_decompress(bool on) { decompress_ = on; } inline void ClientImpl::set_interface(const std::string &intf) { interface_ = intf; } inline void ClientImpl::set_proxy(const std::string &host, int port) { proxy_host_ = host; proxy_port_ = port; } inline void ClientImpl::set_proxy_basic_auth(const std::string &username, const std::string &password) { proxy_basic_auth_username_ = username; proxy_basic_auth_password_ = password; } inline void ClientImpl::set_proxy_bearer_token_auth(const std::string &token) { proxy_bearer_token_auth_token_ = token; } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void ClientImpl::set_proxy_digest_auth(const std::string &username, const std::string &password) { proxy_digest_auth_username_ = username; proxy_digest_auth_password_ = password; } inline void ClientImpl::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { ca_cert_file_path_ = ca_cert_file_path; ca_cert_dir_path_ = ca_cert_dir_path; } inline void ClientImpl::set_ca_cert_store(X509_STORE *ca_cert_store) { if (ca_cert_store && ca_cert_store != ca_cert_store_) { ca_cert_store_ = ca_cert_store; } } inline X509_STORE *ClientImpl::create_ca_cert_store(const char *ca_cert, std::size_t size) { auto mem = BIO_new_mem_buf(ca_cert, static_cast(size)); if (!mem) return nullptr; auto inf = PEM_X509_INFO_read_bio(mem, nullptr, nullptr, nullptr); if (!inf) { BIO_free_all(mem); return nullptr; } auto cts = X509_STORE_new(); if (cts) { for (auto i = 0; i < static_cast(sk_X509_INFO_num(inf)); i++) { auto itmp = sk_X509_INFO_value(inf, i); if (!itmp) { continue; } if (itmp->x509) { X509_STORE_add_cert(cts, itmp->x509); } if (itmp->crl) { X509_STORE_add_crl(cts, itmp->crl); } } } sk_X509_INFO_pop_free(inf, X509_INFO_free); BIO_free_all(mem); return cts; } inline void ClientImpl::enable_server_certificate_verification(bool enabled) { server_certificate_verification_ = enabled; } #endif inline void ClientImpl::set_logger(Logger logger) { logger_ = std::move(logger); } /* * SSL Implementation */ #ifdef CPPHTTPLIB_OPENSSL_SUPPORT namespace detail { template inline SSL *ssl_new(socket_t sock, SSL_CTX *ctx, std::mutex &ctx_mutex, U SSL_connect_or_accept, V setup) { SSL *ssl = nullptr; { std::lock_guard guard(ctx_mutex); ssl = SSL_new(ctx); } if (ssl) { set_nonblocking(sock, true); auto bio = BIO_new_socket(static_cast(sock), BIO_NOCLOSE); BIO_set_nbio(bio, 1); SSL_set_bio(ssl, bio, bio); if (!setup(ssl) || SSL_connect_or_accept(ssl) != 1) { SSL_shutdown(ssl); { std::lock_guard guard(ctx_mutex); SSL_free(ssl); } set_nonblocking(sock, false); return nullptr; } BIO_set_nbio(bio, 0); set_nonblocking(sock, false); } return ssl; } inline void ssl_delete(std::mutex &ctx_mutex, SSL *ssl, bool shutdown_gracefully) { // sometimes we may want to skip this to try to avoid SIGPIPE if we know // the remote has closed the network connection // Note that it is not always possible to avoid SIGPIPE, this is merely a // best-efforts. if (shutdown_gracefully) { SSL_shutdown(ssl); } std::lock_guard guard(ctx_mutex); SSL_free(ssl); } template bool ssl_connect_or_accept_nonblocking(socket_t sock, SSL *ssl, U ssl_connect_or_accept, time_t timeout_sec, time_t timeout_usec) { auto res = 0; while ((res = ssl_connect_or_accept(ssl)) != 1) { auto err = SSL_get_error(ssl, res); switch (err) { case SSL_ERROR_WANT_READ: if (select_read(sock, timeout_sec, timeout_usec) > 0) { continue; } break; case SSL_ERROR_WANT_WRITE: if (select_write(sock, timeout_sec, timeout_usec) > 0) { continue; } break; default: break; } return false; } return true; } template inline bool process_server_socket_ssl( const std::atomic &svr_sock, SSL *ssl, socket_t sock, size_t keep_alive_max_count, time_t keep_alive_timeout_sec, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { return process_server_socket_core( svr_sock, sock, keep_alive_max_count, keep_alive_timeout_sec, [&](bool close_connection, bool &connection_closed) { SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm, close_connection, connection_closed); }); } template inline bool process_client_socket_ssl(SSL *ssl, socket_t sock, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec, T callback) { SSLSocketStream strm(sock, ssl, read_timeout_sec, read_timeout_usec, write_timeout_sec, write_timeout_usec); return callback(strm); } class SSLInit { public: SSLInit() { OPENSSL_init_ssl( OPENSSL_INIT_LOAD_SSL_STRINGS | OPENSSL_INIT_LOAD_CRYPTO_STRINGS, NULL); } }; // SSL socket stream implementation inline SSLSocketStream::SSLSocketStream(socket_t sock, SSL *ssl, time_t read_timeout_sec, time_t read_timeout_usec, time_t write_timeout_sec, time_t write_timeout_usec) : sock_(sock), ssl_(ssl), read_timeout_sec_(read_timeout_sec), read_timeout_usec_(read_timeout_usec), write_timeout_sec_(write_timeout_sec), write_timeout_usec_(write_timeout_usec) { SSL_clear_mode(ssl, SSL_MODE_AUTO_RETRY); } inline SSLSocketStream::~SSLSocketStream() {} inline bool SSLSocketStream::is_readable() const { return detail::select_read(sock_, read_timeout_sec_, read_timeout_usec_) > 0; } inline bool SSLSocketStream::is_writable() const { return select_write(sock_, write_timeout_sec_, write_timeout_usec_) > 0 && is_socket_alive(sock_); } inline ssize_t SSLSocketStream::read(char *ptr, size_t size) { if (SSL_pending(ssl_) > 0) { return SSL_read(ssl_, ptr, static_cast(size)); } else if (is_readable()) { auto ret = SSL_read(ssl_, ptr, static_cast(size)); if (ret < 0) { auto err = SSL_get_error(ssl_, ret); auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_READ || (err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err == SSL_ERROR_WANT_READ) { #endif if (SSL_pending(ssl_) > 0) { return SSL_read(ssl_, ptr, static_cast(size)); } else if (is_readable()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); ret = SSL_read(ssl_, ptr, static_cast(size)); if (ret >= 0) { return ret; } err = SSL_get_error(ssl_, ret); } else { return -1; } } } return ret; } return -1; } inline ssize_t SSLSocketStream::write(const char *ptr, size_t size) { if (is_writable()) { auto handle_size = static_cast( std::min(size, (std::numeric_limits::max)())); auto ret = SSL_write(ssl_, ptr, static_cast(handle_size)); if (ret < 0) { auto err = SSL_get_error(ssl_, ret); auto n = 1000; #ifdef _WIN32 while (--n >= 0 && (err == SSL_ERROR_WANT_WRITE || (err == SSL_ERROR_SYSCALL && WSAGetLastError() == WSAETIMEDOUT))) { #else while (--n >= 0 && err == SSL_ERROR_WANT_WRITE) { #endif if (is_writable()) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); ret = SSL_write(ssl_, ptr, static_cast(handle_size)); if (ret >= 0) { return ret; } err = SSL_get_error(ssl_, ret); } else { return -1; } } } return ret; } return -1; } inline void SSLSocketStream::get_remote_ip_and_port(std::string &ip, int &port) const { detail::get_remote_ip_and_port(sock_, ip, port); } inline void SSLSocketStream::get_local_ip_and_port(std::string &ip, int &port) const { detail::get_local_ip_and_port(sock_, ip, port); } inline socket_t SSLSocketStream::socket() const { return sock_; } static SSLInit sslinit_; } // namespace detail // SSL HTTP server implementation inline SSLServer::SSLServer(const char *cert_path, const char *private_key_path, const char *client_ca_cert_file_path, const char *client_ca_cert_dir_path, const char *private_key_password) { ctx_ = SSL_CTX_new(TLS_server_method()); if (ctx_) { SSL_CTX_set_options(ctx_, SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_min_proto_version(ctx_, TLS1_1_VERSION); // add default password callback before opening encrypted private key if (private_key_password != nullptr && (private_key_password[0] != '\0')) { SSL_CTX_set_default_passwd_cb_userdata( ctx_, reinterpret_cast(const_cast(private_key_password))); } if (SSL_CTX_use_certificate_chain_file(ctx_, cert_path) != 1 || SSL_CTX_use_PrivateKey_file(ctx_, private_key_path, SSL_FILETYPE_PEM) != 1) { SSL_CTX_free(ctx_); ctx_ = nullptr; } else if (client_ca_cert_file_path || client_ca_cert_dir_path) { SSL_CTX_load_verify_locations(ctx_, client_ca_cert_file_path, client_ca_cert_dir_path); SSL_CTX_set_verify( ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); } } } inline SSLServer::SSLServer(X509 *cert, EVP_PKEY *private_key, X509_STORE *client_ca_cert_store) { ctx_ = SSL_CTX_new(TLS_server_method()); if (ctx_) { SSL_CTX_set_options(ctx_, SSL_OP_NO_COMPRESSION | SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION); SSL_CTX_set_min_proto_version(ctx_, TLS1_1_VERSION); if (SSL_CTX_use_certificate(ctx_, cert) != 1 || SSL_CTX_use_PrivateKey(ctx_, private_key) != 1) { SSL_CTX_free(ctx_); ctx_ = nullptr; } else if (client_ca_cert_store) { SSL_CTX_set_cert_store(ctx_, client_ca_cert_store); SSL_CTX_set_verify( ctx_, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr); } } } inline SSLServer::SSLServer( const std::function &setup_ssl_ctx_callback) { ctx_ = SSL_CTX_new(TLS_method()); if (ctx_) { if (!setup_ssl_ctx_callback(*ctx_)) { SSL_CTX_free(ctx_); ctx_ = nullptr; } } } inline SSLServer::~SSLServer() { if (ctx_) { SSL_CTX_free(ctx_); } } inline bool SSLServer::is_valid() const { return ctx_; } inline SSL_CTX *SSLServer::ssl_context() const { return ctx_; } inline bool SSLServer::process_and_close_socket(socket_t sock) { auto ssl = detail::ssl_new( sock, ctx_, ctx_mutex_, [&](SSL *ssl2) { return detail::ssl_connect_or_accept_nonblocking( sock, ssl2, SSL_accept, read_timeout_sec_, read_timeout_usec_); }, [](SSL * /*ssl2*/) { return true; }); auto ret = false; if (ssl) { ret = detail::process_server_socket_ssl( svr_sock_, ssl, sock, keep_alive_max_count_, keep_alive_timeout_sec_, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [this, ssl](Stream &strm, bool close_connection, bool &connection_closed) { return process_request(strm, close_connection, connection_closed, [&](Request &req) { req.ssl = ssl; }); }); // Shutdown gracefully if the result seemed successful, non-gracefully if // the connection appeared to be closed. const bool shutdown_gracefully = ret; detail::ssl_delete(ctx_mutex_, ssl, shutdown_gracefully); } detail::shutdown_socket(sock); detail::close_socket(sock); return ret; } // SSL HTTP client implementation inline SSLClient::SSLClient(const std::string &host) : SSLClient(host, 443, std::string(), std::string()) {} inline SSLClient::SSLClient(const std::string &host, int port) : SSLClient(host, port, std::string(), std::string()) {} inline SSLClient::SSLClient(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : ClientImpl(host, port, client_cert_path, client_key_path) { ctx_ = SSL_CTX_new(TLS_client_method()); detail::split(&host_[0], &host_[host_.size()], '.', [&](const char *b, const char *e) { host_components_.emplace_back(std::string(b, e)); }); if (!client_cert_path.empty() && !client_key_path.empty()) { if (SSL_CTX_use_certificate_file(ctx_, client_cert_path.c_str(), SSL_FILETYPE_PEM) != 1 || SSL_CTX_use_PrivateKey_file(ctx_, client_key_path.c_str(), SSL_FILETYPE_PEM) != 1) { SSL_CTX_free(ctx_); ctx_ = nullptr; } } } inline SSLClient::SSLClient(const std::string &host, int port, X509 *client_cert, EVP_PKEY *client_key) : ClientImpl(host, port) { ctx_ = SSL_CTX_new(TLS_client_method()); detail::split(&host_[0], &host_[host_.size()], '.', [&](const char *b, const char *e) { host_components_.emplace_back(std::string(b, e)); }); if (client_cert != nullptr && client_key != nullptr) { if (SSL_CTX_use_certificate(ctx_, client_cert) != 1 || SSL_CTX_use_PrivateKey(ctx_, client_key) != 1) { SSL_CTX_free(ctx_); ctx_ = nullptr; } } } inline SSLClient::~SSLClient() { if (ctx_) { SSL_CTX_free(ctx_); } // Make sure to shut down SSL since shutdown_ssl will resolve to the // base function rather than the derived function once we get to the // base class destructor, and won't free the SSL (causing a leak). shutdown_ssl_impl(socket_, true); } inline bool SSLClient::is_valid() const { return ctx_; } inline void SSLClient::set_ca_cert_store(X509_STORE *ca_cert_store) { if (ca_cert_store) { if (ctx_) { if (SSL_CTX_get_cert_store(ctx_) != ca_cert_store) { // Free memory allocated for old cert and use new store `ca_cert_store` SSL_CTX_set_cert_store(ctx_, ca_cert_store); } } else { X509_STORE_free(ca_cert_store); } } } inline void SSLClient::load_ca_cert_store(const char *ca_cert, std::size_t size) { set_ca_cert_store(ClientImpl::create_ca_cert_store(ca_cert, size)); } inline long SSLClient::get_openssl_verify_result() const { return verify_result_; } inline SSL_CTX *SSLClient::ssl_context() const { return ctx_; } inline bool SSLClient::create_and_connect_socket(Socket &socket, Error &error) { return is_valid() && ClientImpl::create_and_connect_socket(socket, error); } // Assumes that socket_mutex_ is locked and that there are no requests in flight inline bool SSLClient::connect_with_proxy(Socket &socket, Response &res, bool &success, Error &error) { success = true; Response proxy_res; if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) { Request req2; req2.method = "CONNECT"; req2.path = host_and_port_; return process_request(strm, req2, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are no // requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } if (proxy_res.status == 407) { if (!proxy_digest_auth_username_.empty() && !proxy_digest_auth_password_.empty()) { std::map auth; if (detail::parse_www_authenticate(proxy_res, auth, true)) { proxy_res = Response(); if (!detail::process_client_socket( socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, [&](Stream &strm) { Request req3; req3.method = "CONNECT"; req3.path = host_and_port_; req3.headers.insert(detail::make_digest_authentication_header( req3, auth, 1, detail::random_string(10), proxy_digest_auth_username_, proxy_digest_auth_password_, true)); return process_request(strm, req3, proxy_res, false, error); })) { // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); success = false; return false; } } } } // If status code is not 200, proxy request is failed. // Set error to ProxyConnection and return proxy response // as the response of the request if (proxy_res.status != 200) { error = Error::ProxyConnection; res = std::move(proxy_res); // Thread-safe to close everything because we are assuming there are // no requests in flight shutdown_ssl(socket, true); shutdown_socket(socket); close_socket(socket); return false; } return true; } inline bool SSLClient::load_certs() { auto ret = true; std::call_once(initialize_cert_, [&]() { std::lock_guard guard(ctx_mutex_); if (!ca_cert_file_path_.empty()) { if (!SSL_CTX_load_verify_locations(ctx_, ca_cert_file_path_.c_str(), nullptr)) { ret = false; } } else if (!ca_cert_dir_path_.empty()) { if (!SSL_CTX_load_verify_locations(ctx_, nullptr, ca_cert_dir_path_.c_str())) { ret = false; } } else { auto loaded = false; #ifdef _WIN32 loaded = detail::load_system_certs_on_windows(SSL_CTX_get_cert_store(ctx_)); #elif defined(CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN) && defined(__APPLE__) #if TARGET_OS_OSX loaded = detail::load_system_certs_on_macos(SSL_CTX_get_cert_store(ctx_)); #endif // TARGET_OS_OSX #endif // _WIN32 if (!loaded) { SSL_CTX_set_default_verify_paths(ctx_); } } }); return ret; } inline bool SSLClient::initialize_ssl(Socket &socket, Error &error) { auto ssl = detail::ssl_new( socket.sock, ctx_, ctx_mutex_, [&](SSL *ssl2) { if (server_certificate_verification_) { if (!load_certs()) { error = Error::SSLLoadingCerts; return false; } SSL_set_verify(ssl2, SSL_VERIFY_NONE, nullptr); } if (!detail::ssl_connect_or_accept_nonblocking( socket.sock, ssl2, SSL_connect, connection_timeout_sec_, connection_timeout_usec_)) { error = Error::SSLConnection; return false; } if (server_certificate_verification_) { verify_result_ = SSL_get_verify_result(ssl2); if (verify_result_ != X509_V_OK) { error = Error::SSLServerVerification; return false; } auto server_cert = SSL_get1_peer_certificate(ssl2); if (server_cert == nullptr) { error = Error::SSLServerVerification; return false; } if (!verify_host(server_cert)) { X509_free(server_cert); error = Error::SSLServerVerification; return false; } X509_free(server_cert); } return true; }, [&](SSL *ssl2) { // NOTE: With -Wold-style-cast, this can produce a warning, since // SSL_set_tlsext_host_name is a macro (in OpenSSL), which contains // an old style cast. Short of doing compiler specific pragma's // here, we can't get rid of this warning. :'( SSL_set_tlsext_host_name(ssl2, host_.c_str()); return true; }); if (ssl) { socket.ssl = ssl; return true; } shutdown_socket(socket); close_socket(socket); return false; } inline void SSLClient::shutdown_ssl(Socket &socket, bool shutdown_gracefully) { shutdown_ssl_impl(socket, shutdown_gracefully); } inline void SSLClient::shutdown_ssl_impl(Socket &socket, bool shutdown_gracefully) { if (socket.sock == INVALID_SOCKET) { assert(socket.ssl == nullptr); return; } if (socket.ssl) { detail::ssl_delete(ctx_mutex_, socket.ssl, shutdown_gracefully); socket.ssl = nullptr; } assert(socket.ssl == nullptr); } inline bool SSLClient::process_socket(const Socket &socket, std::function callback) { assert(socket.ssl); return detail::process_client_socket_ssl( socket.ssl, socket.sock, read_timeout_sec_, read_timeout_usec_, write_timeout_sec_, write_timeout_usec_, std::move(callback)); } inline bool SSLClient::is_ssl() const { return true; } inline bool SSLClient::verify_host(X509 *server_cert) const { /* Quote from RFC2818 section 3.1 "Server Identity" If a subjectAltName extension of type dNSName is present, that MUST be used as the identity. Otherwise, the (most specific) Common Name field in the Subject field of the certificate MUST be used. Although the use of the Common Name is existing practice, it is deprecated and Certification Authorities are encouraged to use the dNSName instead. Matching is performed using the matching rules specified by [RFC2459]. If more than one identity of a given type is present in the certificate (e.g., more than one dNSName name, a match in any one of the set is considered acceptable.) Names may contain the wildcard character * which is considered to match any single domain name component or component fragment. E.g., *.a.com matches foo.a.com but not bar.foo.a.com. f*.com matches foo.com but not bar.com. In some cases, the URI is specified as an IP address rather than a hostname. In this case, the iPAddress subjectAltName must be present in the certificate and must exactly match the IP in the URI. */ return verify_host_with_subject_alt_name(server_cert) || verify_host_with_common_name(server_cert); } inline bool SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const { auto ret = false; auto type = GEN_DNS; struct in6_addr addr6; struct in_addr addr; size_t addr_len = 0; #ifndef __MINGW32__ if (inet_pton(AF_INET6, host_.c_str(), &addr6)) { type = GEN_IPADD; addr_len = sizeof(struct in6_addr); } else if (inet_pton(AF_INET, host_.c_str(), &addr)) { type = GEN_IPADD; addr_len = sizeof(struct in_addr); } #endif auto alt_names = static_cast( X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr)); if (alt_names) { auto dsn_matched = false; auto ip_matched = false; auto count = sk_GENERAL_NAME_num(alt_names); for (decltype(count) i = 0; i < count && !dsn_matched; i++) { auto val = sk_GENERAL_NAME_value(alt_names, i); if (val->type == type) { auto name = reinterpret_cast(ASN1_STRING_get0_data(val->d.ia5)); auto name_len = static_cast(ASN1_STRING_length(val->d.ia5)); switch (type) { case GEN_DNS: dsn_matched = check_host_name(name, name_len); break; case GEN_IPADD: if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) { ip_matched = true; } break; } } } if (dsn_matched || ip_matched) { ret = true; } } GENERAL_NAMES_free(const_cast( reinterpret_cast(alt_names))); return ret; } inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const { const auto subject_name = X509_get_subject_name(server_cert); if (subject_name != nullptr) { char name[BUFSIZ]; auto name_len = X509_NAME_get_text_by_NID(subject_name, NID_commonName, name, sizeof(name)); if (name_len != -1) { return check_host_name(name, static_cast(name_len)); } } return false; } inline bool SSLClient::check_host_name(const char *pattern, size_t pattern_len) const { if (host_.size() == pattern_len && host_ == pattern) { return true; } // Wildcard match // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484 std::vector pattern_components; detail::split(&pattern[0], &pattern[pattern_len], '.', [&](const char *b, const char *e) { pattern_components.emplace_back(std::string(b, e)); }); if (host_components_.size() != pattern_components.size()) { return false; } auto itr = pattern_components.begin(); for (const auto &h : host_components_) { auto &p = *itr; if (p != h && p != "*") { auto partial_match = (p.size() > 0 && p[p.size() - 1] == '*' && !p.compare(0, p.size() - 1, h)); if (!partial_match) { return false; } } ++itr; } return true; } #endif // Universal client implementation inline Client::Client(const std::string &scheme_host_port) : Client(scheme_host_port, std::string(), std::string()) {} inline Client::Client(const std::string &scheme_host_port, const std::string &client_cert_path, const std::string &client_key_path) { const static std::regex re( R"((?:([a-z]+):\/\/)?(?:\[([\d:]+)\]|([^:/?#]+))(?::(\d+))?)"); std::smatch m; if (std::regex_match(scheme_host_port, m, re)) { auto scheme = m[1].str(); #ifdef CPPHTTPLIB_OPENSSL_SUPPORT if (!scheme.empty() && (scheme != "http" && scheme != "https")) { #else if (!scheme.empty() && scheme != "http") { #endif #ifndef CPPHTTPLIB_NO_EXCEPTIONS std::string msg = "'" + scheme + "' scheme is not supported."; throw std::invalid_argument(msg); #endif return; } auto is_ssl = scheme == "https"; auto host = m[2].str(); if (host.empty()) { host = m[3].str(); } auto port_str = m[4].str(); auto port = !port_str.empty() ? std::stoi(port_str) : (is_ssl ? 443 : 80); if (is_ssl) { #ifdef CPPHTTPLIB_OPENSSL_SUPPORT cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); is_ssl_ = is_ssl; #endif } else { cli_ = detail::make_unique(host, port, client_cert_path, client_key_path); } } else { cli_ = detail::make_unique(scheme_host_port, 80, client_cert_path, client_key_path); } } inline Client::Client(const std::string &host, int port) : cli_(detail::make_unique(host, port)) {} inline Client::Client(const std::string &host, int port, const std::string &client_cert_path, const std::string &client_key_path) : cli_(detail::make_unique(host, port, client_cert_path, client_key_path)) {} inline Client::~Client() {} inline bool Client::is_valid() const { return cli_ != nullptr && cli_->is_valid(); } inline Result Client::Get(const std::string &path) { return cli_->Get(path); } inline Result Client::Get(const std::string &path, const Headers &headers) { return cli_->Get(path, headers); } inline Result Client::Get(const std::string &path, Progress progress) { return cli_->Get(path, std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, Progress progress) { return cli_->Get(path, headers, std::move(progress)); } inline Result Client::Get(const std::string &path, ContentReceiver content_receiver) { return cli_->Get(path, std::move(content_receiver)); } inline Result Client::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver) { return cli_->Get(path, headers, std::move(content_receiver)); } inline Result Client::Get(const std::string &path, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, headers, std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver) { return cli_->Get(path, std::move(response_handler), std::move(content_receiver)); } inline Result Client::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver) { return cli_->Get(path, headers, std::move(response_handler), std::move(content_receiver)); } inline Result Client::Get(const std::string &path, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, headers, std::move(response_handler), std::move(content_receiver), std::move(progress)); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, Progress progress) { return cli_->Get(path, params, headers, progress); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, params, headers, content_receiver, progress); } inline Result Client::Get(const std::string &path, const Params ¶ms, const Headers &headers, ResponseHandler response_handler, ContentReceiver content_receiver, Progress progress) { return cli_->Get(path, params, headers, response_handler, content_receiver, progress); } inline Result Client::Head(const std::string &path) { return cli_->Head(path); } inline Result Client::Head(const std::string &path, const Headers &headers) { return cli_->Head(path, headers); } inline Result Client::Post(const std::string &path) { return cli_->Post(path); } inline Result Client::Post(const std::string &path, const Headers &headers) { return cli_->Post(path, headers); } inline Result Client::Post(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return cli_->Post(path, body, content_length, content_type); } inline Result Client::Post(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return cli_->Post(path, headers, body, content_length, content_type); } inline Result Client::Post(const std::string &path, const std::string &body, const std::string &content_type) { return cli_->Post(path, body, content_type); } inline Result Client::Post(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->Post(path, headers, body, content_type); } inline Result Client::Post(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Post(path, content_length, std::move(content_provider), content_type); } inline Result Client::Post(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Post(path, std::move(content_provider), content_type); } inline Result Client::Post(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Post(path, headers, content_length, std::move(content_provider), content_type); } inline Result Client::Post(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Post(path, headers, std::move(content_provider), content_type); } inline Result Client::Post(const std::string &path, const Params ¶ms) { return cli_->Post(path, params); } inline Result Client::Post(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Post(path, headers, params); } inline Result Client::Post(const std::string &path, const MultipartFormDataItems &items) { return cli_->Post(path, items); } inline Result Client::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items) { return cli_->Post(path, headers, items); } inline Result Client::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary) { return cli_->Post(path, headers, items, boundary); } inline Result Client::Post(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items) { return cli_->Post(path, headers, items, provider_items); } inline Result Client::Put(const std::string &path) { return cli_->Put(path); } inline Result Client::Put(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return cli_->Put(path, body, content_length, content_type); } inline Result Client::Put(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return cli_->Put(path, headers, body, content_length, content_type); } inline Result Client::Put(const std::string &path, const std::string &body, const std::string &content_type) { return cli_->Put(path, body, content_type); } inline Result Client::Put(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->Put(path, headers, body, content_type); } inline Result Client::Put(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Put(path, content_length, std::move(content_provider), content_type); } inline Result Client::Put(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Put(path, std::move(content_provider), content_type); } inline Result Client::Put(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Put(path, headers, content_length, std::move(content_provider), content_type); } inline Result Client::Put(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Put(path, headers, std::move(content_provider), content_type); } inline Result Client::Put(const std::string &path, const Params ¶ms) { return cli_->Put(path, params); } inline Result Client::Put(const std::string &path, const Headers &headers, const Params ¶ms) { return cli_->Put(path, headers, params); } inline Result Client::Put(const std::string &path, const MultipartFormDataItems &items) { return cli_->Put(path, items); } inline Result Client::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items) { return cli_->Put(path, headers, items); } inline Result Client::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const std::string &boundary) { return cli_->Put(path, headers, items, boundary); } inline Result Client::Put(const std::string &path, const Headers &headers, const MultipartFormDataItems &items, const MultipartFormDataProviderItems &provider_items) { return cli_->Put(path, headers, items, provider_items); } inline Result Client::Patch(const std::string &path) { return cli_->Patch(path); } inline Result Client::Patch(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return cli_->Patch(path, body, content_length, content_type); } inline Result Client::Patch(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return cli_->Patch(path, headers, body, content_length, content_type); } inline Result Client::Patch(const std::string &path, const std::string &body, const std::string &content_type) { return cli_->Patch(path, body, content_type); } inline Result Client::Patch(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->Patch(path, headers, body, content_type); } inline Result Client::Patch(const std::string &path, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Patch(path, content_length, std::move(content_provider), content_type); } inline Result Client::Patch(const std::string &path, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Patch(path, std::move(content_provider), content_type); } inline Result Client::Patch(const std::string &path, const Headers &headers, size_t content_length, ContentProvider content_provider, const std::string &content_type) { return cli_->Patch(path, headers, content_length, std::move(content_provider), content_type); } inline Result Client::Patch(const std::string &path, const Headers &headers, ContentProviderWithoutLength content_provider, const std::string &content_type) { return cli_->Patch(path, headers, std::move(content_provider), content_type); } inline Result Client::Delete(const std::string &path) { return cli_->Delete(path); } inline Result Client::Delete(const std::string &path, const Headers &headers) { return cli_->Delete(path, headers); } inline Result Client::Delete(const std::string &path, const char *body, size_t content_length, const std::string &content_type) { return cli_->Delete(path, body, content_length, content_type); } inline Result Client::Delete(const std::string &path, const Headers &headers, const char *body, size_t content_length, const std::string &content_type) { return cli_->Delete(path, headers, body, content_length, content_type); } inline Result Client::Delete(const std::string &path, const std::string &body, const std::string &content_type) { return cli_->Delete(path, body, content_type); } inline Result Client::Delete(const std::string &path, const Headers &headers, const std::string &body, const std::string &content_type) { return cli_->Delete(path, headers, body, content_type); } inline Result Client::Options(const std::string &path) { return cli_->Options(path); } inline Result Client::Options(const std::string &path, const Headers &headers) { return cli_->Options(path, headers); } inline bool Client::send(Request &req, Response &res, Error &error) { return cli_->send(req, res, error); } inline Result Client::send(const Request &req) { return cli_->send(req); } inline void Client::stop() { cli_->stop(); } inline std::string Client::host() const { return cli_->host(); } inline int Client::port() const { return cli_->port(); } inline size_t Client::is_socket_open() const { return cli_->is_socket_open(); } inline socket_t Client::socket() const { return cli_->socket(); } inline void Client::set_hostname_addr_map(std::map addr_map) { cli_->set_hostname_addr_map(std::move(addr_map)); } inline void Client::set_default_headers(Headers headers) { cli_->set_default_headers(std::move(headers)); } inline void Client::set_address_family(int family) { cli_->set_address_family(family); } inline void Client::set_tcp_nodelay(bool on) { cli_->set_tcp_nodelay(on); } inline void Client::set_socket_options(SocketOptions socket_options) { cli_->set_socket_options(std::move(socket_options)); } inline void Client::set_connection_timeout(time_t sec, time_t usec) { cli_->set_connection_timeout(sec, usec); } inline void Client::set_read_timeout(time_t sec, time_t usec) { cli_->set_read_timeout(sec, usec); } inline void Client::set_write_timeout(time_t sec, time_t usec) { cli_->set_write_timeout(sec, usec); } inline void Client::set_basic_auth(const std::string &username, const std::string &password) { cli_->set_basic_auth(username, password); } inline void Client::set_bearer_token_auth(const std::string &token) { cli_->set_bearer_token_auth(token); } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void Client::set_digest_auth(const std::string &username, const std::string &password) { cli_->set_digest_auth(username, password); } #endif inline void Client::set_keep_alive(bool on) { cli_->set_keep_alive(on); } inline void Client::set_follow_location(bool on) { cli_->set_follow_location(on); } inline void Client::set_url_encode(bool on) { cli_->set_url_encode(on); } inline void Client::set_compress(bool on) { cli_->set_compress(on); } inline void Client::set_decompress(bool on) { cli_->set_decompress(on); } inline void Client::set_interface(const std::string &intf) { cli_->set_interface(intf); } inline void Client::set_proxy(const std::string &host, int port) { cli_->set_proxy(host, port); } inline void Client::set_proxy_basic_auth(const std::string &username, const std::string &password) { cli_->set_proxy_basic_auth(username, password); } inline void Client::set_proxy_bearer_token_auth(const std::string &token) { cli_->set_proxy_bearer_token_auth(token); } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void Client::set_proxy_digest_auth(const std::string &username, const std::string &password) { cli_->set_proxy_digest_auth(username, password); } #endif #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void Client::enable_server_certificate_verification(bool enabled) { cli_->enable_server_certificate_verification(enabled); } #endif inline void Client::set_logger(Logger logger) { cli_->set_logger(std::move(logger)); } #ifdef CPPHTTPLIB_OPENSSL_SUPPORT inline void Client::set_ca_cert_path(const std::string &ca_cert_file_path, const std::string &ca_cert_dir_path) { cli_->set_ca_cert_path(ca_cert_file_path, ca_cert_dir_path); } inline void Client::set_ca_cert_store(X509_STORE *ca_cert_store) { if (is_ssl_) { static_cast(*cli_).set_ca_cert_store(ca_cert_store); } else { cli_->set_ca_cert_store(ca_cert_store); } } inline void Client::load_ca_cert_store(const char *ca_cert, std::size_t size) { set_ca_cert_store(cli_->create_ca_cert_store(ca_cert, size)); } inline long Client::get_openssl_verify_result() const { if (is_ssl_) { return static_cast(*cli_).get_openssl_verify_result(); } return -1; // NOTE: -1 doesn't match any of X509_V_ERR_??? } inline SSL_CTX *Client::ssl_context() const { if (is_ssl_) { return static_cast(*cli_).ssl_context(); } return nullptr; } #endif // ---------------------------------------------------------------------------- } // namespace httplib #if defined(_WIN32) && defined(CPPHTTPLIB_USE_POLL) #undef poll #endif #endif // CPPHTTPLIB_HTTPLIB_H zeal-0.7.1/src/libs/core/httpserver.cpp000066400000000000000000000067451462517734600200670ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "httpserver.h" #include "application.h" #include "httplib.h" #include #include using namespace Zeal::Core; namespace { constexpr char LocalHttpServerHost[] = "127.0.0.1"; // macOS only routes 127.0.0.1 by default. } // namespace static Q_LOGGING_CATEGORY(log, "zeal.core.httpserver") HttpServer::HttpServer(QObject *parent) : QObject(parent) { m_server = std::make_unique(); const int port = m_server->bind_to_any_port(LocalHttpServerHost); m_baseUrl.setScheme(QStringLiteral("http")); m_baseUrl.setHost(LocalHttpServerHost); m_baseUrl.setPort(port); m_server->set_error_handler([](const auto& req, auto& res) { const QString html = QStringLiteral("ERROR %1
Request path: %2
") .arg(res.status) .arg(QString::fromStdString(req.path)); res.set_content(html.toUtf8().data(), "text/html"); }); m_future = std::async(std::launch::async, &httplib::Server::listen_after_bind, m_server.get()); qCDebug(log, "Listening on %s...", qPrintable(m_baseUrl.toString())); } HttpServer::~HttpServer() { m_server->stop(); auto status = m_future.wait_for(std::chrono::seconds(2)); if (status != std::future_status::ready) { qCWarning(log) << "Failed to stop server within timeout."; } } QUrl HttpServer::baseUrl() const { return m_baseUrl; } QUrl HttpServer::mount(const QString &prefix, const QString &path) { const QString pfx = sanitizePrefix(prefix); const bool ok = m_server->set_mount_point(pfx.toStdString(), path.toStdString()); if (!ok) { qCWarning(log, "Failed to mount '%s' to '%s'.", qPrintable(path), qPrintable(pfx)); return QUrl(); } qCDebug(log, "Mounted '%s' to '%s'.", qPrintable(path), qPrintable(pfx)); QUrl mountUrl = m_baseUrl; mountUrl.setPath(m_baseUrl.path() + pfx); return mountUrl; } bool HttpServer::unmount(const QString &prefix) { const QString pfx = sanitizePrefix(prefix); const bool ok = m_server->remove_mount_point(pfx.toStdString()); if (!ok) { qCWarning(log, "Failed to unmount '%s' to '%s'.", qPrintable(prefix), qPrintable(pfx)); } qCDebug(log, "Unmounted prefix '%s' ('%s').", qPrintable(prefix), qPrintable(pfx)); return ok; } QString HttpServer::sanitizePrefix(const QString &prefix) { QString pfx = (prefix.startsWith(QLatin1String("/")) ? prefix.right(1) : prefix).toLower(); pfx.replace(QRegularExpression(QStringLiteral("[^a-zA-Z0-9-_]")), QStringLiteral("_")); pfx.prepend(QLatin1Char('/')); return pfx; } zeal-0.7.1/src/libs/core/httpserver.h000066400000000000000000000032131462517734600175170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2020 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_HTTPSERVER_H #define ZEAL_CORE_HTTPSERVER_H #include #include #include #include namespace httplib { class Server; } // namespace httplib namespace Zeal { namespace Core { class HttpServer : public QObject { Q_OBJECT Q_DISABLE_COPY(HttpServer) public: explicit HttpServer(QObject *parent = nullptr); ~HttpServer() override; QUrl baseUrl() const; QUrl mount(const QString &prefix, const QString &path); bool unmount(const QString &prefix); private: static QString sanitizePrefix(const QString &prefix); std::unique_ptr m_server; std::future m_future; QUrl m_baseUrl; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_HTTPSERVER_H zeal-0.7.1/src/libs/core/networkaccessmanager.cpp000066400000000000000000000047641462517734600220660ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "networkaccessmanager.h" #include "application.h" #include "httpserver.h" #include using namespace Zeal::Core; NetworkAccessManager::NetworkAccessManager(QObject *parent) : QNetworkAccessManager(parent) { } bool NetworkAccessManager::isLocalFile(const QUrl &url) { return url.isLocalFile() || url.scheme() == QLatin1String("qrc"); } bool NetworkAccessManager::isLocalUrl(const QUrl &url) { if (isLocalFile(url)) { return true; } const QUrl &baseUrl = Application::instance()->httpServer()->baseUrl(); if (baseUrl.isParentOf(url)) { return true; } return false; } QNetworkReply *NetworkAccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest &request, QIODevice *outgoingData) { QNetworkRequest overrideRequest(request); overrideRequest.setAttribute(QNetworkRequest::RedirectPolicyAttribute, true); // Forward all non-local schemaless URLs via HTTPS. const QUrl url = request.url(); if (isLocalFile(url) && !url.host().isEmpty()) { QUrl overrideUrl(url); overrideUrl.setScheme(QStringLiteral("https")); overrideRequest.setUrl(overrideUrl); op = QNetworkAccessManager::GetOperation; } QSslConfiguration sslConfig = overrideRequest.sslConfiguration(); sslConfig.setCaCertificates(QSslConfiguration::systemCaCertificates()); overrideRequest.setSslConfiguration(sslConfig); return QNetworkAccessManager::createRequest(op, overrideRequest, outgoingData); } zeal-0.7.1/src/libs/core/networkaccessmanager.h000066400000000000000000000030601462517734600215170ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_NETWORKACCESSMANAGER_H #define ZEAL_CORE_NETWORKACCESSMANAGER_H #include namespace Zeal { namespace Core { class NetworkAccessManager final : public QNetworkAccessManager { Q_OBJECT Q_DISABLE_COPY(NetworkAccessManager) public: NetworkAccessManager(QObject *parent = nullptr); static bool isLocalFile(const QUrl &url); static bool isLocalUrl(const QUrl &url); protected: QNetworkReply *createRequest(Operation op, const QNetworkRequest &request, QIODevice *outgoingData = nullptr) override; }; } // namespace Core } // namespace Zeal #endif // ZEAL_CORE_NETWORKACCESSMANAGER_H zeal-0.7.1/src/libs/core/settings.cpp000066400000000000000000000375031462517734600175150ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "settings.h" #include "application.h" #include #include #include #include #include #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) #include #else #include #endif namespace { // Configuration file groups constexpr char GroupContent[] = "content"; constexpr char GroupDocsets[] = "docsets"; constexpr char GroupGlobalShortcuts[] = "global_shortcuts"; constexpr char GroupSearch[] = "search"; constexpr char GroupTabs[] = "tabs"; constexpr char GroupInternal[] = "internal"; constexpr char GroupState[] = "state"; constexpr char GroupProxy[] = "proxy"; } // namespace using namespace Zeal::Core; Settings::Settings(QObject *parent) : QObject(parent) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) qRegisterMetaTypeStreamOperators("ContentAppearance"); qRegisterMetaTypeStreamOperators("ExternalLinkPolicy"); #else qRegisterMetaType("ContentAppearance"); qRegisterMetaType("ExternalLinkPolicy"); #endif load(); } Settings::~Settings() { save(); } Zeal::Core::Settings::ColorScheme Settings::colorScheme() { #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) return QApplication::styleHints()->colorScheme(); #else // Pre Qt 6.5 detection from https://www.qt.io/blog/dark-mode-on-windows-11-with-qt-6.5. const QPalette p; if (p.color(QPalette::WindowText).lightness() > p.color(QPalette::Window).lightness()) { return ColorScheme::Dark; } return ColorScheme::Light; #endif } void Settings::load() { QScopedPointer settings(qsettings()); migrate(settings.data()); // TODO: Put everything in groups startMinimized = settings->value(QStringLiteral("start_minimized"), false).toBool(); checkForUpdate = settings->value(QStringLiteral("check_for_update"), true).toBool(); showSystrayIcon = settings->value(QStringLiteral("show_systray_icon"), true).toBool(); minimizeToSystray = settings->value(QStringLiteral("minimize_to_systray"), false).toBool(); hideOnClose = settings->value(QStringLiteral("hide_on_close"), false).toBool(); settings->beginGroup(GroupGlobalShortcuts); showShortcut = settings->value(QStringLiteral("show")).value(); settings->endGroup(); settings->beginGroup(GroupTabs); openNewTabAfterActive = settings->value(QStringLiteral("open_new_tab_after_active"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupSearch); isFuzzySearchEnabled = settings->value(QStringLiteral("fuzzy_search_enabled"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupContent); // Dark mode needs to be applied before Qt WebEngine is initialized. contentAppearance = settings->value(QStringLiteral("appearance"), QVariant::fromValue(ContentAppearance::Automatic)).value(); #if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) const bool enableDarkMode = contentAppearance == ContentAppearance::Dark || (contentAppearance == ContentAppearance::Automatic && colorScheme() == ColorScheme::Dark); if (enableDarkMode) { qputenv("QTWEBENGINE_CHROMIUM_FLAGS", "--blink-settings=forceDarkModeEnabled=true,darkModeInversionAlgorithm=4"); } #endif // Fonts QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings(); serifFontFamily = settings->value(QStringLiteral("serif_font_family"), webSettings->fontFamily(QWebEngineSettings::SerifFont)).toString(); sansSerifFontFamily = settings->value(QStringLiteral("sans_serif_font_family"), webSettings->fontFamily(QWebEngineSettings::SansSerifFont)).toString(); fixedFontFamily = settings->value(QStringLiteral("fixed_font_family"), webSettings->fontFamily(QWebEngineSettings::FixedFont)).toString(); static const QMap fontFamilies = { {QStringLiteral("sans-serif"), QWebEngineSettings::SansSerifFont}, {QStringLiteral("serif"), QWebEngineSettings::SerifFont}, {QStringLiteral("monospace"), QWebEngineSettings::FixedFont} }; defaultFontFamily = settings->value(QStringLiteral("default_font_family"), QStringLiteral("serif")).toString(); // Fallback to the serif font family. if (!fontFamilies.contains(defaultFontFamily)) { defaultFontFamily = QStringLiteral("serif"); } webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, sansSerifFontFamily); webSettings->setFontFamily(QWebEngineSettings::SerifFont, serifFontFamily); webSettings->setFontFamily(QWebEngineSettings::FixedFont, fixedFontFamily); const QString defaultFontFamilyResolved = webSettings->fontFamily(fontFamilies.value(defaultFontFamily)); webSettings->setFontFamily(QWebEngineSettings::StandardFont, defaultFontFamilyResolved); defaultFontSize = settings->value(QStringLiteral("default_font_size"), webSettings->fontSize(QWebEngineSettings::DefaultFontSize)).toInt(); defaultFixedFontSize = settings->value(QStringLiteral("default_fixed_font_size"), webSettings->fontSize(QWebEngineSettings::DefaultFixedFontSize)).toInt(); minimumFontSize = settings->value(QStringLiteral("minimum_font_size"), webSettings->fontSize(QWebEngineSettings::MinimumFontSize)).toInt(); webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFontSize); webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, defaultFixedFontSize); webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, minimumFontSize); isHighlightOnNavigateEnabled = settings->value(QStringLiteral("highlight_on_navigate"), true).toBool(); customCssFile = settings->value(QStringLiteral("custom_css_file")).toString(); externalLinkPolicy = settings->value(QStringLiteral("external_link_policy"), QVariant::fromValue(ExternalLinkPolicy::Ask)).value(); isSmoothScrollingEnabled = settings->value(QStringLiteral("smooth_scrolling"), true).toBool(); settings->endGroup(); settings->beginGroup(GroupProxy); proxyType = static_cast(settings->value(QStringLiteral("type"), ProxyType::System).toUInt()); proxyHost = settings->value(QStringLiteral("host")).toString(); proxyPort = static_cast(settings->value(QStringLiteral("port"), 0).toUInt()); proxyAuthenticate = settings->value(QStringLiteral("authenticate"), false).toBool(); proxyUserName = settings->value(QStringLiteral("username")).toString(); proxyPassword = settings->value(QStringLiteral("password")).toString(); isIgnoreSslErrorsEnabled = settings->value(QStringLiteral("ignore_ssl_errors"), false).toBool(); settings->endGroup(); settings->beginGroup(GroupDocsets); if (settings->contains(QStringLiteral("path"))) { docsetPath = settings->value(QStringLiteral("path")).toString(); } else { #ifndef PORTABLE_BUILD docsetPath = QStandardPaths::writableLocation(QStandardPaths::AppLocalDataLocation) + QLatin1String("/docsets"); #else docsetPath = QStringLiteral("docsets"); #endif } settings->endGroup(); // Create the docset storage directory if it doesn't exist. const QFileInfo fi(docsetPath); if (!fi.exists()) { // TODO: Report QDir::mkpath() errors. if (fi.isRelative()) { QDir().mkpath(QCoreApplication::applicationDirPath() + "/" + docsetPath); } else { QDir().mkpath(docsetPath); } } settings->beginGroup(GroupState); windowGeometry = settings->value(QStringLiteral("window_geometry")).toByteArray(); verticalSplitterGeometry = settings->value(QStringLiteral("splitter_geometry")).toByteArray(); tocSplitterState = settings->value(QStringLiteral("toc_splitter_state")).toByteArray(); settings->endGroup(); settings->beginGroup(GroupInternal); installId = settings->value(QStringLiteral("install_id"), // Avoid curly braces (QTBUG-885) QUuid::createUuid().toString().mid(1, 36)).toString(); settings->endGroup(); } void Settings::save() { QScopedPointer settings(qsettings()); // TODO: Put everything in groups settings->setValue(QStringLiteral("start_minimized"), startMinimized); settings->setValue(QStringLiteral("check_for_update"), checkForUpdate); settings->setValue(QStringLiteral("show_systray_icon"), showSystrayIcon); settings->setValue(QStringLiteral("minimize_to_systray"), minimizeToSystray); settings->setValue(QStringLiteral("hide_on_close"), hideOnClose); settings->beginGroup(GroupGlobalShortcuts); settings->setValue(QStringLiteral("show"), showShortcut); settings->endGroup(); settings->beginGroup(GroupTabs); settings->setValue(QStringLiteral("open_new_tab_after_active"), openNewTabAfterActive); settings->endGroup(); settings->beginGroup(GroupSearch); settings->setValue(QStringLiteral("fuzzy_search_enabled"), isFuzzySearchEnabled); settings->endGroup(); settings->beginGroup(GroupContent); settings->setValue(QStringLiteral("default_font_family"), defaultFontFamily); settings->setValue(QStringLiteral("serif_font_family"), serifFontFamily); settings->setValue(QStringLiteral("sans_serif_font_family"), sansSerifFontFamily); settings->setValue(QStringLiteral("fixed_font_family"), fixedFontFamily); settings->setValue(QStringLiteral("default_font_size"), defaultFontSize); settings->setValue(QStringLiteral("default_fixed_font_size"), defaultFixedFontSize); settings->setValue(QStringLiteral("minimum_font_size"), minimumFontSize); settings->setValue(QStringLiteral("appearance"), QVariant::fromValue(contentAppearance)); settings->setValue(QStringLiteral("highlight_on_navigate"), isHighlightOnNavigateEnabled); settings->setValue(QStringLiteral("custom_css_file"), customCssFile); settings->setValue(QStringLiteral("external_link_policy"), QVariant::fromValue(externalLinkPolicy)); settings->setValue(QStringLiteral("smooth_scrolling"), isSmoothScrollingEnabled); settings->endGroup(); settings->beginGroup(GroupProxy); settings->setValue(QStringLiteral("type"), proxyType); settings->setValue(QStringLiteral("host"), proxyHost); settings->setValue(QStringLiteral("port"), proxyPort); settings->setValue(QStringLiteral("authenticate"), proxyAuthenticate); settings->setValue(QStringLiteral("username"), proxyUserName); settings->setValue(QStringLiteral("password"), proxyPassword); settings->setValue(QStringLiteral("ignore_ssl_errors"), isIgnoreSslErrorsEnabled); settings->endGroup(); settings->beginGroup(GroupDocsets); settings->setValue(QStringLiteral("path"), docsetPath); settings->endGroup(); settings->beginGroup(GroupState); settings->setValue(QStringLiteral("window_geometry"), windowGeometry); settings->setValue(QStringLiteral("splitter_geometry"), verticalSplitterGeometry); settings->setValue(QStringLiteral("toc_splitter_state"), tocSplitterState); settings->endGroup(); settings->beginGroup(GroupInternal); settings->setValue(QStringLiteral("install_id"), installId); // Version of configuration file format, should match Zeal version. Used for migration rules. settings->setValue(QStringLiteral("version"), Application::version().toString()); settings->endGroup(); settings->sync(); emit updated(); } /*! * \internal * \brief Migrates settings from older application versions. * \param settings QSettings object to update. * * The settings migration process relies on 'internal/version' option, that was introduced in the * release 0.2.0, so a missing option indicates pre-0.2 release. */ void Settings::migrate(QSettings *settings) const { settings->beginGroup(GroupInternal); const auto version = QVersionNumber::fromString(settings->value(QStringLiteral("version")).toString()); settings->endGroup(); // // 0.6.0 // // Unset content.default_fixed_font_size. // The causing bug was 0.6.1 (#903), but the incorrect setting still comes to haunt us (#1054). if (version == QVersionNumber(0, 6, 0)) { settings->beginGroup(GroupContent); settings->remove(QStringLiteral("default_fixed_font_size")); settings->endGroup(); } // // Pre 0.4 // // Rename 'browser' group into 'content'. if (version < QVersionNumber(0, 4, 0)) { settings->beginGroup(QStringLiteral("browser")); const QVariant tmpMinimumFontSize = settings->value(QStringLiteral("minimum_font_size")); settings->endGroup(); settings->remove(QStringLiteral("browser")); if (tmpMinimumFontSize.isValid()) { settings->beginGroup(GroupContent); settings->setValue(QStringLiteral("minimum_font_size"), tmpMinimumFontSize); settings->endGroup(); } } // // Pre 0.3 // // Unset 'state/splitter_geometry', because custom styles were removed. if (version < QVersionNumber(0, 3, 0)) { settings->beginGroup(GroupState); settings->remove(QStringLiteral("splitter_geometry")); settings->endGroup(); } } /*! * \internal * \brief Returns an initialized QSettings object. * \param parent Optional parent object. * \return QSettings object. * * QSettings is initialized according to build options, e.g. standard vs portable. * Caller is responsible for deleting the returned object. */ QSettings *Settings::qsettings(QObject *parent) { #ifndef PORTABLE_BUILD return new QSettings(parent); #else return new QSettings(QCoreApplication::applicationDirPath() + QLatin1String("/zeal.ini"), QSettings::IniFormat, parent); #endif } QDataStream &operator<<(QDataStream &out, Settings::ContentAppearance policy) { out << static_cast::type>(policy); return out; } QDataStream &operator>>(QDataStream &in, Settings::ContentAppearance &policy) { std::underlying_type::type value; in >> value; policy = static_cast(value); return in; } QDataStream &operator<<(QDataStream &out, Settings::ExternalLinkPolicy policy) { out << static_cast::type>(policy); return out; } QDataStream &operator>>(QDataStream &in, Settings::ExternalLinkPolicy &policy) { std::underlying_type::type value; in >> value; policy = static_cast(value); return in; } zeal-0.7.1/src/libs/core/settings.h000066400000000000000000000104241462517734600171530ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_CORE_SETTINGS_H #define ZEAL_CORE_SETTINGS_H #include #include #include class QSettings; namespace Zeal { namespace Core { class Settings final : public QObject { Q_OBJECT Q_DISABLE_COPY(Settings) public: /* This public members are here just for simplification and should go away * once a more advanced settings management come in place. */ // Startup bool startMinimized; bool checkForUpdate; // TODO: bool restoreLastState; // System Tray bool showSystrayIcon; bool minimizeToSystray; bool hideOnClose; // Global Shortcuts QKeySequence showShortcut; // TODO: QKeySequence searchSelectedTextShortcut; // Tabs Behavior bool openNewTabAfterActive; // Search bool isFuzzySearchEnabled; // Content QString defaultFontFamily; QString serifFontFamily; QString sansSerifFontFamily; QString fixedFontFamily; int defaultFontSize; int defaultFixedFontSize; int minimumFontSize; enum class ExternalLinkPolicy : unsigned int { Ask = 0, Open, OpenInSystemBrowser }; Q_ENUM(ExternalLinkPolicy) ExternalLinkPolicy externalLinkPolicy = ExternalLinkPolicy::Ask; enum class ContentAppearance : unsigned int { Automatic = 0, Light, Dark }; Q_ENUM(ContentAppearance) ContentAppearance contentAppearance = ContentAppearance::Automatic; bool isHighlightOnNavigateEnabled; QString customCssFile; bool isSmoothScrollingEnabled; // Network enum ProxyType : unsigned int { None = 0, System = 1, Http = 3, Socks5 = 4 }; Q_ENUM(ProxyType) // Internal // -------- // InstallId is a UUID used to indentify a Zeal installation. Created on first start or after // a settings wipe. It is not attached to user hardware or software, and is sent exclusevely // to *.zealdocs.org hosts. QString installId; ProxyType proxyType = ProxyType::System; QString proxyHost; quint16 proxyPort; bool proxyAuthenticate; QString proxyUserName; QString proxyPassword; bool isIgnoreSslErrorsEnabled; // Other QString docsetPath; // State QByteArray windowGeometry; QByteArray verticalSplitterGeometry; QByteArray tocSplitterState; explicit Settings(QObject *parent = nullptr); ~Settings() override; // Helper functions. #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) typedef Qt::ColorScheme ColorScheme; #else enum class ColorScheme { Unknown, Light, Dark, }; #endif static ColorScheme colorScheme(); public slots: void load(); void save(); signals: void updated(); private: void migrate(QSettings *settings) const; static QSettings *qsettings(QObject *parent = nullptr); }; } // namespace Core } // namespace Zeal QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ContentAppearance policy); QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ContentAppearance &policy); QDataStream &operator<<(QDataStream &out, Zeal::Core::Settings::ExternalLinkPolicy policy); QDataStream &operator>>(QDataStream &in, Zeal::Core::Settings::ExternalLinkPolicy &policy); Q_DECLARE_METATYPE(Zeal::Core::Settings::ContentAppearance) Q_DECLARE_METATYPE(Zeal::Core::Settings::ExternalLinkPolicy) #endif // ZEAL_CORE_SETTINGS_H zeal-0.7.1/src/libs/registry/000077500000000000000000000000001462517734600160615ustar00rootroot00000000000000zeal-0.7.1/src/libs/registry/CMakeLists.txt000066400000000000000000000007241462517734600206240ustar00rootroot00000000000000add_library(Registry STATIC docset.cpp docsetmetadata.cpp docsetregistry.cpp listmodel.cpp searchmodel.cpp searchquery.cpp # Show headers without .cpp in Qt Creator. cancellationtoken.h itemdatarole.h searchresult.h ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Concurrent Gui Network REQUIRED) target_link_libraries(Registry Util Qt${QT_VERSION_MAJOR}::Concurrent Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Network) zeal-0.7.1/src/libs/registry/cancellationtoken.h000066400000000000000000000027311462517734600217320ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015 Artur Spychaj ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_CANCELLATIONTOKEN_H #define ZEAL_REGISTRY_CANCELLATIONTOKEN_H #include namespace Zeal { namespace Registry { /// Token that stores whether cancel was called on it. /// In async code can be used to check if another thread called cancel. class CancellationToken { public: inline bool isCanceled() const { return m_canceled; } inline void cancel() { m_canceled = true; } inline void reset() { m_canceled = false; } private: std::atomic_bool m_canceled; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_CANCELLATIONTOKEN_H zeal-0.7.1/src/libs/registry/docset.cpp000066400000000000000000001057721462517734600200620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "docset.h" #include "cancellationtoken.h" #include "searchresult.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal::Registry; namespace { constexpr char IndexNamePrefix[] = "__zi_name"; // zi - Zeal index constexpr char IndexNameVersion[] = "0001"; // Current index version constexpr char NotFoundPageUrl[] = "qrc:///browser/404.html"; namespace InfoPlist { constexpr char CFBundleName[] = "CFBundleName"; //const char CFBundleIdentifier[] = "CFBundleIdentifier"; constexpr char DashDocSetFamily[] = "DashDocSetFamily"; constexpr char DashDocSetKeyword[] = "DashDocSetKeyword"; constexpr char DashDocSetPluginKeyword[] = "DashDocSetPluginKeyword"; constexpr char DashIndexFilePath[] = "dashIndexFilePath"; constexpr char DocSetPlatformFamily[] = "DocSetPlatformFamily"; //const char IsDashDocset[] = "isDashDocset"; constexpr char IsJavaScriptEnabled[] = "isJavaScriptEnabled"; } // namespace InfoPlist } // namespace static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv); Docset::Docset(QString path) : m_path(std::move(path)) { QDir dir(m_path); if (!dir.exists()) return; loadMetadata(); // Attempt to find the icon in any supported format const auto iconFiles = dir.entryList({QStringLiteral("icon.*")}, QDir::Files); for (const QString &iconFile : iconFiles) { m_icon = QIcon(dir.filePath(iconFile)); if (!m_icon.availableSizes().isEmpty()) break; } // TODO: Report errors here and below if (!dir.cd(QStringLiteral("Contents"))) return; // TODO: 'info.plist' is invalid according to Apple, and must alsways be 'Info.plist' // https://developer.apple.com/library/mac/documentation/MacOSX/Conceptual/BPRuntimeConfig // /Articles/ConfigFiles.html Util::Plist plist; if (dir.exists(QStringLiteral("Info.plist"))) plist.read(dir.filePath(QStringLiteral("Info.plist"))); else if (dir.exists(QStringLiteral("info.plist"))) plist.read(dir.filePath(QStringLiteral("info.plist"))); else return; if (plist.hasError()) return; if (m_name.isEmpty()) { // Fallback if meta.json is absent if (plist.contains(InfoPlist::CFBundleName)) { m_name = m_title = plist[InfoPlist::CFBundleName].toString(); // TODO: Remove when MainWindow::docsetName() will not use directory name m_name.replace(QLatin1Char(' '), QLatin1Char('_')); } else { m_name = QFileInfo(m_path).fileName().remove(QStringLiteral(".docset")); } } if (m_title.isEmpty()) { m_title = m_name; m_title.replace(QLatin1Char('_'), QLatin1Char(' ')); } // TODO: Verify if this is needed if (plist.contains(InfoPlist::DashDocSetFamily) && plist[InfoPlist::DashDocSetFamily].toString() == QLatin1String("cheatsheet")) { m_name = m_name + QLatin1String("cheats"); } if (!dir.cd(QStringLiteral("Resources")) || !dir.exists(QStringLiteral("docSet.dsidx"))) return; m_db = new Util::SQLiteDatabase(dir.filePath(QStringLiteral("docSet.dsidx"))); if (!m_db->isOpen()) { qWarning("SQL Error: %s", qPrintable(m_db->lastError())); return; } sqlite3_create_function(m_db->handle(), "zealScore", 2, SQLITE_UTF8, nullptr, sqliteScoreFunction, nullptr, nullptr); m_type = m_db->tables().contains(QStringLiteral("searchIndex"), Qt::CaseInsensitive) ? Type::Dash : Type::ZDash; createIndex(); if (m_type == Docset::Type::ZDash) { createView(); } if (!dir.cd(QStringLiteral("Documents"))) { m_type = Type::Invalid; return; } // Setup keywords if (plist.contains(InfoPlist::DocSetPlatformFamily)) m_keywords << plist[InfoPlist::DocSetPlatformFamily].toString(); if (plist.contains(InfoPlist::DashDocSetPluginKeyword)) m_keywords << plist[InfoPlist::DashDocSetPluginKeyword].toString(); if (plist.contains(InfoPlist::DashDocSetKeyword)) m_keywords << plist[InfoPlist::DashDocSetKeyword].toString(); if (plist.contains(InfoPlist::DashDocSetFamily)) { const QString kw = plist[InfoPlist::DashDocSetFamily].toString(); if (!kw.contains(QLatin1String("dashtoc"))) { m_keywords << kw; } } if (plist.contains(InfoPlist::IsJavaScriptEnabled)) { m_isJavaScriptEnabled = plist[InfoPlist::IsJavaScriptEnabled].toBool(); } m_keywords.removeDuplicates(); // Determine index page. This is ridiculous. const QString mdIndexFilePath = m_indexFilePath; // Save path from the metadata. // Prefer index path provided by the docset. if (plist.contains(InfoPlist::DashIndexFilePath)) { const QString indexFilePath = plist[InfoPlist::DashIndexFilePath].toString(); if (dir.exists(indexFilePath)) { m_indexFilePath = indexFilePath; } } // Check the metadata. if (m_indexFilePath.isEmpty() && !mdIndexFilePath.isEmpty() && dir.exists(mdIndexFilePath)) { m_indexFilePath = mdIndexFilePath; } // What if there is index.html. if (m_indexFilePath.isEmpty() && dir.exists(QStringLiteral("index.html"))) { m_indexFilePath = QStringLiteral("index.html"); } // Log if unable to determine the index page. Otherwise the path will be set in setBaseUrl(). if (m_indexFilePath.isEmpty()) { qWarning("Cannot determine index file for docset %s", qPrintable(m_name)); m_indexFileUrl.setUrl(NotFoundPageUrl); } else { m_indexFileUrl = createPageUrl(m_indexFilePath); } countSymbols(); } Docset::~Docset() { delete m_db; } bool Docset::isValid() const { return m_type != Type::Invalid; } QString Docset::name() const { return m_name; } QString Docset::title() const { return m_title; } QStringList Docset::keywords() const { return m_keywords; } QString Docset::version() const { return m_version; } int Docset::revision() const { return m_revision; } QString Docset::feedUrl() const { return m_feedUrl; } QString Docset::path() const { return m_path; } QString Docset::documentPath() const { return QDir(m_path).filePath(QStringLiteral("Contents/Resources/Documents")); } QIcon Docset::icon() const { return m_icon; } QIcon Docset::symbolTypeIcon(const QString &symbolType) const { static const QIcon unknownIcon(QStringLiteral("typeIcon:Unknown.png")); const QIcon icon(QStringLiteral("typeIcon:%1.png").arg(symbolType)); return icon.availableSizes().isEmpty() ? unknownIcon : icon; } QUrl Docset::indexFileUrl() const { return m_indexFileUrl; } QMap Docset::symbolCounts() const { return m_symbolCounts; } int Docset::symbolCount(const QString &symbolType) const { return m_symbolCounts.value(symbolType); } const QMultiMap &Docset::symbols(const QString &symbolType) const { if (!m_symbols.contains(symbolType)) loadSymbols(symbolType); return m_symbols[symbolType]; } QList Docset::search(const QString &query, const CancellationToken &token) const { QString sql; if (m_type == Docset::Type::Dash) { if (m_isFuzzySearchEnabled) { sql = QStringLiteral("SELECT name, type, path, '', zealScore('%1', name) as score" " FROM searchIndex" " WHERE score > 0"); } else { sql = QStringLiteral("SELECT name, type, path, '', -length(name) as score" " FROM searchIndex" " WHERE (name LIKE '%%1%' ESCAPE '\\')"); } } else { if (m_isFuzzySearchEnabled) { sql = QStringLiteral("SELECT name, type, path, fragment, zealScore('%1', name) as score" " FROM searchIndex" " WHERE score > 0"); } else { sql = QStringLiteral("SELECT name, type, path, fragment, -length(name) as score" " FROM searchIndex" " WHERE (name LIKE '%%1%' ESCAPE '\\')"); } } // Limit for very short queries. // TODO: Show a notification about the reduced result set. if (query.size() < 3) { sql += QLatin1String(" LIMIT 1000"); } // Make it safe to use in a SQL query. QString sanitizedQuery = query; sanitizedQuery.replace(QLatin1Char('\''), QLatin1String("''")); m_db->prepare(sql.arg(sanitizedQuery)); QList results; while (m_db->next() && !token.isCanceled()) { results.append({m_db->value(0).toString(), parseSymbolType(m_db->value(1).toString()), m_db->value(2).toString(), m_db->value(3).toString(), const_cast(this), m_db->value(4).toInt()}); } return results; } QList Docset::relatedLinks(const QUrl &url) const { if (!m_baseUrl.isParentOf(url)) { return {}; } // Get page path within the docset. const QString path = url.path().mid(m_baseUrl.path().length() + 1); // Prepare the query to look up all pages with the same url. QString sql; if (m_type == Docset::Type::Dash) { sql = QStringLiteral("SELECT name, type, path" " FROM searchIndex" " WHERE path LIKE \"%1%%\" AND path <> \"%1\""); } else if (m_type == Docset::Type::ZDash) { sql = QStringLiteral("SELECT name, type, path, fragment" " FROM searchIndex" " WHERE path = \"%1\" AND fragment IS NOT NULL"); } QList results; m_db->prepare(sql.arg(path)); while (m_db->next()) { results.append({m_db->value(0).toString(), parseSymbolType(m_db->value(1).toString()), m_db->value(2).toString(), m_db->value(3).toString(), const_cast(this), 0}); } if (results.size() == 1) { return {}; } return results; } QUrl Docset::searchResultUrl(const SearchResult &result) const { return createPageUrl(result.urlPath, result.urlFragment); } void Docset::loadMetadata() { const QDir dir(m_path); // Fallback if meta.json is absent if (!dir.exists(QStringLiteral("meta.json"))) return; QScopedPointer file(new QFile(dir.filePath(QStringLiteral("meta.json")))); if (!file->open(QIODevice::ReadOnly)) return; QJsonParseError jsonError; const QJsonObject jsonObject = QJsonDocument::fromJson(file->readAll(), &jsonError).object(); if (jsonError.error != QJsonParseError::NoError) return; m_name = jsonObject[QStringLiteral("name")].toString(); m_title = jsonObject[QStringLiteral("title")].toString(); m_version = jsonObject[QStringLiteral("version")].toString(); m_revision = jsonObject[QStringLiteral("revision")].toString().toInt(); if (jsonObject.contains(QStringLiteral("feed_url"))) { m_feedUrl = jsonObject[QStringLiteral("feed_url")].toString(); } if (jsonObject.contains(QStringLiteral("extra"))) { const QJsonObject extra = jsonObject[QStringLiteral("extra")].toObject(); if (extra.contains(QStringLiteral("indexFilePath"))) { m_indexFilePath = extra[QStringLiteral("indexFilePath")].toString(); } if (extra.contains(QStringLiteral("keywords"))) { for (const QJsonValueRef kw : extra[QStringLiteral("keywords")].toArray()) { m_keywords << kw.toString(); } } if (extra.contains(QStringLiteral("isJavaScriptEnabled"))) { m_isJavaScriptEnabled = extra[QStringLiteral("isJavaScriptEnabled")].toBool(); } } } void Docset::countSymbols() { static const QString sql = QStringLiteral("SELECT type, COUNT(*)" " FROM searchIndex" " GROUP BY type"); if (!m_db->prepare(sql)) { qWarning("SQL Error: %s", qPrintable(m_db->lastError())); return; } while (m_db->next()) { const QString symbolTypeStr = m_db->value(0).toString(); // A workaround for https://github.com/zealdocs/zeal/issues/980. if (symbolTypeStr.isEmpty()) { qWarning("Empty symbol type in the '%s' docset, skipping...", qPrintable(m_name)); continue; } const QString symbolType = parseSymbolType(symbolTypeStr); m_symbolStrings.insert(symbolType, symbolTypeStr); m_symbolCounts[symbolType] += m_db->value(1).toInt(); } } // TODO: Fetch and cache only portions of symbols void Docset::loadSymbols(const QString &symbolType) const { // Iterator `it` is a QPair, // with it.first and it.second respectively pointing to the start and the end // of the range of nodes having symbolType as key. It effectively represents a // contiguous view over the nodes with a specified key. for (auto it = std::as_const(m_symbolStrings).equal_range(symbolType); it.first != it.second; ++it.first) { loadSymbols(symbolType, it.first.value()); } } void Docset::loadSymbols(const QString &symbolType, const QString &symbolString) const { QString sql; if (m_type == Docset::Type::Dash) { sql = QStringLiteral("SELECT name, path" " FROM searchIndex" " WHERE type='%1'" " ORDER BY name"); } else { sql = QStringLiteral("SELECT name, path, fragment" " FROM searchIndex" " WHERE type='%1'" " ORDER BY name"); } if (!m_db->prepare(sql.arg(symbolString))) { qWarning("SQL Error: %s", qPrintable(m_db->lastError())); return; } QMultiMap &symbols = m_symbols[symbolType]; while (m_db->next()) { symbols.insert(m_db->value(0).toString(), createPageUrl(m_db->value(1).toString(), m_db->value(2).toString())); } } void Docset::createIndex() { static const QString indexListQuery = QStringLiteral("PRAGMA INDEX_LIST('%1')"); static const QString indexDropQuery = QStringLiteral("DROP INDEX '%1'"); static const QString indexCreateQuery = QStringLiteral("CREATE INDEX IF NOT EXISTS %1%2" " ON %3 (%4 COLLATE NOCASE)"); const QString tableName = m_type == Type::Dash ? QStringLiteral("searchIndex") : QStringLiteral("ztoken"); const QString columnName = m_type == Type::Dash ? QStringLiteral("name") : QStringLiteral("ztokenname"); m_db->prepare(indexListQuery.arg(tableName)); QStringList oldIndexes; while (m_db->next()) { const QString indexName = m_db->value(1).toString(); if (!indexName.startsWith(IndexNamePrefix)) continue; if (indexName.endsWith(IndexNameVersion)) return; oldIndexes << indexName; } // Drop old indexes for (const QString &oldIndexName : std::as_const(oldIndexes)) { m_db->execute(indexDropQuery.arg(oldIndexName)); } m_db->execute(indexCreateQuery.arg(IndexNamePrefix).arg(IndexNameVersion).arg(tableName).arg(columnName)); } void Docset::createView() { static const QString viewCreateQuery = QStringLiteral("CREATE VIEW IF NOT EXISTS searchIndex AS" " SELECT" " ztokenname AS name," " ztypename AS type," " zpath AS path," " zanchor AS fragment" " FROM ztoken" " INNER JOIN ztokenmetainformation" " ON ztoken.zmetainformation = ztokenmetainformation.z_pk" " INNER JOIN zfilepath" " ON ztokenmetainformation.zfile = zfilepath.z_pk" " INNER JOIN ztokentype" " ON ztoken.ztokentype = ztokentype.z_pk"); m_db->execute(viewCreateQuery); } QUrl Docset::createPageUrl(const QString &path, const QString &fragment) const { QString realPath; QString realFragment; if (fragment.isEmpty()) { const QStringList urlParts = path.split(QLatin1Char('#')); realPath = urlParts[0]; if (urlParts.size() > 1) realFragment = urlParts[1]; } else { realPath = path; realFragment = fragment; } static const QRegularExpression dashEntryRegExp(QStringLiteral("")); realPath.remove(dashEntryRegExp); realFragment.remove(dashEntryRegExp); QUrl url = m_baseUrl; url.setPath(m_baseUrl.path() + "/" + realPath, QUrl::TolerantMode); if (!realFragment.isEmpty()) { if (realFragment.startsWith(QLatin1String("//apple_ref")) || realFragment.startsWith(QLatin1String("//dash_ref"))) { url.setFragment(realFragment, QUrl::DecodedMode); } else { url.setFragment(realFragment); } } return url; } QString Docset::parseSymbolType(const QString &str) { // Dash symbol aliases const static QHash aliases = { // Attribute {QStringLiteral("Package Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Private Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Protected Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Public Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Package Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Private Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Protected Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("Static Public Attributes"), QStringLiteral("Attribute")}, {QStringLiteral("XML Attributes"), QStringLiteral("Attribute")}, // Binding {QStringLiteral("binding"), QStringLiteral("Binding")}, // Category {QStringLiteral("cat"), QStringLiteral("Category")}, {QStringLiteral("Groups"), QStringLiteral("Category")}, {QStringLiteral("Pages"), QStringLiteral("Category")}, // Class {QStringLiteral("cl"), QStringLiteral("Class")}, {QStringLiteral("specialization"), QStringLiteral("Class")}, {QStringLiteral("tmplt"), QStringLiteral("Class")}, // Constant {QStringLiteral("data"), QStringLiteral("Constant")}, {QStringLiteral("econst"), QStringLiteral("Constant")}, {QStringLiteral("enumdata"), QStringLiteral("Constant")}, {QStringLiteral("enumelt"), QStringLiteral("Constant")}, {QStringLiteral("clconst"), QStringLiteral("Constant")}, {QStringLiteral("structdata"), QStringLiteral("Constant")}, {QStringLiteral("writerid"), QStringLiteral("Constant")}, {QStringLiteral("Notifications"), QStringLiteral("Constant")}, // Constructor {QStringLiteral("structctr"), QStringLiteral("Constructor")}, {QStringLiteral("Public Constructors"), QStringLiteral("Constructor")}, // Enumeration {QStringLiteral("enum"), QStringLiteral("Enumeration")}, {QStringLiteral("Enum"), QStringLiteral("Enumeration")}, {QStringLiteral("Enumerations"), QStringLiteral("Enumeration")}, // Event {QStringLiteral("event"), QStringLiteral("Event")}, {QStringLiteral("Public Events"), QStringLiteral("Event")}, {QStringLiteral("Inherited Events"), QStringLiteral("Event")}, {QStringLiteral("Private Events"), QStringLiteral("Event")}, // Field {QStringLiteral("Data Fields"), QStringLiteral("Field")}, // Function {QStringLiteral("dcop"), QStringLiteral("Function")}, {QStringLiteral("func"), QStringLiteral("Function")}, {QStringLiteral("ffunc"), QStringLiteral("Function")}, {QStringLiteral("signal"), QStringLiteral("Function")}, {QStringLiteral("slot"), QStringLiteral("Function")}, {QStringLiteral("grammar"), QStringLiteral("Function")}, {QStringLiteral("Function Prototypes"), QStringLiteral("Function")}, {QStringLiteral("Functions/Subroutines"), QStringLiteral("Function")}, {QStringLiteral("Members"), QStringLiteral("Function")}, {QStringLiteral("Package Functions"), QStringLiteral("Function")}, {QStringLiteral("Private Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Private Slots"), QStringLiteral("Function")}, {QStringLiteral("Protected Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Protected Slots"), QStringLiteral("Function")}, {QStringLiteral("Public Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Public Slots"), QStringLiteral("Function")}, {QStringLiteral("Signals"), QStringLiteral("Function")}, {QStringLiteral("Static Package Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Private Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Protected Member Functions"), QStringLiteral("Function")}, {QStringLiteral("Static Public Member Functions"), QStringLiteral("Function")}, // Guide {QStringLiteral("doc"), QStringLiteral("Guide")}, // Namespace {QStringLiteral("ns"), QStringLiteral("Namespace")}, // Macro {QStringLiteral("macro"), QStringLiteral("Macro")}, // Method {QStringLiteral("clm"), QStringLiteral("Method")}, {QStringLiteral("enumcm"), QStringLiteral("Method")}, {QStringLiteral("enumctr"), QStringLiteral("Method")}, {QStringLiteral("enumm"), QStringLiteral("Method")}, {QStringLiteral("intfctr"), QStringLiteral("Method")}, {QStringLiteral("intfcm"), QStringLiteral("Method")}, {QStringLiteral("intfm"), QStringLiteral("Method")}, {QStringLiteral("intfsub"), QStringLiteral("Method")}, {QStringLiteral("instsub"), QStringLiteral("Method")}, {QStringLiteral("instctr"), QStringLiteral("Method")}, {QStringLiteral("instm"), QStringLiteral("Method")}, {QStringLiteral("structcm"), QStringLiteral("Method")}, {QStringLiteral("structm"), QStringLiteral("Method")}, {QStringLiteral("structsub"), QStringLiteral("Method")}, {QStringLiteral("Class Methods"), QStringLiteral("Method")}, {QStringLiteral("Inherited Methods"), QStringLiteral("Method")}, {QStringLiteral("Instance Methods"), QStringLiteral("Method")}, {QStringLiteral("Private Methods"), QStringLiteral("Method")}, {QStringLiteral("Protected Methods"), QStringLiteral("Method")}, {QStringLiteral("Public Methods"), QStringLiteral("Method")}, // Operator {QStringLiteral("intfopfunc"), QStringLiteral("Operator")}, {QStringLiteral("opfunc"), QStringLiteral("Operator")}, // Property {QStringLiteral("enump"), QStringLiteral("Property")}, {QStringLiteral("intfdata"), QStringLiteral("Property")}, {QStringLiteral("intfp"), QStringLiteral("Property")}, {QStringLiteral("instp"), QStringLiteral("Property")}, {QStringLiteral("structp"), QStringLiteral("Property")}, {QStringLiteral("Inherited Properties"), QStringLiteral("Property")}, {QStringLiteral("Private Properties"), QStringLiteral("Property")}, {QStringLiteral("Protected Properties"), QStringLiteral("Property")}, {QStringLiteral("Public Properties"), QStringLiteral("Property")}, // Protocol {QStringLiteral("intf"), QStringLiteral("Protocol")}, // Structure {QStringLiteral("_Struct"), QStringLiteral("Structure")}, {QStringLiteral("_Structs"), QStringLiteral("Structure")}, {QStringLiteral("struct"), QStringLiteral("Structure")}, {QStringLiteral("Сontrol Structure"), QStringLiteral("Structure")}, {QStringLiteral("Data Structures"), QStringLiteral("Structure")}, {QStringLiteral("Struct"), QStringLiteral("Structure")}, // Type {QStringLiteral("tag"), QStringLiteral("Type")}, {QStringLiteral("tdef"), QStringLiteral("Type")}, {QStringLiteral("Data Types"), QStringLiteral("Type")}, {QStringLiteral("Package Types"), QStringLiteral("Type")}, {QStringLiteral("Private Types"), QStringLiteral("Type")}, {QStringLiteral("Protected Types"), QStringLiteral("Type")}, {QStringLiteral("Public Types"), QStringLiteral("Type")}, {QStringLiteral("Typedefs"), QStringLiteral("Type")}, // Variable {QStringLiteral("var"), QStringLiteral("Variable")} }; return aliases.value(str, str); } QUrl Docset::baseUrl() const { return m_baseUrl; } void Docset::setBaseUrl(const QUrl &baseUrl) { m_baseUrl = baseUrl; if (!m_indexFilePath.isEmpty()) { m_indexFileUrl = createPageUrl(m_indexFilePath); } } bool Docset::isFuzzySearchEnabled() const { return m_isFuzzySearchEnabled; } void Docset::setFuzzySearchEnabled(bool enabled) { m_isFuzzySearchEnabled = enabled; } bool Docset::isJavaScriptEnabled() const { return m_isJavaScriptEnabled; } /** * \brief Returns score based on a substring position in a string. * \param str Original string. * \param index Index of the substring within \a str. * \param length Substring length. * \return Score value between 1 and 100. */ static int scoreFuzzy(const char *str, int index, int length) { // Score between 66..99, if the match follows a dot, or starts the string. if (index == 0 || str[index - 1] == '.') { return qMax(66, 100 - length); } // Score between 33..66, if the match is at the end of the string. if (str[index + length] == 0) { return qMax(33, 67 - length); } // Score between 1..33 otherwise (match in the middle of the string). return qMax(1, 34 - length); } // Based on https://github.com/bevacqua/fuzzysearch static void matchFuzzy(const char *needle, int needleLength, const char *haystack, int haystackLength, int *start, int *length) { static const int MaxDistance = 8; static const int MaxGroupCount = 3; *start = -1; int groupCount = 0; int bestRecursiveScore = -1; int bestRecursiveStart = -1; int bestRecursiveLength = -1; for (int i = 0, j = 0; i < needleLength; ++i) { bool found = false; bool first = true; int distance = 0; while (j < haystackLength) { if (needle[i] == haystack[j++]) { if (*start == -1) { *start = j; // first matched char // try starting the search later in case the first character occurs again later int recursiveStart; int recursiveLength; matchFuzzy(needle, needleLength, haystack + j, haystackLength - j, &recursiveStart, &recursiveLength); if (recursiveStart != -1) { int recursiveScore = scoreFuzzy(haystack, recursiveStart, recursiveLength); if (recursiveScore > bestRecursiveScore) { bestRecursiveScore = recursiveScore; bestRecursiveStart = recursiveStart; bestRecursiveLength = recursiveLength; } } } *length = j - *start + 1; found = true; break; } // Optimizations to reduce returned number of results // (search was returning too many irrelevant results with large docsets) // Optimization #1: too many mismatches. if (first) { if (++groupCount >= MaxGroupCount) { break; } first = false; } // Optimization #2: too large distance between found chars. if (i != 0 && ++distance >= MaxDistance) { break; } } if (!found) { // End of haystack, char not found. if (bestRecursiveScore != -1) { // Can still match with the same constraints if matching started later // (smaller distance from first char to 2nd char) *start = bestRecursiveStart; *length = bestRecursiveLength; } else { *start = -1; } return; } } int score = scoreFuzzy(haystack, *start, *length); if (bestRecursiveScore > score) { *start = bestRecursiveStart; *length = bestRecursiveLength; } } // Ported from DevDocs (https://github.com/Thibaut/devdocs), see app/searcher.coffee. static int scoreExact(int matchIndex, int matchLen, const char *value, int valueLen) { static const char DOT = '.'; int score = 100; // Remove one point for each unmatched character. score -= (valueLen - matchLen); if (matchIndex > 0) { if (value[matchIndex - 1] == DOT) { // If the character preceding the query is a dot, assign the same // score as if the query was found at the beginning of the string, // minus one. score += matchIndex - 1; } else if (matchLen == 1) { // Don't match a single-character query unless it's found at the // beginning of the string or is preceded by a dot. return 0; } else { // (1) Remove one point for each unmatched character up to // the nearest preceding dot or the beginning of the // string. // (2) Remove one point for each unmatched character // following the query. int i = matchIndex - 2; while (i >= 0 && value[i] != DOT) { --i; } score -= (matchIndex - i) // (1) + (valueLen - matchLen - matchIndex); // (2) } // Remove one point for each dot preceding the query, except for the // one immediately before the query. for (int i = matchIndex - 2; i >= 0; --i) { if (value[i] == DOT) --score; } } // Remove five points for each dot following the query. for (int i = valueLen - matchLen - matchIndex - 1; i >= 0; --i) { if (value[matchIndex + matchLen + i] == DOT) score -= 5; } return qMax(1, score); } static inline int scoreFunction(const char *needleOrig, const char *haystackOrig) { const int needleLength = static_cast(qstrlen(needleOrig)); const int haystackLength = static_cast(qstrlen(haystackOrig)); QVarLengthArray needle(needleLength + 1); QVarLengthArray haystack(haystackLength + 1); for (int i = 0, j = 0; i <= needleLength; ++i, ++j) { const char c = needleOrig[i]; if ((i > 0 && needleOrig[i - 1] == ':' && c == ':') // C++ (::) || c == '/' || c == '_' || c == ' ') { // Go, some Guides needle[j] = '.'; } else if (c >= 'A' && c <= 'Z') { needle[j] = c + 32; } else { needle[j] = c; } } for (int i = 0, j = 0; i <= haystackLength; ++i, ++j) { const char c = haystackOrig[i]; if ((i > 0 && haystackOrig[i - 1] == ':' && c == ':') // C++ (::) || c == '/' || c == '_' || c == ' ') { // Go, some Guides haystack[j] = '.'; } else if (c >= 'A' && c <= 'Z') { haystack[j] = c + 32; } else { haystack[j] = c; } } int score = 0; int matchIndex = -1; int matchLength = 0; int exactIndex = -1; const char *exactMatch = std::strstr(haystack.data(), needle.data()); if (exactMatch != nullptr) { exactIndex = exactMatch - haystack.data(); } if (exactIndex == -1) { matchFuzzy(needle.data(), needleLength, haystack.data(), haystackLength, &matchIndex, &matchLength); } if (matchIndex == -1 && exactIndex == -1) { // no match return 0; } if (exactIndex != -1) { // +100 to make sure exact matches are always on top. score = scoreExact(exactIndex, needleLength, haystack.data(), haystackLength) + 100; } else { score = scoreFuzzy(haystack.data(), matchIndex, matchLength); int indexOfLastDot; for (indexOfLastDot = haystackLength - 1; indexOfLastDot >= 0; --indexOfLastDot) { if (haystack[indexOfLastDot] == '.') break; } if (indexOfLastDot != -1) { matchIndex = -1; matchFuzzy(needle.data(), needleLength, haystack.data() + indexOfLastDot + 1, haystackLength - (indexOfLastDot + 1), &matchIndex, &matchLength); if (matchIndex != -1) { score = qMax(score, scoreFuzzy(haystack.data() + indexOfLastDot + 1, matchIndex, matchLength)); } } } return score; } static void sqliteScoreFunction(sqlite3_context *context, int argc, sqlite3_value **argv) { Q_UNUSED(argc) auto needle = reinterpret_cast(sqlite3_value_text(argv[0])); auto haystack = reinterpret_cast(sqlite3_value_text(argv[1])); sqlite3_result_int(context, scoreFunction(needle, haystack)); } zeal-0.7.1/src/libs/registry/docset.h000066400000000000000000000070421462517734600175160ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_DOCSET_H #define ZEAL_REGISTRY_DOCSET_H #include #include #include #include #include namespace Zeal { namespace Util { class SQLiteDatabase; } namespace Registry { class CancellationToken; struct SearchResult; class Docset final { Q_DISABLE_COPY(Docset) public: explicit Docset(QString path); virtual ~Docset(); bool isValid() const; QString name() const; QString title() const; QStringList keywords() const; QString version() const; int revision() const; QString feedUrl() const; QString path() const; QString documentPath() const; QIcon icon() const; QIcon symbolTypeIcon(const QString &symbolType) const; QUrl indexFileUrl() const; QMap symbolCounts() const; int symbolCount(const QString &symbolType) const; const QMultiMap &symbols(const QString &symbolType) const; QList search(const QString &query, const CancellationToken &token) const; QList relatedLinks(const QUrl &url) const; // FIXME: This a temporary solution to create URL on demand. QUrl searchResultUrl(const SearchResult &result) const; // FIXME: This is an ugly workaround before we have a proper docset sources implementation bool hasUpdate = false; QUrl baseUrl() const; void setBaseUrl(const QUrl &baseUrl); bool isFuzzySearchEnabled() const; void setFuzzySearchEnabled(bool enabled); bool isJavaScriptEnabled() const; private: enum class Type { Invalid, Dash, ZDash }; void loadMetadata(); void countSymbols(); void loadSymbols(const QString &symbolType) const; void loadSymbols(const QString &symbolType, const QString &symbolString) const; void createIndex(); void createView(); QUrl createPageUrl(const QString &path, const QString &fragment = QString()) const; static QString parseSymbolType(const QString &str); QString m_name; QString m_title; QStringList m_keywords; QString m_version; int m_revision = 0; QString m_feedUrl; Docset::Type m_type = Type::Invalid; QString m_path; QIcon m_icon; QUrl m_indexFileUrl; QString m_indexFilePath; QMultiMap m_symbolStrings; QMap m_symbolCounts; mutable QMap> m_symbols; Util::SQLiteDatabase *m_db = nullptr; bool m_isFuzzySearchEnabled = false; bool m_isJavaScriptEnabled = false; QUrl m_baseUrl; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSET_H zeal-0.7.1/src/libs/registry/docsetmetadata.cpp000066400000000000000000000140301462517734600215450ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "docsetmetadata.h" #include #include #include #include #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) #include #endif #include #include using namespace Zeal::Registry; DocsetMetadata::DocsetMetadata(const QJsonObject &jsonObject) { m_name = jsonObject[QStringLiteral("name")].toString(); m_title = jsonObject[QStringLiteral("title")].toString(); m_rawIcon = QByteArray::fromBase64(jsonObject[QStringLiteral("icon")].toString().toLocal8Bit()); m_icon.addPixmap(QPixmap::fromImage(QImage::fromData(m_rawIcon))); m_rawIcon2x = QByteArray::fromBase64(jsonObject[QStringLiteral("icon2x")].toString() .toLocal8Bit()); if (qApp->devicePixelRatio() > 1.0) { QPixmap pixmap = QPixmap::fromImage(QImage::fromData(m_rawIcon2x)); pixmap.setDevicePixelRatio(2.0); m_icon.addPixmap(pixmap); } for (const QJsonValueRef vv : jsonObject[QStringLiteral("aliases")].toArray()) { m_aliases << vv.toString(); } for (const QJsonValueRef vv : jsonObject[QStringLiteral("versions")].toArray()) { m_versions << vv.toString(); } // Unfortunately, API returns revision as a string, so it needs to be converted to integer // for comparison to work properly. m_revision = jsonObject[QStringLiteral("revision")].toString().toInt(); m_feedUrl = QUrl(jsonObject[QStringLiteral("feed_url")].toString()); for (const QJsonValueRef vv : jsonObject[QStringLiteral("urls")].toArray()) { m_urls.append(QUrl(vv.toString())); } m_extra = jsonObject[QStringLiteral("extra")].toObject(); } /*! Creates meta.json for specified docset \a version in the \a path. */ void DocsetMetadata::save(const QString &path, const QString &version) { QScopedPointer file(new QFile(path + QLatin1String("/meta.json"))); if (!file->open(QIODevice::WriteOnly)) return; QJsonObject jsonObject; jsonObject[QStringLiteral("name")] = m_name; jsonObject[QStringLiteral("title")] = m_title; if (!version.isEmpty()) jsonObject[QStringLiteral("version")] = version; if (version == latestVersion() && m_revision > 0) jsonObject[QStringLiteral("revision")] = QString::number(m_revision); if (!m_feedUrl.isEmpty()) jsonObject[QStringLiteral("feed_url")] = m_feedUrl.toString(); if (!m_urls.isEmpty()) { QJsonArray urls; for (const QUrl &url : std::as_const(m_urls)) { urls.append(url.toString()); } jsonObject[QStringLiteral("urls")] = urls; } if (!m_extra.isEmpty()) jsonObject[QStringLiteral("extra")] = m_extra; file->write(QJsonDocument(jsonObject).toJson()); file->close(); if (m_rawIcon.isEmpty()) return; file->setFileName(path + QLatin1String("/icon.png")); if (file->open(QIODevice::WriteOnly)) file->write(m_rawIcon); file->close(); if (m_rawIcon2x.isEmpty()) return; file->setFileName(path + QLatin1String("/icon@2x.png")); if (file->open(QIODevice::WriteOnly)) file->write(m_rawIcon2x); file->close(); } QString DocsetMetadata::name() const { return m_name; } QIcon DocsetMetadata::icon() const { return m_icon; } QString DocsetMetadata::title() const { return m_title; } QStringList DocsetMetadata::aliases() const { return m_aliases; } QStringList DocsetMetadata::versions() const { return m_versions; } QString DocsetMetadata::latestVersion() const { return m_versions.isEmpty() ? QString() : m_versions.first(); } int DocsetMetadata::revision() const { return m_revision; } QUrl DocsetMetadata::feedUrl() const { return m_feedUrl; } QUrl DocsetMetadata::url() const { #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) return m_urls.at(QRandomGenerator::global()->bounded(m_urls.size())); #else return m_urls.at(qrand() % m_urls.size()); #endif } QList DocsetMetadata::urls() const { return m_urls; } DocsetMetadata DocsetMetadata::fromDashFeed(const QUrl &feedUrl, const QByteArray &data) { DocsetMetadata metadata; metadata.m_name = feedUrl.fileName(); // Strip ".xml" extension if any. if (metadata.m_name.endsWith(QLatin1String(".xml"))) { metadata.m_name.chop(4); } metadata.m_title = metadata.m_name; metadata.m_title.replace(QLatin1Char('_'), QLatin1Char(' ')); metadata.m_feedUrl = feedUrl; QXmlStreamReader xml(data); while (!xml.atEnd()) { const QXmlStreamReader::TokenType token = xml.readNext(); if (token != QXmlStreamReader::StartElement) continue; // Try to pull out the relevant data if (xml.name() == QLatin1String("version")) { if (xml.readNext() != QXmlStreamReader::Characters) continue; metadata.m_versions << xml.text().toString(); } else if (xml.name() == QLatin1String("url")) { if (xml.readNext() != QXmlStreamReader::Characters) continue; metadata.m_urls.append(QUrl(xml.text().toString())); } } return metadata; } zeal-0.7.1/src/libs/registry/docsetmetadata.h000066400000000000000000000040311462517734600212120ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_DOCSETMETADATA_H #define ZEAL_REGISTRY_DOCSETMETADATA_H #include #include #include #include namespace Zeal { namespace Registry { class DocsetMetadata { public: explicit DocsetMetadata() = default; explicit DocsetMetadata(const QJsonObject &jsonObject); void save(const QString &path, const QString &version); QString name() const; QString title() const; QStringList aliases() const; QStringList versions() const; QString latestVersion() const; int revision() const; QIcon icon() const; QUrl feedUrl() const; QUrl url() const; QList urls() const; static DocsetMetadata fromDashFeed(const QUrl &feedUrl, const QByteArray &data); private: QString m_name; QString m_title; QStringList m_aliases; QStringList m_versions; int m_revision = 0; QByteArray m_rawIcon; QByteArray m_rawIcon2x; QIcon m_icon; QJsonObject m_extra; QUrl m_feedUrl; QList m_urls; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSETMETADATA_H zeal-0.7.1/src/libs/registry/docsetregistry.cpp000066400000000000000000000151661462517734600216500ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "docsetregistry.h" #include "docset.h" #include "listmodel.h" #include "searchquery.h" #include "searchresult.h" #include #include #include #include #include #include #include using namespace Zeal::Registry; void MergeQueryResults(QList &finalResult, const QList &partial) { finalResult << partial; } DocsetRegistry::DocsetRegistry(QObject *parent) : QObject(parent) , m_model(new ListModel(this)) , m_thread(new QThread(this)) { // Register for use in signal connections. qRegisterMetaType>("QList"); // FIXME: Only search should be performed in a separate thread moveToThread(m_thread); m_thread->start(); } DocsetRegistry::~DocsetRegistry() { m_thread->exit(); m_thread->wait(); qDeleteAll(m_docsets); } QAbstractItemModel *DocsetRegistry::model() const { return m_model; } QString DocsetRegistry::storagePath() const { return m_storagePath; } void DocsetRegistry::setStoragePath(const QString &path) { if (path == m_storagePath) { return; } m_storagePath = path; unloadAllDocsets(); addDocsetsFromFolder(path); } bool DocsetRegistry::isFuzzySearchEnabled() const { return m_isFuzzySearchEnabled; } void DocsetRegistry::setFuzzySearchEnabled(bool enabled) { if (enabled == m_isFuzzySearchEnabled) { return; } m_isFuzzySearchEnabled = enabled; for (Docset *docset : std::as_const(m_docsets)) { docset->setFuzzySearchEnabled(enabled); } } int DocsetRegistry::count() const { return m_docsets.count(); } bool DocsetRegistry::contains(const QString &name) const { return m_docsets.contains(name); } QStringList DocsetRegistry::names() const { return m_docsets.keys(); } void DocsetRegistry::loadDocset(const QString &path) { std::future f = std::async(std::launch::async, [path](){ return new Docset(path); }); f.wait(); Docset *docset = f.get(); // TODO: Emit error if (!docset->isValid()) { qWarning("Could not load docset from '%s'. Reinstall the docset.", qPrintable(docset->path())); delete docset; return; } docset->setFuzzySearchEnabled(m_isFuzzySearchEnabled); const QString name = docset->name(); if (m_docsets.contains(name)) { unloadDocset(name); } // Setup HTTP mount. QUrl url = Core::Application::instance()->httpServer()->mount(name, docset->documentPath()); if (url.isEmpty()) { qWarning("Could not enable docset from '%s'. Reinstall the docset.", qPrintable(docset->path())); delete docset; return; } docset->setBaseUrl(url); m_docsets[name] = docset; emit docsetLoaded(name); } void DocsetRegistry::unloadDocset(const QString &name) { emit docsetAboutToBeUnloaded(name); Core::Application::instance()->httpServer()->unmount(name); delete m_docsets.take(name); emit docsetUnloaded(name); } void DocsetRegistry::unloadAllDocsets() { const auto keys = m_docsets.keys(); for (const QString &name : keys) { unloadDocset(name); } } Docset *DocsetRegistry::docset(const QString &name) const { return m_docsets[name]; } Docset *DocsetRegistry::docset(int index) const { if (index < 0 || index >= m_docsets.size()) return nullptr; auto it = m_docsets.cbegin(); std::advance(it, index); return *it; } Docset *DocsetRegistry::docsetForUrl(const QUrl &url) { for (Docset *docset : std::as_const(m_docsets)) { if (docset->baseUrl().isParentOf(url)) return docset; } return nullptr; } QList DocsetRegistry::docsets() const { return m_docsets.values(); } void DocsetRegistry::search(const QString &query) { m_cancellationToken.cancel(); if (query.isEmpty()) { emit searchCompleted({}); return; } QMetaObject::invokeMethod(this, "_runQuery", Qt::QueuedConnection, Q_ARG(QString, query)); } void DocsetRegistry::_runQuery(const QString &query) { m_cancellationToken.reset(); QList enabledDocsets; const SearchQuery searchQuery = SearchQuery::fromString(query); if (searchQuery.hasKeywords()) { for (Docset *docset : std::as_const(m_docsets)) { if (searchQuery.hasKeywords(docset->keywords())) enabledDocsets << docset; } } else { enabledDocsets = docsets(); } QFuture> queryResultsFuture = QtConcurrent::mappedReduced(enabledDocsets, std::bind(&Docset::search, std::placeholders::_1, searchQuery.query(), std::ref(m_cancellationToken)), &MergeQueryResults); QList results = queryResultsFuture.result(); if (m_cancellationToken.isCanceled()) return; std::sort(results.begin(), results.end()); if (m_cancellationToken.isCanceled()) return; emit searchCompleted(results); } // Recursively finds and adds all docsets in a given directory. void DocsetRegistry::addDocsetsFromFolder(const QString &path) { const QDir dir(path); const auto subDirectories = dir.entryInfoList(QDir::NoDotAndDotDot | QDir::AllDirs); for (const QFileInfo &subdir : subDirectories) { if (subdir.suffix() == QLatin1String("docset")) loadDocset(subdir.filePath()); else addDocsetsFromFolder(subdir.filePath()); } } zeal-0.7.1/src/libs/registry/docsetregistry.h000066400000000000000000000052621462517734600213110ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_DOCSETREGISTRY_H #define ZEAL_REGISTRY_DOCSETREGISTRY_H #include "cancellationtoken.h" #include "searchresult.h" #include #include class QAbstractItemModel; class QThread; namespace Zeal { namespace Registry { class Docset; class DocsetRegistry final : public QObject { Q_OBJECT Q_DISABLE_COPY(DocsetRegistry) public: explicit DocsetRegistry(QObject *parent = nullptr); ~DocsetRegistry() override; QAbstractItemModel *model() const; QString storagePath() const; void setStoragePath(const QString &path); bool isFuzzySearchEnabled() const; void setFuzzySearchEnabled(bool enabled); int count() const; bool contains(const QString &name) const; QStringList names() const; void loadDocset(const QString &path); void unloadDocset(const QString &name); void unloadAllDocsets(); Docset *docset(const QString &name) const; Docset *docset(int index) const; Docset *docsetForUrl(const QUrl &url); QList docsets() const; void search(const QString &query); const QList &queryResults(); signals: void docsetLoaded(const QString &name); void docsetAboutToBeUnloaded(const QString &name); void docsetUnloaded(const QString &name); void searchCompleted(const QList &results); private slots: void _runQuery(const QString &query); private: void addDocsetsFromFolder(const QString &path); QAbstractItemModel *m_model = nullptr; QString m_storagePath; bool m_isFuzzySearchEnabled = false; QThread *m_thread = nullptr; QMap m_docsets; CancellationToken m_cancellationToken; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_DOCSETREGISTRY_H zeal-0.7.1/src/libs/registry/itemdatarole.h000066400000000000000000000005111462517734600207010ustar00rootroot00000000000000#ifndef ZEAL_REGISTRY_ITEMDATAROLE_H #define ZEAL_REGISTRY_ITEMDATAROLE_H #include namespace Zeal { namespace Registry { enum ItemDataRole { DocsetIconRole = Qt::UserRole, DocsetNameRole, UpdateAvailableRole, UrlRole }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_ITEMDATAROLE_H zeal-0.7.1/src/libs/registry/listmodel.cpp000066400000000000000000000205461462517734600205700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "listmodel.h" #include "docset.h" #include "docsetregistry.h" #include "itemdatarole.h" #include using namespace Zeal::Registry; ListModel::ListModel(DocsetRegistry *docsetRegistry) : QAbstractItemModel(docsetRegistry) , m_docsetRegistry(docsetRegistry) { connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, &ListModel::addDocset); connect(m_docsetRegistry, &DocsetRegistry::docsetAboutToBeUnloaded, this, &ListModel::removeDocset); } ListModel::~ListModel() { for (auto &kv : m_docsetItems) { qDeleteAll(kv.second->groups); delete kv.second; } } QVariant ListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); switch (role) { case Qt::DecorationRole: switch (indexLevel(index)) { case Level::DocsetLevel: return itemInRow(index.row())->docset->icon(); case Level::GroupLevel: { auto docsetItem = static_cast(index.internalPointer()); const QString symbolType = docsetItem->groups.at(index.row())->symbolType; return docsetItem->docset->symbolTypeIcon(symbolType); } case Level::SymbolLevel: { auto groupItem = static_cast(index.internalPointer()); return groupItem->docsetItem->docset->symbolTypeIcon(groupItem->symbolType); } default: return QVariant(); } case Qt::DisplayRole: switch (indexLevel(index)) { case Level::DocsetLevel: return itemInRow(index.row())->docset->title(); case Level::GroupLevel: { auto docsetItem = static_cast(index.internalPointer()); const QString symbolType = docsetItem->groups.at(index.row())->symbolType; return QStringLiteral("%1 (%2)").arg(pluralize(symbolType), QString::number(docsetItem->docset->symbolCount(symbolType))); } case Level::SymbolLevel: { auto groupItem = static_cast(index.internalPointer()); auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin(); std::advance(it, index.row()); return it.key(); } default: return QVariant(); } case Qt::ToolTipRole: switch (indexLevel(index)) { case Level::DocsetLevel: { const auto docset = itemInRow(index.row())->docset; return tr("Version: %1r%2").arg(docset->version()).arg(docset->revision()); } default: return QVariant(); } case ItemDataRole::UrlRole: switch (indexLevel(index)) { case Level::DocsetLevel: return itemInRow(index.row())->docset->indexFileUrl(); case Level::SymbolLevel: { auto groupItem = static_cast(index.internalPointer()); auto it = groupItem->docsetItem->docset->symbols(groupItem->symbolType).cbegin(); std::advance(it, index.row()); return it.value(); } default: return QVariant(); } case ItemDataRole::DocsetNameRole: if (index.parent().isValid()) return QVariant(); return itemInRow(index.row())->docset->name(); case ItemDataRole::UpdateAvailableRole: if (index.parent().isValid()) return QVariant(); return itemInRow(index.row())->docset->hasUpdate; default: return QVariant(); } } QModelIndex ListModel::index(int row, int column, const QModelIndex &parent) const { if (!hasIndex(row, column, parent)) return {}; switch (indexLevel(parent)) { case Level::RootLevel: return createIndex(row, column); case Level::DocsetLevel: return createIndex(row, column, static_cast(itemInRow(parent.row()))); case Level::GroupLevel: { auto docsetItem = static_cast(parent.internalPointer()); return createIndex(row, column, docsetItem->groups.at(parent.row())); } default: return {}; } } QModelIndex ListModel::parent(const QModelIndex &child) const { switch (indexLevel(child)) { case Level::GroupLevel: { auto item = static_cast(child.internalPointer()); auto it = std::find_if(m_docsetItems.cbegin(), m_docsetItems.cend(), [item](const auto &pair) { return pair.second == item; }); if (it == m_docsetItems.cend()) { // TODO: Report error, this should never happen. return {}; } const int row = static_cast(std::distance(m_docsetItems.begin(), it)); return createIndex(row, 0); } case SymbolLevel: { auto item = static_cast(child.internalPointer()); return createIndex(item->docsetItem->groups.indexOf(item), 0, item->docsetItem); } default: return {}; } } int ListModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return 1; } int ListModel::rowCount(const QModelIndex &parent) const { if (parent.column() > 0) return 0; switch (indexLevel(parent)) { case Level::RootLevel: return static_cast(m_docsetItems.size()); case Level::DocsetLevel: return itemInRow(parent.row())->docset->symbolCounts().count(); case Level::GroupLevel: { auto docsetItem = static_cast(parent.internalPointer()); return docsetItem->docset->symbolCount(docsetItem->groups.at(parent.row())->symbolType); } default: return 0; } } void ListModel::addDocset(const QString &name) { const int row = static_cast(std::distance(m_docsetItems.begin(), m_docsetItems.upper_bound(name))); beginInsertRows(QModelIndex(), row, row); auto docsetItem = new DocsetItem(); docsetItem->docset = m_docsetRegistry->docset(name); const auto keys = docsetItem->docset->symbolCounts().keys(); for (const QString &symbolType : keys) { auto groupItem = new GroupItem(); groupItem->docsetItem = docsetItem; groupItem->symbolType = symbolType; docsetItem->groups.append(groupItem); } m_docsetItems.insert({name, docsetItem}); endInsertRows(); } void ListModel::removeDocset(const QString &name) { auto it = m_docsetItems.find(name); if (it == m_docsetItems.cend()) { // TODO: Investigate why this can happen (see #420) return; } const int row = static_cast(std::distance(m_docsetItems.begin(), it)); beginRemoveRows(QModelIndex(), row, row); qDeleteAll(it->second->groups); delete it->second; m_docsetItems.erase(it); endRemoveRows(); } QString ListModel::pluralize(const QString &s) { if (s.endsWith(QLatin1String("y"))) { return s.left(s.length() - 1) + QLatin1String("ies"); } return s + (s.endsWith('s') ? QLatin1String("es") : QLatin1String("s")); } ListModel::Level ListModel::indexLevel(const QModelIndex &index) { if (!index.isValid()) { return Level::RootLevel; } if (!index.internalPointer()) { return Level::DocsetLevel; } if (*static_cast(index.internalPointer()) == Level::DocsetLevel) { return Level::GroupLevel; } return Level::SymbolLevel; } ListModel::DocsetItem *ListModel::itemInRow(int row) const { auto it = m_docsetItems.cbegin(); std::advance(it, row); return it->second; } zeal-0.7.1/src/libs/registry/listmodel.h000066400000000000000000000050771462517734600202370ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_LISTMODEL_H #define ZEAL_REGISTRY_LISTMODEL_H #include #include namespace Zeal { namespace Registry { class Docset; class DocsetRegistry; class ListModel final : public QAbstractItemModel { Q_OBJECT Q_DISABLE_COPY(ListModel) public: ~ListModel() override; QVariant data(const QModelIndex &index, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; QModelIndex parent(const QModelIndex &child) const override; int columnCount(const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent) const override; private slots: void addDocset(const QString &name); void removeDocset(const QString &name); private: friend class DocsetRegistry; enum Level { RootLevel, DocsetLevel, GroupLevel, SymbolLevel }; explicit ListModel(DocsetRegistry *docsetRegistry); inline static QString pluralize(const QString &s); inline static Level indexLevel(const QModelIndex &index); DocsetRegistry *m_docsetRegistry = nullptr; struct DocsetItem; struct GroupItem { const Level level = Level::GroupLevel; DocsetItem *docsetItem = nullptr; QString symbolType; }; struct DocsetItem { const Level level = Level::DocsetLevel; Docset *docset = nullptr; QList groups; }; inline DocsetItem *itemInRow(int row) const; Util::CaseInsensitiveMap m_docsetItems; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_LISTMODEL_H zeal-0.7.1/src/libs/registry/searchmodel.cpp000066400000000000000000000063711462517734600210620ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchmodel.h" #include "docset.h" #include "itemdatarole.h" using namespace Zeal::Registry; SearchModel::SearchModel(QObject *parent) : QAbstractListModel(parent) { } SearchModel *SearchModel::clone(QObject *parent) { auto model = new SearchModel(parent); model->m_dataList = m_dataList; return model; } bool SearchModel::isEmpty() const { return m_dataList.isEmpty(); } QVariant SearchModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); auto item = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: return item->name; case Qt::DecorationRole: return item->docset->symbolTypeIcon(item->type); case ItemDataRole::DocsetIconRole: return item->docset->icon(); case ItemDataRole::UrlRole: return item->docset->searchResultUrl(*item); default: return QVariant(); } } QModelIndex SearchModel::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid() || m_dataList.count() <= row || column > 1) return {}; // FIXME: const_cast auto item = const_cast(&m_dataList.at(row)); return createIndex(row, column, item); } int SearchModel::rowCount(const QModelIndex &parent) const { if (!parent.isValid()) return m_dataList.count(); return 0; } bool SearchModel::removeRows(int row, int count, const QModelIndex &parent) { if (row + count <= m_dataList.size() && !parent.isValid()) { beginRemoveRows(parent, row, row + count - 1); while (count) { m_dataList.removeAt(row); --count; } endRemoveRows(); return true; } return false; } void SearchModel::removeSearchResultWithName(const QString &name) { QMutableListIterator iterator(m_dataList); int rowNum = 0; while (iterator.hasNext()) { if (iterator.next().docset->name() == name) { beginRemoveRows(QModelIndex(), rowNum, rowNum); iterator.remove(); rowNum -= 1; endRemoveRows(); } rowNum += 1; } } void SearchModel::setResults(const QList &results) { beginResetModel(); m_dataList = results; endResetModel(); emit updated(); } zeal-0.7.1/src/libs/registry/searchmodel.h000066400000000000000000000037161462517734600205270ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_SEARCHMODEL_H #define ZEAL_REGISTRY_SEARCHMODEL_H #include "searchresult.h" #include namespace Zeal { namespace Registry { class SearchModel final : public QAbstractListModel { Q_OBJECT Q_DISABLE_COPY(SearchModel) public: explicit SearchModel(QObject *parent = nullptr); SearchModel *clone(QObject *parent = nullptr); bool isEmpty() const; QVariant data(const QModelIndex &index, int role) const override; QModelIndex index(int row, int column, const QModelIndex &parent) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override; void removeSearchResultWithName(const QString &name); public slots: void setResults(const QList &results = QList()); signals: void updated(); private: QList m_dataList; }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_SEARCHMODEL_H zeal-0.7.1/src/libs/registry/searchquery.cpp000066400000000000000000000060521462517734600211230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchquery.h" #include using namespace Zeal::Registry; namespace { const char prefixSeparator = ':'; const char keywordSeparator = ','; } // namespace SearchQuery::SearchQuery(QString query, const QStringList &keywords) : m_query(std::move(query)) { setKeywords(keywords); } SearchQuery SearchQuery::fromString(const QString &str) { const int sepAt = str.indexOf(prefixSeparator); const int next = sepAt + 1; QString query; QStringList keywords; if (sepAt > 0 && (next >= str.size() || str.at(next) != prefixSeparator)) { query = str.mid(next).trimmed(); const QString keywordStr = str.left(sepAt).trimmed(); keywords = keywordStr.split(keywordSeparator); } else { query = str.trimmed(); } return SearchQuery(query, keywords); } QString SearchQuery::toString() const { if (m_keywords.isEmpty()) { return m_query; } return m_keywordPrefix + m_query; } bool SearchQuery::isEmpty() const { return m_query.isEmpty() && m_keywords.isEmpty(); } QStringList SearchQuery::keywords() const { return m_keywords; } void SearchQuery::setKeywords(const QStringList &list) { if (list.isEmpty()) return; m_keywords = list; m_keywordPrefix = list.join(keywordSeparator) + prefixSeparator; } bool SearchQuery::hasKeywords() const { return !m_keywords.isEmpty(); } bool SearchQuery::hasKeywords(const QStringList &keywords) const { for (const QString &keyword : keywords) { if (m_keywords.contains(keyword, Qt::CaseInsensitive)) { return true; } } return false; } int SearchQuery::keywordPrefixSize() const { return m_keywordPrefix.size(); } QString SearchQuery::query() const { return m_query; } void SearchQuery::setQuery(const QString &str) { m_query = str; } QDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query) { out << query.toString(); return out; } QDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query) { QString str; in >> str; query = SearchQuery::fromString(str); return in; } zeal-0.7.1/src/libs/registry/searchquery.h000066400000000000000000000061161462517734600205710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_SEARCHQUERY_H #define ZEAL_REGISTRY_SEARCHQUERY_H #include #include namespace Zeal { namespace Registry { /** * @short The search query model. */ class SearchQuery { public: explicit SearchQuery() = default; explicit SearchQuery(QString query, const QStringList &keywords = QStringList()); /// Creates a search query from a string. Single separator will be /// used to contstruct docset filter, but separator repeated twice /// will be left inside coreQuery part since double semicolon is /// used inside qualified symbol names in popular programming /// languages (c++, ruby, perl, etc.). /// /// Examples: /// "android:setTypeFa" #=> docsetFilters = ["android"], coreQuery = "setTypeFa" /// "noprefix" #=> docsetFilters = [], coreQuery = "noprefix" /// ":find" #=> docsetFilters = [], coreQuery = ":find" /// "std::string" #=> docsetFilters = [], coreQuery = "std::string" /// "c++:std::string" #=> docsetFilters = ["c++"], coreQuery = "std::string" /// /// Multiple docsets are supported using the ',' character: /// "java,android:setTypeFa #=> docsetFilters = ["java", "android"], coreQuery = "setTypeFa" static SearchQuery fromString(const QString &str); QString toString() const; bool isEmpty() const; QStringList keywords() const; void setKeywords(const QStringList &list); /// Returns true if there's a docset filter for the given query bool hasKeywords() const; /// Returns true if one the query contains one of the @c keywords. bool hasKeywords(const QStringList &keywords) const; /// Returns the docset filter raw size for the given query int keywordPrefixSize() const; QString query() const; void setQuery(const QString &str); private: QString m_query; QStringList m_keywords; QString m_keywordPrefix; }; } // namespace Registry } // namespace Zeal QDataStream &operator<<(QDataStream &out, const Zeal::Registry::SearchQuery &query); QDataStream &operator>>(QDataStream &in, Zeal::Registry::SearchQuery &query); #endif // ZEAL_REGISTRY_SEARCHQUERY_H zeal-0.7.1/src/libs/registry/searchresult.h000066400000000000000000000030341462517734600207360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_REGISTRY_SEARCHRESULT_H #define ZEAL_REGISTRY_SEARCHRESULT_H #include #include namespace Zeal { namespace Registry { class Docset; struct SearchResult { QString name; QString type; QString urlPath; QString urlFragment; Docset *docset; int score; inline bool operator<(const SearchResult &other) const { if (score == other.score) return QString::compare(name, other.name, Qt::CaseInsensitive) < 0; return score > other.score; } }; } // namespace Registry } // namespace Zeal #endif // ZEAL_REGISTRY_SEARCHRESULT_H zeal-0.7.1/src/libs/sidebar/000077500000000000000000000000001462517734600156225ustar00rootroot00000000000000zeal-0.7.1/src/libs/sidebar/CMakeLists.txt000066400000000000000000000004021462517734600203560ustar00rootroot00000000000000add_library(Sidebar STATIC container.cpp proxyview.cpp view.cpp viewprovider.cpp ) target_link_libraries(Sidebar) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) target_link_libraries(Sidebar Qt${QT_VERSION_MAJOR}::Widgets) zeal-0.7.1/src/libs/sidebar/container.cpp000066400000000000000000000034771462517734600203230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "container.h" #include "view.h" #include #include #include #include #include using namespace Zeal; using namespace Zeal::Sidebar; Container::Container(QWidget *parent) : QWidget(parent) { setMinimumWidth(150); // Setup splitter. m_splitter = new QSplitter(); m_splitter->setOrientation(Qt::Vertical); connect(m_splitter, &QSplitter::splitterMoved, this, [this]() { Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState(); }); // Setup main layout. auto layout = WidgetUi::LayoutHelper::createBorderlessLayout(); layout->addWidget(m_splitter); setLayout(layout); } Container::~Container() = default; void Container::addView(View *view) { if (m_views.contains(view)) return; m_views.append(view); m_splitter->addWidget(view); } zeal-0.7.1/src/libs/sidebar/container.h000066400000000000000000000027341462517734600177630ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_SIDEBAR_CONTAINER_H #define ZEAL_SIDEBAR_CONTAINER_H #include class QSplitter; namespace Zeal { namespace Sidebar { class View; // TODO: Implement view groups (alt. naming: tabs, pages) (move splitter into a group?). class Container : public QWidget { Q_OBJECT Q_DISABLE_COPY(Container) public: explicit Container(QWidget *parent = nullptr); ~Container() override; void addView(View *view); private: QSplitter *m_splitter = nullptr; QList m_views; }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_CONTAINER_H zeal-0.7.1/src/libs/sidebar/proxyview.cpp000066400000000000000000000037561462517734600204150ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "proxyview.h" #include "viewprovider.h" #include #include #include using namespace Zeal; using namespace Zeal::Sidebar; ProxyView::ProxyView(ViewProvider *provider, QString id, QWidget *parent) : View(parent) , m_viewProvider(provider) , m_viewId(std::move(id)) { setLayout(WidgetUi::LayoutHelper::createBorderlessLayout()); connect(m_viewProvider, &ViewProvider::viewChanged, this, [this]() { auto view = m_viewProvider->view(m_viewId); if (view == nullptr) { qWarning("ViewProvider returned invalid view!"); return; } if (m_view == view) return; clearCurrentView(); layout()->addWidget(view); view->show(); m_view = view; }); } ProxyView::~ProxyView() { clearCurrentView(); } void ProxyView::clearCurrentView() { // Unparent the view, because we don't own it. QLayout *l = layout(); if (l->isEmpty()) return; m_view->hide(); l->removeWidget(m_view); m_view->setParent(nullptr); } zeal-0.7.1/src/libs/sidebar/proxyview.h000066400000000000000000000027101462517734600200470ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_SIDEBAR_PROXYVIEW_H #define ZEAL_SIDEBAR_PROXYVIEW_H #include "view.h" namespace Zeal { namespace Sidebar { class ViewProvider; class ProxyView final : public View { Q_OBJECT Q_DISABLE_COPY(ProxyView) public: explicit ProxyView(ViewProvider *provider, QString id = QString(), QWidget *parent = nullptr); ~ProxyView() override; private: void clearCurrentView(); ViewProvider *m_viewProvider = nullptr; QString m_viewId; View *m_view = nullptr; }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_PROXYVIEW_H zeal-0.7.1/src/libs/sidebar/view.cpp000066400000000000000000000017571462517734600173120ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "view.h" using namespace Zeal::Sidebar; View::View(QWidget *parent) : QWidget(parent) { } zeal-0.7.1/src/libs/sidebar/view.h000066400000000000000000000023011462517734600167410ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_SIDEBAR_VIEW_H #define ZEAL_SIDEBAR_VIEW_H #include namespace Zeal { namespace Sidebar { class View : public QWidget { Q_OBJECT Q_DISABLE_COPY(View) public: explicit View(QWidget *parent = nullptr); }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_VIEW_H zeal-0.7.1/src/libs/sidebar/viewprovider.cpp000066400000000000000000000020071462517734600210520ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "viewprovider.h" using namespace Zeal::Sidebar; ViewProvider::ViewProvider(QObject *parent) : QObject(parent) { } zeal-0.7.1/src/libs/sidebar/viewprovider.h000066400000000000000000000025421462517734600205230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_SIDEBAR_VIEWPROVIDER_H #define ZEAL_SIDEBAR_VIEWPROVIDER_H #include namespace Zeal { namespace Sidebar { class View; class ViewProvider : public QObject { Q_OBJECT Q_DISABLE_COPY(ViewProvider) public: explicit ViewProvider(QObject *parent = nullptr); virtual View *view(const QString &id = QString()) const = 0; signals: void viewChanged(); }; } // namespace Sidebar } // namespace Zeal #endif // ZEAL_SIDEBAR_VIEWPROVIDER_H zeal-0.7.1/src/libs/ui/000077500000000000000000000000001462517734600146265ustar00rootroot00000000000000zeal-0.7.1/src/libs/ui/CMakeLists.txt000066400000000000000000000012061462517734600173650ustar00rootroot00000000000000add_subdirectory(qxtglobalshortcut) add_subdirectory(widgets) set(Ui_FORMS aboutdialog.ui docsetsdialog.ui mainwindow.ui settingsdialog.ui ) add_library(Ui STATIC aboutdialog.cpp browsertab.cpp docsetlistitemdelegate.cpp docsetsdialog.cpp mainwindow.cpp searchitemdelegate.cpp searchsidebar.cpp settingsdialog.cpp sidebarviewprovider.cpp ${Ui_FORMS} # For Qt Creator. ) target_link_libraries(Ui Browser Sidebar QxtGlobalShortcut Widgets Registry) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS WebEngineWidgets REQUIRED) target_link_libraries(Ui Qt${QT_VERSION_MAJOR}::WebEngineWidgets) zeal-0.7.1/src/libs/ui/aboutdialog.cpp000066400000000000000000000025471462517734600176340ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "aboutdialog.h" #include "ui_aboutdialog.h" #include using namespace Zeal::WidgetUi; AboutDialog::AboutDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::AboutDialog) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); ui->versionLabel->setText(Core::Application::versionString()); ui->buttonBox->setFocus(Qt::OtherFocusReason); } AboutDialog::~AboutDialog() { delete ui; } zeal-0.7.1/src/libs/ui/aboutdialog.h000066400000000000000000000025121462517734600172710ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_ABOUTDIALOG_H #define ZEAL_WIDGETUI_ABOUTDIALOG_H #include namespace Zeal { namespace WidgetUi { namespace Ui { class AboutDialog; } // namespace Ui class AboutDialog : public QDialog { Q_OBJECT public: explicit AboutDialog(QWidget *parent = nullptr); ~AboutDialog() override; private: Ui::AboutDialog *ui; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_ABOUTDIALOG_H zeal-0.7.1/src/libs/ui/aboutdialog.ui000066400000000000000000000241011462517734600174550ustar00rootroot00000000000000 Zeal::WidgetUi::AboutDialog 0 0 420 420 About Zeal QFrame::StyledPanel QFrame::Sunken 10 :/icons/logo/64x64.png 0 <h1>Zeal</h1> Qt::LinksAccessibleByMouse|Qt::TextSelectableByMouse 0 About <strong>A simple offline documentation browser</strong> <br><br> Copyright &copy; Oleg Shparber and other contributors, 2013-2023. <br> <a href="https://zealdocs.org">zealdocs.org</a> <br> <a href="ircs://irc.libera.chat:6697/zealdocs">#zealdocs</a> on <a href="https://libera.chat">Libera Chat</a> <br><br> Zeal is an open source software available under the terms of the General Public License version 3 (<a href="https://www.gnu.org/copyleft/gpl.html">GPLv3</a>) or later. <br><br> Docsets are courtesy of <a href="https://kapeli.com/dash">Dash</a>. Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop true true Licenses <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Roboto'; font-size:10pt; font-weight:400; font-style:normal;"> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;">Zeal heavily relies on other open source software listed below.</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Bulma</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://bulma.io/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">bulma.io</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: MIT License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">cpp-httplib</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://github.com/yhirose/cpp-httplib"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">github.com/yhirose/cpp-httplib</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: MIT License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Font Awesome</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://fontawesome.io/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">fontawesome.io</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: SIL OFL 1.1 (font), MIT License (css)</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">libarchive</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.libarchive.org/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.libarchive.org</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: Simplified BSD License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">LibQxt</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://bitbucket.org/libqxt/libqxt"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">bitbucket.org/libqxt/libqxt</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: New BSD License</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">Qt</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.qt.io/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.qt.io</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: GNU LGPL version 3</span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:600;">SQLite</span><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"> - </span><a href="https://www.sqlite.org/"><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; text-decoration: underline; color:#007af4;">www.sqlite.org</span></a><span style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br />License: Public Domain</span></p></body></html> true QDialogButtonBox::Close buttonBox clicked(QAbstractButton*) Zeal::WidgetUi::AboutDialog close() 248 254 157 274 zeal-0.7.1/src/libs/ui/browsertab.cpp000066400000000000000000000172301462517734600175070ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "browsertab.h" #include "searchsidebar.h" #include "widgets/layouthelper.h" #include "widgets/toolbarframe.h" #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; namespace { constexpr char WelcomePageUrl[] = "qrc:///browser/welcome.html"; } // namespace BrowserTab::BrowserTab(QWidget *parent) : QWidget(parent) { // Setup WebControl. m_webControl = new Browser::WebControl(this); connect(m_webControl, &Browser::WebControl::titleChanged, this, &BrowserTab::titleChanged); connect(m_webControl, &Browser::WebControl::urlChanged, this, [this](const QUrl &url) { // TODO: Check if changed. emit iconChanged(docsetIcon(url)); Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url); if (docset) { searchSidebar()->pageTocModel()->setResults(docset->relatedLinks(url)); m_webControl->setJavaScriptEnabled(docset->isJavaScriptEnabled()); } else { searchSidebar()->pageTocModel()->setResults(); // Always enable JS outside of docsets. m_webControl->setJavaScriptEnabled(true); } m_backButton->setEnabled(m_webControl->canGoBack()); m_forwardButton->setEnabled(m_webControl->canGoForward()); }); // Setup navigation toolbar. m_backButton = new QToolButton(); m_backButton->setAutoRaise(true); m_backButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowBack)); m_backButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; }")); m_backButton->setText(QStringLiteral("←")); m_backButton->setToolTip(tr("Go back one page")); auto backMenu = new QMenu(m_backButton); connect(backMenu, &QMenu::aboutToShow, this, [this, backMenu]() { backMenu->clear(); QWebEngineHistory *history = m_webControl->history(); QList items = history->backItems(10); for (auto it = items.crbegin(); it != items.crend(); ++it) { const QIcon icon = docsetIcon(it->url()); const QWebEngineHistoryItem item = *it; backMenu->addAction(icon, it->title(), this, [=](bool) { history->goToItem(item); }); } }); m_backButton->setMenu(backMenu); connect(m_backButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::back); m_forwardButton = new QToolButton(); m_forwardButton->setAutoRaise(true); m_forwardButton->setIcon(qApp->style()->standardIcon(QStyle::SP_ArrowForward)); m_forwardButton->setStyleSheet(QStringLiteral("QToolButton::menu-indicator { image: none; }")); m_forwardButton->setText(QStringLiteral("→")); m_forwardButton->setToolTip(tr("Go forward one page")); auto forwardMenu = new QMenu(m_forwardButton); connect(forwardMenu, &QMenu::aboutToShow, this, [this, forwardMenu]() { forwardMenu->clear(); QWebEngineHistory *history = m_webControl->history(); const auto forwardItems = history->forwardItems(10); for (const QWebEngineHistoryItem &item : forwardItems) { const QIcon icon = docsetIcon(item.url()); forwardMenu->addAction(icon, item.title(), this, [=](bool) { history->goToItem(item); }); } }); m_forwardButton->setMenu(forwardMenu); connect(m_forwardButton, &QToolButton::clicked, m_webControl, &Browser::WebControl::forward); auto label = new QLabel(); label->setAlignment(Qt::AlignCenter); connect(m_webControl, &Browser::WebControl::titleChanged, this, [label](const QString &title) { if (title.isEmpty()) return; label->setText(title); }); auto toolBarLayout = new QHBoxLayout(); toolBarLayout->setContentsMargins(4, 0, 4, 0); toolBarLayout->setSpacing(4); toolBarLayout->addWidget(m_backButton); toolBarLayout->addWidget(m_forwardButton); toolBarLayout->addWidget(label, 1); auto toolBarFrame = new ToolBarFrame(); toolBarFrame->setLayout(toolBarLayout); // Setup main layout. auto layout = LayoutHelper::createBorderlessLayout(); layout->addWidget(toolBarFrame); layout->addWidget(m_webControl); setLayout(layout); auto registry = Core::Application::instance()->docsetRegistry(); using Registry::DocsetRegistry; connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded, this, [this, registry](const QString &name) { Registry::Docset *docset = registry->docsetForUrl(m_webControl->url()); if (docset == nullptr || docset->name() != name) { return; } // TODO: Add custom 'Page has been removed' page. navigateToStartPage(); // TODO: Cleanup history. }); } BrowserTab *BrowserTab::clone(QWidget *parent) const { auto tab = new BrowserTab(parent); if (m_searchSidebar) { tab->m_searchSidebar = m_searchSidebar->clone(); connect(tab->m_searchSidebar, &SearchSidebar::activated, tab->m_webControl, &Browser::WebControl::focus); connect(tab->m_searchSidebar, &SearchSidebar::navigationRequested, tab->m_webControl, &Browser::WebControl::load); } tab->m_webControl->restoreHistory(m_webControl->saveHistory()); tab->m_webControl->setZoomLevel(m_webControl->zoomLevel()); return tab; } BrowserTab::~BrowserTab() { if (m_searchSidebar) { // The sidebar is not in this widget's hierarchy, so direct delete is not safe. m_searchSidebar->deleteLater(); } } Browser::WebControl *BrowserTab::webControl() const { return m_webControl; } SearchSidebar *BrowserTab::searchSidebar() { if (m_searchSidebar == nullptr) { // Create SearchSidebar managed by this tab. m_searchSidebar = new SearchSidebar(); connect(m_searchSidebar, &SearchSidebar::activated, m_webControl, &Browser::WebControl::focus); connect(m_searchSidebar, &SearchSidebar::navigationRequested, m_webControl, &Browser::WebControl::load); } return m_searchSidebar; } void BrowserTab::navigateToStartPage() { m_webControl->load(QUrl(WelcomePageUrl)); } void BrowserTab::search(const Registry::SearchQuery &query) { if (query.isEmpty()) return; m_searchSidebar->search(query); } QIcon BrowserTab::docsetIcon(const QUrl &url) const { Registry::Docset *docset = Core::Application::instance()->docsetRegistry()->docsetForUrl(url); return docset ? docset->icon() : QIcon(QStringLiteral(":/icons/logo/icon.png")); } zeal-0.7.1/src/libs/ui/browsertab.h000066400000000000000000000041061462517734600171520ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_BROWSERTAB_H #define ZEAL_WIDGETUI_BROWSERTAB_H #include #include #include class QToolButton; namespace Zeal { namespace Browser { class WebControl; } // namespace Browser namespace Registry { class SearchQuery; } //namespace Registry namespace WidgetUi { class SearchSidebar; class BrowserTab : public QWidget { Q_OBJECT Q_DISABLE_COPY(BrowserTab) public: explicit BrowserTab(QWidget *parent = nullptr); BrowserTab *clone(QWidget *parent = nullptr) const; ~BrowserTab() override; Browser::WebControl *webControl() const; SearchSidebar *searchSidebar(); // TODO: const public slots: void navigateToStartPage(); void search(const Registry::SearchQuery &query); signals: void iconChanged(const QIcon &icon); void titleChanged(const QString &title); private: QIcon docsetIcon(const QUrl &url) const; // Widgets. SearchSidebar *m_searchSidebar = nullptr; Browser::WebControl *m_webControl = nullptr; QToolButton *m_backButton = nullptr; QToolButton *m_forwardButton = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_BROWSERTAB_H zeal-0.7.1/src/libs/ui/docsetlistitemdelegate.cpp000066400000000000000000000110231462517734600220560ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "docsetlistitemdelegate.h" #include #include #include using namespace Zeal::WidgetUi; namespace { constexpr int ProgressBarWidth = 150; } // namespace DocsetListItemDelegate::DocsetListItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } void DocsetListItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { if (index.model()->data(index, ShowProgressRole).toBool()) { paintProgressBar(painter, option, index); return; } QStyledItemDelegate::paint(painter, option, index); if (!index.model()->data(index, Registry::ItemDataRole::UpdateAvailableRole).toBool()) return; const QString text = tr("Update available"); QFont font(painter->font()); font.setItalic(true); const QFontMetrics fontMetrics(font); const int margin = 4; // Random small number QRect textRect = option.rect.adjusted(-margin, 0, -margin, 0); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) textRect.setLeft(textRect.right() - fontMetrics.horizontalAdvance(text) - 2); #else textRect.setLeft(textRect.right() - fontMetrics.width(text) - 2); #endif textRect = QStyle::visualRect(option.direction, option.rect, textRect); // Constant LeftToRight because we don't need to flip it any further. // Vertically align the text in the middle to match QCommonStyle behaviour. const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, option.displayAlignment, QSize(textRect.size().width(), fontMetrics.height()), textRect); painter->save(); QPalette palette = option.palette; #ifdef Q_OS_WIN32 // QWindowsVistaStyle overrides highlight colour. if (option.widget->style()->objectName() == QLatin1String("windowsvista")) { palette.setColor(QPalette::All, QPalette::HighlightedText, palette.color(QPalette::Active, QPalette::Text)); } #endif const QPalette::ColorGroup cg = (option.state & QStyle::State_Active) ? QPalette::Normal : QPalette::Inactive; if (option.state & QStyle::State_Selected) { painter->setPen(palette.color(cg, QPalette::HighlightedText)); } else { painter->setPen(palette.color(cg, QPalette::Text)); } painter->setFont(font); painter->drawText(alignedRect, text); painter->restore(); } void DocsetListItemDelegate::paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { bool ok; const int value = index.model()->data(index, ValueRole).toInt(&ok); if (!ok) { QStyledItemDelegate::paint(painter, option, index); return; } // Adjust maximum text width QStyleOptionViewItem styleOption = option; styleOption.rect.setRight(styleOption.rect.right() - ProgressBarWidth); // Size progress bar QScopedPointer renderer(new QProgressBar()); renderer->resize(ProgressBarWidth, styleOption.rect.height()); renderer->setRange(0, 100); renderer->setValue(value); const QString format = index.model()->data(index, FormatRole).toString(); if (!format.isEmpty()) { renderer->setFormat(format); } painter->save(); // Paint progress bar painter->translate(styleOption.rect.topRight()); renderer->render(painter); painter->restore(); QStyledItemDelegate::paint(painter, styleOption, index); } zeal-0.7.1/src/libs/ui/docsetlistitemdelegate.h000066400000000000000000000034001462517734600215230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H #define ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H #include namespace Zeal { namespace WidgetUi { class DocsetListItemDelegate : public QStyledItemDelegate { Q_OBJECT public: enum ProgressRoles { ValueRole = Qt::UserRole + 10, FormatRole, ShowProgressRole }; explicit DocsetListItemDelegate(QObject *parent = nullptr); void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; private: void paintProgressBar(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_DOCSETLISTITEMDELEGATE_H zeal-0.7.1/src/libs/ui/docsetsdialog.cpp000066400000000000000000000703141462517734600201630ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "docsetsdialog.h" #include "ui_docsetsdialog.h" #include "docsetlistitemdelegate.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; #ifdef Q_OS_WIN32 extern Q_CORE_EXPORT int qt_ntfs_permission_lookup; #endif namespace { constexpr char ApiServerUrl[] = "https://api.zealdocs.org/v1"; constexpr char RedirectServerUrl[] = "https://go.zealdocs.org/d/%1/%2/latest"; // TODO: Each source plugin should have its own cache constexpr char DocsetListCacheFileName[] = "com.kapeli.json"; // TODO: Make the timeout period configurable constexpr int CacheTimeout = 24 * 60 * 60 * 1000; // 24 hours in microseconds // QNetworkReply properties constexpr char DocsetNameProperty[] = "docsetName"; constexpr char DownloadTypeProperty[] = "downloadType"; constexpr char ListItemIndexProperty[] = "listItem"; } // namespace DocsetsDialog::DocsetsDialog(Core::Application *app, QWidget *parent) : QDialog(parent) , ui(new Ui::DocsetsDialog()) , m_application(app) , m_docsetRegistry(app->docsetRegistry()) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); loadDocsetList(); m_isStorageReadOnly = !isDirWritable(m_application->settings()->docsetPath); #ifdef Q_OS_MACOS ui->availableDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false); ui->installedDocsetList->setAttribute(Qt::WA_MacShowFocusRect, false); #endif ui->statusLabel->clear(); // Clear text shown in the designer mode. ui->storageStatusLabel->setVisible(m_isStorageReadOnly); const QFileInfo fi(m_application->settings()->docsetPath); ui->storageStatusLabel->setText(fi.exists() ? tr("Docset storage is read only.") : tr("Docset storage does not exist.")); connect(m_application, &Core::Application::extractionCompleted, this, &DocsetsDialog::extractionCompleted); connect(m_application, &Core::Application::extractionError, this, &DocsetsDialog::extractionError); connect(m_application, &Core::Application::extractionProgress, this, &DocsetsDialog::extractionProgress); // Setup signals & slots connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { if (button == ui->buttonBox->button(QDialogButtonBox::Cancel)) { cancelDownloads(); return; } if (button == ui->buttonBox->button(QDialogButtonBox::Close)) { close(); return; } }); setupInstalledDocsetsTab(); setupAvailableDocsetsTab(); if (m_isStorageReadOnly) { disableControls(); } } DocsetsDialog::~DocsetsDialog() { delete ui; } void DocsetsDialog::addDashFeed() { QString clipboardText = QApplication::clipboard()->text(); if (!clipboardText.startsWith(QLatin1String("dash-feed://"))) clipboardText.clear(); QString feedUrl = QInputDialog::getText(this, QStringLiteral("Zeal"), tr("Feed URL:"), QLineEdit::Normal, clipboardText); feedUrl = feedUrl.trimmed(); if (feedUrl.isEmpty()) return; if (feedUrl.startsWith(QLatin1String("dash-feed://"))) { feedUrl = feedUrl.remove(0, 12); feedUrl = QUrl::fromPercentEncoding(feedUrl.toUtf8()); } QNetworkReply *reply = download(QUrl(feedUrl)); reply->setProperty(DownloadTypeProperty, DownloadDashFeed); } void DocsetsDialog::updateSelectedDocsets() { const auto selectedRows = ui->installedDocsetList->selectionModel()->selectedRows(); for (const QModelIndex &index : selectedRows) { if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) continue; downloadDashDocset(index); } } void DocsetsDialog::updateAllDocsets() { QAbstractItemModel *model = ui->installedDocsetList->model(); for (int i = 0; i < model->rowCount(); ++i) { const QModelIndex index = model->index(i, 0); if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) continue; downloadDashDocset(index); } } void DocsetsDialog::removeSelectedDocsets() { QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); if (!selectionModel->hasSelection()) return; int ret; const QModelIndexList selectedIndexes = selectionModel->selectedRows(); if (selectedIndexes.size() == 1) { const QString docsetTitle = selectedIndexes.first().data().toString(); ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove %1 docset?").arg(docsetTitle)); } else { ret = QMessageBox::question(this, QStringLiteral("Zeal"), tr("Remove %n docset(s)?", nullptr, selectedIndexes.size())); } if (ret == QMessageBox::No) { return; } // Gather names first, because model indicies become invalid when docsets are removed. QStringList names; for (const QModelIndex &index : selectedIndexes) { names.append(index.data(Registry::ItemDataRole::DocsetNameRole).toString()); } for (const QString &name : names) { removeDocset(name); } } void DocsetsDialog::updateDocsetFilter(const QString &filterString) { const bool doSearch = !filterString.simplified().isEmpty(); for (int i = 0; i < ui->availableDocsetList->count(); ++i) { QListWidgetItem *item = ui->availableDocsetList->item(i); // Skip installed docsets if (m_docsetRegistry->contains(item->data(Registry::ItemDataRole::DocsetNameRole).toString())) continue; item->setHidden(doSearch && !item->text().contains(filterString, Qt::CaseInsensitive)); } } void DocsetsDialog::downloadSelectedDocsets() { QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel(); const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { selectionModel->select(index, QItemSelectionModel::Deselect); // Do nothing if a download is already in progress. if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) continue; QAbstractItemModel *model = ui->availableDocsetList->model(); model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole); model->setData(index, 0, DocsetListItemDelegate::ValueRole); model->setData(index, true, DocsetListItemDelegate::ShowProgressRole); downloadDashDocset(index); } } /*! \internal Should be connected to all \l QNetworkReply::finished signals in order to process possible HTTP-redirects correctly. */ void DocsetsDialog::downloadCompleted() { QScopedPointer reply( qobject_cast(sender())); m_replies.removeOne(reply.data()); if (reply->error() != QNetworkReply::NoError) { if (reply->error() != QNetworkReply::OperationCanceledError) { const QString msg = tr("Download failed!

Error: %1
URL: %2") .arg(reply->errorString(), reply->request().url().toString()); const int ret = QMessageBox::warning(this, QStringLiteral("Zeal"), msg, QMessageBox::Retry | QMessageBox::Cancel); if (ret == QMessageBox::Retry) { QNetworkReply *newReply = download(reply->request().url()); // Copy properties newReply->setProperty(DocsetNameProperty, reply->property(DocsetNameProperty)); newReply->setProperty(DownloadTypeProperty, reply->property(DownloadTypeProperty)); newReply->setProperty(ListItemIndexProperty, reply->property(ListItemIndexProperty)); return; } bool ok; QListWidgetItem *listItem = ui->availableDocsetList->item( reply->property(ListItemIndexProperty).toInt(&ok)); if (ok && listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); } updateStatus(); return; } switch (reply->property(DownloadTypeProperty).toUInt()) { case DownloadDocsetList: { const QByteArray data = reply->readAll(); QScopedPointer file(new QFile(cacheLocation(DocsetListCacheFileName))); if (file->open(QIODevice::WriteOnly)) file->write(data); const QString updateTime = QFileInfo(file->fileName()) .lastModified().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); ui->lastUpdatedLabel->setText(updateTime); QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); if (jsonError.error != QJsonParseError::NoError) { // TODO: Log QJsonParseError. const QMessageBox::StandardButton rc = QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Server returned a corrupted docset list."), QMessageBox::Retry | QMessageBox::Cancel); if (rc == QMessageBox::Retry) { downloadDocsetList(); } break; } processDocsetList(jsonDoc.array()); break; } case DownloadDashFeed: { Registry::DocsetMetadata metadata = Registry::DocsetMetadata::fromDashFeed(reply->request().url(), reply->readAll()); if (metadata.urls().isEmpty()) { QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Invalid docset feed!")); break; } m_userFeeds[metadata.name()] = metadata; Registry::Docset *docset = m_docsetRegistry->docset(metadata.name()); if (docset == nullptr) { // Fetch docset only on first feed download, // since further downloads are only update checks QNetworkReply *reply = download(metadata.url()); reply->setProperty(DocsetNameProperty, metadata.name()); reply->setProperty(DownloadTypeProperty, DownloadDocset); } else { // Check for feed update if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) { docset->hasUpdate = true; if (!m_isStorageReadOnly) { ui->updateAllDocsetsButton->setEnabled(true); } ui->installedDocsetList->reset(); } } break; } case DownloadDocset: { const QString docsetName = reply->property(DocsetNameProperty).toString(); const QString docsetDirectoryName = docsetName + QLatin1String(".docset"); if (QDir(m_application->settings()->docsetPath).exists(docsetDirectoryName)) { removeDocset(docsetName); } QTemporaryFile *tmpFile = m_tmpFiles[docsetName]; if (!tmpFile) { tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp").arg(Core::Application::cacheLocation(), docsetName), this); tmpFile->open(); m_tmpFiles.insert(docsetName, tmpFile); } while (reply->bytesAvailable()) { tmpFile->write(reply->read(1024 * 1024)); // Use small chunks. } tmpFile->close(); QListWidgetItem *item = findDocsetListItem(docsetName); if (item) { item->setData(DocsetListItemDelegate::ValueRole, 0); item->setData(DocsetListItemDelegate::FormatRole, tr("Installing: %p%")); } m_application->extract(tmpFile->fileName(), m_application->settings()->docsetPath, docsetDirectoryName); break; } } // If all enqueued downloads have finished executing. updateStatus(); } // creates a total download progress for multiple QNetworkReplies void DocsetsDialog::downloadProgress(qint64 received, qint64 total) { // Don't show progress for non-docset pages if (total == -1 || received < 10240) return; auto reply = qobject_cast(sender()); if (!reply || !reply->isOpen()) return; if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset) { const QString docsetName = reply->property(DocsetNameProperty).toString(); QTemporaryFile *tmpFile = m_tmpFiles[docsetName]; if (!tmpFile) { tmpFile = new QTemporaryFile(QStringLiteral("%1/%2.XXXXXX.tmp").arg(Core::Application::cacheLocation(), docsetName), this); tmpFile->open(); m_tmpFiles.insert(docsetName, tmpFile); } tmpFile->write(reply->read(received)); } // Try to get the item associated to the request QListWidgetItem *item = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt()); if (item) { item->setData(DocsetListItemDelegate::ValueRole, percent(received, total)); } } void DocsetsDialog::extractionCompleted(const QString &filePath) { const QString docsetName = docsetNameForTmpFilePath(filePath); const QDir dataDir(m_application->settings()->docsetPath); const QString docsetPath = dataDir.filePath(docsetName + QLatin1String(".docset")); // Write metadata about docset Registry::DocsetMetadata metadata = m_availableDocsets.count(docsetName) ? m_availableDocsets[docsetName] : m_userFeeds[docsetName]; metadata.save(docsetPath, metadata.latestVersion()); m_docsetRegistry->loadDocset(docsetPath); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) { listItem->setHidden(true); listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); } delete m_tmpFiles.take(docsetName); updateStatus(); } void DocsetsDialog::extractionError(const QString &filePath, const QString &errorString) { const QString docsetName = docsetNameForTmpFilePath(filePath); QMessageBox::warning(this, QStringLiteral("Zeal"), tr("Cannot extract docset %1: %2").arg(docsetName, errorString)); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); delete m_tmpFiles.take(docsetName); } void DocsetsDialog::extractionProgress(const QString &filePath, qint64 extracted, qint64 total) { const QString docsetName = docsetNameForTmpFilePath(filePath); QListWidgetItem *listItem = findDocsetListItem(docsetName); if (listItem) listItem->setData(DocsetListItemDelegate::ValueRole, percent(extracted, total)); } void DocsetsDialog::loadDocsetList() { loadUserFeedList(); const QFileInfo fi(cacheLocation(DocsetListCacheFileName)); if (!fi.exists() || fi.lastModified().msecsTo(QDateTime::currentDateTime()) > CacheTimeout) { downloadDocsetList(); return; } QScopedPointer file(new QFile(fi.filePath())); if (!file->open(QIODevice::ReadOnly)) { downloadDocsetList(); return; } QJsonParseError jsonError; const QJsonDocument jsonDoc = QJsonDocument::fromJson(file->readAll(), &jsonError); if (jsonError.error != QJsonParseError::NoError) { downloadDocsetList(); return; } // TODO: Show more user friendly labels, like "5 hours ago" const QString updateTime = fi.lastModified().toString(QLocale::system().dateTimeFormat(QLocale::ShortFormat)); ui->lastUpdatedLabel->setText(updateTime); processDocsetList(jsonDoc.array()); } void DocsetsDialog::setupInstalledDocsetsTab() { ui->installedDocsetList->setItemDelegate(new DocsetListItemDelegate(this)); ui->installedDocsetList->setModel(m_docsetRegistry->model()); if (m_isStorageReadOnly) { return; } connect(ui->installedDocsetList, &QListView::activated, this, [this](const QModelIndex &index) { if (!index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { return; } downloadDashDocset(index); }); QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() { ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection()); const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { ui->updateSelectedDocsetsButton->setEnabled(true); return; } } ui->updateSelectedDocsetsButton->setEnabled(false); }); connect(ui->addFeedButton, &QPushButton::clicked, this, &DocsetsDialog::addDashFeed); connect(ui->updateSelectedDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateSelectedDocsets); connect(ui->updateAllDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::updateAllDocsets); connect(ui->removeDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::removeSelectedDocsets); } void DocsetsDialog::setupAvailableDocsetsTab() { using Registry::DocsetRegistry; ui->availableDocsetList->setItemDelegate(new DocsetListItemDelegate(this)); connect(m_docsetRegistry, &DocsetRegistry::docsetUnloaded, this, [this](const QString &name) { QListWidgetItem *item = findDocsetListItem(name); if (!item) return; item->setHidden(false); }); connect(m_docsetRegistry, &DocsetRegistry::docsetLoaded, this, [this](const QString &name) { QListWidgetItem *item = findDocsetListItem(name); if (!item) return; item->setHidden(true); }); connect(ui->refreshButton, &QPushButton::clicked, this, &DocsetsDialog::downloadDocsetList); connect(ui->docsetFilterInput, &QLineEdit::textEdited, this, &DocsetsDialog::updateDocsetFilter); if (m_isStorageReadOnly) { return; } connect(ui->availableDocsetList, &QListView::activated, this, [this](const QModelIndex &index) { // TODO: Cancel download if it's already in progress. if (index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) return; ui->availableDocsetList->selectionModel()->select(index, QItemSelectionModel::Deselect); QAbstractItemModel *model = ui->availableDocsetList->model(); model->setData(index, tr("Downloading: %p%"), DocsetListItemDelegate::FormatRole); model->setData(index, 0, DocsetListItemDelegate::ValueRole); model->setData(index, true, DocsetListItemDelegate::ShowProgressRole); downloadDashDocset(index); }); QItemSelectionModel *selectionModel = ui->availableDocsetList->selectionModel(); connect(selectionModel, &QItemSelectionModel::selectionChanged, this, [this, selectionModel]() { const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (!index.data(DocsetListItemDelegate::ShowProgressRole).toBool()) { ui->downloadDocsetsButton->setEnabled(true); return; } } ui->downloadDocsetsButton->setEnabled(false); }); connect(ui->downloadDocsetsButton, &QPushButton::clicked, this, &DocsetsDialog::downloadSelectedDocsets); } void DocsetsDialog::enableControls() { if (m_isStorageReadOnly || !m_replies.isEmpty() || !m_tmpFiles.isEmpty()) { return; } // Dialog buttons. ui->buttonBox->setStandardButtons(QDialogButtonBox::Close); // Available docsets ui->refreshButton->setEnabled(true); // Installed docsets ui->addFeedButton->setEnabled(true); QItemSelectionModel *selectionModel = ui->installedDocsetList->selectionModel(); bool hasSelectedUpdates = false; const auto selectedRows = selectionModel->selectedRows(); for (const QModelIndex &index : selectedRows) { if (index.data(Registry::ItemDataRole::UpdateAvailableRole).toBool()) { hasSelectedUpdates = true; break; } } ui->updateSelectedDocsetsButton->setEnabled(hasSelectedUpdates); ui->updateAllDocsetsButton->setEnabled(updatesAvailable()); ui->removeDocsetsButton->setEnabled(selectionModel->hasSelection()); } void DocsetsDialog::disableControls() { // Dialog buttons. if (!m_isStorageReadOnly) { // Always show the close button if storage is read only. ui->buttonBox->setStandardButtons(QDialogButtonBox::Cancel); } // Installed docsets ui->addFeedButton->setEnabled(false); ui->updateSelectedDocsetsButton->setEnabled(false); ui->updateAllDocsetsButton->setEnabled(false); ui->downloadDocsetsButton->setEnabled(false); ui->removeDocsetsButton->setEnabled(false); // Available docsets ui->refreshButton->setEnabled(false); } QListWidgetItem *DocsetsDialog::findDocsetListItem(const QString &name) const { for (int i = 0; i < ui->availableDocsetList->count(); ++i) { QListWidgetItem *item = ui->availableDocsetList->item(i); if (item->data(Registry::ItemDataRole::DocsetNameRole).toString() == name) return item; } return nullptr; } bool DocsetsDialog::updatesAvailable() const { const auto docsets = m_docsetRegistry->docsets(); for (Registry::Docset *docset : docsets) { if (docset->hasUpdate) return true; } return false; } QNetworkReply *DocsetsDialog::download(const QUrl &url) { QNetworkReply *reply = m_application->download(url); connect(reply, &QNetworkReply::downloadProgress, this, &DocsetsDialog::downloadProgress); connect(reply, &QNetworkReply::finished, this, &DocsetsDialog::downloadCompleted); m_replies.append(reply); disableControls(); updateStatus(); return reply; } void DocsetsDialog::cancelDownloads() { for (QNetworkReply *reply : std::as_const(m_replies)) { // Hide progress bar QListWidgetItem *listItem = ui->availableDocsetList->item(reply->property(ListItemIndexProperty).toInt()); if (listItem) listItem->setData(DocsetListItemDelegate::ShowProgressRole, false); if (reply->property(DownloadTypeProperty).toInt() == DownloadDocset) delete m_tmpFiles.take(reply->property(DocsetNameProperty).toString()); reply->abort(); } updateStatus(); } void DocsetsDialog::loadUserFeedList() { const auto docsets = m_docsetRegistry->docsets(); for (Registry::Docset *docset : docsets) { if (!docset->feedUrl().isEmpty()) { QNetworkReply *reply = download(QUrl(docset->feedUrl())); reply->setProperty(DownloadTypeProperty, DownloadDashFeed); } } } void DocsetsDialog::downloadDocsetList() { ui->availableDocsetList->clear(); m_availableDocsets.clear(); QNetworkReply *reply = download(QUrl(ApiServerUrl + QLatin1String("/docsets"))); reply->setProperty(DownloadTypeProperty, DownloadDocsetList); } void DocsetsDialog::processDocsetList(const QJsonArray &list) { for (const QJsonValue &v : list) { QJsonObject docsetJson = v.toObject(); Registry::DocsetMetadata metadata(docsetJson); m_availableDocsets.insert({metadata.name(), metadata}); } // TODO: Move into dedicated method for (const auto &kv : m_availableDocsets) { const auto &metadata = kv.second; auto listItem = new QListWidgetItem(metadata.icon(), metadata.title(), ui->availableDocsetList); listItem->setData(Registry::ItemDataRole::DocsetNameRole, metadata.name()); if (!m_docsetRegistry->contains(metadata.name())) { continue; } listItem->setHidden(true); Registry::Docset *docset = m_docsetRegistry->docset(metadata.name()); if (metadata.latestVersion() != docset->version() || metadata.revision() > docset->revision()) { docset->hasUpdate = true; if (!m_isStorageReadOnly) { ui->updateAllDocsetsButton->setEnabled(true); } } } ui->installedDocsetList->reset(); } void DocsetsDialog::downloadDashDocset(const QModelIndex &index) { const QString name = index.data(Registry::ItemDataRole::DocsetNameRole).toString(); if (m_availableDocsets.count(name) == 0 && !m_userFeeds.contains(name)) return; QUrl url; if (!m_userFeeds.contains(name)) { // No feed present means that this is a Kapeli docset QString urlString = QString(RedirectServerUrl).arg("com.kapeli").arg(name); url = QUrl(urlString); } else { url = m_userFeeds[name].url(); } QNetworkReply *reply = download(url); reply->setProperty(DocsetNameProperty, name); reply->setProperty(DownloadTypeProperty, DownloadDocset); reply->setProperty(ListItemIndexProperty, ui->availableDocsetList->row(findDocsetListItem(name))); } void DocsetsDialog::removeDocset(const QString &name) { if (m_docsetRegistry->contains(name)) { m_docsetRegistry->unloadDocset(name); } const QString docsetPath = QDir(m_application->settings()->docsetPath).filePath(name + QLatin1String(".docset")); if (!m_application->fileManager()->removeRecursively(docsetPath)) { const QString error = tr("Cannot remove directory %1! It might be in use" " by another process.").arg(docsetPath); QMessageBox::warning(this, QStringLiteral("Zeal"), error); return; } QListWidgetItem *listItem = findDocsetListItem(name); if (listItem) { listItem->setHidden(false); } } void DocsetsDialog::updateStatus() { QString text; if (!m_replies.isEmpty()) { text = tr("Downloading: %n.", nullptr, m_replies.size()); } if (!m_tmpFiles.isEmpty()) { text += QLatin1String(" ") + tr("Installing: %n.", nullptr, m_tmpFiles.size()); } ui->statusLabel->setText(text); enableControls(); } QString DocsetsDialog::docsetNameForTmpFilePath(const QString &filePath) const { for (auto it = m_tmpFiles.cbegin(), end = m_tmpFiles.cend(); it != end; ++it) { if (it.value()->fileName() == filePath) { return it.key(); } } return QString(); } int DocsetsDialog::percent(qint64 fraction, qint64 total) { if (!total) return 0; return static_cast(fraction / static_cast(total) * 100); } QString DocsetsDialog::cacheLocation(const QString &fileName) { return QDir(Core::Application::cacheLocation()).filePath(fileName); } bool DocsetsDialog::isDirWritable(const QString &path) { auto file = std::make_unique(path + QLatin1String("/.zeal_writable_check_XXXXXX.tmp")); return file->open(); } zeal-0.7.1/src/libs/ui/docsetsdialog.h000066400000000000000000000071141462517734600176260ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_DOCSETSDIALOG_H #define ZEAL_WIDGETUI_DOCSETSDIALOG_H #include #include #include #include #include class QListWidgetItem; class QNetworkReply; class QTemporaryFile; class QUrl; namespace Zeal { namespace Registry { class DocsetRegistry; } // namespace Registry namespace Core { class Application; } namespace WidgetUi { namespace Ui { class DocsetsDialog; } // namespace Ui class DocsetsDialog : public QDialog { Q_OBJECT public: explicit DocsetsDialog(Core::Application *app, QWidget *parent = nullptr); ~DocsetsDialog() override; private slots: void addDashFeed(); void updateSelectedDocsets(); void updateAllDocsets(); void removeSelectedDocsets(); void updateDocsetFilter(const QString &filterString); void downloadSelectedDocsets(); void downloadCompleted(); void downloadProgress(qint64 received, qint64 total); void extractionCompleted(const QString &filePath); void extractionError(const QString &filePath, const QString &errorString); void extractionProgress(const QString &filePath, qint64 extracted, qint64 total); void loadDocsetList(); private: enum DownloadType { DownloadDashFeed, DownloadDocset, DownloadDocsetList }; Ui::DocsetsDialog *ui = nullptr; Core::Application *m_application = nullptr; Registry::DocsetRegistry *m_docsetRegistry = nullptr; bool m_isStorageReadOnly = false; QList m_replies; // TODO: Create a special model Util::CaseInsensitiveMap m_availableDocsets; QMap m_userFeeds; QHash m_tmpFiles; void setupInstalledDocsetsTab(); void setupAvailableDocsetsTab(); void enableControls(); void disableControls(); QListWidgetItem *findDocsetListItem(const QString &name) const; bool updatesAvailable() const; QNetworkReply *download(const QUrl &url); void cancelDownloads(); void loadUserFeedList(); void downloadDocsetList(); void processDocsetList(const QJsonArray &list); void downloadDashDocset(const QModelIndex &index); void removeDocset(const QString &name); void updateStatus(); // FIXME: Come up with a better approach QString docsetNameForTmpFilePath(const QString &filePath) const; static inline int percent(qint64 fraction, qint64 total); static QString cacheLocation(const QString &fileName); static bool isDirWritable(const QString &path); }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_DOCSETSDIALOG_H zeal-0.7.1/src/libs/ui/docsetsdialog.ui000066400000000000000000000156641462517734600200250ustar00rootroot00000000000000 Zeal::WidgetUi::DocsetsDialog Qt::ApplicationModal 0 0 600 500 Docsets 0 Installed QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 16 16 Add feed Qt::Horizontal 40 20 false Update false Update all false Remove Available Filter docsets true QAbstractItemView::ExtendedSelection QAbstractItemView::SelectRows 16 16 Last updated: Refresh Qt::Horizontal 40 20 false Download <i>Docsets are provided by <a href="https://kapeli.com/dash">Dash</a>.</i> Qt::RichText true Downloading: 1. Installing: 5. Qt::Horizontal 40 20 0 0 QDialogButtonBox::Close zeal-0.7.1/src/libs/ui/mainwindow.cpp000066400000000000000000000425341462517734600175160ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "mainwindow.h" #include "ui_mainwindow.h" #include "aboutdialog.h" #include "browsertab.h" #include "docsetsdialog.h" #include "searchsidebar.h" #include "settingsdialog.h" #include "sidebarviewprovider.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; MainWindow::MainWindow(Core::Application *app, QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) , m_application(app) , m_settings(app->settings()) { ui->setupUi(this); // Initialize the global shortcut handler if supported. if (QxtGlobalShortcut::isSupported()) { m_globalShortcut = new QxtGlobalShortcut(m_settings->showShortcut, this); connect(m_globalShortcut, &QxtGlobalShortcut::activated, this, &MainWindow::toggleWindow); } setupTabBar(); // Setup application wide shortcuts. // Focus search bar. auto shortcut = new QShortcut(QStringLiteral("Ctrl+K"), this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->searchSidebar()->focusSearchEdit(); }); shortcut = new QShortcut(QStringLiteral("Ctrl+L"), this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->searchSidebar()->focusSearchEdit(); }); // Duplicate current tab. shortcut = new QShortcut(QStringLiteral("Ctrl+Alt+T"), this); connect(shortcut, &QShortcut::activated, this, [this]() { duplicateTab(m_tabBar->currentIndex()); }); restoreGeometry(m_settings->windowGeometry); // Menu // File // Some platform plugins do not define QKeySequence::Quit. if (QKeySequence(QKeySequence::Quit).isEmpty()) ui->actionQuit->setShortcut(QStringLiteral("Ctrl+Q")); else ui->actionQuit->setShortcut(QKeySequence::Quit); // Follow Windows HIG. #ifdef Q_OS_WIN32 ui->actionQuit->setText(tr("E&xit")); #endif connect(ui->actionQuit, &QAction::triggered, qApp, &QCoreApplication::quit); // Edit ui->actionFind->setShortcut(QKeySequence::Find); connect(ui->actionFind, &QAction::triggered, this, [this]() { currentTab()->webControl()->activateSearchBar(); }); if (QKeySequence(QKeySequence::Preferences).isEmpty()) { ui->actionPreferences->setShortcut(QStringLiteral("Ctrl+,")); } else { ui->actionPreferences->setShortcut(QKeySequence::Preferences); } connect(ui->actionPreferences, &QAction::triggered, this, [this]() { if (m_globalShortcut) { m_globalShortcut->setEnabled(false); } QScopedPointer dialog(new SettingsDialog(this)); dialog->exec(); if (m_globalShortcut) { m_globalShortcut->setEnabled(true); } }); shortcut = new QShortcut(QKeySequence::Back, this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->back(); }); shortcut = new QShortcut(QKeySequence::Forward, this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->forward(); }); shortcut = new QShortcut(QKeySequence::ZoomIn, this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->zoomIn(); }); shortcut = new QShortcut(QStringLiteral("Ctrl+="), this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->zoomIn(); }); shortcut = new QShortcut(QKeySequence::ZoomOut, this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->zoomOut(); }); shortcut = new QShortcut(QStringLiteral("Ctrl+0"), this); connect(shortcut, &QShortcut::activated, this, [this]() { currentTab()->webControl()->resetZoom(); }); // Tools Menu connect(ui->actionDocsets, &QAction::triggered, this, [this]() { QScopedPointer dialog(new DocsetsDialog(m_application, this)); dialog->exec(); }); // Help Menu connect(ui->actionSubmitFeedback, &QAction::triggered, []() { QDesktopServices::openUrl(QUrl(QStringLiteral("https://github.com/zealdocs/zeal/issues"))); }); connect(ui->actionCheckForUpdates, &QAction::triggered, m_application, &Core::Application::checkForUpdates); connect(ui->actionAboutZeal, &QAction::triggered, this, [this]() { QScopedPointer dialog(new AboutDialog(this)); dialog->exec(); }); // Update check connect(m_application, &Core::Application::updateCheckError, this, [this](const QString &message) { QMessageBox::warning(this, QStringLiteral("Zeal"), message); }); connect(m_application, &Core::Application::updateCheckDone, this, [this](const QString &version) { if (version.isEmpty()) { QMessageBox::information(this, QStringLiteral("Zeal"), tr("You are using the latest version.")); return; } // TODO: Remove this ugly workaround for #637. qApp->setQuitOnLastWindowClosed(false); const int ret = QMessageBox::information(this, QStringLiteral("Zeal"), tr("Zeal %1 is available. Open download page?").arg(version), QMessageBox::Yes | QMessageBox::No, QMessageBox::Yes); qApp->setQuitOnLastWindowClosed(true); if (ret == QMessageBox::Yes) { QDesktopServices::openUrl(QUrl(QStringLiteral("https://zealdocs.org/download.html"))); } }); // Setup sidebar. auto m_sbViewProvider = new SidebarViewProvider(this); auto sbView = new Sidebar::ProxyView(m_sbViewProvider, QStringLiteral("index")); auto sb = new Sidebar::Container(); sb->addView(sbView); // Setup splitter. ui->splitter->insertWidget(0, sb); ui->splitter->restoreState(m_settings->verticalSplitterGeometry); // Setup web settings. auto webSettings = new Browser::Settings(m_settings, this); // Setup web bridge. m_webBridge = new Browser::WebBridge(this); connect(m_webBridge, &Browser::WebBridge::actionTriggered, this, [this](const QString &action) { // TODO: In the future connect directly to the ActionManager. if (action == "openDocsetManager") { ui->actionDocsets->trigger(); } else if (action == "openPreferences") { ui->actionPreferences->trigger(); } }); createTab(); ui->actionNewTab->setShortcut(QKeySequence::AddTab); connect(ui->actionNewTab, &QAction::triggered, this, [this]() { createTab(); }); addAction(ui->actionNewTab); connect(m_tabBar, &QTabBar::tabBarDoubleClicked, this, [this](int index) { if (index == -1) createTab(); }); ui->actionCloseTab->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::Key_W)}); addAction(ui->actionCloseTab); connect(ui->actionCloseTab, &QAction::triggered, this, [this]() { closeTab(); }); // TODO: Use QKeySequence::NextChild, when QTBUG-112193 is fixed. ui->actionNextTab->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::Key_Tab), QKeySequence(Qt::ControlModifier | Qt::Key_PageDown)}); addAction(ui->actionNextTab); connect(ui->actionNextTab, &QAction::triggered, this, [this]() { m_tabBar->setCurrentIndex((m_tabBar->currentIndex() + 1) % m_tabBar->count()); }); // TODO: Use QKeySequence::PreviousChild, when QTBUG-15746 and QTBUG-112193 are fixed. ui->actionPreviousTab->setShortcuts({QKeySequence(Qt::ControlModifier | Qt::ShiftModifier | Qt::Key_Tab), QKeySequence(Qt::ControlModifier | Qt::Key_PageUp)}); addAction(ui->actionPreviousTab); connect(ui->actionPreviousTab, &QAction::triggered, this, [this]() { m_tabBar->setCurrentIndex((m_tabBar->currentIndex() - 1 + m_tabBar->count()) % m_tabBar->count()); }); connect(m_settings, &Core::Settings::updated, this, &MainWindow::applySettings); applySettings(); if (m_settings->checkForUpdate) m_application->checkForUpdates(true); } MainWindow::~MainWindow() { m_settings->verticalSplitterGeometry = ui->splitter->saveState(); m_settings->windowGeometry = saveGeometry(); delete ui; } void MainWindow::search(const Registry::SearchQuery &query) { currentTab()->search(query); } void MainWindow::closeTab(int index) { if (index == -1) { index = m_tabBar->currentIndex(); } if (index == -1) return; BrowserTab *tab = tabAt(index); ui->webViewStack->removeWidget(tab); tab->deleteLater(); // Handle the tab bar last to avoid currentChanged signal coming too early. m_tabBar->removeTab(index); if (ui->webViewStack->count() == 0) { createTab(); } } void MainWindow::moveTab(int from, int to) { const QSignalBlocker blocker(ui->webViewStack); QWidget *w = ui->webViewStack->widget(from); ui->webViewStack->removeWidget(w); ui->webViewStack->insertWidget(to, w); } BrowserTab *MainWindow::createTab() { auto tab = new BrowserTab(); tab->navigateToStartPage(); addTab(tab); return tab; } void MainWindow::duplicateTab(int index) { BrowserTab *tab = tabAt(index); if (tab == nullptr) return; // Add a duplicate next to the `index`. addTab(tab->clone(), index + 1); } void MainWindow::addTab(BrowserTab *tab, int index) { connect(tab, &BrowserTab::iconChanged, this, [this, tab](const QIcon &icon) { const int index = ui->webViewStack->indexOf(tab); Q_ASSERT(m_tabBar->tabData(index).value() == tab); m_tabBar->setTabIcon(index, icon); }); connect(tab, &BrowserTab::titleChanged, this, [this, tab](const QString &title) { if (title.isEmpty()) return; #ifndef PORTABLE_BUILD setWindowTitle(QStringLiteral("%1 - Zeal").arg(title)); #else setWindowTitle(QStringLiteral("%1 - Zeal Portable").arg(title)); #endif const int index = ui->webViewStack->indexOf(tab); Q_ASSERT(m_tabBar->tabData(index).value() == tab); m_tabBar->setTabText(index, title); m_tabBar->setTabToolTip(index, title); }); tab->webControl()->setWebBridgeObject("zAppBridge", m_webBridge); tab->searchSidebar()->focusSearchEdit(); if (index == -1) { index = m_settings->openNewTabAfterActive ? m_tabBar->currentIndex() + 1 : ui->webViewStack->count(); } ui->webViewStack->insertWidget(index, tab); m_tabBar->insertTab(index, tr("Loading…")); m_tabBar->setCurrentIndex(index); m_tabBar->setTabData(index, QVariant::fromValue(tab)); } BrowserTab *MainWindow::currentTab() const { return tabAt(m_tabBar->currentIndex()); } BrowserTab *MainWindow::tabAt(int index) const { return qobject_cast(ui->webViewStack->widget(index)); } void MainWindow::setupTabBar() { m_tabBar = new QTabBar(this); m_tabBar->installEventFilter(this); m_tabBar->setTabsClosable(true); m_tabBar->setSelectionBehaviorOnRemove(QTabBar::SelectPreviousTab); m_tabBar->setExpanding(false); m_tabBar->setUsesScrollButtons(true); m_tabBar->setDocumentMode(true); m_tabBar->setElideMode(Qt::ElideRight); m_tabBar->setStyleSheet(QStringLiteral("QTabBar::tab { width: 150px; }")); m_tabBar->setMovable(true); connect(m_tabBar, &QTabBar::currentChanged, this, [this](int index) { if (index == -1) return; BrowserTab *tab = tabAt(index); #ifndef PORTABLE_BUILD setWindowTitle(QStringLiteral("%1 - Zeal").arg(tab->webControl()->title())); #else setWindowTitle(QStringLiteral("%1 - Zeal Portable").arg(tab->webControl()->title())); #endif ui->webViewStack->setCurrentIndex(index); emit currentTabChanged(); }); connect(m_tabBar, &QTabBar::tabCloseRequested, this, &MainWindow::closeTab); connect(m_tabBar, &QTabBar::tabMoved, this, &MainWindow::moveTab); for (int i = 1; i < 10; i++) { auto action = new QAction(m_tabBar); #ifdef Q_OS_LINUX action->setShortcut(QStringLiteral("Alt+%1").arg(i)); #else action->setShortcut(QStringLiteral("Ctrl+%1").arg(i)); #endif if (i == 9) { connect(action, &QAction::triggered, this, [=]() { m_tabBar->setCurrentIndex(m_tabBar->count() - 1); }); } else { connect(action, &QAction::triggered, this, [=]() { m_tabBar->setCurrentIndex(i - 1); }); } addAction(action); } ui->centralWidgetLayout->insertWidget(0, m_tabBar); } void MainWindow::createTrayIcon() { if (m_trayIcon) return; m_trayIcon = new QSystemTrayIcon(this); m_trayIcon->setIcon(QIcon::fromTheme(QStringLiteral("zeal-tray"), windowIcon())); m_trayIcon->setToolTip(QStringLiteral("Zeal")); connect(m_trayIcon, &QSystemTrayIcon::activated, this, [this](QSystemTrayIcon::ActivationReason reason) { if (reason != QSystemTrayIcon::Trigger && reason != QSystemTrayIcon::DoubleClick) return; toggleWindow(); }); auto trayIconMenu = new QMenu(this); QAction *toggleAction = trayIconMenu->addAction(tr("Show Zeal"), this, &MainWindow::toggleWindow); connect(trayIconMenu, &QMenu::aboutToShow, this, [this, toggleAction]() { toggleAction->setText(isVisible() ? tr("Minimize to Tray") : tr("Show Zeal")); }); trayIconMenu->addSeparator(); trayIconMenu->addAction(ui->actionQuit); m_trayIcon->setContextMenu(trayIconMenu); m_trayIcon->show(); } void MainWindow::removeTrayIcon() { if (!m_trayIcon) return; QMenu *trayIconMenu = m_trayIcon->contextMenu(); delete m_trayIcon; m_trayIcon = nullptr; delete trayIconMenu; } void MainWindow::bringToFront() { show(); setWindowState((windowState() & ~Qt::WindowMinimized) | Qt::WindowActive); raise(); activateWindow(); currentTab()->searchSidebar()->focusSearchEdit(); } void MainWindow::changeEvent(QEvent *event) { if (m_settings->showSystrayIcon && m_settings->minimizeToSystray && event->type() == QEvent::WindowStateChange && isMinimized()) { hide(); } QMainWindow::changeEvent(event); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_settings->showSystrayIcon && m_settings->hideOnClose) { event->ignore(); toggleWindow(); } } bool MainWindow::eventFilter(QObject *object, QEvent *event) { if (object == m_tabBar) { switch (event->type()) { case QEvent::MouseButtonRelease: { auto e = static_cast(event); if (e->button() == Qt::MiddleButton) { const int index = m_tabBar->tabAt(e->pos()); if (index != -1) { closeTab(index); return true; } } break; } case QEvent::Wheel: // TODO: Remove in case QTBUG-8428 is fixed on all platforms return true; default: break; } } return QMainWindow::eventFilter(object, event); } // Captures global events in order to pass them to the search bar. void MainWindow::keyPressEvent(QKeyEvent *keyEvent) { switch (keyEvent->key()) { case Qt::Key_Escape: currentTab()->searchSidebar()->focusSearchEdit(true); break; case Qt::Key_Question: currentTab()->searchSidebar()->focusSearchEdit(); break; default: QMainWindow::keyPressEvent(keyEvent); break; } } void MainWindow::applySettings() { if (m_globalShortcut) { m_globalShortcut->setShortcut(m_settings->showShortcut); } if (m_settings->showSystrayIcon) createTrayIcon(); else removeTrayIcon(); } void MainWindow::toggleWindow() { const bool checkActive = m_globalShortcut && sender() == m_globalShortcut; if (!isVisible() || (checkActive && !isActiveWindow())) { bringToFront(); } else { if (m_trayIcon) { hide(); } else { showMinimized(); } } } zeal-0.7.1/src/libs/ui/mainwindow.h000066400000000000000000000056401462517734600171600ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_MAINWINDOW_H #define ZEAL_WIDGETUI_MAINWINDOW_H #include class QxtGlobalShortcut; class QSystemTrayIcon; class QTabBar; namespace Zeal { namespace Browser { class WebBridge; } // namespace Browser namespace Core { class Application; class Settings; } // namespace Core namespace Registry { class SearchQuery; } //namespace Registry namespace WidgetUi { namespace Ui { class MainWindow; } // namespace Ui class BrowserTab; class SidebarViewProvider; class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(Core::Application *app, QWidget *parent = nullptr); ~MainWindow() override; void search(const Registry::SearchQuery &query); void bringToFront(); BrowserTab *createTab(); public slots: void toggleWindow(); signals: void currentTabChanged(); protected: void changeEvent(QEvent *event) override; void closeEvent(QCloseEvent *event) override; bool eventFilter(QObject *object, QEvent *event) override; void keyPressEvent(QKeyEvent *keyEvent) override; private slots: void applySettings(); void closeTab(int index = -1); void moveTab(int from, int to); void duplicateTab(int index); private: void setupTabBar(); void addTab(BrowserTab *tab, int index = -1); BrowserTab *currentTab() const; BrowserTab *tabAt(int index) const; void createTrayIcon(); void removeTrayIcon(); void syncTabState(BrowserTab *tab); Ui::MainWindow *ui = nullptr; Core::Application *m_application = nullptr; Core::Settings *m_settings = nullptr; Browser::WebBridge *m_webBridge = nullptr; QMenu *m_backMenu = nullptr; QMenu *m_forwardMenu = nullptr; QxtGlobalShortcut *m_globalShortcut = nullptr; QTabBar *m_tabBar = nullptr; friend class SidebarViewProvider; SidebarViewProvider *m_sbViewProvider = nullptr; QSystemTrayIcon *m_trayIcon = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_MAINWINDOW_H zeal-0.7.1/src/libs/ui/mainwindow.ui000066400000000000000000000115541462517734600173470ustar00rootroot00000000000000 Zeal::WidgetUi::MainWindow 0 0 900 600 Zeal 0 0 0 0 0 Qt::Horizontal false 0 400 0 0 0 900 20 &File &Help &Edit &Tools .. &Quit .. Prefere&nces .. About &Zeal .. New &Tab &Close Tab Next Tab Previous Tab &Submit Feedback… &Check for updates… .. &Find &Docsets… zeal-0.7.1/src/libs/ui/qxtglobalshortcut/000077500000000000000000000000001462517734600204175ustar00rootroot00000000000000zeal-0.7.1/src/libs/ui/qxtglobalshortcut/CMakeLists.txt000066400000000000000000000024301462517734600231560ustar00rootroot00000000000000list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut.cpp ) if(APPLE) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_mac.cpp ) elseif(UNIX) find_package(X11) if(X11_FOUND) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_x11.cpp ) endif() elseif(WIN32) list(APPEND QxtGlobalShortcut_SOURCES qxtglobalshortcut_win.cpp ) endif() add_library(QxtGlobalShortcut STATIC ${QxtGlobalShortcut_SOURCES}) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Gui REQUIRED) target_link_libraries(QxtGlobalShortcut Qt${QT_VERSION_MAJOR}::Gui) if(APPLE) find_library(CARBON_LIBRARY Carbon) target_link_libraries(QxtGlobalShortcut ${CARBON_LIBRARY}) elseif(UNIX AND X11_FOUND) target_link_libraries(QxtGlobalShortcut ${X11_LIBRARIES}) if(QT_VERSION_MAJOR EQUAL 5) find_package(Qt5 COMPONENTS X11Extras REQUIRED) target_link_libraries(QxtGlobalShortcut Qt5::X11Extras) else() target_link_libraries(QxtGlobalShortcut Qt${QT_VERSION_MAJOR}::GuiPrivate) endif() find_package(ECM REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_FIND_MODULE_DIR}) find_package(XCB COMPONENTS XCB KEYSYMS REQUIRED) target_link_libraries(QxtGlobalShortcut XCB::XCB XCB::KEYSYMS) endif() zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.cpp000066400000000000000000000205211462517734600247140ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut.h" #include "qxtglobalshortcut_p.h" #include #include #ifndef Q_OS_MACOS int QxtGlobalShortcutPrivate::ref = 0; #endif // Q_OS_MACOS QHash, QxtGlobalShortcut *> QxtGlobalShortcutPrivate::shortcuts; QxtGlobalShortcutPrivate::QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq) : q_ptr(qq) { #ifndef Q_OS_MACOS if (ref == 0) QAbstractEventDispatcher::instance()->installNativeEventFilter(this); ++ref; #endif // Q_OS_MACOS } QxtGlobalShortcutPrivate::~QxtGlobalShortcutPrivate() { #ifndef Q_OS_MACOS --ref; if (ref == 0) { QAbstractEventDispatcher *ed = QAbstractEventDispatcher::instance(); if (ed != nullptr) { ed->removeNativeEventFilter(this); } } #endif // Q_OS_MACOS } bool QxtGlobalShortcutPrivate::setShortcut(const QKeySequence &shortcut) { Q_Q(QxtGlobalShortcut); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) const int combination = shortcut[0]; #else const int combination = shortcut[0].toCombined(); #endif key = shortcut.isEmpty() ? Qt::Key(0) : Qt::Key(combination & ~Qt::KeyboardModifierMask); mods = shortcut.isEmpty() ? Qt::NoModifier : Qt::KeyboardModifiers(combination & Qt::KeyboardModifierMask); const quint32 nativeKey = nativeKeycode(key); const quint32 nativeMods = nativeModifiers(mods); const bool res = registerShortcut(nativeKey, nativeMods); if (res) shortcuts.insert({nativeKey, nativeMods}, q); else qWarning("QxtGlobalShortcut failed to register: %s", qPrintable(QKeySequence(key | mods).toString())); return res; } bool QxtGlobalShortcutPrivate::unsetShortcut() { Q_Q(QxtGlobalShortcut); bool res = false; const quint32 nativeKey = nativeKeycode(key); const quint32 nativeMods = nativeModifiers(mods); if (shortcuts.value({nativeKey, nativeMods}) == q) res = unregisterShortcut(nativeKey, nativeMods); if (res) shortcuts.remove({nativeKey, nativeMods}); else qWarning("QxtGlobalShortcut failed to unregister: %s", qPrintable(QKeySequence(key | mods).toString())); key = Qt::Key(0); mods = Qt::KeyboardModifiers(Qt::NoModifier); return res; } bool QxtGlobalShortcutPrivate::activateShortcut(quint32 nativeKey, quint32 nativeMods) { QxtGlobalShortcut *shortcut = shortcuts.value({nativeKey, nativeMods}); if (!shortcut || !shortcut->isEnabled()) return false; emit shortcut->activated(); return true; } /*! \class QxtGlobalShortcut \inmodule QxtWidgets \brief The QxtGlobalShortcut class provides a global shortcut aka "hotkey". A global shortcut triggers even if the application is not active. This makes it easy to implement applications that react to certain shortcuts still if some other application is active or if the application is for example minimized to the system tray. Example usage: \code QxtGlobalShortcut *shortcut = new QxtGlobalShortcut(window); connect(shortcut, SIGNAL(activated()), window, SLOT(toggleVisibility())); shortcut->setShortcut(QKeySequence("Ctrl+Shift+F12")); \endcode \bold {Note:} Since Qxt 0.6 QxtGlobalShortcut no more requires QxtApplication. */ /*! \fn QxtGlobalShortcut::activated() This signal is emitted when the user types the shortcut's key sequence. \sa shortcut */ /*! Constructs a new QxtGlobalShortcut with \a parent. */ QxtGlobalShortcut::QxtGlobalShortcut(QObject *parent) : QObject(parent) , d_ptr(new QxtGlobalShortcutPrivate(this)) { } /*! Constructs a new QxtGlobalShortcut with \a shortcut and \a parent. */ QxtGlobalShortcut::QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent) : QObject(parent) , d_ptr(new QxtGlobalShortcutPrivate(this)) { setShortcut(shortcut); } /*! Destructs the QxtGlobalShortcut. */ QxtGlobalShortcut::~QxtGlobalShortcut() { Q_D(QxtGlobalShortcut); if (d->key != 0) d->unsetShortcut(); delete d; } /*! \property QxtGlobalShortcut::shortcut \brief the shortcut key sequence \bold {Note:} Notice that corresponding key press and release events are not delivered for registered global shortcuts even if they are disabled. Also, comma separated key sequences are not supported. Only the first part is used: \code qxtShortcut->setShortcut(QKeySequence("Ctrl+Alt+A,Ctrl+Alt+B")); Q_ASSERT(qxtShortcut->shortcut() == QKeySequence("Ctrl+Alt+A")); \endcode */ QKeySequence QxtGlobalShortcut::shortcut() const { Q_D(const QxtGlobalShortcut); return QKeySequence(d->key | d->mods); } bool QxtGlobalShortcut::setShortcut(const QKeySequence &shortcut) { Q_D(QxtGlobalShortcut); if (d->key != 0 && !d->unsetShortcut()) return false; if (shortcut.isEmpty()) return true; return d->setShortcut(shortcut); } /*! \property QxtGlobalShortcut::enabled \brief whether the shortcut is enabled A disabled shortcut does not get activated. The default value is \c true. \sa setDisabled() */ bool QxtGlobalShortcut::isEnabled() const { Q_D(const QxtGlobalShortcut); return d->enabled; } /*! * \brief QxtGlobalShortcut::isSupported checks if the current platform is supported. */ bool QxtGlobalShortcut::isSupported() { return QGuiApplication::platformName() == QLatin1String("windows") || QGuiApplication::platformName() == QLatin1String("xcb") || QGuiApplication::platformName() == QLatin1String("cocoa"); } void QxtGlobalShortcut::setEnabled(bool enabled) { Q_D(QxtGlobalShortcut); d->enabled = enabled; } zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut.h000066400000000000000000000071361462517734600243700ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #ifndef QXTGLOBALSHORTCUT_H #define QXTGLOBALSHORTCUT_H #include #include class QxtGlobalShortcutPrivate; class QxtGlobalShortcut : public QObject { Q_OBJECT QxtGlobalShortcutPrivate *d_ptr = nullptr; Q_DECLARE_PRIVATE(QxtGlobalShortcut) Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled) Q_PROPERTY(QKeySequence shortcut READ shortcut WRITE setShortcut) public: explicit QxtGlobalShortcut(QObject *parent = nullptr); explicit QxtGlobalShortcut(const QKeySequence &shortcut, QObject *parent = nullptr); ~QxtGlobalShortcut() override; QKeySequence shortcut() const; bool setShortcut(const QKeySequence &shortcut); bool isEnabled() const; static bool isSupported(); public slots: void setEnabled(bool enabled); signals: void activated(); }; #endif // QXTGLOBALSHORTCUT_H zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_mac.cpp000066400000000000000000000235361462517734600255450ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include #include #include typedef QPair Identifier; static QMap keyRefs; static QHash keyIDs; static quint32 hotKeySerial = 0; static bool qxt_mac_handler_installed = false; OSStatus qxt_mac_handle_hot_key(EventHandlerCallRef nextHandler, EventRef event, void *data) { Q_UNUSED(nextHandler) Q_UNUSED(data) if (GetEventClass(event) == kEventClassKeyboard && GetEventKind(event) == kEventHotKeyPressed) { EventHotKeyID keyID; GetEventParameter(event, kEventParamDirectObject, typeEventHotKeyID, NULL, sizeof(keyID), NULL, &keyID); Identifier id = keyIDs.key(keyID.id); QxtGlobalShortcutPrivate::activateShortcut(id.second, id.first); } return noErr; } bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(eventType) Q_UNUSED(message) Q_UNUSED(result) return false; } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= shiftKey; if (modifiers & Qt::ControlModifier) native |= cmdKey; if (modifiers & Qt::AltModifier) native |= optionKey; if (modifiers & Qt::MetaModifier) native |= controlKey; if (modifiers & Qt::KeypadModifier) native |= kEventKeyModifierNumLockMask; return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { UTF16Char ch; // Constants found in NSEvent.h from AppKit.framework switch (key) { case Qt::Key_Return: return kVK_Return; case Qt::Key_Enter: return kVK_ANSI_KeypadEnter; case Qt::Key_Tab: return kVK_Tab; case Qt::Key_Space: return kVK_Space; case Qt::Key_Backspace: return kVK_Delete; case Qt::Key_Control: return kVK_Command; case Qt::Key_Shift: return kVK_Shift; case Qt::Key_CapsLock: return kVK_CapsLock; case Qt::Key_Option: return kVK_Option; case Qt::Key_Meta: return kVK_Control; case Qt::Key_F17: return kVK_F17; case Qt::Key_VolumeUp: return kVK_VolumeUp; case Qt::Key_VolumeDown: return kVK_VolumeDown; case Qt::Key_F18: return kVK_F18; case Qt::Key_F19: return kVK_F19; case Qt::Key_F20: return kVK_F20; case Qt::Key_F5: return kVK_F5; case Qt::Key_F6: return kVK_F6; case Qt::Key_F7: return kVK_F7; case Qt::Key_F3: return kVK_F3; case Qt::Key_F8: return kVK_F8; case Qt::Key_F9: return kVK_F9; case Qt::Key_F11: return kVK_F11; case Qt::Key_F13: return kVK_F13; case Qt::Key_F16: return kVK_F16; case Qt::Key_F14: return kVK_F14; case Qt::Key_F10: return kVK_F10; case Qt::Key_F12: return kVK_F12; case Qt::Key_F15: return kVK_F15; case Qt::Key_Help: return kVK_Help; case Qt::Key_Home: return kVK_Home; case Qt::Key_PageUp: return kVK_PageUp; case Qt::Key_Delete: return kVK_ForwardDelete; case Qt::Key_F4: return kVK_F4; case Qt::Key_End: return kVK_End; case Qt::Key_F2: return kVK_F2; case Qt::Key_PageDown: return kVK_PageDown; case Qt::Key_F1: return kVK_F1; case Qt::Key_Left: return kVK_LeftArrow; case Qt::Key_Right: return kVK_RightArrow; case Qt::Key_Down: return kVK_DownArrow; case Qt::Key_Up: return kVK_UpArrow; default: ; } if (key == Qt::Key_Escape) ch = 27; else if (key == Qt::Key_Return) ch = 13; else if (key == Qt::Key_Enter) ch = 3; else if (key == Qt::Key_Tab) ch = 9; else ch = key; CFDataRef currentLayoutData; TISInputSourceRef currentKeyboard = TISCopyCurrentKeyboardInputSource(); if (currentKeyboard == NULL) return 0; currentLayoutData = (CFDataRef)TISGetInputSourceProperty(currentKeyboard, kTISPropertyUnicodeKeyLayoutData); CFRelease(currentKeyboard); if (currentLayoutData == NULL) return 0; UCKeyboardLayout *header = (UCKeyboardLayout *)CFDataGetBytePtr(currentLayoutData); UCKeyboardTypeHeader *table = header->keyboardTypeList; uint8_t *data = (uint8_t *)header; // God, would a little documentation for this shit kill you... for (quint32 i = 0; i < header->keyboardTypeCount; ++i) { UCKeyStateRecordsIndex *stateRec = 0; if (table[i].keyStateRecordsIndexOffset != 0) { stateRec = reinterpret_cast(data + table[i].keyStateRecordsIndexOffset); if (stateRec->keyStateRecordsIndexFormat != kUCKeyStateRecordsIndexFormat) stateRec = 0; } UCKeyToCharTableIndex *charTable = reinterpret_cast(data + table[i].keyToCharTableIndexOffset); if (charTable->keyToCharTableIndexFormat != kUCKeyToCharTableIndexFormat) continue; for (quint32 j = 0; j < charTable->keyToCharTableCount; ++j) { UCKeyOutput *keyToChar = reinterpret_cast(data + charTable->keyToCharTableOffsets[j]); for (quint32 k = 0; k < charTable->keyToCharTableSize; ++k) { if (keyToChar[k] & kUCKeyOutputTestForIndexMask) { long idx = keyToChar[k] & kUCKeyOutputGetIndexMask; if (stateRec && idx < stateRec->keyStateRecordCount) { UCKeyStateRecord *rec = reinterpret_cast(data + stateRec->keyStateRecordOffsets[idx]); if (rec->stateZeroCharData == ch) return k; } } else if (!(keyToChar[k] & kUCKeyOutputSequenceIndexMask) && keyToChar[k] < 0xFFFE) { if (keyToChar[k] == ch) return k; } } // for k } // for j } // for i return 0; } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { if (!qxt_mac_handler_installed) { EventTypeSpec t; t.eventClass = kEventClassKeyboard; t.eventKind = kEventHotKeyPressed; InstallApplicationEventHandler(&qxt_mac_handle_hot_key, 1, &t, NULL, NULL); } EventHotKeyID keyID; keyID.signature = 'cute'; keyID.id = ++hotKeySerial; EventHotKeyRef ref = 0; bool rv = !RegisterEventHotKey(nativeKey, nativeMods, keyID, GetApplicationEventTarget(), 0, &ref); if (rv) { keyIDs.insert(Identifier(nativeMods, nativeKey), keyID.id); keyRefs.insert(keyID.id, ref); } return rv; } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { Identifier id(nativeMods, nativeKey); if (!keyIDs.contains(id)) return false; EventHotKeyRef ref = keyRefs.take(keyIDs[id]); keyIDs.remove(id); return !UnregisterEventHotKey(ref); } zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_p.h000066400000000000000000000101311462517734600246740ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #ifndef QXTGLOBALSHORTCUT_P_H #define QXTGLOBALSHORTCUT_P_H #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #define NativeEventFilterResult long #else #define NativeEventFilterResult qintptr #endif class QKeySequence; class QxtGlobalShortcut; class QxtGlobalShortcutPrivate : public QAbstractNativeEventFilter { QxtGlobalShortcut *q_ptr = nullptr; Q_DECLARE_PUBLIC(QxtGlobalShortcut) public: QxtGlobalShortcutPrivate(QxtGlobalShortcut *qq); ~QxtGlobalShortcutPrivate() override; bool enabled = true; Qt::Key key = Qt::Key(0); Qt::KeyboardModifiers mods = Qt::NoModifier; #ifndef Q_OS_MACOS static int ref; #endif // Q_OS_MACOS bool setShortcut(const QKeySequence &shortcut); bool unsetShortcut(); bool nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) override; static bool activateShortcut(quint32 nativeKey, quint32 nativeMods); private: static quint32 nativeKeycode(Qt::Key keycode); static quint32 nativeModifiers(Qt::KeyboardModifiers modifiers); static bool registerShortcut(quint32 nativeKey, quint32 nativeMods); static bool unregisterShortcut(quint32 nativeKey, quint32 nativeMods); static QHash, QxtGlobalShortcut *> shortcuts; }; #endif // QXTGLOBALSHORTCUT_P_H zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_win.cpp000066400000000000000000000174261462517734600256030ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(eventType) Q_UNUSED(result) MSG *msg = static_cast(message); if (msg->message == WM_HOTKEY) { const quint32 keycode = HIWORD(msg->lParam); const quint32 modifiers = LOWORD(msg->lParam); activateShortcut(keycode, modifiers); } return false; } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { // MOD_ALT, MOD_CONTROL, (MOD_KEYUP), MOD_SHIFT, MOD_WIN quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= MOD_SHIFT; if (modifiers & Qt::ControlModifier) native |= MOD_CONTROL; if (modifiers & Qt::AltModifier) native |= MOD_ALT; if (modifiers & Qt::MetaModifier) native |= MOD_WIN; // TODO: resolve these? //if (modifiers & Qt::KeypadModifier) //if (modifiers & Qt::GroupSwitchModifier) return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { switch (key) { case Qt::Key_Escape: return VK_ESCAPE; case Qt::Key_Tab: case Qt::Key_Backtab: return VK_TAB; case Qt::Key_Backspace: return VK_BACK; case Qt::Key_Return: case Qt::Key_Enter: return VK_RETURN; case Qt::Key_Insert: return VK_INSERT; case Qt::Key_Delete: return VK_DELETE; case Qt::Key_Pause: return VK_PAUSE; case Qt::Key_Print: return VK_PRINT; case Qt::Key_Clear: return VK_CLEAR; case Qt::Key_Home: return VK_HOME; case Qt::Key_End: return VK_END; case Qt::Key_Left: return VK_LEFT; case Qt::Key_Up: return VK_UP; case Qt::Key_Right: return VK_RIGHT; case Qt::Key_Down: return VK_DOWN; case Qt::Key_PageUp: return VK_PRIOR; case Qt::Key_PageDown: return VK_NEXT; case Qt::Key_F1: return VK_F1; case Qt::Key_F2: return VK_F2; case Qt::Key_F3: return VK_F3; case Qt::Key_F4: return VK_F4; case Qt::Key_F5: return VK_F5; case Qt::Key_F6: return VK_F6; case Qt::Key_F7: return VK_F7; case Qt::Key_F8: return VK_F8; case Qt::Key_F9: return VK_F9; case Qt::Key_F10: return VK_F10; case Qt::Key_F11: return VK_F11; case Qt::Key_F12: return VK_F12; case Qt::Key_F13: return VK_F13; case Qt::Key_F14: return VK_F14; case Qt::Key_F15: return VK_F15; case Qt::Key_F16: return VK_F16; case Qt::Key_F17: return VK_F17; case Qt::Key_F18: return VK_F18; case Qt::Key_F19: return VK_F19; case Qt::Key_F20: return VK_F20; case Qt::Key_F21: return VK_F21; case Qt::Key_F22: return VK_F22; case Qt::Key_F23: return VK_F23; case Qt::Key_F24: return VK_F24; case Qt::Key_Space: return VK_SPACE; case Qt::Key_Asterisk: return VK_MULTIPLY; case Qt::Key_Plus: return VK_ADD; case Qt::Key_Comma: return VK_SEPARATOR; case Qt::Key_Minus: return VK_SUBTRACT; case Qt::Key_Slash: return VK_DIVIDE; case Qt::Key_MediaNext: return VK_MEDIA_NEXT_TRACK; case Qt::Key_MediaPrevious: return VK_MEDIA_PREV_TRACK; case Qt::Key_MediaPlay: return VK_MEDIA_PLAY_PAUSE; case Qt::Key_MediaStop: return VK_MEDIA_STOP; // couldn't find those in VK_* //case Qt::Key_MediaLast: //case Qt::Key_MediaRecord: case Qt::Key_VolumeDown: return VK_VOLUME_DOWN; case Qt::Key_VolumeUp: return VK_VOLUME_UP; case Qt::Key_VolumeMute: return VK_VOLUME_MUTE; // numbers case Qt::Key_0: case Qt::Key_1: case Qt::Key_2: case Qt::Key_3: case Qt::Key_4: case Qt::Key_5: case Qt::Key_6: case Qt::Key_7: case Qt::Key_8: case Qt::Key_9: return key; // letters case Qt::Key_A: case Qt::Key_B: case Qt::Key_C: case Qt::Key_D: case Qt::Key_E: case Qt::Key_F: case Qt::Key_G: case Qt::Key_H: case Qt::Key_I: case Qt::Key_J: case Qt::Key_K: case Qt::Key_L: case Qt::Key_M: case Qt::Key_N: case Qt::Key_O: case Qt::Key_P: case Qt::Key_Q: case Qt::Key_R: case Qt::Key_S: case Qt::Key_T: case Qt::Key_U: case Qt::Key_V: case Qt::Key_W: case Qt::Key_X: case Qt::Key_Y: case Qt::Key_Z: return key; default: return 0; } } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { return RegisterHotKey(0, nativeMods ^ nativeKey, nativeMods, nativeKey); } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { return UnregisterHotKey(0, nativeMods ^ nativeKey); } zeal-0.7.1/src/libs/ui/qxtglobalshortcut/qxtglobalshortcut_x11.cpp000066400000000000000000000154241462517734600254130ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ /**************************************************************************** ** Copyright (c) 2006 - 2011, the LibQxt project. ** See the Qxt AUTHORS file for a list of authors and copyright holders. ** All rights reserved. ** ** Redistribution and use in source and binary forms, with or without ** modification, are permitted provided that the following conditions are met: ** * Redistributions of source code must retain the above copyright ** notice, this list of conditions and the following disclaimer. ** * Redistributions in binary form must reproduce the above copyright ** notice, this list of conditions and the following disclaimer in the ** documentation and/or other materials provided with the distribution. ** * Neither the name of the LibQxt project nor the ** names of its contributors may be used to endorse or promote products ** derived from this software without specific prior written permission. ** ** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ** ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED ** WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE ** DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY ** DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ** (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; ** LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ** ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT ** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS ** SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ** ** *****************************************************************************/ #include "qxtglobalshortcut_p.h" #include #include #include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) #include #else #include #endif #include #include #include namespace { constexpr quint32 maskModifiers[] = { 0, XCB_MOD_MASK_2, XCB_MOD_MASK_LOCK, (XCB_MOD_MASK_2 | XCB_MOD_MASK_LOCK) }; } // namespace bool QxtGlobalShortcutPrivate::nativeEventFilter(const QByteArray &eventType, void *message, NativeEventFilterResult *result) { Q_UNUSED(result) if (eventType != "xcb_generic_event_t") return false; auto event = static_cast(message); if ((event->response_type & ~0x80) != XCB_KEY_PRESS) return false; auto keyPressEvent = static_cast(message); // Avoid keyboard freeze xcb_connection_t *xcbConnection = QX11Info::connection(); xcb_allow_events(xcbConnection, XCB_ALLOW_REPLAY_KEYBOARD, keyPressEvent->time); xcb_flush(xcbConnection); unsigned int keycode = keyPressEvent->detail; unsigned int keystate = 0; if (keyPressEvent->state & XCB_MOD_MASK_1) keystate |= XCB_MOD_MASK_1; if (keyPressEvent->state & XCB_MOD_MASK_CONTROL) keystate |= XCB_MOD_MASK_CONTROL; if (keyPressEvent->state & XCB_MOD_MASK_4) keystate |= XCB_MOD_MASK_4; if (keyPressEvent->state & XCB_MOD_MASK_SHIFT) keystate |= XCB_MOD_MASK_SHIFT; return activateShortcut(keycode, keystate); } quint32 QxtGlobalShortcutPrivate::nativeModifiers(Qt::KeyboardModifiers modifiers) { quint32 native = 0; if (modifiers & Qt::ShiftModifier) native |= XCB_MOD_MASK_SHIFT; if (modifiers & Qt::ControlModifier) native |= XCB_MOD_MASK_CONTROL; if (modifiers & Qt::AltModifier) native |= XCB_MOD_MASK_1; if (modifiers & Qt::MetaModifier) native |= XCB_MOD_MASK_4; return native; } quint32 QxtGlobalShortcutPrivate::nativeKeycode(Qt::Key key) { quint32 native = 0; KeySym keysym = XStringToKeysym(QKeySequence(key).toString().toLatin1().data()); if (keysym == XCB_NO_SYMBOL) keysym = static_cast(key); xcb_key_symbols_t *xcbKeySymbols = xcb_key_symbols_alloc(QX11Info::connection()); QScopedPointer keycodes( xcb_key_symbols_get_keycode(xcbKeySymbols, keysym)); if (!keycodes.isNull()) native = keycodes.data()[0]; // Use the first keycode xcb_key_symbols_free(xcbKeySymbols); return native; } bool QxtGlobalShortcutPrivate::registerShortcut(quint32 nativeKey, quint32 nativeMods) { xcb_connection_t *xcbConnection = QX11Info::connection(); QList xcbCookies; for (quint32 maskMods : maskModifiers) { xcbCookies << xcb_grab_key_checked(xcbConnection, 1, QX11Info::appRootWindow(), nativeMods | maskMods, nativeKey, XCB_GRAB_MODE_ASYNC, XCB_GRAB_MODE_ASYNC); } bool failed = false; for (xcb_void_cookie_t cookie : std::as_const(xcbCookies)) { QScopedPointer error(xcb_request_check(xcbConnection, cookie)); failed = !error.isNull(); } if (failed) unregisterShortcut(nativeKey, nativeMods); return !failed; } bool QxtGlobalShortcutPrivate::unregisterShortcut(quint32 nativeKey, quint32 nativeMods) { xcb_connection_t *xcbConnection = QX11Info::connection(); QList xcbCookies; for (quint32 maskMods : maskModifiers) { xcb_ungrab_key(xcbConnection, nativeKey, QX11Info::appRootWindow(), nativeMods | maskMods); } bool failed = false; for (xcb_void_cookie_t cookie : xcbCookies) { QScopedPointer error(xcb_request_check(xcbConnection, cookie)); failed = !error.isNull(); } return !failed; } zeal-0.7.1/src/libs/ui/searchitemdelegate.cpp000066400000000000000000000205701462517734600211550ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchitemdelegate.h" #include #include #include #include #include #include using namespace Zeal::WidgetUi; SearchItemDelegate::SearchItemDelegate(QObject *parent) : QStyledItemDelegate(parent) { } QList SearchItemDelegate::decorationRoles() const { return m_decorationRoles; } void SearchItemDelegate::setDecorationRoles(const QList &roles) { m_decorationRoles = roles; } bool SearchItemDelegate::helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) { if (event->type() != QEvent::ToolTip) return QStyledItemDelegate::helpEvent(event, view, option, index); if (sizeHint(option, index).width() < view->visualRect(index).width()) { QToolTip::hideText(); return QStyledItemDelegate::helpEvent(event, view, option, index); } QToolTip::showText(event->globalPos(), index.data().toString(), view); return true; } void SearchItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); QStyle *style = opt.widget->style(); // Find decoration roles with data present. QList roles; for (int role : m_decorationRoles) { if (!index.data(role).isNull()) roles.append(role); } // TODO: Implemented via initStyleOption() overload if (!roles.isEmpty()) { opt.features |= QStyleOptionViewItem::HasDecoration; opt.icon = index.data(roles.first()).value(); const QSize actualSize = opt.icon.actualSize(opt.decorationSize); opt.decorationSize = {std::min(opt.decorationSize.width(), actualSize.width()), std::min(opt.decorationSize.height(), actualSize.height())}; } style->drawControl(QStyle::CE_ItemViewItem, &opt, painter, opt.widget); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1; if (!roles.isEmpty()) { QIcon::Mode mode = QIcon::Normal; if (!(opt.state & QStyle::State_Enabled)) mode = QIcon::Disabled; else if (opt.state & QStyle::State_Selected) mode = QIcon::Selected; const QIcon::State state = (opt.state & QStyle::State_Open) ? QIcon::On : QIcon::Off; // All icons are sized after the first one. QRect iconRect = style->subElementRect(QStyle::SE_ItemViewItemDecoration, &opt, opt.widget); // Undo RTL mirroring iconRect = style->visualRect(opt.direction, opt.rect, iconRect); const int dx = iconRect.width() + margin; for (int i = 1; i < roles.size(); ++i) { opt.decorationSize.rwidth() += dx; iconRect.translate(dx, 0); // Redo RTL mirroring const auto iconVisualRect = style->visualRect(opt.direction, opt.rect, iconRect); const QIcon icon = index.data(roles[i]).value(); icon.paint(painter, iconVisualRect, opt.decorationAlignment, mode, state); } } // This should not happen unless a docset is corrupted. if (index.data().isNull()) return; // Match QCommonStyle behaviour. opt.features |= QStyleOptionViewItem::HasDisplay; opt.text = index.data().toString(); const QRect textRect = style->subElementRect(QStyle::SE_ItemViewItemText, &opt, opt.widget) .adjusted(margin, 0, -margin, 0); const QFontMetrics &fm = opt.fontMetrics; const QString elidedText = fm.elidedText(opt.text, opt.textElideMode, textRect.width()); if (!m_highlight.isEmpty()) { painter->save(); painter->setRenderHint(QPainter::Antialiasing); painter->setPen(QColor::fromRgb(255, 253, 0)); const QColor highlightColor = (opt.state & (QStyle::State_Selected | QStyle::State_HasFocus)) ? QColor::fromRgb(255, 255, 100, 20) : QColor::fromRgb(255, 255, 100, 120); for (int i = 0;;) { const int matchIndex = opt.text.indexOf(m_highlight, i, Qt::CaseInsensitive); if (matchIndex == -1 || matchIndex >= elidedText.length() - 1) break; #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) QRect highlightRect = textRect.adjusted(fm.horizontalAdvance(elidedText.left(matchIndex)), 2, 0, -2); highlightRect.setWidth(fm.horizontalAdvance(elidedText.mid(matchIndex, m_highlight.length()))); #else QRect highlightRect = textRect.adjusted(fm.width(elidedText.left(matchIndex)), 2, 0, -2); highlightRect.setWidth(fm.width(elidedText.mid(matchIndex, m_highlight.length()))); #endif QPainterPath path; path.addRoundedRect(highlightRect, 2, 2); painter->fillPath(path, highlightColor); painter->drawPath(path); i = matchIndex + m_highlight.length(); } painter->restore(); } painter->save(); #ifdef Q_OS_WIN32 // QWindowsVistaStyle overrides highlight colour. if (style->objectName() == QLatin1String("windowsvista")) { opt.palette.setColor(QPalette::All, QPalette::HighlightedText, opt.palette.color(QPalette::Active, QPalette::Text)); } #endif const QPalette::ColorGroup cg = (opt.state & QStyle::State_Active) ? QPalette::Normal : QPalette::Inactive; if (opt.state & QStyle::State_Selected) painter->setPen(opt.palette.color(cg, QPalette::HighlightedText)); else painter->setPen(opt.palette.color(cg, QPalette::Text)); // Constant LeftToRight because we don't need to flip it any further. // Vertically align the text in the middle to match QCommonStyle behaviour. const auto alignedRect = QStyle::alignedRect(Qt::LeftToRight, opt.displayAlignment, QSize(textRect.size().width(), fm.height()), textRect); const auto textPoint = QPoint(alignedRect.x(), alignedRect.y() + fm.ascent()); // Force LTR, so that BiDi won't reorder ellipsis to the left. painter->drawText(textPoint, elidedText, Qt::TextFlag::TextForceLeftToRight, 0); painter->restore(); } QSize SearchItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const { QStyleOptionViewItem opt(option); QStyle *style = opt.widget->style(); QSize size = QStyledItemDelegate::sizeHint(opt, index); size.setWidth(0); const int margin = style->pixelMetric(QStyle::PM_FocusFrameHMargin, &opt, opt.widget) + 1; // Find decoration roles with data present. QList roles; for (int role : m_decorationRoles) { if (!index.data(role).isNull()) roles.append(role); } if (!roles.isEmpty()) { const QIcon icon = index.data(roles.first()).value(); const QSize actualSize = icon.actualSize(opt.decorationSize); const int decorationWidth = std::min(opt.decorationSize.width(), actualSize.width()); size.rwidth() = (decorationWidth + margin) * roles.size() + margin; } #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) size.rwidth() += opt.fontMetrics.horizontalAdvance(index.data().toString()) + margin * 2; #else size.rwidth() += opt.fontMetrics.width(index.data().toString()) + margin * 2; #endif return size; } void SearchItemDelegate::setHighlight(const QString &text) { m_highlight = text; } zeal-0.7.1/src/libs/ui/searchitemdelegate.h000066400000000000000000000036511462517734600206230ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H #define ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H #include namespace Zeal { namespace WidgetUi { class SearchItemDelegate : public QStyledItemDelegate { Q_OBJECT public: explicit SearchItemDelegate(QObject *parent = nullptr); QList decorationRoles() const; void setDecorationRoles(const QList &roles); bool helpEvent(QHelpEvent *event, QAbstractItemView *view, const QStyleOptionViewItem &option, const QModelIndex &index) override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override; public slots: void setHighlight(const QString &text); private: QList m_decorationRoles = {Qt::DecorationRole}; QString m_highlight; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHITEMDELEGATE_H zeal-0.7.1/src/libs/ui/searchsidebar.cpp000066400000000000000000000332041462517734600201330ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchsidebar.h" #include "searchitemdelegate.h" #include "widgets/layouthelper.h" #include "widgets/searchedit.h" #include "widgets/toolbarframe.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; SearchSidebar::SearchSidebar(QWidget *parent) : SearchSidebar(nullptr, parent) { } SearchSidebar::SearchSidebar(const SearchSidebar *other, QWidget *parent) : Sidebar::View(parent) { // Setup search result view. m_treeView = new QTreeView(); m_treeView->setFrameShape(QFrame::NoFrame); m_treeView->setHeaderHidden(true); m_treeView->setUniformRowHeights(true); #ifdef Q_OS_MACOS m_treeView->setAttribute(Qt::WA_MacShowFocusRect, false); #endif // Save expanded items. connect(m_treeView, &QTreeView::expanded, this, [this](const QModelIndex &index) { if (m_expandedIndexList.indexOf(index) == -1) { m_expandedIndexList.append(index); } }); connect(m_treeView, &QTreeView::collapsed, this, [this](const QModelIndex &index) { m_expandedIndexList.removeOne(index); }); auto delegate = new SearchItemDelegate(m_treeView); delegate->setDecorationRoles({Registry::ItemDataRole::DocsetIconRole, Qt::DecorationRole}); m_treeView->setItemDelegate(delegate); connect(m_treeView, &QTreeView::activated, this, &SearchSidebar::navigateToIndexAndActivate); connect(m_treeView, &QTreeView::clicked, this, &SearchSidebar::navigateToIndex); // Setup Alt+Up, Alt+Down, etc shortcuts. const auto keyList = {Qt::Key_Up, Qt::Key_Down, Qt::Key_PageUp, Qt::Key_PageDown, Qt::Key_Home, Qt::Key_End}; for (const auto key : keyList) { auto shortcut = new QShortcut(key | Qt::AltModifier, this); connect(shortcut, &QShortcut::activated, this, [this, key]() { QKeyEvent event(QKeyEvent::KeyPress, key, Qt::NoModifier); QCoreApplication::sendEvent(m_treeView, &event); }); } // Setup page TOC view. // TODO: Move to a separate Sidebar View. m_pageTocView = new QListView(); m_pageTocView->setFrameShape(QFrame::NoFrame); m_pageTocView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_pageTocView->setItemDelegate(new SearchItemDelegate(m_pageTocView)); m_pageTocView->setUniformItemSizes(true); m_pageTocView->setVisible(false); #ifdef Q_OS_MACOS m_pageTocView->setAttribute(Qt::WA_MacShowFocusRect, false); #endif m_pageTocModel = new Registry::SearchModel(this); connect(m_pageTocModel, &Registry::SearchModel::updated, this, [this]() { if (m_pageTocModel->isEmpty()) { m_pageTocView->hide(); } else { m_pageTocView->show(); m_splitter->restoreState(Core::Application::instance()->settings()->tocSplitterState); } }); m_pageTocView->setModel(m_pageTocModel); connect(m_pageTocView, &QListView::activated, this, &SearchSidebar::navigateToIndexAndActivate); connect(m_pageTocView, &QListView::clicked, this, &SearchSidebar::navigateToIndex); // Setup search input box. m_searchEdit = new SearchEdit(); m_searchEdit->installEventFilter(this); setupSearchBoxCompletions(); // Connect delegate first to make cloning work. connect(m_searchEdit, &QLineEdit::textChanged, this, [delegate](const QString &text) { delegate->setHighlight(Registry::SearchQuery::fromString(text).query()); }); // Clone state if `other` is provided. if (other) { if (other->m_searchEdit->text().isEmpty()) { m_searchModel = new Registry::SearchModel(this); setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); for (const QModelIndex &index : other->m_expandedIndexList) { m_treeView->expand(index); } } else { m_searchEdit->setText(other->m_searchEdit->text()); m_searchModel = other->m_searchModel->clone(this); setTreeViewModel(m_searchModel, false); } // Clone current selection. Signals must be disabled to avoid crash in the event handler. m_treeView->selectionModel()->blockSignals(true); for (const QModelIndex &index : other->m_treeView->selectionModel()->selectedIndexes()) { m_treeView->selectionModel()->select(index, QItemSelectionModel::Select); } m_treeView->selectionModel()->blockSignals(false); // Cannot update position until layout geomentry is calculated, so set it in showEvent(). m_pendingVerticalPosition = other->m_treeView->verticalScrollBar()->value(); } else { m_searchModel = new Registry::SearchModel(this); setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); } connect(m_searchEdit, &QLineEdit::textChanged, this, [this](const QString &text) { QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel(); if (text.isEmpty()) { setTreeViewModel(Core::Application::instance()->docsetRegistry()->model(), true); } else { setTreeViewModel(m_searchModel, false); } QItemSelectionModel *newSelectionModel = m_treeView->selectionModel(); if (newSelectionModel != oldSelectionModel) { // TODO: Remove once QTBUG-49966 is addressed. if (oldSelectionModel) { oldSelectionModel->deleteLater(); } // Connect to the new selection model. connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SearchSidebar::navigateToSelectionWithDelay); } m_treeView->reset(); Core::Application::instance()->docsetRegistry()->search(text); }); auto toolBarLayout = new QVBoxLayout(); toolBarLayout->setContentsMargins(4, 0, 4, 0); toolBarLayout->setSpacing(4); toolBarLayout->addWidget(m_searchEdit); auto toolBarFrame = new ToolBarFrame(); toolBarFrame->setLayout(toolBarLayout); // Setup splitter. m_splitter = new QSplitter(); m_splitter->setOrientation(Qt::Vertical); m_splitter->addWidget(m_treeView); m_splitter->addWidget(m_pageTocView); connect(m_splitter, &QSplitter::splitterMoved, this, [this]() { Core::Application::instance()->settings()->tocSplitterState = m_splitter->saveState(); }); // Setup main layout. auto layout = LayoutHelper::createBorderlessLayout(); layout->addWidget(toolBarFrame); layout->addWidget(m_splitter); setLayout(layout); // Setup delayed navigation to a page until user makes a pause in typing a search query. m_delayedNavigationTimer = new QTimer(this); m_delayedNavigationTimer->setInterval(400); m_delayedNavigationTimer->setSingleShot(true); connect(m_delayedNavigationTimer, &QTimer::timeout, this, [this]() { const QModelIndex index = m_delayedNavigationTimer->property("index").toModelIndex(); if (!index.isValid()) { return; } navigateToIndex(index); }); // Setup Docset Registry. auto registry = Core::Application::instance()->docsetRegistry(); using Registry::DocsetRegistry; connect(registry, &DocsetRegistry::searchCompleted, this, [this](const QList &results) { if (!isVisible()) { return; } m_delayedNavigationTimer->stop(); m_searchModel->setResults(results); const QModelIndex index = m_searchModel->index(0, 0, QModelIndex()); m_treeView->setCurrentIndex(index); m_delayedNavigationTimer->setProperty("index", index); m_delayedNavigationTimer->start(); }); connect(registry, &DocsetRegistry::docsetAboutToBeUnloaded, this, [this](const QString &name) { if (isVisible()) { // Disable updates because removeSearchResultWithName can // call {begin,end}RemoveRows multiple times, and cause // degradation of UI responsiveness. m_treeView->setUpdatesEnabled(false); m_searchModel->removeSearchResultWithName(name); m_treeView->setUpdatesEnabled(true); } else { m_searchModel->removeSearchResultWithName(name); } setupSearchBoxCompletions(); }); connect(registry, &DocsetRegistry::docsetLoaded, this, [this](const QString &) { setupSearchBoxCompletions(); }); } void SearchSidebar::setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated) { QItemSelectionModel *oldSelectionModel = m_treeView->selectionModel(); m_treeView->setModel(model); m_treeView->setRootIsDecorated(isRootDecorated); QItemSelectionModel *newSelectionModel = m_treeView->selectionModel(); if (newSelectionModel != oldSelectionModel) { // TODO: Remove once QTBUG-49966 is addressed. if (oldSelectionModel) { oldSelectionModel->deleteLater(); } // Connect to the new selection model. connect(m_treeView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &SearchSidebar::navigateToSelectionWithDelay); } } SearchSidebar *SearchSidebar::clone(QWidget *parent) const { return new SearchSidebar(this, parent); } Registry::SearchModel *SearchSidebar::pageTocModel() const { return m_pageTocModel; } void SearchSidebar::focusSearchEdit(bool clear) { if (!isVisible()) { m_pendingSearchEditFocus = true; return; } m_searchEdit->setFocus(); if (clear) { m_searchEdit->clearQuery(); } } void SearchSidebar::search(const Registry::SearchQuery &query) { // TODO: Pass Registry::SearchQuery, converting to QString is dumb at this point. m_searchEdit->setText(query.toString()); } void SearchSidebar::navigateToIndex(const QModelIndex &index) { // When triggered by click, cancel delayed navigation request caused by the selection change. if (m_delayedNavigationTimer->isActive() && m_delayedNavigationTimer->property("index").toModelIndex() == index) { m_delayedNavigationTimer->stop(); } const QVariant url = index.data(Registry::ItemDataRole::UrlRole); if (url.isNull()) { return; } emit navigationRequested(url.toUrl()); } void SearchSidebar::navigateToIndexAndActivate(const QModelIndex &index) { const QVariant url = index.data(Registry::ItemDataRole::UrlRole); if (url.isNull()) { return; } emit navigationRequested(url.toUrl()); emit activated(); } void SearchSidebar::navigateToSelectionWithDelay(const QItemSelection &selection) { if (selection.isEmpty()) { return; } m_delayedNavigationTimer->setProperty("index", selection.indexes().first()); m_delayedNavigationTimer->start(); } void SearchSidebar::setupSearchBoxCompletions() { QStringList completions; const auto docsets = Core::Application::instance()->docsetRegistry()->docsets(); for (const Registry::Docset *docset : docsets) { const QStringList keywords = docset->keywords(); if (keywords.isEmpty()) continue; completions << keywords.constFirst() + QLatin1Char(':'); } if (completions.isEmpty()) return; m_searchEdit->setCompletions(completions); } bool SearchSidebar::eventFilter(QObject *object, QEvent *event) { if (object == m_searchEdit && event->type() == QEvent::KeyPress) { auto e = static_cast(event); switch (e->key()) { case Qt::Key_Return: emit activated(); break; case Qt::Key_Home: case Qt::Key_End: case Qt::Key_Left: case Qt::Key_Right: if (!m_searchEdit->text().isEmpty()) { break; } [[fallthrough]]; case Qt::Key_Down: case Qt::Key_Up: case Qt::Key_PageDown: case Qt::Key_PageUp: QCoreApplication::sendEvent(m_treeView, event); break; } } return Sidebar::View::eventFilter(object, event); } void SearchSidebar::showEvent(QShowEvent *event) { Q_UNUSED(event) if (m_pendingVerticalPosition > 0) { m_treeView->verticalScrollBar()->setValue(m_pendingVerticalPosition); m_pendingVerticalPosition = 0; } if (m_pendingSearchEditFocus) { m_searchEdit->setFocus(); m_pendingSearchEditFocus = false; } } zeal-0.7.1/src/libs/ui/searchsidebar.h000066400000000000000000000055001462517734600175760ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SEARCHSIDEBAR_H #define ZEAL_WIDGETUI_SEARCHSIDEBAR_H #include #include #include class QItemSelection; class QSplitter; class QListView; class QTimer; class QTreeView; namespace Zeal { namespace Registry { class SearchModel; class SearchQuery; } // namespace Registry namespace WidgetUi { class SearchEdit; class SearchSidebar final : public Sidebar::View { Q_OBJECT Q_DISABLE_COPY(SearchSidebar) public: explicit SearchSidebar(QWidget *parent = nullptr); SearchSidebar *clone(QWidget *parent = nullptr) const; Registry::SearchModel *pageTocModel() const; signals: void activated(); void navigationRequested(const QUrl &url); public slots: void focusSearchEdit(bool clear = false); void search(const Registry::SearchQuery &query); private slots: void navigateToIndex(const QModelIndex &index); void navigateToIndexAndActivate(const QModelIndex &index); void navigateToSelectionWithDelay(const QItemSelection &selection); void setupSearchBoxCompletions(); protected: bool eventFilter(QObject *object, QEvent *event) override; void showEvent(QShowEvent *event) override; private: explicit SearchSidebar(const SearchSidebar *other, QWidget *parent = nullptr); void setTreeViewModel(QAbstractItemModel *model, bool isRootDecorated); SearchEdit *m_searchEdit = nullptr; bool m_pendingSearchEditFocus = false; // Index and search results tree view state. QTreeView *m_treeView = nullptr; QModelIndexList m_expandedIndexList; int m_pendingVerticalPosition = 0; Registry::SearchModel *m_searchModel = nullptr; // TOC list view state. QListView *m_pageTocView = nullptr; Registry::SearchModel *m_pageTocModel = nullptr; QSplitter *m_splitter = nullptr; QTimer *m_delayedNavigationTimer = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHSIDEBAR_H zeal-0.7.1/src/libs/ui/settingsdialog.cpp000066400000000000000000000340311462517734600203530ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "settingsdialog.h" #include "ui_settingsdialog.h" #include #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; namespace { // QFontDatabase::standardSizes() lacks some sizes, like 13, which QtWK uses by default. constexpr int AvailableFontSizes[] = {9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 20, 22, 24, 26, 28, 30, 32, 34, 36, 40, 44, 48, 56, 64, 72}; constexpr QWebEngineSettings::FontFamily BasicFontFamilies[] = {QWebEngineSettings::SerifFont, QWebEngineSettings::SansSerifFont, QWebEngineSettings::FixedFont}; } // namespace SettingsDialog::SettingsDialog(QWidget *parent) : QDialog(parent) , ui(new Ui::SettingsDialog()) { ui->setupUi(this); setWindowFlags(windowFlags() & ~Qt::WindowContextHelpButtonHint); // Setup signals & slots connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &SettingsDialog::saveSettings); connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &SettingsDialog::loadSettings); connect(ui->buttonBox, &QDialogButtonBox::clicked, this, [this](QAbstractButton *button) { if (button == ui->buttonBox->button(QDialogButtonBox::Apply)) saveSettings(); }); // Fonts ui->defaultFontComboBox->addItem(tr("Serif"), QStringLiteral("serif")); ui->defaultFontComboBox->addItem(tr("Sans-serif"), QStringLiteral("sans-serif")); ui->defaultFontComboBox->addItem(tr("Monospace"), QStringLiteral("monospace")); ui->minFontSizeComboBox->addItem(tr("None"), 0); for (int fontSize : AvailableFontSizes) { ui->fontSizeComboBox->addItem(QString::number(fontSize), fontSize); ui->fixedFontSizeComboBox->addItem(QString::number(fontSize), fontSize); ui->minFontSizeComboBox->addItem(QString::number(fontSize), fontSize); } // Fix tab order. setTabOrder(ui->defaultFontComboBox, ui->fontSizeComboBox); setTabOrder(ui->fontSizeComboBox, ui->serifFontComboBox); // Disable global shortcut settings if not supported. if (!QxtGlobalShortcut::isSupported()) { ui->globalHotKeyGroupBox->setEnabled(false); ui->globalHotKeyGroupBox->setToolTip(tr("Global shortcuts are not supported on the current platform.")); } QWebEngineSettings *webSettings = QWebEngineProfile::defaultProfile()->settings(); // Avoid casting in each connect. auto currentIndexChangedSignal = static_cast(&QComboBox::currentIndexChanged); auto syncStandardFont = [this, webSettings](QWebEngineSettings::FontFamily fontFamily, const QFont &font) { const int index = ui->defaultFontComboBox->currentIndex(); if (BasicFontFamilies[index] == fontFamily) { webSettings->setFontFamily(QWebEngineSettings::StandardFont, font.family()); } }; connect(ui->defaultFontComboBox, currentIndexChangedSignal, this, [webSettings](int index) { const QString fontFamily = webSettings->fontFamily(BasicFontFamilies[index]); webSettings->setFontFamily(QWebEngineSettings::StandardFont, fontFamily); }); connect(ui->serifFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::SerifFont, font.family()); syncStandardFont(QWebEngineSettings::SerifFont, font); }); connect(ui->sansSerifFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::SansSerifFont, font.family()); syncStandardFont(QWebEngineSettings::SansSerifFont, font); }); connect(ui->fixedFontComboBox, &QFontComboBox::currentFontChanged, this, [webSettings, syncStandardFont](const QFont &font) { webSettings->setFontFamily(QWebEngineSettings::FixedFont, font.family()); syncStandardFont(QWebEngineSettings::FixedFont, font); }); connect(ui->fontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { webSettings->setFontSize(QWebEngineSettings::DefaultFontSize, AvailableFontSizes[index]); }); connect(ui->fixedFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { webSettings->setFontSize(QWebEngineSettings::DefaultFixedFontSize, AvailableFontSizes[index]); }); connect(ui->minFontSizeComboBox, currentIndexChangedSignal, this, [webSettings](int index) { const int fontSize = index == 0 ? 0 : AvailableFontSizes[index-1]; webSettings->setFontSize(QWebEngineSettings::MinimumFontSize, fontSize); }); loadSettings(); } SettingsDialog::~SettingsDialog() { delete ui; } void SettingsDialog::chooseCustomCssFile() { const QString file = QFileDialog::getOpenFileName(this, tr("Choose CSS File"), ui->customCssFileEdit->text(), tr("CSS Files (*.css);;All Files (*.*)")); if (file.isEmpty()) return; ui->customCssFileEdit->setText(QDir::toNativeSeparators(file)); } void SettingsDialog::chooseDocsetStoragePath() { QString path = QFileDialog::getExistingDirectory(this, tr("Open Directory"), ui->docsetStorageEdit->text()); if (path.isEmpty()) { return; } #ifdef PORTABLE_BUILD // Use relative path if selected directory is under the application binary path. if (path.startsWith(QCoreApplication::applicationDirPath() + QLatin1String("/"))) { const QDir appDirPath(QCoreApplication::applicationDirPath()); path = appDirPath.relativeFilePath(path); } #endif ui->docsetStorageEdit->setText(QDir::toNativeSeparators(path)); } void SettingsDialog::loadSettings() { const Core::Settings * const settings = Core::Application::instance()->settings(); // General Tab ui->startMinimizedCheckBox->setChecked(settings->startMinimized); ui->checkForUpdateCheckBox->setChecked(settings->checkForUpdate); ui->systrayGroupBox->setChecked(settings->showSystrayIcon); ui->minimizeToSystrayCheckBox->setChecked(settings->minimizeToSystray); ui->hideToSystrayCheckBox->setChecked(settings->hideOnClose); ui->toolButton->setKeySequence(settings->showShortcut); ui->docsetStorageEdit->setText(QDir::toNativeSeparators(settings->docsetPath)); // Tabs Tab ui->openNewTabAfterActive->setChecked(settings->openNewTabAfterActive); // Search Tab ui->fuzzySearchCheckBox->setChecked(settings->isFuzzySearchEnabled); // Content Tab for (int i = 0; i < ui->defaultFontComboBox->count(); ++i) { if (ui->defaultFontComboBox->itemData(i).toString() == settings->defaultFontFamily) { ui->defaultFontComboBox->setCurrentIndex(i); break; } } ui->serifFontComboBox->setCurrentText(settings->serifFontFamily); ui->sansSerifFontComboBox->setCurrentText(settings->sansSerifFontFamily); ui->fixedFontComboBox->setCurrentText(settings->fixedFontFamily); ui->fontSizeComboBox->setCurrentText(QString::number(settings->defaultFontSize)); ui->fixedFontSizeComboBox->setCurrentText(QString::number(settings->defaultFixedFontSize)); ui->minFontSizeComboBox->setCurrentText(QString::number(settings->minimumFontSize)); switch (settings->contentAppearance) { case Core::Settings::ContentAppearance::Automatic: ui->appearanceAutoRadioButton->setChecked(true); break; case Core::Settings::ContentAppearance::Light: ui->appearanceLightRadioButton->setChecked(true); break; case Core::Settings::ContentAppearance::Dark: ui->appearanceDarkRadioButton->setChecked(true); break; } ui->highlightOnNavigateCheckBox->setChecked(settings->isHighlightOnNavigateEnabled); ui->customCssFileEdit->setText(QDir::toNativeSeparators(settings->customCssFile)); switch (settings->externalLinkPolicy) { case Core::Settings::ExternalLinkPolicy::Ask: ui->radioExternalLinkAsk->setChecked(true); break; case Core::Settings::ExternalLinkPolicy::Open: ui->radioExternalLinkOpen->setChecked(true); break; case Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser: ui->radioExternalLinkOpenDesktop->setChecked(true); break; } ui->useSmoothScrollingCheckBox->setChecked(settings->isSmoothScrollingEnabled); // Network Tab switch (settings->proxyType) { case Core::Settings::ProxyType::None: ui->noProxySettings->setChecked(true); break; case Core::Settings::ProxyType::System: ui->systemProxySettings->setChecked(true); break; case Core::Settings::ProxyType::Http: ui->manualProxySettings->setChecked(true); ui->proxyTypeHttpRadioButton->setChecked(true); break; case Core::Settings::ProxyType::Socks5: ui->manualProxySettings->setChecked(true); ui->proxyTypeSocks5RadioButton->setChecked(true); break; } ui->proxyHostEdit->setText(settings->proxyHost); ui->proxyPortEdit->setValue(settings->proxyPort); ui->proxyRequiresAuthCheckBox->setChecked(settings->proxyAuthenticate); ui->proxyUsernameEdit->setText(settings->proxyUserName); ui->proxyPasswordEdit->setText(settings->proxyPassword); ui->ignoreSslErrorsCheckBox->setChecked(settings->isIgnoreSslErrorsEnabled); } void SettingsDialog::saveSettings() { Core::Settings * const settings = Core::Application::instance()->settings(); // General Tab settings->startMinimized = ui->startMinimizedCheckBox->isChecked(); settings->checkForUpdate = ui->checkForUpdateCheckBox->isChecked(); settings->showSystrayIcon = ui->systrayGroupBox->isChecked(); settings->minimizeToSystray = ui->minimizeToSystrayCheckBox->isChecked(); settings->hideOnClose = ui->hideToSystrayCheckBox->isChecked(); settings->showShortcut = ui->toolButton->keySequence(); settings->docsetPath = QDir::fromNativeSeparators(ui->docsetStorageEdit->text()); // Tabs Tab settings->openNewTabAfterActive = ui->openNewTabAfterActive->isChecked(); // Search Tab settings->isFuzzySearchEnabled = ui->fuzzySearchCheckBox->isChecked(); // Content Tab settings->defaultFontFamily = ui->defaultFontComboBox->currentData().toString(); settings->serifFontFamily = ui->serifFontComboBox->currentText(); settings->sansSerifFontFamily = ui->sansSerifFontComboBox->currentText(); settings->fixedFontFamily = ui->fixedFontComboBox->currentText(); settings->defaultFontSize = ui->fontSizeComboBox->currentData().toInt(); settings->defaultFixedFontSize = ui->fixedFontSizeComboBox->currentData().toInt(); settings->minimumFontSize = ui->minFontSizeComboBox->currentData().toInt(); if (ui->appearanceAutoRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Automatic; } else if (ui->appearanceLightRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Light; } else if (ui->appearanceDarkRadioButton->isChecked()) { settings->contentAppearance = Core::Settings::ContentAppearance::Dark; } settings->isHighlightOnNavigateEnabled = ui->highlightOnNavigateCheckBox->isChecked(); settings->customCssFile = QDir::fromNativeSeparators(ui->customCssFileEdit->text()); if (ui->radioExternalLinkAsk->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Ask; } else if (ui->radioExternalLinkOpen->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::Open; } else if (ui->radioExternalLinkOpenDesktop->isChecked()) { settings->externalLinkPolicy = Core::Settings::ExternalLinkPolicy::OpenInSystemBrowser; } settings->isSmoothScrollingEnabled = ui->useSmoothScrollingCheckBox->isChecked(); // Network Tab // Proxy settings if (ui->noProxySettings->isChecked()) { settings->proxyType = Core::Settings::ProxyType::None; } else if (ui->systemProxySettings->isChecked()) { settings->proxyType = Core::Settings::ProxyType::System; } else if (ui->manualProxySettings->isChecked()) { if (ui->proxyTypeSocks5RadioButton->isChecked()) { settings->proxyType = Core::Settings::ProxyType::Socks5; } else { settings->proxyType = Core::Settings::ProxyType::Http; } } settings->proxyHost = ui->proxyHostEdit->text(); settings->proxyPort = ui->proxyPortEdit->text().toUShort(); settings->proxyAuthenticate = ui->proxyRequiresAuthCheckBox->isChecked(); settings->proxyUserName = ui->proxyUsernameEdit->text(); settings->proxyPassword = ui->proxyPasswordEdit->text(); settings->isIgnoreSslErrorsEnabled = ui->ignoreSslErrorsCheckBox->isChecked(); settings->save(); } zeal-0.7.1/src/libs/ui/settingsdialog.h000066400000000000000000000031111462517734600200130ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SETTINGSDIALOG_H #define ZEAL_WIDGETUI_SETTINGSDIALOG_H #include namespace Zeal { namespace WidgetUi { namespace Ui { class SettingsDialog; } // namespace Ui class SettingsDialog : public QDialog { Q_OBJECT public: explicit SettingsDialog(QWidget *parent = nullptr); ~SettingsDialog() override; public slots: void chooseCustomCssFile(); void chooseDocsetStoragePath(); private: void loadSettings(); void saveSettings(); private: Ui::SettingsDialog *ui = nullptr; friend class Ui::SettingsDialog; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SETTINGSDIALOG_H zeal-0.7.1/src/libs/ui/settingsdialog.ui000066400000000000000000000733201462517734600202120ustar00rootroot00000000000000 Zeal::WidgetUi::SettingsDialog Qt::ApplicationModal 0 0 600 700 Preferences 0 General Startup Start minimized Check for update true Show system tray icon true Minimize to system tray instead of taskbar Hide to system tray on window close Global shortcuts QFormLayout::AllNonFixedFieldsGrow Show Zeal: Click to set shortcut true Docset storage QFormLayout::AllNonFixedFieldsGrow &Directory: docsetStorageEdit true &Browse… Qt::Vertical 20 40 Tabs Behavior Open new tab after active Qt::Vertical 20 40 Search Local search Use fuzzy search (experimental) Qt::Vertical 20 40 Content Style Appearance (requires restart): A&utomatic true &Light &Dark Qt::Horizontal 40 20 &Custom CSS file: customCssFileEdit true Bro&wse… &Highlight on navigate Fonts QLayout::SetMinimumSize De&fault: defaultFontComboBox &Serif: serifFontComboBox QFontComboBox::ScalableFonts Sa&ns-serif: sansSerifFontComboBox QFontComboBox::ScalableFonts &Monospace: fixedFontComboBox QFontComboBox::MonospacedFonts|QFontComboBox::ScalableFonts Si&ze: fontSizeComboBox Qt::Vertical 20 40 Siz&e: fixedFontSizeComboBox Qt::Horizontal 40 20 Minimum f&ont size: minFontSizeComboBox External Link Behavior &Ask every time true Open in desktop &browser O&pen in Zeal Other Use smoo&th scrolling Qt::Vertical 20 40 Network Proxy Settings No prox&y false true &Use system proxy settings true &Manual proxy configuration false Pro&xy host: proxyHostEdit Type: &HTTP true proxyTypeButtonGroup &SOCKS5 proxyTypeButtonGroup Qt::Horizontal 40 20 &Port: proxyPortEdit 65535 &Authentication required false User&name: proxyUsernameEdit false Pass&word: proxyPasswordEdit false false QLineEdit::Password Security Ignore SSL errors Qt::Vertical 20 40 QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok ShortcutEdit QLineEdit
ui/widgets/shortcutedit.h
SettingsDialog QDialog
ui/settingsdialog.h
buttonBox accepted() Zeal::WidgetUi::SettingsDialog accept() 248 254 157 274 buttonBox rejected() Zeal::WidgetUi::SettingsDialog reject() 316 260 286 274 customCssFileBrowseButton clicked() Zeal::WidgetUi::SettingsDialog chooseCustomCssFile() 526 232 299 249 docsetStorageBrowseButton clicked() Zeal::WidgetUi::SettingsDialog chooseDocsetStoragePath() 526 371 299 249 manualProxySettings toggled(bool) manualProxySettingsGroup setEnabled(bool) 299 131 299 222 proxyRequiresAuthCheckBox toggled(bool) proxyUsernameLabel setEnabled(bool) 299 224 66 250 proxyRequiresAuthCheckBox toggled(bool) proxyUsernameEdit setEnabled(bool) 299 224 330 250 proxyRequiresAuthCheckBox toggled(bool) proxyPasswordLabel setEnabled(bool) 299 224 65 278 proxyRequiresAuthCheckBox toggled(bool) proxyPasswordEdit setEnabled(bool) 299 224 330 278
zeal-0.7.1/src/libs/ui/sidebarviewprovider.cpp000066400000000000000000000027571462517734600214240ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "sidebarviewprovider.h" #include "browsertab.h" #include "mainwindow.h" #include "searchsidebar.h" using namespace Zeal; using namespace Zeal::WidgetUi; SidebarViewProvider::SidebarViewProvider(MainWindow *mainWindow) : Sidebar::ViewProvider(mainWindow) , m_mainWindow(mainWindow) { connect(m_mainWindow, &MainWindow::currentTabChanged, this, &SidebarViewProvider::viewChanged, Qt::QueuedConnection); } Sidebar::View *SidebarViewProvider::view(const QString &id) const { if (id == QLatin1String("index")) return m_mainWindow->currentTab()->searchSidebar(); return nullptr; } zeal-0.7.1/src/libs/ui/sidebarviewprovider.h000066400000000000000000000027071462517734600210640ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H #define ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H #include namespace Zeal { namespace WidgetUi { class MainWindow; class SidebarViewProvider : public Sidebar::ViewProvider { Q_OBJECT Q_DISABLE_COPY(SidebarViewProvider) public: explicit SidebarViewProvider(MainWindow *mainWindow); Sidebar::View *view(const QString &id = QString()) const override; private: MainWindow *m_mainWindow = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SIDEBARVIEWPROVIDER_H zeal-0.7.1/src/libs/ui/widgets/000077500000000000000000000000001462517734600162745ustar00rootroot00000000000000zeal-0.7.1/src/libs/ui/widgets/CMakeLists.txt000066400000000000000000000003561462517734600210400ustar00rootroot00000000000000add_library(Widgets STATIC layouthelper.cpp searchedit.cpp shortcutedit.cpp toolbarframe.cpp ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets REQUIRED) target_link_libraries(Widgets Qt${QT_VERSION_MAJOR}::Widgets) zeal-0.7.1/src/libs/ui/widgets/layouthelper.cpp000066400000000000000000000016401462517734600215160ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "layouthelper.h" zeal-0.7.1/src/libs/ui/widgets/layouthelper.h000066400000000000000000000026551462517734600211720ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2019 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_LAYOUTHELPER_H #define ZEAL_WIDGETUI_LAYOUTHELPER_H #include namespace Zeal { namespace WidgetUi { namespace LayoutHelper { template Layout *createBorderlessLayout() { static_assert(std::is_base_of::value, "Layout must derive from QLayout"); auto layout = new Layout(); layout->setContentsMargins(0, 0, 0, 0); layout->setSpacing(0); return layout; } } // namespace LayoutHelper } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_LAYOUTHELPER_H zeal-0.7.1/src/libs/ui/widgets/searchedit.cpp000066400000000000000000000131141462517734600211130ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "searchedit.h" #include #include #include #include #include #include using namespace Zeal; using namespace Zeal::WidgetUi; SearchEdit::SearchEdit(QWidget *parent) : QLineEdit(parent) { setClearButtonEnabled(true); setPlaceholderText(tr("Search")); m_completionLabel = new QLabel(this); m_completionLabel->setObjectName(QStringLiteral("completer")); m_completionLabel->setStyleSheet(QStringLiteral("QLabel#completer { color: gray; }")); m_completionLabel->setFont(font()); connect(this, &SearchEdit::textChanged, this, &SearchEdit::showCompletions); } // Makes the line edit use autocompletions. void SearchEdit::setCompletions(const QStringList &completions) { delete m_prefixCompleter; m_prefixCompleter = new QCompleter(completions, this); m_prefixCompleter->setCompletionMode(QCompleter::InlineCompletion); m_prefixCompleter->setCaseSensitivity(Qt::CaseInsensitive); m_prefixCompleter->setWidget(this); } // Clear input with consideration to docset filters void SearchEdit::clearQuery() { setText(text().left(queryStart())); } void SearchEdit::selectQuery() { if (text().isEmpty()) return; const int pos = hasSelectedText() ? selectionStart() : cursorPosition(); const int queryPos = queryStart(); const int textSize = text().size(); #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) if (pos >= queryPos && selectionEnd() < textSize) { #else const int selectionEnd = hasSelectedText() ? pos + selectedText().size() : -1; if (pos >= queryPos && selectionEnd < textSize) { #endif setSelection(queryPos, textSize); return; } // Avoid some race condition which breaks Ctrl+K shortcut. QTimer::singleShot(0, this, &QLineEdit::selectAll); } bool SearchEdit::event(QEvent *event) { switch (event->type()) { case QEvent::KeyPress: { auto keyEvent = static_cast(event); // Tab key cannot be overriden in keyPressEvent(). if (keyEvent->key() == Qt::Key_Tab) { const QString completed = currentCompletion(text()); if (!completed.isEmpty()) { setText(completed); } return true; } else if (keyEvent->key() == Qt::Key_Escape) { clearQuery(); return true; } break; } case QEvent::ShortcutOverride: { // TODO: Should be obtained from the ActionManager. static const QStringList focusShortcuts = { QStringLiteral("Ctrl+K"), QStringLiteral("Ctrl+L") }; auto keyEvent = static_cast(event); const int keyCode = keyEvent->key() | static_cast(keyEvent->modifiers()); if (focusShortcuts.contains(QKeySequence(keyCode).toString())) { selectQuery(); event->accept(); return true; } break; } default: break; } return QLineEdit::event(event); } void SearchEdit::focusInEvent(QFocusEvent *event) { QLineEdit::focusInEvent(event); // Do not change the default behaviour when focused with mouse. if (event->reason() == Qt::MouseFocusReason) return; selectQuery(); } void SearchEdit::showCompletions(const QString &newValue) { if (!isVisible()) return; const int frameWidth = style()->pixelMetric(QStyle::PM_DefaultFrameWidth); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) const int textWidth = fontMetrics().horizontalAdvance(newValue); #else const int textWidth = fontMetrics().width(newValue); #endif if (m_prefixCompleter) m_prefixCompleter->setCompletionPrefix(text()); const QString completed = currentCompletion(newValue).mid(newValue.size()); #if QT_VERSION >= QT_VERSION_CHECK(5, 11, 0) const QSize labelSize(fontMetrics().horizontalAdvance(completed), size().height()); #else const QSize labelSize(fontMetrics().width(completed), size().height()); #endif const int shiftX = static_cast(window()->devicePixelRatioF() * (frameWidth + 2)) + textWidth; m_completionLabel->setMinimumSize(labelSize); m_completionLabel->move(shiftX, 0); m_completionLabel->setText(completed); } QString SearchEdit::currentCompletion(const QString &text) const { if (text.isEmpty() || !m_prefixCompleter) return QString(); return m_prefixCompleter->currentCompletion(); } int SearchEdit::queryStart() const { const Registry::SearchQuery currentQuery = Registry::SearchQuery::fromString(text()); // Keep the filter for the first Escape press return currentQuery.query().isEmpty() ? 0 : currentQuery.keywordPrefixSize(); } zeal-0.7.1/src/libs/ui/widgets/searchedit.h000066400000000000000000000033621462517734600205640ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SEARCHEDIT_H #define ZEAL_WIDGETUI_SEARCHEDIT_H #include class QCompleter; class QEvent; class QLabel; namespace Zeal { namespace WidgetUi { class SearchEdit : public QLineEdit { Q_OBJECT public: explicit SearchEdit(QWidget *parent = nullptr); void clearQuery(); void selectQuery(); void setCompletions(const QStringList &completions); protected: bool event(QEvent *event) override; void focusInEvent(QFocusEvent *event) override; private slots: void showCompletions(const QString &text); private: QString currentCompletion(const QString &text) const; int queryStart() const; QCompleter *m_prefixCompleter = nullptr; QLabel *m_completionLabel = nullptr; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SEARCHEDIT_H zeal-0.7.1/src/libs/ui/widgets/shortcutedit.cpp000066400000000000000000000061121462517734600215210ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "shortcutedit.h" #include #include using namespace Zeal::WidgetUi; ShortcutEdit::ShortcutEdit(QWidget *parent) : ShortcutEdit(QString(), parent) { } ShortcutEdit::ShortcutEdit(const QString &text, QWidget *parent) : QLineEdit(text, parent) { connect(this, &QLineEdit::textChanged, [this](const QString &text) { #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) m_key = QKeySequence(text, QKeySequence::NativeText)[0]; #else m_key = QKeySequence(text, QKeySequence::NativeText)[0].toCombined(); #endif }); } bool ShortcutEdit::event(QEvent *event) { switch (event->type()) { case QEvent::KeyPress: { auto keyEvent = static_cast(event); switch (keyEvent->key()) { case Qt::Key_Alt: case Qt::Key_Control: case Qt::Key_Meta: case Qt::Key_Shift: return true; default: m_key = keyEvent->key(); m_key |= translateModifiers(keyEvent->modifiers(), keyEvent->text()); setText(keySequence().toString(QKeySequence::NativeText)); } return true; } case QEvent::ShortcutOverride: case QEvent::KeyRelease: case QEvent::Shortcut: return true; default: return QLineEdit::event(event); } } QKeySequence ShortcutEdit::keySequence() const { return QKeySequence(m_key); } void ShortcutEdit::setKeySequence(const QKeySequence &keySequence) { setText(keySequence.toString(QKeySequence::NativeText)); } // Inspired by QKeySequenceEditPrivate::translateModifiers() int ShortcutEdit::translateModifiers(Qt::KeyboardModifiers state, const QString &text) { int modifiers = 0; // The shift modifier only counts when it is not used to type a symbol // that is only reachable using the shift key if ((state & Qt::ShiftModifier) && (text.isEmpty() || !text.at(0).isPrint() || text.at(0).isLetterOrNumber() || text.at(0).isSpace())) { modifiers |= Qt::ShiftModifier; } modifiers |= state & Qt::ControlModifier; modifiers |= state & Qt::MetaModifier; modifiers |= state & Qt::AltModifier; return modifiers; } zeal-0.7.1/src/libs/ui/widgets/shortcutedit.h000066400000000000000000000031071462517734600211670ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Copyright (C) 2013-2014 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_WIDGETUI_SHORTCUTEDIT_H #define ZEAL_WIDGETUI_SHORTCUTEDIT_H #include namespace Zeal { namespace WidgetUi { class ShortcutEdit : public QLineEdit { Q_OBJECT public: explicit ShortcutEdit(QWidget *parent = nullptr); explicit ShortcutEdit(const QString &text, QWidget *parent = nullptr); bool event(QEvent *event) override; QKeySequence keySequence() const; void setKeySequence(const QKeySequence &keySequence); private: int translateModifiers(Qt::KeyboardModifiers state, const QString &text); int m_key = 0; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_SHORTCUTEDIT_H zeal-0.7.1/src/libs/ui/widgets/toolbarframe.cpp000066400000000000000000000007201462517734600214540ustar00rootroot00000000000000#include "toolbarframe.h" #include using namespace Zeal::WidgetUi; ToolBarFrame::ToolBarFrame(QWidget *parent) : QWidget(parent) { setMaximumHeight(40); setMinimumHeight(40); } void ToolBarFrame::paintEvent(QPaintEvent *event) { QWidget::paintEvent(event); // Draw a line at the bottom. QPainter painter(this); painter.setPen(palette().mid().color()); painter.drawLine(0, height() - 1, width() - 1, height() - 1); } zeal-0.7.1/src/libs/ui/widgets/toolbarframe.h000066400000000000000000000006131462517734600211220ustar00rootroot00000000000000#ifndef ZEAL_WIDGETUI_TOOLBARFRAME_H #define ZEAL_WIDGETUI_TOOLBARFRAME_H #include namespace Zeal { namespace WidgetUi { class ToolBarFrame : public QWidget { Q_OBJECT public: explicit ToolBarFrame(QWidget *parent = nullptr); private: void paintEvent(QPaintEvent *event) override; }; } // namespace WidgetUi } // namespace Zeal #endif // ZEAL_WIDGETUI_TOOLBARFRAME_H zeal-0.7.1/src/libs/util/000077500000000000000000000000001462517734600151665ustar00rootroot00000000000000zeal-0.7.1/src/libs/util/CMakeLists.txt000066400000000000000000000007741462517734600177360ustar00rootroot00000000000000# Nothing to moc here, so avoid empty build units. set(CMAKE_AUTOMOC OFF) add_library(Util STATIC plist.cpp sqlitedatabase.cpp # Show headers without .cpp in Qt Creator. caseinsensitivemap.h ) find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Core REQUIRED) target_link_libraries(Util Qt${QT_VERSION_MAJOR}::Core) find_package(SQLite3 REQUIRED) target_link_libraries(Util SQLite::SQLite3) # TODO: Do not export SQLite headers. target_include_directories(Util PUBLIC ${SQLite3_INCLUDE_DIRS}) zeal-0.7.1/src/libs/util/caseinsensitivemap.h000066400000000000000000000026251462517734600212360ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2018 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_UTIL_CASEINSENSITIVEMAP_H #define ZEAL_UTIL_CASEINSENSITIVEMAP_H #include #include namespace Zeal { namespace Util { struct CaseInsensitiveStringComparator { bool operator()(const QString &lhs, const QString &rhs) const { return QString::compare(lhs, rhs, Qt::CaseInsensitive) < 0; } }; template using CaseInsensitiveMap = std::map; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_CASEINSENSITIVEMAP_H zeal-0.7.1/src/libs/util/plist.cpp000066400000000000000000000043021462517734600170240ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "plist.h" #include #include using namespace Zeal::Util; bool Plist::read(const QString &fileName) { QScopedPointer file(new QFile(fileName)); if (!file->open(QIODevice::ReadOnly)) { // TODO: Report/log error m_hasError = true; return false; } QXmlStreamReader xml(file.data()); while (!xml.atEnd()) { const QXmlStreamReader::TokenType token = xml.readNext(); if (token != QXmlStreamReader::StartElement) continue; if (xml.name() != QLatin1String("key")) continue; // TODO: Should it fail here? const QString key = xml.readElementText(); // Skip whitespaces between tags while (xml.readNext() == QXmlStreamReader::Characters); if (xml.tokenType() != QXmlStreamReader::StartElement) continue; QVariant value; if (xml.name() == QLatin1String("string")) value = xml.readElementText(); else if (xml.name() == QLatin1String("true")) value = true; else if (xml.name() == QLatin1String("false")) value = false; else continue; // Skip unknown types insert(key, value); } return m_hasError; } bool Plist::hasError() const { return m_hasError; } zeal-0.7.1/src/libs/util/plist.h000066400000000000000000000024141462517734600164730ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2015-2016 Oleg Shparber ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_UTIL_PLIST_H #define ZEAL_UTIL_PLIST_H #include #include namespace Zeal { namespace Util { class Plist : public QHash { public: Plist() = default; bool read(const QString &fileName); bool hasError() const; private: bool m_hasError = false; }; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_PLIST_H zeal-0.7.1/src/libs/util/sqlitedatabase.cpp000066400000000000000000000142401462517734600206610ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #include "sqlitedatabase.h" #include using namespace Zeal::Util; namespace { constexpr char ListTablesSql[] = "SELECT name" " FROM" " (SELECT * FROM sqlite_master UNION ALL" " SELECT * FROM sqlite_temp_master)" " WHERE type='table'" " ORDER BY name"; constexpr char ListViewsSql[] = "SELECT name" " FROM" " (SELECT * FROM sqlite_master UNION ALL" " SELECT * FROM sqlite_temp_master)" " WHERE type='view'" " ORDER BY name"; // sqlite3_exec() callback used in tables() and views(). auto ListCallback = [](void *ptr, int, char **data, char **) { static_cast(ptr)->append(QString::fromUtf8(data[0])); return 0; }; } // namespace SQLiteDatabase::SQLiteDatabase(const QString &path) { if (sqlite3_initialize() != SQLITE_OK) { return; } if (sqlite3_open16(path.constData(), &m_db) != SQLITE_OK) { updateLastError(); close(); } } SQLiteDatabase::~SQLiteDatabase() { finalize(); close(); } bool SQLiteDatabase::isOpen() const { return m_db != nullptr; } QStringList SQLiteDatabase::tables() { if (m_db == nullptr) { return {}; } QStringList list; char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, ListTablesSql, ListCallback, &list, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return {}; } return list; } QStringList SQLiteDatabase::views() { if (m_db == nullptr) { return {}; } QStringList list; char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, ListViewsSql, ListCallback, &list, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return {}; } return list; } bool SQLiteDatabase::prepare(const QString &sql) { if (m_db == nullptr) { return false; } if (m_stmt != nullptr) { finalize(); } m_lastError.clear(); sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const void *pzTail = nullptr; const int res = sqlite3_prepare16_v2(m_db, sql.constData(), (sql.size() + 1) * 2, // 2 = sizeof(QChar) &m_stmt, &pzTail); sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); if (res != SQLITE_OK) { // "Unable to execute statement" updateLastError(); finalize(); return false; } if (pzTail && !QString(static_cast(pzTail)).trimmed().isEmpty()) { // Unable to execute multiple statements at a time updateLastError(); finalize(); return false; } return true; } bool SQLiteDatabase::next() { if (m_stmt == nullptr) { return false; } sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const int res = sqlite3_step(m_stmt); sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); switch (res) { case SQLITE_ROW: return true; case SQLITE_DONE: case SQLITE_CONSTRAINT: case SQLITE_ERROR: case SQLITE_MISUSE: case SQLITE_BUSY: default: updateLastError(); } return false; } bool SQLiteDatabase::execute(const QString &sql) { if (m_db == nullptr) { return false; } m_lastError.clear(); char *errmsg = nullptr; const int rc = sqlite3_exec(m_db, sql.toUtf8(), nullptr, nullptr, &errmsg); if (rc != SQLITE_OK) { if (errmsg) { m_lastError = QString::fromUtf8(errmsg); sqlite3_free(errmsg); } return false; } return true; } QVariant SQLiteDatabase::value(int index) const { Q_ASSERT(index >= 0); // sqlite3_data_count() returns 0 if m_stmt is nullptr. if (index >= sqlite3_data_count(m_stmt)) { return QVariant(); } sqlite3_mutex_enter(sqlite3_db_mutex(m_db)); const int type = sqlite3_column_type(m_stmt, index); QVariant ret; switch (type) { case SQLITE_INTEGER: ret = sqlite3_column_int64(m_stmt, index); break; case SQLITE_NULL: ret = QVariant(); break; default: ret = QString(static_cast(sqlite3_column_text16(m_stmt, index)), sqlite3_column_bytes16(m_stmt, index) / 2); // 2 = sizeof(QChar) break; } sqlite3_mutex_leave(sqlite3_db_mutex(m_db)); return ret; } QString SQLiteDatabase::lastError() const { return m_lastError; } void SQLiteDatabase::close() { sqlite3_close(m_db); m_db = nullptr; } void SQLiteDatabase::finalize() { sqlite3_finalize(m_stmt); m_stmt = nullptr; } void SQLiteDatabase::updateLastError() { if (m_db == nullptr) { return; } m_lastError = QString(static_cast(sqlite3_errmsg16(m_db))); } sqlite3 *SQLiteDatabase::handle() const { return m_db; } zeal-0.7.1/src/libs/util/sqlitedatabase.h000066400000000000000000000033371462517734600203330ustar00rootroot00000000000000/**************************************************************************** ** ** Copyright (C) 2016 Jerzy Kozera ** Contact: https://go.zealdocs.org/l/contact ** ** This file is part of Zeal. ** ** Zeal is free software: you can redistribute it and/or modify ** it under the terms of the GNU General Public License as published by ** the Free Software Foundation, either version 3 of the License, or ** (at your option) any later version. ** ** Zeal is distributed in the hope that it will be useful, ** but WITHOUT ANY WARRANTY; without even the implied warranty of ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** GNU General Public License for more details. ** ** You should have received a copy of the GNU General Public License ** along with Zeal. If not, see . ** ****************************************************************************/ #ifndef ZEAL_UTIL_SQLITEDATABASE_H #define ZEAL_UTIL_SQLITEDATABASE_H #include #include struct sqlite3; struct sqlite3_stmt; namespace Zeal { namespace Util { class SQLiteDatabase { Q_DISABLE_COPY(SQLiteDatabase) public: explicit SQLiteDatabase(const QString &path); virtual ~SQLiteDatabase(); bool isOpen() const; QStringList tables(); QStringList views(); bool prepare(const QString &sql); bool next(); bool execute(const QString &sql); QVariant value(int index) const; QString lastError() const; sqlite3 *handle() const; private: void close(); void finalize(); void updateLastError(); sqlite3 *m_db = nullptr; sqlite3_stmt *m_stmt = nullptr; QString m_lastError; }; } // namespace Util } // namespace Zeal #endif // ZEAL_UTIL_SQLITEDATABASE_H zeal-0.7.1/vcpkg.json000066400000000000000000000004431462517734600144770ustar00rootroot00000000000000{ "$schema": "https://raw.githubusercontent.com/microsoft/vcpkg-tool/main/docs/vcpkg.schema.json", "name": "zeal", "version": "0.7.1", "dependencies": [ { "name": "libarchive", "default-features": false }, "openssl", "sqlite3", "vulkan-headers" ] }