pax_global_header00006660000000000000000000000064146605024710014517gustar00rootroot0000000000000052 comment=52c4448b8d2d85ed5a896250def342f276a03334 lektor-lektor-52c4448/000077500000000000000000000000001466050247100145745ustar00rootroot00000000000000lektor-lektor-52c4448/.codecov.yml000066400000000000000000000002741466050247100170220ustar00rootroot00000000000000# https://codecov.io/gh/lektor/lektor/ comment: false coverage: precision: 2 range: - 70.0 - 100.0 round: down status: changes: false patch: true project: true lektor-lektor-52c4448/.editorconfig000066400000000000000000000007621466050247100172560ustar00rootroot00000000000000# -*- mode: conf-unix; -*- # EditorConfig is awesome: http://EditorConfig.org # top-most EditorConfig file root = true # defaults [*] end_of_line = lf insert_final_newline = false indent_style = space [*.{ini,py,rst}] indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true max_line_length = 88 [*.{js,jsx}] indent_size = 2 [*.{html,css}] indent_size = 2 trim_trailing_whitespace = true insert_final_newline = true [*.yml] indent_size = 2 trim_trailing_whitespace = true lektor-lektor-52c4448/.git-blame-ignore-revs000066400000000000000000000016451466050247100207020ustar00rootroot00000000000000# Since version 2.23 (released in August 2019), git-blame has a feature # to ignore or bypass certain commits. # # This file contains a list of commits that are not likely what you # are looking for in a blame, such as mass reformatting or renaming. # You can set this file as a default ignore file for blame by running # the following command. # # $ git config blame.ignoreRevsFile .git-blame-ignore-revs # 2021-09-18 - update pylint #19486da795ba5dff7a8abf39302e3ec2c6429dd0 # (commented out due to non-cosmetic changes related to adding explict # encoding arguments.) # 2021-02-06 - remove useless object inheritance from classes 3ae5397f4b7344cdd2a745d458e19b8dfe9be2e1 # 2020-11-12 - js: reformat all files with prettier 7d83658c4fc95b3dc6dc7f8fa3d16e5a471c944f # 2020-10-23 - Beautify code again (#823) 3faff8fae62604dcc234b9c2cd22588592c5572b # 2020-03-28 - lint: update standard caffd8a1259e8aa82635306c522cefd42dca857f lektor-lektor-52c4448/.github/000077500000000000000000000000001466050247100161345ustar00rootroot00000000000000lektor-lektor-52c4448/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000017761466050247100217500ustar00rootroot00000000000000 ### Issue(s) Resolved Fixes # ### Related Issues / Links ### Description of Changes - [ ] Wrote at least one-line docstrings (for any new functions) - [ ] Added unit test(s) covering the changes (if testable) - [ ] Included a screenshot or animation (if affecting the UI, see [Licecap](https://www.cockos.com/licecap/)) - [ ] Link to corresponding documentation pull request for [getlektor.com](https://github.com/lektor/lektor-website) lektor-lektor-52c4448/.github/workflows/000077500000000000000000000000001466050247100201715ustar00rootroot00000000000000lektor-lektor-52c4448/.github/workflows/ci.yml000066400000000000000000000124661466050247100213200ustar00rootroot00000000000000name: Tests master on: # This avoids having duplicate builds for a pull request push: branches: - master - "*-maintenance" pull_request: branches: - master - "*-maintenance" jobs: ############################################################################ # Lint jobs ############################################################################ lint: name: lint runs-on: ubuntu-latest strategy: fail-fast: false steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "lts/*" cache: "npm" cache-dependency-path: "**/package-lock.json" - name: Install node dependencies run: make frontend/node_modules - uses: actions/setup-python@v5 with: # pylint==2.11.1 (pinned in tox.ini) will only run with python <=3.10 python-version: "3.10" cache: "pip" cache-dependency-path: "**/setup.cfg" - uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit|${{ env.pythonLocation }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Install python dependencies id: pip run: | python -m pip install --upgrade pip python -m pip install tox pre-commit python -m pip freeze --local - name: Run pylint run: tox -e lint - name: Run pre-commit # run pre-commit check even if pylint check fails if: steps.pip.outcome == 'success' run: pre-commit run --show-diff-on-failure --color=always --all-files ############################################################################ # Node tests ############################################################################ node: name: ${{ matrix.os}} node-${{ matrix.node }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: node: ["lts/*"] os: ["ubuntu-latest", "macos-latest", "windows-latest"] include: - node: "current" os: "ubuntu-latest" steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node }} cache: "npm" cache-dependency-path: "**/package-lock.json" - name: Build frontend run: make - name: Typecheck and run frontend tests run: make test-js ############################################################################ # Python tests ############################################################################ python-tests: name: ${{ matrix.os }} py${{ matrix.python }} runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: os: ["ubuntu-latest"] python: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] include: - python: "3.6" os: "ubuntu-20.04" - python: "3.8" os: "macos-latest" - python: "3.12" os: "macos-latest" - python: "3.7" os: "windows-latest" - python: "3.12" os: "windows-latest" - python: "3.12" install-imagemagick: true steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} cache: "pip" cache-dependency-path: "**/setup.cfg" - name: Install macOS system dependencies if: startsWith(runner.os, 'macos') && matrix.install-imagemagick run: brew install imagemagick ffmpeg - name: Install Windows system dependencies if: startsWith(runner.os, 'windows') && matrix.install-imagemagick run: | choco install --no-progress --timeout 600 imagemagick.app ffmpeg # The imagemagick.app package, for whatever reason, installs # magick.exe into a directory which is not in the default # search path. Currently, it seems to get installed in a # directory named something like: # # "C:\Program Files\ImageMagick-7.1.0-Q16-HDRI" $ImDirs = ( Get-ChildItem $env:ProgramFiles 'ImageMagick*' -Directory | Select-Object -ExpandProperty FullName ) if ($ImDirs.Length -eq 0) { Throw "Could not find path to ImageMagick" } $ImDirs | Out-File $env:GITHUB_PATH utf8 -Append $ImDirs | % { "::notice title=ImageMagick::ImageMagick installed at $_" } continue-on-error: true - name: Workaround for UnicodeDecodeError from tox on Windows # Refs: # https://github.com/lektor/lektor/pull/933#issuecomment-923107580 # https://github.com/tox-dev/tox/issues/1550 if: startsWith(runner.os, 'windows') run: Out-File $env:GITHUB_ENV utf8 -Append -InputObject 'PYTHONIOENCODING=utf-8' - name: Install python dependencies run: | python -m pip install --upgrade pip python -m pip install tox tox-gh-actions coverage[toml] - name: Run python tests run: tox - name: Generate coverage.xml shell: bash run: | coverage combine --append || true coverage xml - name: Publish coverage data to codecov uses: codecov/codecov-action@v4 with: token: ${{ secrets.CODECOV_TOKEN }} lektor-lektor-52c4448/.github/workflows/publish.yml000066400000000000000000000013551466050247100223660ustar00rootroot00000000000000name: Upload Python Package on tags on: push: tags: - "v*" jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: actions/setup-python@v5 with: python-version: 3.x - uses: actions/setup-node@v4 with: node-version: "lts/*" - name: Build frontend run: make - name: Install dependencies run: python -m pip install tox twine - name: Build release artefacts run: tox -e build-dist - name: Publish release env: TWINE_USERNAME: __token__ TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} run: | python -m twine upload dist/* lektor-lektor-52c4448/.gitignore000066400000000000000000000003131466050247100165610ustar00rootroot00000000000000.DS_Store *~ *.pyc *.egg-info *.pex .cache #* build dist venv lektor/admin/static example-project coverage/ .coverage .coverage.* coverage.xml coverage.lcov .nyc_output .pytest_cache .tox node_modules lektor-lektor-52c4448/.pre-commit-config.yaml000066400000000000000000000013471466050247100210620ustar00rootroot00000000000000repos: - repo: https://github.com/ambv/black rev: "22.12.0" hooks: - id: black - repo: https://github.com/asottile/reorder_python_imports rev: "v3.9.0" hooks: - id: reorder-python-imports - repo: https://github.com/pycqa/flake8 rev: "6.0.0" hooks: - id: flake8 language_version: python3 - repo: local hooks: - id: eslint name: eslint language: node entry: ./frontend/node_modules/eslint/bin/eslint.js --fix --max-warnings 0 files: \.(ts|tsx)$ - id: prettier name: prettier language: node entry: ./frontend/node_modules/prettier/bin-prettier.js --write --list-different files: \.(ts|tsx|js|less|ya?ml|md)$ lektor-lektor-52c4448/CHANGES.md000066400000000000000000000747101466050247100161770ustar00rootroot00000000000000# Changelog These are all the changes in Lektor since the first public release. ## 3.3.12 (2024-08-18) - Test under python 3.12 - Fix the admin API so that the `/matchurl` endpoint doesn't fail with an exception when passwed a URL to a non-Record artifact (e.g. a VirtualSourceObject). ## 3.3.11 (2024-02-27) ### Security Prior to this release it was possible to create files outside of the `content` tree using the admin API. (Normally, the admin API should not be made accessible to untrusted parties, since the point of the API to to allow for editing of the Lektor project content.) - Better sanitation of DB file paths, better validation of path passed to `make_editor_session`. ([#1180]) - Better validation of API parameters. ([#1182]) [#1180]: https://github.com/lektor/lektor/pull/1180 [#1182]: https://github.com/lektor/lektor/pull/1182 ## 3.3.10 (2023-05-05) - Address recent deprecations of various bits of `werkzeug.urls`, avoiding use of the deprecated functions where possible. ([#1144], [#1142]) - Pin `werkzeug<2.41`. Since the deprecated `werkzeug.urls.URL` is part of our Publisher API, we can not disuse that without changing public API. ([#1144]) [#1142]: https://github.com/lektor/lektor/issues/1142 [#1144]: https://github.com/lektor/lektor/pull/1144 ## 3.3.9 (2023-04-16) ### Bit-Rot - Fix installation of local plugins (from the `packages/` subdirectory). This was broken by release 23.1 of `pip` which dropped support for the `--install-option` flag to `pip install`. ([#1129], [#1127]) ### Bugs - Implement better input validation for the date/time-formatting jinja filters. Prior to this, passing a `jinja2.Undefined` value to the `date`, `time`, or `datetime` filters would elicit an assertion error. ([#1122], [#1121]) - Fix for spurious rebuilds. Recent versions of watchdog (>=2.3.0) enabled tracking of IN_OPEN events. These fire when a file is opened — even just for reading. Now we're pickier about only responding to events that indicate file modifications. ([#1117]) [#1117]: https://github.com/lektor/lektor/pull/1117 [#1121]: https://github.com/lektor/lektor/issues/1121 [#1122]: https://github.com/lektor/lektor/pull/1122 [#1127]: https://github.com/lektor/lektor/issues/1127 [#1129]: https://github.com/lektor/lektor/pull/1129 ## 3.3.8 (2023-02-28) Test under python 3.11. ([#1084][]) ### Bugs Fixed #### Plugins - `PluginController.emit` would, under certain circumstances, silently ignore `TypeError`s thrown by plugin hook methods. ([#1086][], [#1085][]) #### Packaging - List all directly imported packages as dependencies. Fixes [#1109][]. [#1084]: https://github.com/lektor/lektor/pull/1084 [#1085]: https://github.com/lektor/lektor/issues/1085 [#1086]: https://github.com/lektor/lektor/pull/1086 [#1109]: https://github.com/lektor/lektor/issues/1109 ## 3.3.7 (2022-09-21) ### Bugs #### Dev Server - Remove frontend javascript rebuild machinery altogether. Since the frontend source code is not included in distributed Lektor wheels this was breaking `LEKTOR_DEV` functionality for all but those running Lektor from git. ([#1072][]) [#1072]: https://github.com/lektor/lektor/pull/1072 ## 3.3.6 (2022-07-27) ### Changes #### Admin Frontend - The _save_ hotkey (`-s`) now always switches to the _preview_ view. Previously, the _save_ hotkey was disabled unless there were changes to be saved. ([#1022][]) ### Bugs #### Admin Frontend - Fix for spurious page scrolling when typing in textareas. ([#1038][], [#1050][]) - Make `size = {small|large}` field option work again. ([#1022][]) - Fix the _edit_ (`-e`) hotkey. ([#1022][]) - Handle hotkeys when the preview iframe has the focus. ([#1022][]) #### Dev Server - Fix building of javascript when devserver is run with `LEKTOR_DEV` set. Broken since Lektor 3.3.2 by PR [#1003][]. [#1022]: https://github.com/lektor/lektor/issues/1022 [#1038]: https://github.com/lektor/lektor/issues/1038 [#1050]: https://github.com/lektor/lektor/pull/1050 ## 3.3.5 (2022-07-18) ### Bugs #### Admin Server - Fix 404 from `/admin`. ([#1043][], [#1044][]) [#1043]: https://github.com/lektor/lektor/issues/1043 [#1044]: https://github.com/lektor/lektor/pull/1044 ## 3.3.4 (2022-05-02) ### Bugs #### Bit-rot - Fixes for `click==8.1.3`. ([#1031][], [#1033][]) [#1031]: https://github.com/lektor/lektor/issues/1031 [#1033]: https://github.com/lektor/lektor/pull/1033 ## 3.3.3 (2022-03-29) ### Bugs #### Bit-rot - Fixes for `werkzeug>=2.1.0`. ([#1019][], [#1018][]) #### CI - Update pre-commit config to use `black==22.3.0` to avoid breakage caused by `click>=8.1.0`. ([#1019][]) [#1018]: https://github.com/lektor/lektor/issues/1018 [#1019]: https://github.com/lektor/lektor/pull/1019 ## 3.3.2 (2022-03-01) ### Features #### Command Line - Enabled the [Jinja debug extension][jinja-dbg-ext] when the `LEKTOR_DEV` env var is set to 1 and `lektor server` is used. ([#984][]) ### License - The wording in the LICENSE file was standardized to that of the current [BSD 3-Clause License][bsd]. ([#972][]) ### Bugs #### Markdown Renderer - Fix overzealous HTML-entity escaping of link and image attributes. ([#989][]) #### Admin API - Fix a bug in `make_editor_session` when editing non-existant pages with a non-primary alt. ([#964][]) - Fix the ability to add an initial flowblock to a page. (Broken in 3.3.1.) - Refactor API views to move business logic back into the `Tree` adapter ([#967][]). This fixes [#962][]. #### Admin UI - Changed the structure of the URLs used by the GUI single-page app ([#976][]). This fixes problems with the "edit" pencil when using alternatives ([#975][]), and issues when page ids include colons ([#610][]). - Other React refactors and fixes ([#988][]). #### Database - Fix `Attachment.url_path` when _alternatives_ are in use. There is only one copy of each attachment emitted — the `url_path` should always be that corresponding to the _primary alternative_. ([#958][]) - `Pad.get`, if not passed an explicit value for the `alt` parameter, now returns the record for the _primary alternative_ rather than the fallback record. Similarly, `Pad.root` now returns the root record for the _primary alternative_. ([#958][], [#965][]) - Fix for uncaught `OSError(error=EINVAL)` on Windows when `Pad.get` was called with a path containing characters which are not allowed in Windows filenames (e.g. `<>*?|\/":`). #### Builder - Pages now record a build dependency on their datamodel `.ini` file. - Fix sqlite version detection so that we use "without rowid" optimization with current versions of sqlite. ([#1002][]) #### Command Line - When running `lektor dev new-theme`: fix check for ability to create symlinks under Windows. ([#996][]) - Fix _rsync_ publisher when deletion enabled on macOS. ([#946][], [#954][]) #### Tests - Fix for test failures when git is not installed. ([#998][], [#1000][]) ### Refactorings - Cleaned up `EditorSession` to split mapping methods (for access to record data) to a separate class, now available as `EditorSession.data`. ([#969][]) #### Testing - Cleaned up and moved our `pylint` and `coverage` configuration to `pyproject.toml`. ([#990][], [#991][]) #### Admin UI - Move frontend source from `lektor/admin/static/` to `frontend/`. Compiled frontend code moved from `lektor/admin/static/gen/` to 'lektor/admin/static/`. ([#1003][]) #### Packaging - Omit `example` subdirectory, frontend source code, developer-centric config files, as well as other assorted cruft from sdist. ([#986][]) [bsd]: https://opensource.org/licenses/BSD-3-Clause [#610]: https://github.com/lektor/lektor/issues/610 [#946]: https://github.com/lektor/lektor/issues/946 [#954]: https://github.com/lektor/lektor/pull/954 [#958]: https://github.com/lektor/lektor/pull/958 [#962]: https://github.com/lektor/lektor/issues/962 [#964]: https://github.com/lektor/lektor/pull/964 [#965]: https://github.com/lektor/lektor/issues/965 [#967]: https://github.com/lektor/lektor/pull/967 [#969]: https://github.com/lektor/lektor/pull/969 [#972]: https://github.com/lektor/lektor/pull/972 [#975]: https://github.com/lektor/lektor/issues/975 [#976]: https://github.com/lektor/lektor/pull/976 [#984]: https://github.com/lektor/lektor/pull/984 [#986]: https://github.com/lektor/lektor/pull/986 [#988]: https://github.com/lektor/lektor/pull/988 [#989]: https://github.com/lektor/lektor/pull/989 [#990]: https://github.com/lektor/lektor/pull/990 [#991]: https://github.com/lektor/lektor/pull/991 [#996]: https://github.com/lektor/lektor/pull/996 [#998]: https://github.com/lektor/lektor/issues/998 [#1000]: https://github.com/lektor/lektor/pull/1000 [#1002]: https://github.com/lektor/lektor/pull/1002 [#1003]: https://github.com/lektor/lektor/pull/1003 [jinja-dbg-ext]: https://jinja.palletsprojects.com/en/latest/extensions/#debug-extension ## 3.3.1 (2022-01-09) ### Bugs Fixed - Fixed an import cycle which caused in `ImportError` if `lektor.types` was imported before `lektor.environemnt`. [#974][] #### Deprecations - Disuse deprecated `Thread.setDaemon()`. [#979][] #### Admin UI - Fix spastic scroll behavior when editing flow elements. [#640][] - Fix admin GUI when page contains an unknown flowblock type. [#968][] - Fix admin GUI layout on mobile devices. [#981][] #### Tests - Increased timeout in `test_watcher.IterateInThread` to prevent random spurious failures during CI testing. - Fix `tests/test_prev_next_sibling.py` so as to allow running multiple test runs in parallel. - Use per-testenv coverage files to prevent contention when running `tox --parallel`. - Mark tests that require a working internet connections with pytest mark `requiresinternet`. [#983][] ### Refactors #### Admin UI - Finish rewriting React class-based components to function-based components. [#977][] - Finish adding types for all API endpoints. [#980][] - Remove disused event-source polyfill. [#640]: https://github.com/lektor/lektor/issues/640 [#968]: https://github.com/lektor/lektor/issues/968 [#974]: https://github.com/lektor/lektor/pull/974 [#977]: https://github.com/lektor/lektor/pull/977 [#979]: https://github.com/lektor/lektor/pull/979 [#980]: https://github.com/lektor/lektor/pull/980 [#981]: https://github.com/lektor/lektor/pull/981 [#983]: https://github.com/lektor/lektor/pull/983 ## 3.3.0 (2021-12-14) This release drops support for versions of Python before 3.6. In particular, Python 2.7 is no longer supported. Quite a few bugs have been fixed since the previous release. The Admin UI has seen a major refactor and various performance optimisations. It has been rewritten in Typescript, and updated to use v5 of the Bootstrap CSS framework. ### Bugs Fixed #### Database - Fix queries with offset but without a limit. [#827][] - Fix the handling of deferred (descriptor-type) model fields when used in `slug_format` and when used as a sort key. [#789][] - Refrain from issuing warning about future change in implicit image upscaling behavior in cases that do not involve upscaling. [#885][] - Fix bug with translation fallback of record label. [#897][] #### Data Modelling - Fixed pagination issue which caused child-less paginated pages to not be built. [#952][] #### Publisher - Allow rsync deployment to a local path. [#830][] - Clean up subprocess handling in `lektor.publisher`. This fixes "ResourceWarning: unclosed file" warnings which were being emitted when using the rsync publisher, as well as possible other buglets. [#896][] #### Command Line - Fix circular imports in `lektor.cli` to allow its use as an executable module (`python -m lektor.cli`). [#682][], [#856][] - Fall back to `watchdog` `PollingObserver` if default `Observer` type fails to start. This fixes "OSError: inotify watch limit reached" and perhaps other similar failures. [#861][], [#886][] #### Plugins - Fix the `Plugin.emit` method so that it works. [#859][] - Reword the (previously incomprehensible) exception message emitted when attempting to load a plugin from an improperly named distribution. [#875][], [#879][] #### Build System - Fix bug in `lektor.sourcesearch.find_files` which was causing intermittent exceptions. [#895][], [#897][] #### Miscellaneous - Fix reference cycle in `Environment`. [#882][] - Fix "unclosed file" `ResourceWarning`s. [#898][] #### Admin UI - Fix the checkboxes widget. They were broken so as to be uncheckable. [#812][], [#817][] - Fix page data being incorrectly marked as _changed_ when flow block is expanded/collapsed in the edit UI. [#828][], [#842][] - Fix encoding of URLs when opening the admin UI from the pencil button. [#815][], [#837][] - Rename a CSS class in the admin UI to prevent breakage by ad blockers. The class `add-block` was being blocked by the _EasyList FR_ ad blocker. [#785][], [#841][] - Relax URL checking to allow all valid URLs in URL fields. [#793][], [#840][], [#864][] - Preview iframe was not always updating when it should. [#844][], [#846][] - Make the "Save" button always visible (without need to scroll on long pages). [#43][], [#870][] - Disable the "Save" button unless there are changes. [#872][] - Add "<ctl>-e" hotkey shortcut to edit page. [#876][] - Update UI to Bootstrap v4. (This fixes a layout issue with the date picker.) [#648][], [#884][] - Fix edit page failure for select and checkbox widgets with no choices. [#890][], [#900][] - Update UI to Bootstrap v5. [#917][], [#926][] - Add missing translation strings, show error dialogs on top of other dialogs [#934][]. ### Internal changes #### Python code - Drop python 2 compatibility. [#822][], [#850][], [#871][], [#920][], [#922][] - Drop python 3.5 compatibility. [#878][], [#880][] - Support python 3.10. [#938][] - Switch to [PEP-518][]-compatible (pyproject.toml) build process. [#933][], [#942][] - Code beautification/reformatting. We now use [black][], [reorder-python-imports][], [flake8][], _and_ [pylint][]. [#823][], [#916][], [#925][], [#936][] - Refactor rsync publisher tests. [#836][] - Restructure code to prevent circular imports. [#856][], [#871][], [#873][] - Minor docstring fixes. [#874][] - Enabled pylint's no-self-use policy. [#887][] #### JS code - We now require node >= 14. [#940][] - Update NPM/JS dependencies. Update to webpack v5. [#816][], [#834][], [#848][], [#852][], [#860][], [#905][], [#917][], [#926][], [#945][], [#957][] - Use [prettier][] and [eslint][] for JS (and YAML) beautification and style enforcement. [#825][], [#936][] - Disuse unmaintained `jsdomify` to prevent hanging tests. [#839][] - Disuse jQuery. [#851][] - Convert JS code to Typescript. Various other refactoring and cleanups. [#857][], [#869][], [#872][] - Refactor code to handle hotkeys. [#876][] ### Dependencies - Relax `werkzeug<1` to `werkzeug<3`. [#829][], [#833][], [#911][], [#923][] - Drop support for Python 2. [#818][], [#819][] - We now require `Jinja2>=3.0`. [#921][] ### Testing/CI - Use `tox` for local testing. [#824][] - Pin version of `pylint` used for tests. [#891][] - Complete rewrite of the tests for `lektor.pluginsystem` to increase coverage and reduce running time. [#881][]. - Test under Python 3.9. [#845][] - Test under Node v14 and v16. [#852][], [#927][] - Do not run `brew update` in MacOS CI workflow. [#853][] - CI workflow simplification. [#927][] [black]: https://black.readthedocs.io/en/stable/ [pylint]: https://pylint.org/ [reorder-python-imports]: https://github.com/asottile/reorder_python_imports [flake8]: https://flake8.pycqa.org/en/latest/ [prettier]: https://prettier.io/ [eslint]: https://eslint.org/ [pep-518]: https://www.python.org/dev/peps/pep-0518/ [#43]: https://github.com/lektor/lektor/issues/43 [#648]: https://github.com/lektor/lektor/issues/648 [#682]: https://github.com/lektor/lektor/issues/682 [#785]: https://github.com/lektor/lektor/issues/785 [#789]: https://github.com/lektor/lektor/pull/789 [#793]: https://github.com/lektor/lektor/issues/793 [#812]: https://github.com/lektor/lektor/issues/812 [#815]: https://github.com/lektor/lektor/issues/815 [#816]: https://github.com/lektor/lektor/pull/816 [#817]: https://github.com/lektor/lektor/pull/817 [#818]: https://github.com/lektor/lektor/issues/818 [#819]: https://github.com/lektor/lektor/pull/819 [#822]: https://github.com/lektor/lektor/pull/822 [#823]: https://github.com/lektor/lektor/pull/823 [#824]: https://github.com/lektor/lektor/pull/824 [#825]: https://github.com/lektor/lektor/pull/825 [#827]: https://github.com/lektor/lektor/pull/827 [#828]: https://github.com/lektor/lektor/issues/828 [#829]: https://github.com/lektor/lektor/issues/829 [#830]: https://github.com/lektor/lektor/pull/830 [#833]: https://github.com/lektor/lektor/pull/833 [#834]: https://github.com/lektor/lektor/pull/834 [#836]: https://github.com/lektor/lektor/pull/836 [#837]: https://github.com/lektor/lektor/pull/837 [#839]: https://github.com/lektor/lektor/pull/839 [#840]: https://github.com/lektor/lektor/pull/840 [#841]: https://github.com/lektor/lektor/pull/841 [#842]: https://github.com/lektor/lektor/pull/842 [#844]: https://github.com/lektor/lektor/issues/844 [#845]: https://github.com/lektor/lektor/pull/845 [#846]: https://github.com/lektor/lektor/pull/846 [#848]: https://github.com/lektor/lektor/pull/848 [#850]: https://github.com/lektor/lektor/pull/850 [#851]: https://github.com/lektor/lektor/pull/851 [#852]: https://github.com/lektor/lektor/pull/852 [#853]: https://github.com/lektor/lektor/pull/853 [#856]: https://github.com/lektor/lektor/pull/856 [#857]: https://github.com/lektor/lektor/pull/857 [#859]: https://github.com/lektor/lektor/pull/859 [#860]: https://github.com/lektor/lektor/pull/860 [#861]: https://github.com/lektor/lektor/issues/861 [#864]: https://github.com/lektor/lektor/pull/864 [#869]: https://github.com/lektor/lektor/pull/869 [#870]: https://github.com/lektor/lektor/pull/870 [#871]: https://github.com/lektor/lektor/pull/871 [#872]: https://github.com/lektor/lektor/pull/872 [#873]: https://github.com/lektor/lektor/pull/873 [#874]: https://github.com/lektor/lektor/pull/874 [#875]: https://github.com/lektor/lektor/pull/875 [#876]: https://github.com/lektor/lektor/pull/876 [#878]: https://github.com/lektor/lektor/issues/878 [#879]: https://github.com/lektor/lektor/pull/879 [#880]: https://github.com/lektor/lektor/pull/880 [#881]: https://github.com/lektor/lektor/pull/881 [#882]: https://github.com/lektor/lektor/pull/882 [#884]: https://github.com/lektor/lektor/pull/884 [#885]: https://github.com/lektor/lektor/pull/885 [#886]: https://github.com/lektor/lektor/pull/886 [#887]: https://github.com/lektor/lektor/pull/887 [#890]: https://github.com/lektor/lektor/issues/890 [#891]: https://github.com/lektor/lektor/pull/891 [#895]: https://github.com/lektor/lektor/issues/895 [#896]: https://github.com/lektor/lektor/pull/896 [#897]: https://github.com/lektor/lektor/pull/897 [#898]: https://github.com/lektor/lektor/pull/898 [#900]: https://github.com/lektor/lektor/pull/900 [#905]: https://github.com/lektor/lektor/pull/905 [#911]: https://github.com/lektor/lektor/pull/911 [#916]: https://github.com/lektor/lektor/pull/916 [#917]: https://github.com/lektor/lektor/pull/917 [#920]: https://github.com/lektor/lektor/pull/920 [#921]: https://github.com/lektor/lektor/pull/921 [#922]: https://github.com/lektor/lektor/pull/922 [#923]: https://github.com/lektor/lektor/pull/923 [#925]: https://github.com/lektor/lektor/pull/925 [#926]: https://github.com/lektor/lektor/pull/926 [#927]: https://github.com/lektor/lektor/pull/927 [#933]: https://github.com/lektor/lektor/pull/933 [#934]: https://github.com/lektor/lektor/pull/934 [#936]: https://github.com/lektor/lektor/pull/936 [#938]: https://github.com/lektor/lektor/pull/938 [#940]: https://github.com/lektor/lektor/pull/940 [#942]: https://github.com/lektor/lektor/pull/942 [#945]: https://github.com/lektor/lektor/pull/945 [#952]: https://github.com/lektor/lektor/pull/952 [#957]: https://github.com/lektor/lektor/pull/957 ## 3.2.3 (2021-12-11) ### Compatibility - Restore python 2.7 compatibility. It was broken in leketor 3.2.2. [#951][] - Pin inifile>=0.4.1 to support python 3.10 [#943][], [#953][] [#943]: https://github.com/lektor/lektor/issues/943 [#951]: https://github.com/lektor/lektor/pull/951 [#953]: https://github.com/lektor/lektor/pull/953 ## 3.2.2 (2021-09-18) ### Packaging - Fixes a problem with the uploaded wheel in 3.2.1. ### Compatibility - Fixes to support werkzeug 2.x. [#911][] ## 3.2.1 (2021-09-18) ### Compatibility - Pin `pytest-click<1` for python 2.7. [#924][] - Fixes to support werkzeug 1.x. [#833][] ### Bugs - Allow rsync deployment to a local path. [#830][], [#836][] - Fix queries with offset but without a limit. [#827][] ### Admin UI - Fix select and checkboxes widgets when choices is empty. [#900][] - Update npm packages. [#848][], [#834][], [#816][] - Fix updating of the preview iframe. [#846][] - Allow `ftps:` and `mailto:` URLs in url fields. [#840][] - Fix the toggling of flow widgets in the admin UI to not mark the content as changed. [#842][] - Rename CSS class to prevent conflict with EasyList FR adblock list. [#841][] - Fix the handling of the URLs when opening the admin UI from the pencil button. [#837][] - Fix the checkboxes widget in the admin UI. [#817][] ### Testing / CI - Test under python 3.9. [#845][] - Various CI test fixes. [#932][], [#839][], [#832][], [#826][] - Added a `tox.ini`. [#824][] Code Reformatting - Blackify, reorder-python-imports, flake8. [#823][] - Reformatted js code with prettier. [#825][] - Update pylint config. [#822][] [#826]: https://github.com/lektor/lektor/pull/826 [#832]: https://github.com/lektor/lektor/pull/832 [#924]: https://github.com/lektor/lektor/pull/924 [#932]: https://github.com/lektor/lektor/pull/932 ## 3.2.0 Release date 20th of August, 2020 - Fix off-by-one error in pagination's iter_pages in the interpretation of the right_current argument, and adding an appropriate trailing `None` for some uses. - Add support for setting an output_path in the project file. - Replaced the slugify backend to handle unicode more effectively. This may break some slugs built from unicode. - Several modernization and performance improvements to the admin UI - Improved speed of source info updates. - Set colorspace to sRGB for thumbnails. - Now stripping profiles and comments from thumbnails. - Added support for deleting and excluding files for the rsync deployment publisher. - Improved speed of flow rendering in the admin UI. - Bugfix to correctly calculate relative urls from slugs that contain dots. - Bugfix to allow negative integers in integer fields in the admin UI. - Improved image-heavy build speeds by reducing the amount of data extracted from EXIFs. - Added the ability to collapse flow elements in the admin UI. - Now `extra_flags` is passed to all plugin events. - Extra flags can now be passed to the `clean` and `dev shell` CLI commands. - Bugfix where `lektor plugins reinstall` triggered `on_setup_env` instead of just reinstalling plugins. - Added the ability to generate video thumbnails with ffmpeg. - Added `mode` and `upscale` thumbnail arguments, changing the preferred method to crop to using `mode`. `mode` can be `crop`, `fit`, or `stretch`. `upscale=False` can now prevent upscaling. - Added a new CLI command `lektor dev new-theme`. - Made admin use full UTF-8 version of RobotoSlab. Fixes missing glyphs for some languages - Bumped minimum Jinja2 version to 2.11 - Bumped filetype dependency to 1.0.7 because of API changes - Relative urls are now as short as possible. - Changed default slug creation to use slugify. This should mean greater language support, but this may produce slightly different results than before for some users - Automatically include setup.cfg configured for universal wheels when creating plugins ## 3.1.3 Release date 26th of January, 2019 - Release with universal build. ## 3.1.2 Release date 7th of September 2018 - Fix pagination and virtual pathing for alts - Fixing deply from local server in Python 3 - Now passing server_info to publisher from local server, providing better support for plugin provided publishers. - Added a more full-featured example project. - Adding Jinja2 `do` extension. - Better new-plugin command. - More tests. - Added the ability to sort child pages in admin according to models. - Better image handling and info detection for JPGs and SVGs - Lektor can now be ran with `python -m lektor` - New plugins now come with a more full featured setup.py ## 3.1.1 Release date 18th of April 2018 - Better Image dimension detection. - Fix backwards compatibility with thumbnail generation. - Adding safety check when runnning new build in non-empty dir since that could delete data. - Adding command aliases. ## 3.1.0 Release date 29th of January 2018. - Adding ability to use Lektor Themes. - Adding Markdown event hook between instantiating the Renderer and creating the Markdown Processor - Improving tests for GitHub deployment. - Added the ability to use IPython in the lektor dev shell if it's available. - Added ability to publish from different filesystems. - Adding new option to turn disable editing fields on alternatives. - Added automated testing for Windows. - Expanded automated testing environments to Python 2.7, 3.5, 3.6, & Node 6, 7, 8. - Windows bugfixes. - Improved exif image data. - Improved date handling in admin. - Make GitHub Pages branch detection case insensitive. - Set sqlite isolation to autocommit. - Fixed errors in the example project. - Enabling pylint and standard.js. - Improved image rotation. - Now measuring tests and pull requests with code coverage. - Thumbnails can now have a defined quality. - Moved Windows cache to local appdata. - README tweaks. - Beter translations. - Better file tracking in watcher. - Upgraded many node dependencies. - Upgraded from ES5 to ES6. - Added mp4 attachment type. - Bugfixes for Python 3. ## 3.0.1 Released on 13th of June 2017. - Bugfixes and improved Python 2 / 3 compatibility ## 3.0 Released on 15th of July 2016. - Switch to newer mistune (markdown parser). - Rename `--build-flags` to `--extra-flags`, allow the deploy command to also accept extra flags. ## 2.4 Released on 7th of July 2016. - Resolved an issue with unicode errors being caused by the quickstart. ## 2.3 Released on 31st of May 2016 - Fixed an issue with `get_alts` not being available in the template environment. ## 2.2 Released on 12th of April 2016 - Corrected an issue where certain translations would not make the admin panel load. ## 2.1 Released on 12th of April 2016 - Fixed a code signing issue on OS X 10.10.3 and lower. ## 2.0 Released on 11th of April 2016 - Added `_discoverable` system field which controls if a page should show up in `children`. The default is that a page is discoverable. Setting it to `False` means in practical terms that someone needs to know the URL as all collection operations will not return it. - Added `for_page` function to pagination that returns the pagiantion for a specific page. - Make pagination next_page and prev_page be None on the edges. - Allow plugins to provide publishers. - Added `|markdown` filter. - Added French translations. - Unicode filenames as final build artifacts are now explicitly disallowed. - Serve up a 404.html as an error page in the dev server. - Improvements to the path normalization and alt handling. This should support URL generation in more complex cases between alts now. - Show a clearer error message when URL generation fails because a source object is virtual (does not have a path). - Empty text is now still valid markdown. - Lektor clean now loads the plugins as well. - Basic support for type customization. - Fields that are absent in a content file from an alternative are now pulled from the primary content file. - Development server now resolves index.html for assets as well. - Markdown processing now correctly adjusts links relative to where the rendered output is rendered. - Added Dutch translations. - Added Record.get_siblings() - Added various utilties: build_url, join_path, parse_path - Added support for virtual paths and made pagination work with it. - Added support for Query.distinct - Add support for pagination url resolving on root URL. - Server information can now also contain extra key/value pairs that can be used by publishers to affect the processing. - The thumbnails will now always have the correct width and height set as an attribute. - added datetime type - added support for the process_image utility functions so that plugins can use it directly. - added support for included_assets and excluded_assets in the project file. - added Spanish translations. - added Japanese translations. - added support for discovering existing alts of sources. - added support for image cropping. - added preliminary support for publishing on windows. - children and attachments can now have a hidden flag configured explicitly. Attachments will also no longer inherit the hidden flag of the parent record as that is not a sensible default. - changed internal sqlite consistency mode to improve performance on HDDs. - allow SVG files to be treated as images. This is something that does not work in all situations yet (in particular thumbnailing does not actually do anything for those) ## 1.2.1 Released on 3rd of February 2016 - Bugfix release primarily for OS X which fixes a code signing issue. ## 1.2 Released on 1st of February 2016 - Fixed an error that caused unicode characters in the project name to be mishandled in the quickstart. - Do not create empty folders when the quickstart skips over files. - Empty values for the slug field now pull in the default. - Corrected a bug in hashing in the FTP publisher that could cause files to not upload correctly. - Improved error message for when imagemagick cannot be found. - Fixed scrolling in the admin for firefox and some other browsers. - Fixed a problem with deleting large projects due to sqlite limitations. - Fixed admin preview of root page in firefox. - Changed FTPS data channel to use TLS. ## 1.1 Released on 27th of December 2015 - Fixed a bug where resolving URL paths outside of alts did not fall back to asset resolving. - verbose mode now correctly displays traceback of build failures. - Fixed a bug that caused build failures not to be remembered. - Fixed a bad EXIF attribute (longitude was misspelt) - Use requests for URL fetching instead of urllib. This should fix some SSL errors on some Python versions. - Parent of page now correctly resolves to the right alt. - Publish from a temporary folder on the same device which solves problems on machines with `/tmp` on a different drive. ## 1.0 Released on 21st of December 2015 - Improved ghpages and rsync deployments. - Implemented options for default URL styles. - All artifacts now depend on the project file. - Fixed an issue with renames from tempfile in the quickstart. ## 0.96 Initial test release. Release date 19th of December 2015 lektor-lektor-52c4448/LICENSE000066400000000000000000000027261466050247100156100ustar00rootroot00000000000000Copyright (c) 2015-2016 by the Armin Ronacher. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. lektor-lektor-52c4448/MANIFEST.in000066400000000000000000000005161466050247100163340ustar00rootroot00000000000000# NB: Everything in git gets automatically included by setuptools_scm prune frontend graft lektor/admin/static global-exclude *.py[cdo] __pycache__ *.so .DS_Store .gitkeep *~ prune .github exclude .gitignore .git-blame-ignore-revs exclude .codecov.yml .editorconfig .pre-commit-config.yaml exclude Makefile pylintrc prune example lektor-lektor-52c4448/Makefile000066400000000000000000000020151466050247100162320ustar00rootroot00000000000000all: build-js .PHONY: build-js build-js: frontend/node_modules @echo "---> building static files" @cd frontend; npm run webpack frontend/node_modules: frontend/package-lock.json @echo "---> installing npm dependencies" @cd frontend; npm install @touch -m frontend/node_modules # Run tests on Python files. test-python: @echo "---> running python tests" tox -e py # Run tests on the Frontend code. test-js: frontend/node_modules @echo "---> running javascript tests" @cd frontend; npx tsc @cd frontend; npm test .PHONY: lint # Lint code. lint: pre-commit run -a tox -e lint .PHONY: test test: lint test-python test-js .PHONY: test-all # Run tests on all supported Python versions. test-all: test-js pre-commit run -a tox # This creates source distribution and a wheel. dist: build-js setup.cfg MANIFEST.in rm -r build dist python -m build # Before making a release, CHANGES.md needs to be updated and # a tag should be created (and pushed with `git push --tags`). .PHONY: upload upload: dist twine upload dist/* lektor-lektor-52c4448/README.md000066400000000000000000000040511466050247100160530ustar00rootroot00000000000000# Lektor [![Tests master](https://github.com/lektor/lektor/workflows/Tests%20master/badge.svg)](https://github.com/lektor/lektor/actions?query=workflow%3A%22Tests+master%22) [![Code Coverage](https://codecov.io/gh/lektor/lektor/branch/master/graph/badge.svg)](https://codecov.io/gh/lektor/lektor) [![PyPI version](https://badge.fury.io/py/Lektor.svg)](https://pypi.org/project/Lektor/) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/Lektor.svg)](https://pypi.org/project/Lektor/) [![Code Style: Black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Join the chat at https://gitter.im/lektor/lektor](https://badges.gitter.im/lektor/lektor.svg)](https://gitter.im/lektor/lektor?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) Lektor is a static website generator. It builds out an entire project from static files into many individual HTML pages and has a built-in admin UI and minimal desktop app. To see how it works look at the top-level `example/` folder, which contains a showcase of the wide variety of Lektor's features. For a more complete example look at the [lektor/lektor-website](https://github.com/lektor/lektor-website) repository, which contains the sourcecode for the official lektor website. ## How do I use this? For installation instructions head to the official documentation: - [Installation](https://www.getlektor.com/docs/installation/) - [Quickstart](https://www.getlektor.com/docs/quickstart/) ## Want to develop on Lektor? This gets you started (assuming you have Python, pip, Make and pre-commit installed): ``` $ git clone https://github.com/lektor/lektor $ cd lektor $ virtualenv venv $ . venv/bin/activate $ pip install --editable . $ make build-js $ pre-commit install $ export LEKTOR_DEV=1 $ cp -r example example-project $ lektor --project example-project server ``` If you want to run the test suite (you'll need tox installed): ``` $ tox ``` lektor-lektor-52c4448/example/000077500000000000000000000000001466050247100162275ustar00rootroot00000000000000lektor-lektor-52c4448/example/Example.lektorproject000066400000000000000000000004041466050247100224310ustar00rootroot00000000000000[project] name = Example [alternatives.en] name = English primary = yes locale = en_US [alternatives.fr] name = French url_prefix = /fr/ locale = fr [alternatives.de] name = German url_prefix = /de/ locale = de [packages] lektor-markdown-highlighter = 0.1 lektor-lektor-52c4448/example/assets/000077500000000000000000000000001466050247100175315ustar00rootroot00000000000000lektor-lektor-52c4448/example/assets/static/000077500000000000000000000000001466050247100210205ustar00rootroot00000000000000lektor-lektor-52c4448/example/assets/static/favicon.png000066400000000000000000000132721466050247100231600ustar00rootroot00000000000000PNG  IHDR))@CyiCCPICC Profile(}M+DaBCaYLs͋;WFYb6Ȇ'`#J),$eoAHy r<9ꦙ*tƶ"AmrjZs㦉*Ս9"%*^v(Z\,k@IpaZpm*Vz|JxMqy~ԌG'šoFJK{j8Z4>&e&Tx Y"D#t'|t ;lb'6 i|]~P{bnA}\1݇4s[SST+ {|Gq o8o{|gŽ>ػU=-hw7gF ` pHYs  YiTXtXML:com.adobe.xmp 1 L'YIDATx_UWևO231yΤb%cD-"M)*H *PQ D40wLD)s>s{v'>====================ACmg''g$g'_>r:𩜃'O\ڟ/)uS!_MC== J.{𼳫xZIc!ۦ{-ȃOY Okoqt::2喸|Ըn~10+>Ynn=Ώ}q_=C_|&yWWE.+Od`M{쑶+x3K{zx;u_ܸiB.iEsNn/=|)ZuIvuIjY)\GSUJޱ4uOW[.()@4eᚁX]1jWlJHɺW+ͭS=ĸgG[ٌgilD۹W{ ,?$Eqq  -PXn~g1;?xc^:ڞIg=>|==5LCUM=?Ks'@s5>u)owE81}\^{rI-dE ~=pi/=xy"ّq~T.g ㆳ/#q^M7l@ `&܃-^m0?s p %Qˋ=ٽh2>^ir\. [pț-߿=#JX@<{m)}S{!G<.mȾl03Od/CP=)JˤmiNڶ}Wk^߼iFsjq3Ѭ~qԷ{(tC~H)m{qq1*.91A3LZY &.`2I=215UʕgqOx\_+=IKs]`]G=Uq5w+cE:|&Jvq*XD=%9?pώ^; 8g;PGp^,ȃ&Ȫ-g/ңK")g#?ɕ$'lD>2%{=L؅+q&=bAv=[m"I,GD F9zgmL-a3pb< Z4-{{* Ѵ|qfZԢii=0n> LEQ)YL~rh`n=+S,wvQ&P*YIZ>HYɓT9,c%[{L S=f"WNGޣ?s79nNYÓsY!ᑙtgG޸gR|&G {fտ&cT$2%xS|h9x,I\X#$|lܳQqE3p/}gxJ3qO9u/Y@቙{Pxgr_h:{Uw?|$ܓe;;?sq1ܳ5n)>-= ȄFN"g(A3;{QWYE{OB3s`zܳiX LWG|V%'4} q:sPL=!?9ʶy;6tL4=ɥtOԷm#u{Ӧ-4*ad+:'l@CAKs{efe'Տx&p2=a<!rns !ҷ6aˑ8d`aGb o|T$+>uO4a!}Oif6MwO8\G K4׺(-uOpdxɅˈɕ&." rR4<x5 7R&"3ѱs~ޛsUy-r2ӕF2qTʍ,fk8$M5u#f ˕gg '78tހc*>՛+>(`&}cTe 9O>noBrɎ2<gTHՏȔQv{,D½>nܪһ4<}_x{}nxOFI݊m^Xj:ҕ9n^w P^!nw*l T9S:)s`ke½^>rvlf~:0Jd?{~Wxm;Ewqw.zx8*mͭlplUr1PG\^F?Gz<܃|䴜0$._{:A$J{$N]yƽ_d߽hge)2N dҁw5*yiT:t!Yrx~G#~r3vLaC R{lB3lͤS0)plulАoF>5}3=5~'gXLKf(|oDDfftܳ/2UV{ ˻eAA(~eU`M/r {\^)埁Q}bø<#jnW%넲b;kiy,u1{ +іX).J^8jћk&Lyӳ,NZldlAo J.}&YI18utE/+4[I7?):k%ʸV}>ח@|,;]|DsG3Qί)?]e.{AH*\iLLH`q ]RF3Y7bvYV+/O#8*˞dfGuVLy&$-'e6l@XKٌ8dFulWw27imS*0p䩯Y W) &.Y$Sc9Փ nܻE.NvL.0={ى'h'˲ٚe_Dũ։D+M}\THq͝ 4;KꝊ:wN:MH1{AiwL.2prZ/r I'K',g,r^'8 RӾާrךWF1M7rSs[9][KV/{ e~S*]t ᠰ[ؓ_7*Q/q&.00AgD!^YQ#_cCb5u1X#E=nJI2!om䀿|s_-gYߧU,Cr_؇wU{]j3C3b%!ݞL9DuSx#gvP pHYs   IDATx}-Yku=sι q%+\#a-,*#) v@"ccH1)RJ\VaQ*!CdX1]!0_=k׳kz4sU93{^Yz^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!B!B!B!B![j҃9::3C !4M2M(9gQUQWDU% ""): 4s~^~>1S!XkfR}/1FQUI)8ulӜs]O:QU9v}/"bZv9uQlkW{uu;b-n/IAVv !Hg&I)Iy!l-ӈHV+~0 s}/JܺuK̬qEU[}xx(1F9c?ڶ?8ׯ͛7el#X~u}cƍ""30 g ywQiZ^aB!B!KF׃Ne%Ȗ :1V5%FCԊV^m?uٖ`B3!\z^nDK"f^kV-ZQ·~vB} zBǤ3!z!ۋ8$1ؾWm01kj#`ċ~[c/o> 惶x ӆTDI.i!B!BmzJx:i 4A*z:{Ԋ;3KnF8mEX8R l3tX qŸ'ܘKb5n,>h: r zg+*׳öio! qt^Kι:ovzY G?֭>[~8qsN^rP/yyuCm'~"lin3Z߹Ps,okK!B!s*}"[n>B"Rb:;uz![`0T>ۺg1lVrhH=$m䷇_$Jpea/ciXN:Ck&#/:)yI9q|CnEw}ߟu.qxia;]m[l/J1 >Xh5?],>k-m;9UZ׺l(B!B!du@!Rx3M"PED<.V@U;HϢB^kEٔl6-qӋas~8-*0,滞%zAq( `B۝v;yA_;tmg ǩ ^ۂF63{l KB:x+![KYKY߾}nݪ/9_[ \^}Q0l#/mȖZߦ-^jzt}|VӶ/^VlsCy|Ԅf[/}wZv.ԥj_q/m _>;|z1>,o x{SUu4lFu} !B!Bs7^mDB-ði *\q.`K-ssk`SҊ"Rp!.9'];%p.EQwo9B1%,[˥yKo_+BYJIz] byt^W/ajݜKNg/"f) 8LEDKПRt ۷Uϊ"aɭourppP5`ǰeٜ*emhU׻+0HYCr^UeP%B!Bs]xCU-n=X[%p+R2OљSYmuvQ LKV]R>c=P#Vk^"/?+"ڒ[u%|VMd{~mj; ~9i/B֊u.E\%7_iݶKեyw.iaSgKXOw=8z8 !wjsVJ1΋W1ZJ kP.ƨ)%B!ByNp]7t[""8Q iӈqEt5! aɑ.:ժJ)ɭ[$缕:(rBXįjUHmn]<^;C>waoC|<<<U-!g{xSVFFZqfk|j^$Y|eWȫmVoi폵6~8̿px%7ۯ fHfc=NviI[nTJA33+N^+̥moʐ;V}/GGGgnB!B!wKZ{ABzRJ:M\^P]'U!&Acu6½aQxՙ sxgzѻ<b0=2ߊ|KX/ o/ԶNc]@T,/|ylW/X'Yh ލ$Yٽ^nV8Eˑx79qa"*puOӴ/}u^b_g̣4Lf!ηlwRJrm+Il,"s"ΪovN}m!B!BBje*JvMU5J5`[E(Q)Q NԭgZ h݃wzG(N9c!l];}aOd;fɵ$D#)vxqЋ9S\!c>ο]?ϊirr֊(FfB_r⨟fzӷǩ? ڷui~z/H/( gA;%G8ma}f/mo] ]dj1"ffyMUB;)`w޼y1 B!B\+\zEX,7 "aPD܀򾊈+2 (α$$fE[B$Z!i1zjϛRajW= b@׺!ba  1 PB FmE ` 6*C4#؋v):gz!Ol֑F|~9C =Zv+sn[׵M1/AaHu> ^kcJ&SXf~T)A෕ m.q}EnwEJN-h*-SJ:sP.B0dwlc!cruuE^B!B!5.Lu:Grf4J9R(QU58OT`U Eܩ^E 6"8>^8yx'wz;!I?t/NjޑBfl^`E޺lX o i?v{[^{R1-~Bq"r?M['(DQ/&۩ Xv{cimN}}i>v>3M[u9h!lfR> =b>}M)Y !B!B. xƒ{׉*BВi )h0 "1 78"WB^4MUsn!͖[q".Y/t"ayq.[ͦNz5QU}vubmKcr1mH .Nj^x 't+A"f9X\!8ChkQպV(bfq A|Ygbp Xw=<-|9v.[l7:{k1U_<sfn=co/ Ch.RJv||,!SU[Vu]VU1OӔsRD"~&g3 8f9)(iw1B!B!nr!!t\ubܻ*'N^`f!,"F2BH)2_L-lY)1@"KBwP|-պ3[ȉpyk"Xmݕ^`lnO/lY>B{^|($y1o!|.rmC~[-ڶm36lrKߗQt- `V9q{yiY5+q VS^UBPsVFSUw&+B!B!\4"j&\y|(2a "!B/ү?x,\tցN a*ZZLR_19s^ɶ0nN,^7="ɶH=̩., 'S{JV湵~>l$n ۦο~["ޭW#p!Oߴ{nl;mgf[[hۼcuX:Vݴd8Vsx "s=eRr,X,T4>w?m.~BXV984M:t^͛gmAB!B!sq!o "s5jo2tnøzEBcNhf"⧠++U1>WK4CB6)^XB99g0 9FƬuoB!B!Kz2.+7fB!S`Zi.-9筢x8u:Ualebf2Mt_4Mu!^TUi=|;pUU}6/B! +o .ƨ9gUՐsaB^BȥNJ!ƨiŭSiنaf#w !<5 -E}s^x:?U-B0z-!;>>-#1-x^T,\B!ܳ[V7Y W\nQxYdr9[FUC N-oVB!习*DD{D(OrĊ!RFy!"X~Z\AVUth!-[څ@ afu^t !rOrnw^-_`-*'B%ƴ:UUQSJsax !Iu:J0jf茬V!6:BgB9fEDj&wu#PgZrxxX[nQ%B=Å8xn[n"5kȫ)#!R2M)!n>, (6!B.rzIF2Tޡ[m lu5>"9,_\2e= SU+M!"1/B'8+sT7QΚ5!\0!;M"П ˩}%B.G@-X)N$ƨezx[ㆈT)-bU݊`1jZ)soyAD0d7Y*@E@8k8AB]@SJu]V(¬T="> n_/Ǜ6Ph-X\??'Rtxw䊈7kR2EDDqdX1*xB!#xa7n1FY>G)*Xx !q[QWDN3BEcqEU{-: 4_UU\e"A`|m^D\\a75?8y oy߼+' lZXI" E !B sf&C JqF buw]./^QK !+J}7E4 +^7端!rRT;`YMlV4/b0DڥSUfD[68|Urd\ݼ%®]ořB!{ɅeԬ]1 *2DJ͗!*%EݥgVB 2t54Mtk -'& !@[+{8/p"[39KN5M+o~J\圭vsh, 8Zq2[JIOo޼ɋ2!B x}QZ0;B8حفxg i !\}%u- E wK Pu&/x\rז ,^Fu8{!b:\gs7`%v^8x3B 'N_/JK!B dR`|/L 7/!\r^%ǒRRTGpTGfCBȹ9<<p<'f,GoTJeQU5C^ߜʚ_g !ANT}ATUVl6i+lG1j2Ui,7Lfj.Bv.iBuĽG!W[ENP-])*4Q%\;s:A{PI!SeZUvѩXӔYP KossAUC^KVVƼx8"`0g[L5s<;^wyrEFA,"'b%C=::B!{Ź޾sMdK) 3-:o!n!N@Uqsw]'׮]~zgm'r#ɃjR۷oiB֍"T5t]T5 !Hl6Af4F֩vA-hR4Q{Ŵ0HW@-""ͦMd:K"b1,"yg;o.QBO) Q8J:#E^B! 7ɟB.g9\,e !\m)d{Dd !Dݻw )PYp- IDAT6]m\[{}Dăl3+2}N)Yb9cD0d3KX_X^upjia!-־"B x+ kz}^+ w@*l:;c}p1OPDD{Zd{prxx(Z!"pkw%QDq+;)$j~f 2G8bJ nB^䅸+'4<RJlĻgqD禈B^xv(#^LJf.j]eU4qJpR՚ynu: 󯪪mEeB!/n0zp\ w CBSgˑ_o"ep]X(BD\urppP t]Waq0!A}78rtBT++K^D3F]n>+~x;~5` ƈl^EVj!D)B!xQ7 0ʹɄrPlfVB C9fb^70L\K܅Ջ=KY `qٲ${Ȝ-Xr4Yw#us} _v]ZMfnPc7;U]}e,ŭ]r1aVYy"ouZp}{M!=B2xE+^v7{2sKerεC^:Źe+ ԉ8v!tC 7TݸЊc~ޙ{Cطk>:ñb|Nn7gX/lhX[rʺX#Z­̣vCrpp`]8:͆"/!{H^eG9>>c.m:M4ƨ%AWSJ1Mr5}o^##hB!}w0*>>>FFau]#l!rornA;(J /XYm]>OUDF~zuBH-V*aR\Sũ.bCR\pÅ&r: >}oow+Dk`2WDd+zb[<'r+D$rAgk5B!d0 z-4fm~"DKHgGiQ-G>d?C5;7\ t]DEnNWYASJ{"lppQ pbEf# "s~%>2B=:vUU01 #wڪ92 [qYيפ)%[VB0WqAi2+yy ܨws^95ZwWDBQshhGB!¹^\\^2hsxi.r79ó|No.긼ݭX4!RDspwX79Q ?mhϭ-#?,9OBY~8tCV2y3X1fš6WNј'w !v䛏yU$rغk:A!r9}K${p샫r/p"sS}8mypt9f ep14M[BoJpꌄחS.\/|l69/-XiY5 %uB.nf1XJ2f7H0,"yG9KupY\N'윾eSה뤌5ys)jfZ^k 'EU3f#@!/[fjQ "B !22뼻E=lW'XVq%1 "TUc⹔jepn]C4ը \CP\ 9-ğ[S|;ZŽտnykD6"ZQ1m%RvYUCqǛ}c]0 W#{c=LSG촵w];u+A!O[C/TA`[dQ%\뵤]?V2^!]cib~~T,!h!rAD"~)j!)%UUd'4M2 "!pcuo6_Cr K< ?:±^.suynR'X4J۬l !&\Mql;Pk}o"")%K)Ysa6^e-#$jE)B!YrjN>GB9NJlU zppT-]-NSM)#B9YTՠ1(NWosyʼn$Bh~{cO^458_[vwJ׶sEaUUz!G?J+?i|v=Ϡ8{+hEt#B!Ws pFM+vwgy&f/\^ňjH)U.2pºsb48V17CQDBYЍ"tvP) E4B~{JDZžEU}>C~[oܝ,deă*b^E"1} 8'Տ๒B!¹^KE|Ε7)y;J*V-]/B~_u_u 5z 8Yǒ>O/~HUIh5⏆x%dH)Y3#Nrp v\ BG-o+;{EN\O,Fj抋"h;LADn dv+Tuc|xB7_URJv||l8¡Wc3i"ZEL\ro !XuR1Kȍ_2rQ\.y*Ny &rs KÎp׳.Ny !*qVfR._t⇹apֿ+"; CYQļQԮd'+B_ LUkۿ fpclBK羫nǫa Gwq='(\W^98d8 xsy\m@Yc 5r>ΪTU77?_}w}׋_׼/|+|߸qT>+sYf|5BCu5 np]._DGB>Ɓ=ax'{ C,:YVRYC.;/|K'>GY/G^4% w?ԓ)O}7'S~W>(ܺAu$X+V ݇8{SJi~(4B(BcH)LC>uJC<{?|HU53)d3K8裏>?~}Q/q h/Hu*&݁X~Xtx:1Mnx?WB$hh3n !9x17T=bŇMrx3'/' e_rrVE~կM(DG纔LӴ.9 Nb_ͽx[S )%Q"1\eC !Wٶ_)%[:ڲ|:k&"1\2~,CJ)<ַ'xً_=#/}^zppm !>, n<WCw6 յ [= wr,O~!Zq[)g33M)Z%=/ ʜk׾zu pg.nKs%G>.C;|b|~8չ}߾<"ϰ9ce̴*~g!C<48 ɻ\BuTU%d'e+ c̫Jr6 Cu4M뺠9i 4R93 ˿|}{/qO8F_p➳tXܻRFTrZu2O[l6u2nRJ57^> $ yq?9ϚxcU?Vy|?{r 0G$eZm9Pߠc8nC|+)E 3ɹ}jbzttǿ\vMo߾ B3-Ba[J^ǾD^/ Wx<|~-\~kn=v,?̫/"wV﫻—QPܱ ."B6\8EU:۪d5UE/VD^qEU5YN)eU%GE`Nþq# l3#ɝ9-[SJ d= /KD9P:pMk R5_7a]VU!2^7LhJ~()!^Boj#rx]7Bq?#O~7?؆៛Fa,`t]!Ys6UվĽ %*8Ylff6 C}]iqg^uI pG,E5\ePqUTBVoE[)Bfc׮]Si#Wȋ~DVgyU5o+9e~Qyn̯Cag'[q_>Γ![PmCAsΡ:KB$UY"Zof:sY7{=9 @!{ą9xSJ:xí|^8N>Yy |kag!ɻ]7ăȜC."[]:gj]5x*._f]> 庙PDEd~3/hx*|_Ktu](!fkEPiR)c ZJ4ƨ]ׅ#.jⲪ8dvFHh0RA4XoׂVd^9wc|9go0k !\}έʞj/1CŅ=`.b-ěLo<\j.9IA0uQYT}_xbNRJRZBɈRqU++N4F 3 ð }HddQwPw!r)%+YVߝoƽmq5R59j)* gYi&p\`-t]D$>RJȳV35`|WӢ 28KJ1F?Ѧi"*pj&0Bs m/.X!$φ?|~쵟\U/CI?ӻno y{;&i]ğ2_-(!Z]J),bvT5*~[gcᬇFBN{a('k1](F`ѩ\6Gd3⮔sC 缓vvQ6~Z#׮]J.:]=!H4W/zыg~+_?͹/fqUrl/!;(48"8vEjVPZt[^ۿVb8O *o Zw.^% 8nU~8؃GTb>(!1?~||㪺\5q77o"J7DsGK)YuZ<ŝ!Ķ9!#pj9ʈ +QEdtU>h8?GS~/ϡZqڢ)\pg8̬Zxm09 GBq!J+ꝋ9y^+_MOuS4*_)ߖ_uspP?$Ͽ2uDl  g wO_׼5| ^|oܸg=8u֓O?>]zׯゥ)cf6oAAz*!D !!Ӱ"nY1iXي(Z޾]|Wl6zÏ?F/_uq׃s:Ky/C#)U͛}2!31}x%ٟ#'z'?3?3x{W8IUS1suYD0 T7ղl 4߻KX/H/H문,`Rfμ#/ ڷ (cG~ȶsWHsVu]Swǘs9 w?vs !\}-zQRl- {$~gI7o/|c]_',?tMkc{C[KU[Y.nu9"4QDR95¿}S_L^=7ezի^裏y{c>gVszG>~'__OOέ[ƜsNsHe@Q*HZR< Y*Wwrn3Ri݊(Y]%SJ!SP`UXlf?&"9wfs7緲/ʼn"R#v'OHՐOk8bRk;/õ}I"r xx3EnWE"]!W,3c1jqBq´"b׮]۷oL!\\XkɽywMU3C~.+O9W9PVբK^ũ~-!z^}Md" 1F8LK?<,Ugx"4M;_SI"2$sRšxZijMU_SJYʈhevDg}ZD38ǝC!C9>>>qu]u4{Ipzt]w}DONXBD]ϥ%ZSJcj9gEivv7c1jqҖMG]B!`9KK<(owguSIU/oV}7o CBt"!B4,3~wYboNrΚR$y3~rm.`lײUfau'rΓM)IUR9c*n-wnqBWWnYW1Y#*s}N¥^!W;/bzV=9٣uR%%d8r9 E n@[vզiwB x}$ \~_O#)4~5_pN0duσjfV*GW'W`D uE%Bm<+՞Y2ı@VudG~w)jD$m6)4 0$%wIDrJuݖpc1F(^ުHIj(P^!W[nm@^o] ^ŵ 9ժupf"k_o߾u #^뵨 À[F8SqiBP|Ѿr/VG@u]'}W/!Ϲ^ӽp- t/*_JۿV4\}>/ rt֮re%q凳ElD ̖RRhE ?YSW$t>kj3}wruYU8)4M4T&3KV>zY-DR2X4 ;uxʒ# κq Ʒ,C%DDqV.(t:7 wj?~B5. 3A\,i7>g7Wn q\?6nʕ紥|S$;퓛̬{]VsYϜ nH`qH!AcZRfSJpf+A"q-l]W/Wx }R5UM!B@n1)ƘaH4nZ&Եg4݋Jnc$ < OO+4RJRk%"u#m.8M.}KY1;9RJ: C;k0 Ztڞm3x 1wF!jq;?,w)_qc8ƸþNr?vD^]/!fSo_; ?|Y!WUo׽-_Jsd>|R< ^ͦ:fSWdaRDjFc,%n7*Fw9'fPQQä^0q0E uH!HEЊ1*<~`$Bw9gcu޽TCUS<穪3[< V<w:&2e'"1Y8EHs!z5"r^o= CYٍEK~چkvs}4&a2 ǎBN^s}nۚE 8ԼM!2.RUuhB-Z 7['5 ޷-SS]z]E8qMJĚM蔧=_kP Ǐhx CFDol@B-'o#O9æe`SڢE ((MXeٜ9"AZ(j:XĭNiJι{RYآYul4Mbd?DsKs}@E-w-&L~ b9eYW~n{=dcӇlv%ޭZ ݽEp<ڤnCb՛E%< X+{7`ARCU-Zhjq ט cy"KoL&ZTp pQ5*_/M6z%Ql'~ТE+Պ\= 2Vmex'GroXKg>YolP̭f5-E%P>``T0M_W٭^cfxcCQMr-Z,1 '(̯b-XYkijjfff( J$mGYdc-u)=eYF~Y&x5Yz >fG{!B3NJMJEcЗn+æe|æzCs6`=UJ%s.hPa{$I&Hv5uҭ> 㪮RI։2| C={4hqDn\l}bV1L@UҞ? `>WZ@ԮC& NDqz5{{=_ |>33'IRCR_-W5xzLDFXeۨ^cA&y'?^)MJEtҭe1N/F ,^X#IOc\dS@$ኂ#cLTɥiJ^ƎE`Nd4MǑLTNSjZv7OT T.:;#KFD;/9#O h?uz|Ƙ7YcQ9GIDSW$OOJ!)$q>4MiժUDDk׮ec?n:&Cq{OᐉׇE$N3&- AD aq-'w&nK c x۟eQ_-zR:(prԺwb{,bܱkDgʱOK QkE3|> _!ƪuj%1&()H@D1kVv ZB-3UաVM&XD~~n9UK_cHcѮn]ubdϲ&u ԆJ54tsyN G0EngaIR ÛюK?q.EQ"GX# Ƙ཯k)uNZwَD455 4yG{LtԔ=q~> Zܧ_/]㔨ƙ'1q#m *ٚ5kp8D;Aׁ}9 |VM!Ny\vHQ;aJNLwVkJV5)_v$I8˲F~Y·3E/Zr#~|K> /v d[30.HZvQKۚXX@ /jUW.Z,zkO?QYѯ5bfXcGR&@f>랏ԮATPA>a0H:C)`.KU<&O+|tJZ9æx^N.$I$l8HHIDާ/Ǒ>C?Pb1s?~Bhs$&io]6n:qiJL333( y2RSY^8iae!{o Lc5B33;XYN$I<Ԅ!vd٠ctM~F4R koMM]%~ED %Xhu.;g$Ճ>w*߯4zje.eV1Ј0=RǭB(<ͽ^s{ \ʞx]cހB[[s[58qUW3z(cC}Ub% {$Y0k#X8׫Z/c W"9䜃i1$ u]~衇k /s={ w"?}34iPZ4m{. g:ptfc%15u- V04 ,pWX%8nb1ᜋ6%vu)(θhjA4TW/6<DEQ6]rycL9jS R^SDյ<&7n7ܠ]d!pM6"_X!{/4yE}`D _ȪW)s1<1iD`.V>{A Xx(5Me`Igj*cFΥ ֈʱm(`5Zc1r !W46 U_*BŲ P*'pcUBP}Vەt뎹)B!ľ*?gC$yfC+ԽD'+z>Ą+D^f:Q5E7Q5Fq>Av3s4^juu]tJF%"YYͳsb>fq4MZpH"Ltk1MXNTΎ07qa@T͌ξhИeu 0 |Jz3 VftHy᳁( /KHJup@@c "92$%ZѨz>JV:G_xߤiC?`w}hUTi c w]FЪʵZI=*޽Vj5EJ߰W>_ߜNkcnfggQ:/%BW|q$d r.M\W\TŤ7MؔE"`k %yAD(wsi ̧r.Q.C;DRNz$gbDcжF-14elT+sM%ysT@v+ qsKD+Ou]㈞Hv:N>q*rTJ } 550 ""cLe./3¢($4)oB4իW7]y.ҋXԓyЍtهuk> s|0wFNxXkŨVK=[$.tϘjJm(@ JcZ!APP&# ';hR>!Tv')y+1mAwjRAMO@!TOp B!*p0t'TI W!W+YeEaY"Dd}i`C A6&EQъAMq/ nl IZHz?ev$I2z#$H 6̌)J@A3`0/Ź]%κ( ʲ H,,l`^$Tfdy(&T<L^soL mV Z2QPE$iPg?W/F?z% }'uPΩ$ ƑM>U[CkS-Z /(r$訩"@y&=%(R}սBߑMق_=T={e>勒dV1|?g3m5qmk(ky_Lb]2._bJzNxϸ9ڀ%z6mFD+U0^"W}y|uud >6-횊<^ܗ̞ *!#䤴%_?. n8\vM6?⸦ń'=%xA$IT-|JW+P%KN9J$*_ZT?sSjz,MXQ$?^$} ŋ;&?p,43xB WU))d1;✬ǣ)cje,TN@ʋX $CFrNJ~*wSNy'쬓DTY6}L1WCzskժ,cTn]Y &^s7Uϗz> c އ~jދ>~pc 4&|2 M" SD?q#/12F+4@D孴B{o1VH mz={HHwkmkO%gDd@Py=]lB$Yk.DstMb`\&@z5F6R-&\]"])w^kTƱ]qF^gHDQzךOȍk1Ej܍Cfff*XQTN'>_Dz֢91ež,8A$D1 "2ZFʂFA5%+BD4"` $j.^H Mzx"0PY%x T<F0VYzS7ub~Wa$ TG4M9`A LG2G1εuV|EMy3""c(}u_>Effx,`0j7+. xxDs.eEQ@ 6gEQcqQZ ť1ƀuЕZfn-Kebk!{QnnJ:FdJ˸饽{ a$Di:mPN0搻ҟ*jo) ]CeQFqUdWܼQ{"vq*2h1H:C8@KT ZnPW7-9OκB Km";ǧ&bIJP#j4&E/3B4ER=R7t9#G>SG}͛ׯ_?jժή]>`{_~{?02/}cNHiKJHCpz_z wg#Jr$Ew)UFxfff/fDs8M΃A)Pq:9cx7Ǎ $|G6j| \N$e! }OF)jkr ~:)]\O}y?HTH!K"/!(Wjf!DLEY)jGD^)Z {ɣqJE18'\1^CTU@z>Mɼ{*-'^b(׫\א$Ik|Tbż 19 I+R2~ĹהV<̗2qVZeG=ةC=tjӦMkz^k׮?}3?g~_F=7[$^_H$pIG#\AH|s(7*$ljhdq{Io {?4,gQF"deL.gY1&s!H7q QSף(xff&1P UJBGHSp#s \w]hӓ#>-|֭2==l41ޞ~ v

*D"GU|d12zkmcͲ ^HbIS/>!3^ϐWB܂%E5͛7w8S6o|ʚ5k'I9w}(y?;;wmwqǎ;nF WH⾧i4 < lnl.'klL]M/ytXrqtgØ(0UwUƸI[N"@P=W{x7vHpxpznbwjȸ%YFO2(9*F"BE).O8ǜvigz衏z[1{M>yNMMmݸq#mݺ5vWP׿w;v 7~qH :#KDA( Y,;RhE0%+Bɫ%.4=vMawǀYoZ 'HR]f+IJV?+،*):8z_w*^܈ʄ < ThbB[&+cS%T<ص2Ym#Bq V,(r%/5D6ൡLqM}y.bP >QHDѣ~hx523IQcb>rsPU&/׵jwN X Xk㼪I?xe/{a_|c9Ǭ_ny_܇1c$InժUr!tӓdzK^B>}og|K짾( Zex (ў8TU^]LF}Vқ) $,U+(A*Bhk:vBzlW {ڎpp.HJu\ZDBLa]D2M7^pJN"qgBy>իW_ZZl>eƍؾ};=O,|~!%A7dM+~sIEǂu&B&6C ~MJe'oo&=q FP삄ėMDc0TF8P63Jܧܢ8tA9"҆( vαx|2<DWo]/иX&Q? 7Ikf33n:0ι6l8sÆ gtIۿa׮]wWq׿ n$RaCB!DTE*={ı IК5klEAιXj:EezТ&@Qu k` m -`6{뜳N|c{d7fYf<7ʼnBX)_g?繦h&D)^׳<M5]ޑ쁂4M9/ AB*@f0+ɨ󜡂D!u֭QFw֏64c$x( ޛ͛ l=Xk (̠ *>McU*ws9/|_~C9 A,hl !)}ӞD P5)S'* !*h<+3W?}̶mqGw" Ƙtzzcl>w{O}SЇ>YyR3Ix![uL@IFkizQEi{oc`0/jFĪ#`o㭵pheSc &4<_>/ySzX 0>'.|!I8|{߻|/C1%f 6(S:)ef̓VW䆭I:̳ox.ؾ}SSS,$I:{o||e\E%Ak yp86 IP$s>L"ZWgzufY/ &W ueG5}-9f %eAӇ2QE8Ykuy(E+\:n:8"~> @BZwi-رuR ɔr @Lp0lԛy=,@T\e {eAOFH^B^x罜BD15 hszM%H*KxzzNwIpUW쭗_~sHU<}4\vuUӞ&YZFJ:lXJJY_ͩPjUd.b+߶m۳zQ\}@=|˖-oٲ傳>wo žCH>H^*aխ.&!xg|*E7j&ImJGdGBTR톈9&}Ǹ{;η=6i1I.r]xTm1ܕ?Ǟwy۴i9v`$C(d}ĀK7keW8lO,UT<љ/8h`#N<~/?hP&ı~ĞkUMh+{\UH2zNDUxʖC=ᷘ08 oY/Mx bMI$u0d9}>u衇 3'6m:)OyY{nk_;S_#sDGΔJ7 6I) [HFaE[c*+'mS@U>$IDe!DB=iewC#,OoV {K4`L"B2Zr g܋/9ρ{oGy?Z;::ӿoꗿ~_mb@R])m$0E 5Zhh)v/d$njuMaTif+j\t:Gs$<ͪkFX=3Vx㍯ܶm y-:;+gEȊsU(KHX8~ -K]zR MÑT6,zwL<kT$ߞ'=uiEvmwCE2_!F=i׃~/<+,v$b  Ӛ7}-1M`h6WԦ- $Vj C;MB޴"(Bɒ.D%Q yn@%"kuZ'OI!!t:3̹?>@ w5K?O?O+_ι9c.)frC 3Vc佧N b3 h[59k1(V|w3ŝPT<9g)F2JJkICh QZkrG/6 }k)V^u9;I9Z(mKey].sʼ{uם_؁FֱnݺG_z;9~wxEኢpT["+$e:I\%Id& .nFhZ]qsvݣը^)IÆ\r}Qٯ"rZgq!=nݝw{'%"馛~V'v1ܕcS"W+s1T6XԓZfX~.{>xן ^!]eN% l$Zk,3~^T#eYƲӢONZ䃊l'ZbF~(ui-;Z@R FR" #kwY3SO=uէ?ݺus&駟{v۳/{=B]'eQ(}k3qhp$?Qk-E1^In]^3\[:|\A|0%M!jg5=B;ԼW+H$H(A{7Ki*yq,r5U2EY̵x0=S?Oc}@n!aÆ33//e/{W죹Uzޅ7W{&W؜-:*cuEb.LA$ 9KuhNw8VnF ј,gsg^kn4bff.\0a,ӱj\j~/lBJ^:ὧ2 *rń ӺZVߣxD4t\T+c^I4V}s o^w@cgz 4ʓPA FQ_nbM`zp8teI$?=VZuk^^'>q(L\Q{B.s;p8tY26M]R4MHiZ[(@7]>i v[Yir4NTUg2Zw^cLn 5 J<24;;K0*2 8˲9qŃdYᰒ ջaMz(qoLB š<"Jnr->CDYH$IOD N+wݮq\++j]K//7wqR;76o5 IDAT|>|<&ιl $Ib$x%1d0uQ[q ] yR￁ᾒk֬9dCBc>UQ2 !,}oGy?]= U>,7JįKii  ŠBzk[łUz~wlP q`[,:z-ZWaY`>`3($taf/|w[1iS`I>?'<=կZYI@ TZˁPɺ8 އ( 9Q6F(%M:٪ p8|c~${7 [aP+^61c,!] <`aT-c=*QTL$[KO;ߡ]Pi /~ǎ7qrZKk*fH,[ 9=ߗ\)1==}]o\X-"}画>9n3˲\ϲE`Ds#xB~Z+;BAk>+oYEeɯUTQ_Rt8(.އ2F%IKF.ɆCZ6UT1:[3$I\InWtIl,Wm _W~$9Ykh$̬+"B>^ j^(&ƴZQ=æ^"18 2A[w*] ZE%01wPE%F ,}1"I`1-!-wHr1+VR(v1;Z$C=o~MGu ~-"n[.o%͍8ڎJ6EQxIÅL¼ц"q&W"z!9yk *kff{뭷n۶m1)n滘9`? Jq={wdE|; m+a/% i7}-V(~HӇ0QЁl={ `D_W Sr蹄i:p3Ǖs-Ԓ@1@ jP-GbT ?֭[\ג?ӟ_o'an^lq`{@s8猂uE(bBpozӛ}qڵjz-Їn6$,CMDPkC)"7XA!LԆkƪq~$ :T|l5A&%C`$[y A36ƓhGjVLmP\53 v ^]EK#?{V( {U=OO>:55ûh=F$?曟FJU-:l$؛ڤ}6zdNg`ǂ A$$v EQO>T 1iREy׼f\MbΝ?cfOVӉҖhw#M 3,*v9yzMMM)OY.B-CzVt{=o |who|%I:jժ'?'?y @"iPjkX@DяW)xyR ]gt]5[3M>rANE}h !+]r z PG_A#ε @}Rt}RsE Py 3$Iօfj!wמƘeGDP9~cpDt YSn>k4FzcQe%ڿ0l,&xr:S]u>@mUnV<{x#B!ױ*ulP'h7 *.^6#V{v Pg=/3seQY"97޻,^o{nHtCgw`c'}ӟ~_s ;aRJJy/M'GJoRFD0E 3WBQs*"TooY8Ȝ0OE#DU:K>zhu.z-,ݶ: `LӾN18nI=C]=+v '(p/;/546GwdI;\LOO3z2}Ϙrϗ(1$IbB62о/?xt:6}~-F8_|%\rK(Y{tIE;0EK- (Q&Uj&Q'ᯨ 22)4(4($IE[Y! Ts8wߓR}TխL{w-|`ҥynVኢ |kX*1DZB_c<ǻ`AxY{ ~"MSyBH$"4WyÇWϝ;wQoݐR}ӟpҥr)B+EQ(<y)C즶z'_8aƍqI{Cu^H)-SO=u&lFc&E7x? Bo4t)|vC>7c+7tRL܂I,4^֡"j$;c 9n,-N)w0䭷0zxm#gưM$nJ,;u]UU}p.\X~ ݠKF}3DAYĊ~~ٜRsyZ444beFc|8MӍ$@O?|-2K/$#"_g)jP (Cq,# ՟.WgU%}w/WTnI)/Bh~%SLo[l[ oFq62v4}MyBC/~'xn{ߩ$\ *6Ǜds+4w9>AGm2dz#x33DdSAc[VZ5?Yg#Dh{ڇ՗隈я nj\Z<_jī(kmn&I2Fkݧ$FuBk=c_ą^yRi@qBϜjCl7B]gF9!%"1}Ks~Gy6)Som6h[';v4Mue?ɓ'OV)!ebq!QgBm6sSLؿ+&{e˫I6v`݇1ͺaTQeDD22|qN (Q[+$BDu\ j?u+z^EB&rW)sκ[??h+vRJ11@?ӯs.m 7co:Jd`XYuU߫\t=cCC܄EJ$qDd1B!k]pOyRC^,!"-oy[6c=S?O|:IQkZ/]^xG}뮻n IVRZuVe1Εe@r^Y'?,Lr{ k^BuB4uEQX[ޘlR:'*yJԿR!9WdY&sBeUι@&Ig?;ioޫ`Yx?/Zɧ~{+GydHDH`ZTxiv|dwySn馛n馛S٭$SO >裏x)3H7VvK)`x %k6Fcyx"BF_:tQG=qRo}똃>xwyiӦm;iҤm'Lmј.@JwmIZ3wyuϺbppiXP-: y}ױI3]To"b2t\O GPH>^)xK$hU-o&0Wߐv4al]4+߉_=Ηs:uxw}wП9|vɒ%RHy2 l$M"G[DD:wRJwq;@Bș3gNG?N;ԩStM0ai e3O~rNvW*^ s/$I:谋U%9Fגpnwuo-jHXaCE\ qy\F~܅c|1yEe59.=liDY{VFC !1F[kΎno6aҥ^xYgj…K.M<(p2QBjD%뭵lvڮ~߽vu%mݡ_tIQ{"OVτ>I)dQDr q7MӠ6kyk-ZشL{hLNvWR[k}$*fxw1qY/MY¡ 71}]uU{͘1z> ,?{2e/Z${e (wAWgoׄ)R'O!=QJf)HZ-齗IHY>$Z+ZID:@i蜓eUD$Rhh"R{n^[oEc :q^k퉈y]&w!AƎޗDKBb[&nm@LʊTBJ)${iķ9>MR~K_Ҷ'>V[OJ9*fozӛg}>n IDATR{km!^XkRͦm4PiRk_k4d V{kȲ;#izCNwOi\RUJ)y]w?}DR2MS5{M{mwambvۙFc°]ļyq=w}`ɒ%Or|EHjW+DcaNO>.=wJ1z m`o6V1Frt &Fe=~ՍH*vv8ZAP &&H&" AMDD{c;(|WZ;oC—ڊF;KGtP\x"kٗ};9|m+FB~_9>988XRqeɒ"Wφ%넸.U4g18z³˜q~XIj$(:Jqxqyrë7x͂Oxy <9V+xrM}n֕RzĄ6_\]uAY3Q[şw(E$:V"uCy)Pw )e:$;T6rZGD*RJ7L&խr!XWP|\"s|/0uʍ2|taeYF d)%H~ cTJ{^I)ٳ7>#B$xV{翾/]]wSm̄yHJ)Q9bGq/ZF x ؐ#wޙ{N>vi&u\Ua֬Y_׾7G(쉯"ȏ7N,_|K5.螯YD ){86JdQeCJ)rR8筷޺nz{?RJOLb-1̗U1cƌ#?rg*%RXj0,M<],~]1;v?iid_^#[6ĭ؃0dʣ޳gxĊxsx:h=ly5nR"˲خ&oVqZk$BYk(; w4Us;l>Ei0jO/<#p—1adO(7' MV j\ׄ؜S'AMDˢ(RJvmַu1_7tQcaFpW{VZi*jɢ($rYm|L'IxGEޕMA@8yp F܈běMkHFQ>-TJ)o}o}[g~7*3gܹso_*xH@ὧcNJ pOUaQ_&Qg5QoT:gW5{]P.{ D9_l9a?0u+8le;{ 1KrcFy )R|d?ޕ_8>$Dt_$ܥȿ">p^zmGe˖=/>?[nGydhhhcIJ9q41I)]$Z͛7M/~q;xЈ_ ./]{'$B$Z }`QPr)rmxsΑ֚xWDDQz*C5HDaY97|Xib} inI&m?g-`m}^s53QhS _WDwιV3mo]oґBIx$(CrI̋$Ps"2$^͆B7k|M I(B\߫m眳RJ$5RTk(,ҭy@$ (v|h؆Fk0 %E 7]ٚneZ(*Uפ U!HBוZN8mf̘c^C9QJ4M-+1Aq MI(y8TbsN +vxRV!#omS]t_|~1k\2e?'?ܹso2 )V*J8{*1'̻D)g眓7<N wcp_'bY5ÈVkyزe˂98u QrߓV߷~߾>rF !\Q֗NJi4u{繳%>umˏڪU^-TV渱4Ra.͈7qgFY^[9b"Z񱪍@lWu4MJRfP/,H?fyܹ^Ȳ+5sRhgO=3.;f̘qH^n6mھ{O=yoy#|9*"s"p(8FDAcMNSQ+)GEE! ǎ]mɕI(Kנx |0GnFMSk'xBC犏RJ++^؍΀m*b5A*_ >1@:5j:$| >ZǷ+.b(UsPM"գ(Kk[0礔%"ٗWp3z#N>x.3&Yz*iLHkfWc "uDINZk}QG}E|Ǟ{OJcq7f IL{TQ;<>xvhFyH,ኢpoqyI'=v '|lnV 3gΗwm?yK1(WcpNx&Z\ {ւmLV 7uXDcBx\ ulwT >@#j'g&56HXk[>mկ~_{`` P-"Jb5UHTE%oѿPuyuyx!E@ y:Iv0f2ƄXQϡ 4fuF=)xE _ٵz6DZ k<Ҟ,seYPE^7dJ60쩶"2QYvCVSNmq~…8R&I8,ooPoP#HAΨ~ʍ 6o^k)5k%vy~D.Ƽy>uI'( yKNgdzJ eD+X@2Ɛ֚fH Vn$RJ*=.G8^Zkv 眸袋>{%s=T(I^+ZutlyBՄhk!Jΐ͈,Ykihh($c 7ND iYj\kḲ3~.٧SrRkmek#SVa ;{ TR t~` KV)@HVdY&ps"sVfSYkА",XϤIFT7,ZOx~K Z aż$}HڱoՖe=wi=|w/?cҤIt94|e}vѭVKv>ǠU( ?vXo?r|TV(],1)"yT_c)9I)l6 74'x↣> ,X bnYYP#8n;WBe u|$SJ(ޅZV-u',pb)~f, G Mڈg[̊Q2v8r`żWufv{@}!V], ռꗿaƍ n﾿BDTx "*&MSIH) !D!ڪ#2vFse{osxY|NDFJiRιZksXkKš_fmI'=E> &$ 3^YǥjŦ8Y'⸊L4|6MS0P?ˈE[ADBk]UJ!<æ~S:7gY8sQ!(RFJidZ( F)(E߷ {2Z}ۯ5)$#XԍXY|Ǽj)Pƍ K)c *ex4k['qA^ x/1c}s.w^2^|Zx1ywBס2WJXRV̭׎sౕ+ =PDL4O`PJu;o]dg)]kM'^>1ʦzo4MÄ A$hY&:ЍªHVr` "RUb?;*{Xw(:T܋ Ur$_3qwd]͟?$);# 7qUZI3)a9CS$)T,; @&`͞=o_C׾{0MS)yCȉbJ9jZ{J)J^a-Ww 4X.I5#$~ou! ](Y)n 2IAB `iƞs9g&I2a}\5ҥKӊ\H)4M°'qi1P&gI=_wݢ(BU7dY&.A)W^ZC_߰LK&c.i: \튢7Ńx "^^{3rN2=CR!˲@I)m,sAJ\BdL Y=W" HD J1F !dQJi>{9wԻᄏ9Wu?wwY(I)Z k6!H},[++˗x*bB!i _R0yW6cx*( gˇDYk]`1]wYtЯAsy[V;!A_EhOp -De 叏$Lsyy̳ZGc-(ưf#j+{ X#?rhOL(, B$XAT~+QEp x"vbrll}xnv.v zUQ !GuQ5?SԂBmW؄/~?xjm$5 EV?8mDjC]cf?;؁)S2at<7B*"<;L@QxDF>k bjUAk{BV{%K]tWc1FyR 8~muYZO?=HO7|>ucle TBo+Kd-|q/֫$wcԡnj˰o#o]$X!0,[Ԏa-c-"̯AX+,^8s=OZxu^g"x5|N7x#&Jlķs.To bXv,갯+Լ |uY7aVOwqx7cr)eͭ1sR%,)LA)O^xlyv`G*6rb2UK}/!Ͳ,4g?}vtT1, C(GBҊӂ]zqni/}yO4f̘,Cp4dA$IDT+T\QJPM%JG5… Ow}zϱ5 !LN)e7+*9ADP8bV VAFTT.<;R{VA0I)ž_>Ds'o~E &w $a  @]__cjC dtVtM"8wC&EQWT;ҳBZ9 [$IΏo|\UZ;tg5EHEa$n| $Ț ":6Pk5-[l'š!)nPij⒀W>bQ90Y'X)9iz9- 1o 39+Dkڋcc(8I:电VYkUL>P].]{y 5czn t IDAT1\ry7ݎCEE ]Y!#$R01$kgh}9oĉ2Wr1cƌϙ3뮻E*BBGXk/(ETV!IL $g@T k`eix>\e[4<Ї>?Oo19_/JEoXRwSXuD k h EG2|D\]a> j_ؠHi@=GciSm^=؞wZ(Wⶇ7(G@Q(be51E/ۂG%+|5[kC{jB=,"?:"2J)ˍZ[6u}MA4"*򟢲|: @qxZ>ѣ~qêz0޿t!$eI?kpM\O)_箻z!V?I~#JBkmVkmZkPxBLw\?wι5"w<.E065H8^yVcAC,ħ@Qw8Z \PAY MDJcžܢTy?K\pRjgqg]RJWZQm9pB2 !V2a\ŕԶU q'&1 ޿/?n펿_*(xEXk [7 =Yin:p//wBƜ6(1&T0IǽsS7K4L/~%LI^UR#_= =oFF qHd}$ 7`CMx+:_xPbb!G찡a„ Bho nМ`->\?$I$ OͲfYf1Fa45f4M$ޛ&+/Y \(BdY&<y(dQA)qiVJc=v8T뮻{Aʪ)h4L__I(Yh7K cG%LV]ā$I|izJByڥiͦZ;a-k^cV_kO118&;~?DDM<,АoZ!m&PR=uB kRJ{~z]p'?ɩ7p"kmˆQFa8qj6Ih4l$9gIeiF4u0qQ0 4w4 բ(̂ ?:L0nv?RJl4{@Nc܍?Nq1H!&D8dvWJ)h4 k4; [V.օPMph7UuY)%<@zl)0'$9!|Տ4Z:Ƞ^CYTI}`dFcz~Ta 0yO#Vlb3-QUuPn c%v(eu"Vk$}Ib/$#$<[Uqnر#,@qL-xg5ssE܍Ṣ`~ AM@CSIq+xw$qI8b;q%H?L` rh^Lرrmu&!ёĮ75}=_u!2bfʲ,-<~sDM1c Zy6$A֍~AAIP$A)FVβsz{؀cnBk-{/_*z'YgIԘ:'P"*LĦiF(lR(_P`l[kC@Iutq3t* (79ZmZ6s㜳d+8̗峬5\j;v춧~L_!Rh*Clt琪H#VVp%#:fVg0v좽`D@'7T*I5nܸꫯ>u̘1oZW/߻{~3˲m1V˴Z-c ~lE@{` d˗/K.kcX|k "<)dSyL#{AbB1+۾#tz]N88OQVx8GIiώ~Vz(QQkZh53F4 U5Rh%{J)Z+)*!3gNgE\ ! ϩV)e( jle{֮h>l}LzR/&٩#JD'c;462j³'byА+,֗62344VT"2sҥK^Y&fY&,C>g !DEcFAcƌIV獹 &=5$`W(`/is/v}S-[VPƘ}ލՂRʤijJ]n$hIWGIfV2P+ `T]a¤EN: ѳ$ R;ܷ衇_C&(@ůX2eoɂX_x.h~nHDg(O蹩>umHdɒ{f͚u9-P2^!,΅HeV+b %Jtx Jű]hLj},{Y^qqphOJuhܰ"f_3fzaksĴ%nP7Wܼڱ&I"fjxR ;ɓ'16zv޼y>CP¿1g0,׿~… PgexnD]+ rRP۳WD>P!vk8XCcBϕ۞:&ZjZ#z.gDe ƘP%CDUUZk{>]s=w)sMj,*ަiAAbdS|Sk]}vrFxq2Rj\Ֆ o75sGy޺6E;pО{ywsNy{هD"29w$Ǐ#Y린hV .8\NgHPЅWpVgqEX{:z|?7k~f$~7.Q{2a[Az}C衇wWDD`tH!V z&6q6nbE!8Z6 @'hŤ T BkC9diӦ}[sW,gF:V v[՟fw_<9_Dg__1[J xi*^{}I'M>pJ=W{ӟoe%ahXC;. ]j+C?A>7Tr@DvtVF=k Gima„:サ袋C𐧲hNgC Ƚe˖˗ y%jyh<8*@|6Q$79W[c~JObxNh;D$J)j4~!ztHT= jAnң,XVZ-=k֬M/j;;4MCCEZCϕi|{ÚdPwVխxVIYF(wT1ZmHW+!c}l`·޳RJ͙3:駟>ZךPlqqV")%O`!DDCY(@9&]$V)e6 ީ3zz}`s4EE֢F:derppPcBBe<u`Q!xCLVEօK>2$o&r]l: =mA *?۞gI}X ?.j`WdլxUF7i2 sGłG!"Nj@\;0!ܜĄZ #|& Ѳ a e_ T(h VH!dM߾M~ɲe Vt(m bŞe4(w(ZCճ Z Jx[^M5+w߽'q}0yZk~Y8񷈨0U:4'U2,>Oj7US{8,T)"ғ&Mj\~g4)?ճgϾUuFJiRF)UU9Z$IjaC)[)˲FknMxkx#m?"ӈh|G"C9gΜ'O]ױ{3>:() b[kwqZkFh6.h·d`k0@f*O6<` tsyc(I38_θZden\ ~#JS_U 'Y(|o:4T8Kȃ#.ƴXaJ#.ste |CCC=j d518x^S444DV+4-0/am<:<EQ>眀'o#}{4}C]?00Լyn&I ]XɲdiGC<ω>e ˗/`*'exL:ni$IG?}ݐ䣏>zkjsFCpx|$ZyqKcz9oB/U%y c Q>脯Q)RFv)k)~M6dG%Kqg+ O]bTٴcƌqIeP5M.Q[юp޶ҷD3 URX"i5h86HDjDQh`HRU'|G^7{ $^Dxo4.I4Z*!Jr(:H^S@vʕeKl$IsEao{MTi⹔MqC8EQVG2g q7BԾ?""z'^tE7M ɎU .2j/oRH4]PB+yZv1,Oh1{++""z4 szXoyFRu݀Aj`Aґ^k>Ɏ5ELC}{ȲRqэc ?#Ur&n\[JNw#ume˖*"(xTݠTeB3l xל:53dQP~||ɒ%g}f !RJ`C (q3;xo}VuKEDz_cQs L5rC  .kmivIxUT ,o?щ>^z饌<1}q?9Qz<$ھ۳ZALV˞AOX"H@UP'ג.peae xK)rvuN-Ӛ)+X Q)SAyk O d7©sg3:7sw"c|=ъ%#9ޫS{W =3/l3C^ j8got/yZ$jL#^`ULXNo?8C衇Ubc $IT}}}(Cɨ1FE!czҤIuLDP .B IDAT(C1UpG-h,6Q9إ)~ZfRJ$LD!sNc|xsJס c̲~G׿~9sc5J)Z+"xZk]CCCZѴ7ա2( xM`qbn_ zWB7j3Tu4D.yq HrF !OiǏߡ衇~z 7,'9Ͳ,(vܕeϲsyJZdI ]\/ɶ˲ ,˂/Y~wsRz{gY&(QF ~'&mb>Xx_z˲eYf!E;'7ֺ \*u)k{"[ٵndt_ v݁&n^[f=z Nı.,V n1vn@ KD+ HԺU'$>/v{>zk g J"p^mC9V~ޱ&x.u*w.oj{h k9(:|n"_54[4iۧM֠b7^3Gi''cı<;渘ܭZLT⯈G{Ʋ,2|iE~hrWD01pߪ<ޛ. .sZkd1Dds֔Y![1?ӃlZ ]'%|iN+b{8qUB+e'xD SJ)<ԻƘ{ۓ=k IGj+=*V "yFCCC恲B<$/q"Id̻Pl !ܡV|];qʹW6<Զh D#T^$qZ߇k9W|߸LJg!GZ+FlHp€O+LkLF*ۢ ߥ]7C衇WEZ'z+B  =:V5MOm\@5w,uQ0+^0, 5:|C9a[xz޼y73;Jĥi$wrCN@*$Gacy,sy;c 6o[tGR}'|y,ˤsHl Pu 7q`l!ʲVY\B)g¼֢lJȲLyv1|O !"4V6mZ%\F1yϽn>1I"MS#4{Sjmm$OcА7Ƅlu&y7Nޓ֚&ιC d4Yp|DZaTms.񣁜&GTJ4MRE!A'sr7Oru>wq+ BPEB?(1fj{WRC*@%^*ݔI)=7j M>3??<&N5Wȡ!988('G|߁*hbTˣGHaɒ%_/rBWp\x9yIF@g81QvW*xQaëb٢%u¨b[iu=ݔWxб:ݼvcye=~," (+!Va=!<묳vIdb]??W)Z['IPbb28n|>&3F D{{3,wF91?we 48nh)%b&Íxo[pIDP% uG-g&Ik-$U={ow5*{w;qĝ9N<#WD/-39A/@]<(^ӛn7<[19ɄGU86Ku2ZkZu":'AeUMh轗Zq駿ι+xL<YԻMD1|B]*`ʖH?EQOQB}­.puqu{ Q]/ *VEy֚4˗`b34Us PJ "EQ ZYfͮ/[$ M,|9|Q^͔jX^'DL6{_0Vxw}.#~m@:d27$ xAx$Ý4ZP#4MgI*fԅʉb%]/Z8,sRk-$Q9yK{d뭷JhѢ߽=n$cR^%yJ>ۃl :Kԙܰ`G'~.`}c11qS)EywH&xE *JX U!dWxW]Ǽ|';'$ H R0GKfu6T{5F)[k}e.I cTJF5nmE8yZۛw%Irc+-*xRE,H$I#2~$M<:jq ?Dy vZs}$Aŋ qƾs!p#T7&G/~>|b&fRӣeɜ<˾WՉLмU(պz#2/~AtI9u:Sfhرv(dXk;%l`.Ks<3ϚI=QeY RM$AHJKZ(RzC#mN0 7?:kf<Xv΁֑f F|6Mh=H&Ia_n\DR$Ku[拋;!}urmˏƀCwߛzs#yt2om|;8!f0sS)ۈp-VVV968ZF?dR4HĜ 3oocRg@ B6:{G[FرvIKKK!SH]/k=–261IQ#:DH :( T/#z#GlXZZK/y;(@di#]snǤkQx6;D$?/?/@3ܺ,KӶ-TXl=ɛ1s&"d p//'jޤ7[ny,KPeZۢk9.EQ‚dYe;є $(: ަn&"oLdHC eYvje#i@b+"Uzy;vxl#MT/ͰJK%E|m%QW $sNVVVd<N[YYGG,sc;/H&%l7Cy[w5kFؾfQZIs>"z{HL, Nt\ LP*" 8Խ06;){CZ˗^zi R Akoi<)""T%QPkPg o^58DPF#ZYY A`ani@%1OHQ4/Mz5\a6T]LD컔:yhfv !A5MkX4}A׶Ҏ<ϩi>z-Š-2c,my^|3yݻu4" C v7aUi4Vt9 몪h<&aڶ,;&t0O5 )׾Y^@Jfh 쳝BCEVdwy1 ̖|]kսY1F4ڊiu]K>Bd˜jb/Խ'U—ho^wu?b.:ZcLh܇y>LŦ2r>ZD.OxKPEfP.85;kɧ#zXzQSA@Ox֏ƀ'?1Son 8y {p`>UYjE6K^AJQn$/8<#?=c޽OI5o7C5@jޠtgP(7M+X Á\*8(@iŌ?\qUU'4M4LH=|fb?UDn;O /| q"V [D<=o(k|>=c52M5"b1mZ%"?&Ϝsd__~E=7ַnxID[i;cE7≥r]iKs1ћ ڷջ@\&>RPBqs)#Wx;oۗ۷IODWY|<ūvq4uﭸ6\ p_AM^y[E\&raa׿!=<ڻ75/--}_^t0n&>KoŠǼ`CuʇV("rлD{U/RcBDP+!MSPPk8@ZUUQYCzsc.mȌkX;נ7_Υk]OEV 4~ T_wE)/f8tw/ drFQ$hE&g'&4:'с4nX}}i Є@u&"nqXYs[7] Z==Xdv̓+'V 3,ˬ qe-MU9gE6McLι5yc^򗿝nÇs,<ϛ4Uk!6zdr2a'B{y +SZ!w2V,":.Xk^)v"0VEե5\$'sĤ)Y7G@&|3B CoEcW9կ~/xA.R&A &M*Åmyߌ}*4QLSE{ :ϧ yNz-ԟ6b[dπ' >8& '&=L& =F )Ext/HH=kԖ?gL0X\\$($i9 h4⺮sMF>80{jDxC(QaM!lVM`'2ZXX f.t&"|vr6H7eNg,h4:m[QJ"㼌P x5`U4yv)Im@ WKQʕlA9I9/pUWz;,v'P~׾?cǎ3iЄh,k1iCKWU:t!έtYd-;]Fy+%@=Ձ!m?:oWh4 e ~]s=$'|#/(o!C/t__.y""Zk|c-q>H~gvlYݻrv=9کݎ&~ ="BpYflsmƹS:Xx e`ܡ,˨X77LQ׊Xt:`{K/N=NC~D$A-")p,.XGScMpk,z챓6`{cN*/cc Hߠeݳg7gNMk2OkDDJߵV*A:C⃉)XqPJ] yE('ʊ4Ms8M%,}W8Pv@ :gZiG^k"t$˲$Z=י!ȴr IDAT%GmϘ T ^KxeteY`/f"a1YQNF>dO}S[!,FDvA:fvڠ`}ӣLGZD:PsXpDFd:J9ɵyTȲDF xM4|饗.,--]b܇m[Z<霯mh4(%r6ag6V,,sN꺖m/J8q?p̯ʯ<׿~->i G>s}iӴbZxaޘ{l|7džCKĊ{Mzě, &k1Hy9"hy_h:\~Uyk.M#P% ޾5 }ie#CHߙn xׇuHF#MXvå;`9G|뻩1@R8CK4Qu(4<<$IҾII=N c̨Iv|](5uΛH5e]<Ç9p@l5 Z|@zRylI:Scʣ+1Z٦ m4p/"b_"8cK+1>|F b'WHs/d w>d z-ˆ5/(UsUj)FD &zT-c >GN4 O2ưJ0re&2ffӶIi̙y%"rt#⬳ڇ@`4qm*BH'f_uJ{ֶaf˾.4)Њ͚dZk;KE]:t[~&6V ] ]CmB P|D)hۤDQۥӵ=OZ++cƬHC T*}/d ־}A#`cNTTIiz3=zt[?_dADDMӈO+mۊ1c3nGf3ϼ8Kk7{/tc/TxC A};<?C[hE4u}N0mu'}@M1)A &xo?8̜eF#rqUUFRc /..^jsKKK*㜳PN֮떈( m[%Z5^]d$bCMHV2V >_(ln]סņL۶4iֶmks^d^۶I ;ǡy*lV:{+֋U6I۶ qS^n6,c"j2W|#nkQb?WB3C|ƀnξ h4,//Key(#"b;seyyޥc߹seguVvСMi 5YY@jټq. l,*/ձ1@\;h.۝3 J?O=EL\fYb"[ :mX UHN/DȀ:ݒ)Ps9h4:;՘oӤI|C39Y4! zxBbZ)È(Cp5Y+=׾'Y79gj&"̌k3}&k!˲Q]?\F"~n@>mۚm30/"sdCHӃ&y_>rڶ]y׻o Oj64U𶓏AKD(ˀuQ!50ZY&xA躗'y1 {r")j5`ՖKKKAr4a9DyϚAvAۍBG% Hnf]3ˡCJAceL>CKڐ!D:bofqҤ \ҽy ^`px<^uȆBM4gvgx/2YȀ'={8԰{PQLwlyNEQP۶TUU']#J q{b5y~xLDf^H?$8ш溮 ?~x<6DĨ4'Q4F]Iw񘙪Z_b| "t@À%_w4eyYk %J}pZ,`ee㤅rn^AS~%/y:恝ߕI*q ٳ笫.ECn{{1m; Mu] %WPĉQג襠p-ή H=J8wvt2, ?Vb Lp]!eZ̲lWq9)24#D"U/=sAa qgKPXcbEMx?sEMITY3rޜsG8'>w9\GÇGXI/|:.H=N+wߓzs lFhQA#D=KY#sV<{Mhʀ1=}P9~Sz^-oop{ijs v5%֧}c2cׇ Tb5? ÙR ܶѩ{=dF!˲:Z0|*O4ل@Fxֲ)m3'3׾܊H.i9́q%~BGC/MZVUDD!PfJ,C.~~>Ml|.Po=T}voo(ר=LS(%/w꫓ />W !-2&?v2 8i(Q](3MQQ~/z["}S ptM 59tOxS;$|_U t샚Ýd:즍炱bfԧ>uK_&>mY8TUmi@Ky P<=ILסy X6$ V3i"D?<| `Mqc7̀ڱ+4aΜ&&Sl`iqǵ>Un0Нӱvuc?uEB5j>;wAe?G \sG}pK-oN4>Tř ̜B*tD_+{x|ڶ > lT<):9 Xki޽I<٫+ID( 2ưuƛP5Z:ke`ee^{˴n˓ -3Y9fnqM8AIڶנ6hZkYA?d/L Q4(7eJ,Y@PjJc E؋ۗs>SDPhMŬu]SUU)?s,4Aw&"JQ✓{ٳgǽk׮sQ:,K !Y^ %P 7׍&S3dd""]>kZDq>"^:(xl gP A!I8ʋkN<9z^DHm*2 =3J2Kx-u{w9(x#mz|OV*h[?Mj^zh9LPB`gNطuIDTim|(l== QwڕD4͑mь5hBz/%vRҗfqD$u]Q-..# %&ά3lV$nJľ  o8bu[랍KB4M i^G;K+9^Ja琵@4ݤbǀ)[ !=(걪*5MCsɳ*|cǎTQ]z*סlW5eEA""u]HY,,,H]I uy3 MӰ*9i1S:UUuF#j7ƅmҜ!|k|cC 4Hi0{̅1` [zs(66Sjj ]?.DY3(ZhPGeTH#]H} `ϜsD ǯ9 ߍ.3{1ыvl?W_Q ]'"ND1 :Bz83 DYkȑ#7-|xkEΒՄ&NY*4t3^@VjUq4*M.,,$#xH*J=j.V$ܮ10V۶1^-ܑD-⬥%s=1=|VEV;H2gD4`։uZePa`g^p"0 8!_O=&Yڶ!:Nv`tzT&DSgANRg`D]/>jMFY,"et=Z;N` մ"V=g#n0>h2zko$eH1RXksyW@ d(M<M54 ?%ui<wP#X1TUUš?rda,iֶǗ,8˲6XԽȧ/̀Ī8Q=?!"jV p~IJ4UU12)UUrcш>s赁,yIxBgF>WrQol7V[M#1ԊyQ_ .ԻqIN=9G2tՎ>H lR:o'h4D&=͆&h^ XQ|N?zk1fl=L76V1Zpjg>2I&C]]Z+dMջu]o&":p; ^COvĄPJ? >P\L!pJXkM 9N&xm'vI4?dE]"qIݷWJlN<zB0j wuImKM~q]LYaY{SaTX`P^}QaZZHvtϚ8)Ix<5S SG jVVV4K9Vρp+DIk#lmRbt V:p]qGmhZRڶ ,˄iiFTeYXX<1B4yk:dHTO-|(B!HywT+#E#__A4٪@+jx@RB7Z.XGyS4 e) < ree%=QDC`s~S,:kʲp*ZYY {<<:8uU3wL=NwPdӷ&}Jx{pqݨmI 5 # % } /ݸI˺F޴JVW=xBQ;?3:+H3-\zxOVҾ>hTC뵺X%^LhrmN#S5Ǝ'"|$Ԫ`/J=LEQߋN` -ܛGr= Ly{=< Ѽ̱u M`ĉN$ywJ= _ja=ch41&t'?ؽ:I@Y j?(کűz!95~|aqqW "%_?s%]3beYv$)Z G;_rCЪmV`K 81FW^yxʿL;3-PʲES¡,A'bdjb]!0n`[ 5D#[T~ݻwToYG78O? 8h{e2|;)u pt'2Z\\眬ѣG4Y#":3FQ{}o4͆_ u4 -//S4;zscm 6Xa3 ~XԧMM"}l(nB73jCD1y̠pmXTUd-;o z; .HV~ee(Ƅld47lFb"hJžvDL2z{&s*;%3~8Y]v-dYƣ{-GauTgluMP{`Rsm<Ł$3=G`Y0dr20~>k x?RaK6MъGSuP x 8.Z2`eG]SMWQ`BktZiiVek H;T^{x1d3"bsxs:wBGt fVtPc7ٝ7l98`fAψ&Ͱ6q9r&b9'y.>JsńZ=}9x`2(V;8xYڌ<'5ۧ>^V†4Y;/튥=C jyLw/eDNEb;ujm~Vi_V@f)eY&~@9 yWXk 3"ռDddYf,3>@6ASa9I)άx?V"?/H?iIMDF,/A՘5[d7OF-VL Q,=}c:+̱ge"YItXyXiHU l~lwRA+煘 h2/X:/~1.}yVu]R%}DԻ^=r7^316;`;bCfkƶ 1k4>Pu^oZpQpܵ#CwGp41¼@Db3^|FZūUr564cfg"rm:_P(*J DY7^Z4G*yys򶷽i9'L^KD]g3iǞJ472rCɛ[=k;?I_5ڷiդ)2%s2CRPN(;] ehҗ+tyqA,s jx u-4mi5}W!5`hBs.xڎZ,R~8C=l6Ƙ?""7tk[Vw"øv9P{kFe.bb VeˊT~2(PMzE bZ 埩<#&JRH}Mz?xF0ڿDvy(DDw@8ax2eV|C-q8`BI4=R)U׎O1`o߾>,2efKXe1ڶ {&U4 :CUgzmǦT V$[z۶✣?jqqqnbOGߗyD9@C_X3H=rHIiEW(g~ƶrǽ1:'XQޮܦ~󩇰%PFp`FL֭ G0k#iYH8 GR Oy(,:!Dپ;>`+ZݠͲcPAtsYNcEH4Nff*:t辶m+Oc@1)!޽{c1@j4c[~y.,Ko21RPVSŤЄm\?͓ uEQC=T*ﰙXZZډ~wQl,9+k4YD݀+ݹiffs'Eٓ=zhIv)B C+1~Yehnq`}Dp EY=m`UuYcv8 8*⴨?CDT sG}|;%7̊ӎhUj3V[qRj z&MÀ|zmv"0sr_>bqZա@*߸ʱ>Z{8beZ_qc{UA럅dY欵ѣ۶ǔx9KBF۶͉ y ^׿n8`m,kefȭH۶_̓oڶe1loƟ~ ^𦄗^]k3"xNle&*KKK3Ĥhd@N/}*n_~{۶2s4,lXG٫W}F}[IiQЂ>Uud?_UG޹sg2??: ^|ּg?٦i6cŒ<1mgizc݇݅xLDt!AmS97ob!lYyN:|_zNXmC .iS꺦x ڹ[G, L<3he>06sNfh$eitmYz֟UUՈHciun-3:4QvRIAVD]r,ˠȲ,4Qʲ]J*bfZXX6CwQvOdZZǁyu3HeBDk'"Z.2,K񝼵2(5gy;P^h[ÆDau8ڦiZ"js-3+++Bs2眨yyӶ1ƴ?s?n'>/JvQ ڥ7|_WK4#>@D$Niq4=h!XmPw5``7t Q'"VbMs4uzꦍ5"BUUuM j(RڶJEym`s\%33eA=OsfMS(2dii)i=1~28'dYFmʬ"蔎Wm'еv1ĸxcCD'wz(0` "O}!0,+vcr"&jm( u)zL u9󁾀* UM?Xk>w} ۊHJJ%JWu]m`Z֤~/7_KY&(cXAM7k( _֊L:k1{ ysq hZR7XV v>JhqG*@+"h4m]-Mv:olOOYgdtɍ7\rɛ|pm۶O eD&Tk8Va܊[Ux? y}|)"23EUUmuS-s,2]ŝ<䀃~PQpc,dք&=U^3R~^SJݰ!>?E$g|Sa]K=-, Z;1Q7]9sR4C VˆLXh?uUeާvZ,5> d'Xڶ,15Zѝ{af$1ikEġ\ODG((=}b}-AIW UU:}1鋿EV`}c F$3ƄBU0?q;Odsι,3;utsʉON;/j}Ȳy%/fm[=&i9g-GyĽ?>я~p4bʧ?)#^l<,3c 2hx[ftH& Si{P3c/Ku8Ұ}W,$Jg~{H=:zw=:h( *Y}40N@%vp /h"R4R!{ Q۶NoW\q.f̍Lc+G5e!"p6n>cA.PMu>K6%tN\ /Ѷ,ugY&fyjl dVz 亮QM7t}oo|1f.-'*xÜ ~Reh ypuHM6}<Xs}c" m߯v#z7ci6Xē a"b?X+?w`ns))QE q/s4R c(QAO]t@M}z̺$؀Sú ^l"ɪhx`=tQlχ};`~P]⳩j\8(ԑB_vuÀ~FzO=yZ+rՄp܀r {a4^ɱgVE X(Osnc'K.dG@z5MӶmf^ƫ ^54q]:x=M<y|CE迩yܜC+n&`jTDԩ˲\S;d2$'%3Cq|ʢY4ҁxJ+M2nh!&󖙝k rWr۶e5pOsdF-{'EHR>裨}朿UQ\[b_ٍ\͓gP;u=N[c+,9u]/}#|ʼ~t۶rС:wE#} 7M~d"XHt%>Y`~S_3*8x+СCɌ={oն-:䮵Ͳ-ɲͲ^FU UZc"jin|9>MUs뺩jw,kcLǏza } EDM۶M۶ODZfn72ѴSPY:H{=N2d%4:ι9bN9x4'1ϱ;xʯگ&.n馗F|9眑Ia$n馵0?P,8+6Sa G!Z=F2lJXZ~${1f,˘7ռa\07]IYh@]@2)~ȑ*F_C'XK_wV>-r{>6&.ہpjXQD04'^ @iػ;0Sa[`qq1jCMWfj,..RYa5½ʙ(R?uZQ("4ZK+++Tup|~ġfh6_Їx]Lw%V>ֽC=]ZZ!FQjfY抢p qbS]>|<(kL+L5҇QwCԾ Όk{3;lLQ&"Ŀ'w_/ы^tMum۲/96pQleF#G!' "l cT'/|YEQb>ARAVT"i֝jkV<(޵?5Ai*Vֺ<]ahJW׵71{ QhԮE#5(vJrΗ֊/w! }hӌVkZeT +25_\$|#/a]C߼0F`fW_ڶ͜sX"Y٢(103Hlc&*D΁"k?7t#&(EuJ#lKI6u-(a2cб޿Cǁ }gBrYvE7["XI@9rd(j)*SY Ti6""mjuA4UsN=Qbe4 k?v]w=K.yak(}_}}-4M#cdee6Me/4QQvxop{CjRSH r6SgmhE٢(tq8ӟ>f"s=qs(eY1*vαy >ۄ۶e#?ZEkb?K1n" }U,e,,,HeT%g=sؚiB]mWkٯοڷ}9]:_#kcCz9og|o€/ƶN3ðmqQUUԪݵ'(lR"VI&= (wp˲x-:Mvo5ӣ˰ "kn鳫%v5CQ**r75\EagPEZxγbJ !DR0rJ@> MB`5g8N/k'0̻cM ; 4["3*?5|<:gNmz_ԟOGld1wnsp}w7 dS$t&s~$keTh3HӪRl0T,ot]_Src nw_2~Ƀ*h'vK`YStw{s .+c=5eg}0mt6h2?mOWspbrboip\5WUeYrUU4 {'tw!*d8R:E}[J$j&hfQ\ZZyW>0s 'wC9szA^I gT#)w1FP1KLsNOGdMq 3Ln+i_{be)d(IM&GhPh05489}xG>WIB7gum}{_[fLooo}룲,sZBpM#"B{+^3J#;K ']J<-ΝƖ9j)AhXs^HǺD 3;Ut;N!Z6p^'WabӒ>1|J3ʯJx`cߺu ?kR>N,'@ᐊ<{ J`8hE]q:I+dXoh,L /13knem&B e 04ЯM%P 4tvaea& \"J`@YM4fy3,ߵdH(RCJqhT"I4N8!GqHIG%On;RL%1 Fo=GtfbN?I4 `i&swnk}cYZ6 }3$ ώϑ.erV8Lj1d*oĘ5. Wl!HjQ4M sg m u^ ofr8_0U$ Vn۾W h`X4i0PQ)UuO* (e9u1gcjR-C"(BMӴZш+|7@϶> chɋt##0 Kci3 1 ccXަEwoڴiSccpMQc<<+̀=hBvefXsz*2s`\jն[&V$iyQKDŅd_{L-Piϐia"|Ϊ5!"%,;?g#<}[oTrg4z-o,$~_׿,lov4l8Tuj=fv^C{23wW?t{k$Í,!CsbpvWbv18K741M4_k--//5j7nd"@$<leݝƺ<"z}s|#Etm+! I)e1F   1sK'mƍ5YV np  Ny]״o명b1ƩH{3ߓt6b~ }Β3J4{@946n$;J7=-,#"v`y42$Okz+}w@٦M _vᇿ{|KWFVԸm8{&oG{}B^7V䕊j6=t҇) .18Ī{O<1.j=~4qڈte|C&f^\C$~I*H)d(UW]]R >$XZkiw&9j&YЗaoF)牎_s> ,nְ3DkߥjyD?z/}aiӬ.C_68F8't Hex~o1L}qǽ|ü.i4k "I2h]a.;L05: nC<͹3fPFI;S,} ^p3'nW:zy_\M\UUj $gׯEayy.lvH驤N4Aѻ͐ ;lkZ5fNТ(h0LCVsq%/]ں~A]ƛ׺]~:nwz;}O=5ki4ݐ/%h4۶Y̸ tÐ.EY طܵnur,u1FBMh0lgjBJ7O]?$?h{|647)Tɉ5kcn{un: {Vi iZM45Љ ȭt1BB!OlC̞N:[^zW٬׼5o}Hg۲+I`,w:8O (McRۭZA>#j6I3'?Iog _'rl#OlWULFHJݑι}]wu+=ξ{wum; i _߇L]c]^^[TWAGqwO3vָ]}4þ'!; M!uvOAh}=@Tӷfr#C?'t/]fZu,%k-yySe9CS!"nTUe2MXaYB :cc挈Bs!cs\fmBe0U/:Q&>!@HLѸ`r_'fy'y ff c cM 6ik:@9kˆ9oTO`҆?u'9g16s繑\Z_`!RQkn AWtIv Ѹi~%Wc&dR"UQ" _~U}1#՜eC3 8s ,ffۇ& &I3 tG=?1&96a(8 6J>^-1(0_ۺݦ_cK 5K.U}jd,4M_p\u&㶡bcL|k_{~Kcw}'' $c<5<;h:90˪~p@?]i . ػ8ui+ wQ:Yhp9.ѤED+okٮv]=5o`AA4 eI1t`m gkmbbuٱ}HkF'åCF*^ZZ⪪40Բ7,6skuu],͚ɼ{BΈ(3Ƹ(\QZ2dctMتlY,KS`PMP*։n$5QGH 87]WUzyDW``,3pf6EQ`0HC =Iأm'kƐԺg_skZD-cwii{I yqqq1 Ceϲe'&ȿk]z_Wu]ߵOcvaǟuYGh4r6Mk[U !L4]# K`ᬓj5aTwg0}N`^eA4:5WK4ʚp_PΗi,}ܢ(BQ1˲ӟ~}ͲKׄ*lʲ, &8V#yݺu{:Y *d%mh;cj!a&:S@ ptpji^gf `h4Nj4lkЀ\XXXQn=ٌ1"Lk<D$fP0VL@Zm͘9c1sn͊AeYcFQ9.kl,KxY,4|OaɉeYz{FQ++!"7Z檪LYf4 d gc Dܤ5muet@U$߾^CG:Ϩ26؅FDD|l&.//Ǫ"cgf4ZKs~0߼Ksfι}o-~>9; `0pZh[nXi1iޛ,Y51SMnC0iEb~Mkׇ]ZrZ_F}>Ys+ TN1pYg}[چ1Ea!lÄ I$pUc֭TK_ϲVj$ qQf0!2ؼy}W_ fN;AyBHz,s7nFfΦe &iNsoUUuִ/vvDzjjRgɮT-֐]Э|a{O4V*J۬*9bZ辔CMcɦ Y9bt׼Fؼ5& [ksa6D  δ-w ݆VrrbbD @Qyi]"^ӚA#;<{nL4{ǃ7s۶ah20L7Ӻ>q?ȝk&`bi1s flA16\?Ͼ[.Xg-[\\=_6DF2`,H+pOib9?4VrHk/--y{ޱ+h❐8]|di=JZ $aЬ]$\REd0'|㬵 =B8/~3NsD+cՈC'm$Dݷ}13{/dvv:K{x9G O+'YVJ'.{(s[>{{R_]5f((s*˒Fybg[e:YPPRU]KR7y{ƽ;i^ZZJRpM 0hdB6`O?/xZ s^\ZwQ>O۸w9a+y睗oڴC9~cwXq,b( " !ĺI8koN15ȃ+L>"hxzNYM_W:iDd0)a]wkUaSs9i[^Zn;С;\rC׭[xfzЃ|E:s. 5zʲ4ffk$" 6apMF vHgzkDrHZ pثؗ='ڄR~}CyHc?CeMxEJMbiy]MWk̟Bk*:$#kiVX0'pwuC{j"nBU|QD2UOL؏pm&UnVU}}O]-GSiqz_yw};na5egB) :{>= Ȼ%.g"Ӭ YV z?,T(У#>_x}7 qp@Wn05܅o/nZ^KӶE`5i  m4\k {&+p e*rYO\0涇ۭ|p9}cn ,ˉ@^dQg1k& hImeY l&B 4=]eLDZ -@<ύh 65c$-,,,3 8˲ F|R1CSXZ#0u]ÿۿKD23Mט<99TFnyyٍF#뽷 9ggbk.˒by~ov_Rx{i5yqqYk>+)L{i֭}9yyl>UV4k}sI~@ݩI8Z[Wg}tR0nNkSj+}㽷!sꩧ>Z;^.Aa8(\?>%siZFv wv{8 R<GIX/2(SN&%]=9Wk.~ϵE涃T5}o0޵huI7E3(snB5sW!]W9ZpZ7auړM@ x͊41wMkvqqqU*K`aVƁNΩ -;Qz(QއBx[í[&qH"¦5E(%`f6I>F\R:7 &.IS bC)(AUOOMhڲ>Lk]VjW@fd̈́>74+2b Ac`65]16!c9Zۼe/.{ת<`Ymx:hi[׵޻ !ؖZcawڔ_@o7 sWW_ca, ;c`&`IIg*Qc7RˊMBo7G=Q/*I3Z)i>KfNC>;KMwٓu/[l114C@\$F4 q)`l7d 8s5K,haaT ]meʍ1{~w~B}cn{}΢ۿwS,ˉ#r}]?Dnt,$v5hm((ɦpә4McBeZk`0X7HUU٭[-[%޳E!FD\؝`{@!V+0|;ոm ^ kI^mu x *PH@}gw8s9(iAN<%QÊSAhk6-aK%&i"gt5 g>K,K\V~O9}Ρ9Ay("fYs`0h1O~nϭӹw[~#.W{vpUUٲ,~z\~%;}BOD~8}.q=F#޺u+F#I2UFDmܮfN}A0Dh4J:9@my}8><{+@[9*Du=}4FQ:zTf 0񾇱WZ7ӺН۶MāJBuR`]fmgM3\tϽFmQ !Xi4A31E)4ƉRm1 1F׽S!ju==qefH3c8Ғ:.=ɋe4z""r1ƖޛM!b`6&}BjPMs34lھ7n}_RDRIQU^h ><֥E5J>Mst rGz,c*eD<4uTUTU4M㯽-7i~;Gw3sws62C$vHܰ#9s$0/aXzsHC$VʁGc`}]eYȲ,w" \s'voIeԅ 4zF%QZG}'IҜVP-7p%=묳6X;/׾vB&JhZu}viB>UU0I_qWUE4&z֠JY_l<=@Z y:Q0涇gna{by$\Wh6-ͺVH_UΪwF1hX{g35P]IgAc&XF0%ے";&- y7" g]ܖ|O{[z)-<ͮ|CpsBcSRFed\1&5K+IK̐rSBDdV5JgIdi2g >Y"ktK2!4 VMxh̞Zu훦!OD 5|9?#z7fN87c6XjHښXs$:(ў=͟sI Q)-[,[oj^|WFR{"|Ծ "_W+ Z k䜍17{˞z衿?Aϔmذ׭[אbYky8`0`H#"s1m]שA_&1ZksI*SUFn \S3ܰO ݊ nHt f4}5T^HUFD85Ol!4u]ϲq_ߵVgyi|`}iFLnj)d'3@^Dv0 (Ե^h?϶5<@%bInϓ^8Y&[DDz,4ARfMӄ|3w'?Dr)+{M^Cv5mniu7, JKc:%16seYw|*UU%߀n4 ͐$ ^9!ػ/4$X?kק CZ\\LG*ZZZS7ZtiLg4 fjpVX}ۗ?p.vfتJ6rc7 IDAT6mMk3|>M"hRmR#BkQK1,xaaagYEQp8L`0HA0k|E)%IMx 80#I}l,Ypc$JL{<gwED}u]af4kzmnݺ?O>+hM8"cI SՈ 33;8˲x R|@ \J0bz(yU%:V捕 <!B@ɭ%;Hߡ9;YxnDl%R׌ &%qjw儽]0I$BMӄiC{'&fs}~'.7_җ_fv!'\!AЫAxI=_UDp*)&4_ pk7nGKĄ. b psibg=|msӔ@ m$xC_|Ž6+9蠃W`zI5\a1@{TRp=%H|6V|+kyne9gs3c=vaGqj7p'lxrъe&]ge {dL\cp})풧Ѥ~*RLާCto}F?ٵOtǭ=4p=&OK5>ܦC R.}h^W tWw^Y>~fA# :iDcv.28@:z4& Vل) L]f4, Ox¾j=vS{m'`B0 \eQ( EDS%jfz'C q/zы^t%6256in5I1r3z\Uv:]4UM)2IQmH@nmͲ,-$檪,ה$wPeiiFC/}Q: a1$ NK=-}B -HDjA*_UՄTC4S7lݺ{=a{'?kWU4 %n0 }+X>߷e[n,+gCtE$H4m&tM")=`k{OI;H70$x}wd(ul˲̌Fkayi _K8vsI'@flkWT!ITeA"]_$( g}xqqqpEQLhŃk᪪EE"cEI؞~/._ן'> H8Q}/Jc>p [^iƘWfYu`<.6,h0`0Hl`;c E ֵb5izQ&˲J}a䂝-kumʳr6r9LZ{gbi3r=Y4+{Љ44{ Z8s5MP_K$.?3{X1믺ꪻ_U4];c_s)<{bփU&k 477BRcUUsQ$[J!f+7ׯ5a0j~ xв3x~ƍ9};s/5 ]DiW3[}`iF~1Ζ}]~Ӿ1;o ڷ=AKK0!㤶 UcӇ=@S0K,#;q],:X9[k{r& W5@< j}rUUdOQ\K@% (Z~ +!<蠃VMWL EER6s.B?,Ƙ~N۱($` ι` `_K|<G"Ol^c zδz9gMRSfaDBa@-敞øsŜ# f8 -LIEQ<]1kƅ1mܸ1{ֳ&k:ե+9Xfsy]=~;y??<#'o R Xj*+z*0w5{@[D-..&T9+`} 1>&(>_OIm uYq\i#Dt6obO;qaf>i}ڃJMrFgi033޼yÝs Ej~E+׬ffw?cKhģшNMv>_u>D`s/ 3cY$_UUWu9O$1[ ^뽷1Fk5y(ʝ Y*Sh> PHXgj>#|"w]tFXHp}sY=ܟ6M@Dxg[S2/ƃ^|E5/L-J,`u 0G~Me`p$vhWq% _#p}h䗿g:zRKKKDmЇΐ$ ֚M6KDIsSFIg`x΋ b]A9A>>9nA׊߹{#񈿼;#/I"ܫgfVK4R(6:K$$R:e249r@1BRbwE Y%'uc4UU w-..>袋.:Z07PD0FUUy q(3s,˲^9Gu]s.`EcR|V${a`]a`v1o=9beiȜg&ζtn?2ߢ6 +;u=!N"k_v_OI1jI"<[?`:l.--(3'-oyayc^ӣ'"Buꩧ~iZ(њs.y $B꺎|( ~.ln>?F#6xIq.liL6l`=---rb|H#!Ռ1 b 9-9}[vBNyDa ]߿6z2S/t.{sc)JZUU1nBnp.H(s"0=1 9tI!ecGK7mҬ}eESq<#ɄNsWM@$%KD] %>Ɯ_Хߛ_#@1S9I11*׭[72+ih=]:(4 ir;/Ӗzn*/w>t5A|_G>odK3L2y TMӤ*xF"J@s?BTe UMDXK9K @-I+mI+)KQwyAq<я.QǵE({_@@S7.O|r].?Iͼ5odܕcn1?j|yn1A90iwzЛ3jY4$V7U7g< Z\\|>kk/>v\BOP'[JH2s6yn68Żhq0ƍe==gZiB2="{a>կ~gyۢ(<` 2gH ?c4*|J:A ?Q2DKj潾OvsE{b$I"Q(FK_ҙOyS޲{}?y3y#룞_ :Pwx,4PRF73$XH|E`r2 ~8Laf4Rc߆%;ꨣ6e/{|o|#;<"U-ҁ"{*4H%@ ψ@^l46s;mQZNZk`0H 1EaA[^s5y0<-;Nݐc6;m#牭~E+^ yYE F1W oY/ݫthy҇?cֺt!+ҍF#y6eF${ d4ϫ =~{oi&ˢ(k45X'NBc̼y4>>Ow>?3򐇼O~hNg^rF/.ۗ<ύ4LGK8faեwF0˪Zh2 fF(.ʲ|;oG?z H5k^oƾEᬵ{ESB+50O%6LRL9vp-sJ<_I{x'BȎF#BH>%眭<%k0:`2eY&7u]`1ZF5=w88c4PUܥJ*zѨ>D=?_t>UU9}FDY t9cloUKӶ\'E75Użt\UUY]i!lÆ ť^zD?OzlW]uՙo1!|I}XHްT2M &eV+I_b "  E1H𹰧qЊqYGA78eeT1(l5c3āAѓE0Ֆ֦|u73}涛nWln;`簯afXޭR 0-05SAD"YF͜bHҵ@*_f:b0iHn)(tX40c8n_70Buyj+3Fi8ne:ckX3: -$b ӟs~_^a13?8 .]cLfpEab8 5*9a &];A1ub[k<ϭuM`#cj !u$l]a eD1seYq{y~I'7EQdssnIZDA$[PƁy;@4FY5{1YS|=5 ڈJ,7EQ(L)WGgvm]?[WAoO_Z}V&ւmذ!| E4q f8i`<`Ą}YViͲeY1`>,QP"4ާ $1İ&UUUs|-ƘvGei˲t`}g8P6i(UxPh,}}o,䄖:CK? -z۽Qց.D mUUv4%-xMev8T9bJycM@!$hў<!XUU,2uP09sdOHBȾ@DE " DAdO⧠ (A"`ٲ@ trQܾݙtg&>O?3=kխ{y{2K9h>[hbԨQ,XDcLPNQ]1BP((.CTI8m|_R{Oے8W'$[M0"\diJW2oR^|ZTp0gk|Ѻzja23)eW++Pj`QH0 {Ԣ)؅9D t$goZk2ޛ/_OГcZ9sg/^D@"PJZk5 {Iqȯ=`e|C80"*8!p.ؤ Hݐ#ʎ5koˬmC=s{$a"G&\$bJž+)))3!@`E9I):^WC)kz}usUrc_> dBsͪfZ)M${{{͛_hv]wu rvق5*a@sbCc|y>uDs7 S?яUFc=Ze{߭<7>y>aUF\q_GGZ)5Zk0`@W7۫ {yQǽOZDY z}}'a̘1=}e:iTW/zjwk2:{ ̙3,]'O@ǪU~ug> hWP3/VߋJmmB?;0 ! CgW@BW-@ę8~CnYuH$+\[YSIn`.W'1vFہqX.UFLtEpc .Mt epҿ>Ci0Y?7aտ4Px%b|ڤ\>yzwnPJ֦Ҩ}_GWpH|P{~3R%J)@ 5^o49q0AE#H!WmQQJPZkCkm"Rޡ!"@*D~!b&Ii*BD ,so , $ B---AE;3΁=(TZ*25u6:!8\.WuI?9Z͛7˕{+Pa - .DDï|Xރxӛt3'|_iڰyVz*9р 'JQ1_} ^ۼK"˒'$"}{""{2h]}/HW* eٖe$I~L1>NJ*Rf}exE&a_&XkcL|fw}ȑ# 4]g^%xDCnc$q[k-*0",B|mND+W䀧.RBPQ JuƏ|OrTAD_k\d?6A窆*8sHEy b/}Y,$ Z4/p JHE:w#KR`h4Q'dI?`5Px3>+tj%Mx'N1-E24+v.0U '|2X`77X) )#Rn.׌1eQs{7 5/KHv)J˄>3G/@Uպ> D?MSVx`eL:جשER6Ag߿W`ذatMz뭇ZkATh:o[~T5yc&y!$uOL.qR.92cƌA{O<g;nӦM;ϟ`ϖTRkA(''lLl1$/d.{JVq.q@5%@P/؝OWs' `rCA9$D-bD^U~j 믿~~Ⱦ xW~mڴi:DI?G0q 6fgUs/M8BXX%IRU ]%kw#yɏ!p 3Rqfif_Yk{-DQ4sϽOaF?Kn3[Ly%[(>K?CZ?3[UNZoþoommݭzo.X`H)RY̋1cP|=\܄ar a֨+ =>'*Da(|9봫TuYޕe]U/JϺ%`hu$DbӐG$N!ɗZZZ :6еo!Kkx?&7}bx^Dn ·dL)v9x㍪O e![;gZT;IrY'Ib) 4o\R.{D156lWTa6Rt^N/>QL^up`1_)DQDQ#m}RVQ.ΠT*4 , Zk88Z?)(&QO4iXO_+gT)&_QaT*rH,L9ҥKNAG}e.>ꨣveZ b'f~@.^I9yqe>D zvD >OLXx-|AE#5fB\Pк PEliiR#z>`V ߣQB^/Me1w>Kj̓tNO؆&ڔR[IGEP(0>gDγB8?#1)W^FUeR X,W;wV_u1n\5V<蓍P!y]2O/b|xGqYϟ'?8,B#g[^IQZJ"y2d_z*6wqǡP] ߽Ԝ=G]Ykr{P2Eo!_,\VT6R^Lҽ攋t>1RH^B'5O@pF*o_KvO^?~w}QL8 [̈́3J7y׬rN:nU|x6X+} = C)g x!cDξAM<,@J)[, bժUSO=[oGaMUZH)7MnWz]wݵVP#TSR PJD4`ȉ2?{qZB!1FнZe?@{d wuh޼yo;;vAa={{?61AHDϟ|T:q{օ1J8DH)%ǪR$3F'B$Ԋ-%TR)%i~v y$7)cLa O, bM~ $IbM1RA`I/aIzB&9L`  ube[3:}@pe8gΜ.\1~=̚5+ .I!LTaҚmlk6 CKe$B,ˬ0"/25J%afuuk5 0"4@\FD PPhʧO9L b}+f\}ϛ7o ])$IPaiEڟ`$4|6Z숬fȐ!0uTxs@",=Q$ > .J^Rj O^k-Kx͛ӣ:꒻{ZX5B1}>K.몫mbǖh*X.1MSg۷{߼&:ofΜ9h޼y{ ^7֦ϿR*B, 1RJIӔrt߻>$˲mP8Ze4CueL&I⚰OP(KBȖI{lj` q0۵\!8v! ))dh>5XUZGyLc,QȢCtspT2t geNrâ'tTv.0\|6s +V }^m_Cib;P6#}TubM1EZcRcLFbRJe YDK)qV'jyI"^\we& 1dTL#C:4;w}wI&9rСC' 0`F65Ƥ[V_$oGqg r<$J)()eqiyP`ZZZlE62Kڼk>2SC Y TXT8wGk|gؓc@D ~uYg-ȀN"P]xmq _A{I@D;#ƘHo g͚53w}5zY>lٲM-H B\EfLk&?~҈#& :tBE1{ IDATRiݺuVYfʕ+W<~=sZkBEUJY̕eeWJi J)WK`uZk+@ 8hOƄ? To.t}cnyA1n~ c,BF)e0J1^BXkP$ED, r!$65Ez-\*kPFQJYmնNqQcz-:o޼7m^[5 $EV2pq G>2eڴiSF1mjPbŊoLuH)mcHfzRɛo:34A@TAFҜΛ7o.5{~wyDž^rY!l27vD VtvvdU qKDgǟviGM4iR.\xfYZkS7X˲,ˤYX4J)S.M$n>D9okm2%Ud5/ݣZ+rD+=<{#w]N:q>aРAdnУ@D~^y啕W^lٲ{ﳿ/V[k3$2DZk ukbiJnNZW[y8G:q !:]gJ@%0EMw;OAlr e+ӿ_YCib;Q2#yꏧJ)DL2BdYUp p˝ ^t{u(YVr!.QL˲,mmmnx &L1bĤOjmmKkJMalڴ#FOI2kmjuoE6$MӔշpk`LR c 7r{*veLgS{Çrtq'xb~ ,XTP"#/* |;%i_O 0yg9msܸq3:X,ژQ^z⋗1 9ٛ)2CR6JjGR J6Lpey-Xjy"~J+RJ TǼGw<`9~ܹs'>Sƍ7y 4X,jАn }Æ +֮]?|ѢE|+׭[k="BNnRNVYpTˊ"WO $eV ! _aUq+@ @i/!"ꫮj>VP*^Zn%<3/뮻^L%Ib,"P(%ؒĈ̅:췔R#00 LӔ+ȚrpҥM2c=$Iֵ?O?#,_tGydݲeA;ֽ{رc=z4h`kkAmmmMZwYnݺ_60Zk[ggMӔ/RbņaZk0=}z d@{Ъ 0 "r?ٜ>}*$BDJj!Dijɠ5ʈcVyJ1(Z P]PUP'|C=t 1bʠA&NŊ&6mȑ#&D& #$yx]gԩ@/ _qωR^+s}~ĉG6mZvڧ׮]Š+_x zk/[X8{[m!Ţ|7l̙&O.2jȐ!0`L uџJ$B%˲̐͊R,JP.qȐ!nf#6|'wyv6 J)eBÇԧϜ9sq㦌1b'(oYgg 6lxvڵϮ\rC=_qƔ?&g- ) %\%[R=h4W异bRG>aEAgg'wmmmѻQo}[L4iѣwy~HKKƍC/oذa/s=nʸy@NsQJcfH¹7BQ(U&]{/K.=6FˋBLQ+!DDZa5""Z^ö6W͐ {BNIR+ qY_:vw{h7l̺ur+~8 fq쾔R"1s'wu׉Æ 0dȐ7i:蠏? Ȣ(JY\β,D^5`oV" q3z~oxD4+y}OKRJqBϯ,jiya :ꨣL>}ڨQ&2dr[[ R-#͛7?~gۗ/Ydٝw޹{]Ov2+>kTl1I$,+ U#q./ 36ZK"ťP1W Bv Mb)īCwmHr] snZ3eMȷXp*$RDJ*"xi3WB*/ze@GY8EYj'k#ԧr9s>a„iÆ 6uRl&شi3Æ ; b UCBd\*l5`AnzsyrVDJZ)1c O>~",6fYV2Ɣ,̲RR RT(,ٳ'̴֩R*SJe@" !ޑs-@~A 1"IW=TA7xMN{cgΜرc :tZ[[۔b8HڸM6-_nUV-k8N(LȕsL )z=wYJ)80ƨDs.3g?v݆>vРA0`b8?<,o޼W_}'x]|+yIz*5dZ* `oYJ)As Pi**"eQqkcL A"Vnذat{Y޲pRʌK jÒ aR5aP"Je/v"$UI0YiD!WYF`ƌ= zsO ΎƘ,:4̲$EuֺUkݢӪ4M_|ş˗s┒)vDZͲJ|C9ٌX&ڦ}&9 Qe"?i]veڠAN pdǭ!I57lذl͚5K,Yo^lFČ|F@u)}Qa~ՈدmBz^Y(1FeeT׹S:{xIW~&M>&jyxee'\ mha5Q6#])#x1K X(k=]ɸ͛IR)J"8rWJR>E4)e/Mn*Kᆵy'Y2y$ERP6/sr P͛O~0 w p"ji7MR/˧^BVREm#x;::liIp3$EU)a bx뭷2wܫt,} ?O?|1Yn %E )1ZÆ ^:u\]&=*[Ib^eٳg;0`ڎ&cLW_? *,TceaoOg8iZRn:E')S.@O ب!x%[dY^vmmmaԇYIڼy߆~LR)IlCa$I_uIZ JC7q@~:o|up4ykCw ߹/eE2x_K/wڜMxe5%2LJ4qc\5g53f֝QD/?BTUwiVJn4 -nwf@ZRbRͳ/}C򗿼m: G= F|gL4i4tCT-|R#t)t^P=(ZE5bĈIbq:~)e[KKF9gZJuq_7#DW55jжy Rʶnj3ZE{[kVXk_/}[ *͗p,Re!kG̉'М9s], v}bBNj!""&I,!ɻ8~a  C0`r J)@ MTP%`ow}z꩟n(w̨4]Xko,"w3D6d甽iF.rm8 |Tn | Ca8p`(4McZFh@eQJ J)VuP2zjC9~D}a?}v`>&! 540眓_^;pi.{NZzޑ>x~c<Ժρ˛@}$_R!l& Q Szh~UI@-Tk9J !,W>7o޼tbԨQ=Bc p#(~h:PDXkYkA0 '\ rJ)ٿjcuYwvw碋.:=vȠ4- bSN9!vLZk45i"wٞv}發"Pk_MtUykoZX?.7N2@\c P'`[pk;k;꣉ލ]&N=w_|}0rn>&X7>c '2 G/p 9˝ۅBA R4 y \Ñ$Iu|maKMUD̛ _5 rǞye&zix-p2TePYA$[ܧ+ ` *EBAje2Mf@D#IP wK ) kty}1FEQ "ς$I cAs#U *E~H nkh-y /1T#4h EN"j^<@ͱ*J9%6 Cg !2 2!Dm6/4]cGm'pRJep$A`0d2abOۭ¼&e(J"I@DZj,˪z0Km*)%b*Ajau-@q,*.fW[2ew{Ml;?^Zч6`S> ;FN.Y+x2|[o^͹MS8`M4jHVzmKeٚ=#m.DjZE-I){ 'pjGGdz}PM(:;;y=S ]@2P~J@tM5z *ZP1 ƹ9Ϡk!RMVeS;ke=X1]uּ::-٬/ˆUV.ԇP,'mok0 ERBk2JZ(ȮO"_Ymڑcs.zN*^_Q<lUP;N΃ v& V+)w%'d'"Ӻժ۲,Rq/[lgsN͛Im|;y?.#@;u@8ETaǸۭ `277IcIh`L5DQě-Y.{fy(0 Q jP#4ZkC 0 8N ց5ñnݺg^W ܚEE!+#HTjY5D7Ap @RJE b5P("IVt$IU &ݘs{N ք-|\$M Rn6D)g)]uAX,k ^O}  pjkJ֚՚rlLZCyLk@/rl@XkwΞ=HMq0̢(ʴ֎GDeMB 4$I^¿[Z$@DP(݌Uf{T-#4 CZCP& +VC]6> +ykjk0Y\w|O;FN >3~{ٍ>|}XI_w}Oy4cbŊssyN̻:;@(4^?^O{K Y! ^UWQ!H Yˊ,RPHxU\ ٍ%T #z6N+@K +bO?=ӗ{)DnY2nz$/3k]j%rψ͛c=ax4mHd^{G,2kmҐ;Rrr~K14h ~)!˒&Zg?2o lǪ³8Suo.FP+|Jm5^⤉m5/6\ugn}mÇQ FN#:TS[BZ[[(j́~" KPƦ& N:RJGU1J)1MS)\bM\rdYf ZgReYXswtѢE_üQLItwib;^3\ Y:dP"%*|6煕mM2*O$"]V{T~uW#'hb;?gA.Zk@N>[5LǞ>qzes>&B y%>&iz]w7~/>[ZZbh(2RJk5qrlU䮵'Ɵg1)+DG4dYUU\R'~ר cD$5y f=_]PbBAD*ņah162c5J)S,MX.'.\8nQ,'-R JcQ(IyqCǐ$  Zk ;;;T*Yk-' k"GJ25$oy[?ῌ1u&o>}970˕Kl9k45iZ(,_M]%oIT5GUiB\3ftj_'xM;[&7EU䗺4Xurƍ1.HuRʲR5BW-7ec>seML}ŝ.USP^/]m0އg8vIA~? LCWP)<+xx5ŦrH{FQE L5ы$Zс,]iד( /nI$I0cyJidM$ɞz꩎=.؍7>Qnb _k'No~5~4M3Rk!@naIm8F)% lmmVȑi"lL5%"( j6}~ SqU~DrKXK75<"_~G;A1z*04@$a40 mKKiiiJ)C1&C-oy˷֬Y`/ě\&1MS$''=ZZJu [ZZ0k6B(2!L)j믿~ըQNTv+WC=SB Tze3T&^W6I[*2HRIw'w9{sY&{lEI rEUgTjO])$k0-3# _p?$g(k+ݮ<>/$}9HS5iHSmM4tEHl& Y*0r}xcrdeX>@ J)/ό?ć~ƘRO`'FTz;c &I(J O bё>\d/èQfkOy׹8WӚs^2'SzZ*hZ!DirqYgnݺE?B;/rzĉ/[hDt~@{ȕ+!GDYa$$-5-n-|9#k?rs>ksa *(Үpoee p3geMkC ~0>~FNuB0g//.`,vJ < x} VJ1ԞTrbn^.@ϑjF J)7o({ǺGR 43V^]8qW.\|H/[4h~HU ʷZeYAt5RXkd$]$44M$#Ky"~wh̙jrvŊ?G?ŋoNQfA!HyfY&7iϞED TGǤ @&)QE meO^kaÆcGSyOM2 P!|kpXVѽAXOiqS [kB*='R%ؘDM M>1y# &+|'r.N4Qͻ[ṈBaˤ8Cm",)&rB䍡\ (ÓO>yc=v%\rUKK:ٰ@H?7$o2T{?2|&/G(!bӢ^Q;c5,ls/&8xM Oyp 9;`G̈́09~i=vW裏>yȑz?uYsFinXH *Y=SrXڭ.ibEM0Ay9KcOA~Sy/RϲTXw Jj>]*.d!ю, ;^^V:; x׮vOKwe; &y[sp2z+Y߰iÐڏVVZ!gZku{w+<~ԩ)u;>5ko_R^CbΎh yUJd9{;~҆oAȪ4aW q sOchCqG 0gUەcmǠu #G mP "l\ /ڕ>"ھLj Ig6+kOe cvޜc߽'迾oǞVkl=&X־NfO47얕7A4&zS-@%TUaԼG $b/A7lTfIIĔ9y9}饗N;?0nܸ#0ObƍO.[믿~ 7^\)Z  s~ +%\Vqc5kyHi@fQH%kBMh@^؆E@T#\ ʄt{& in'Hg{%HR`dO{e:(q-h,Bl˳hѢ y}!CCOdc{9c%HD5+Ya݋/XkVIܕ/ϿiJMŏL1fLz;wϷÂܺG Y$/χO+}29H!BJ)m ]sQyJ]ʡ7 ",|5@XpprO@E!C)ɘ#IKK~kG>nڴe˖s7e 's YNr  OzSƻNoWA'86HBaQ{pˮ\cι֪ wT"rQQ"ގx"[n~ll}x@Ӣxh[<A@!HHBr*Tus|c~X{${u'+{՚s9ƴ뺳A;"ۈhwGуR<\X}a3ƨ)KK{ ^KsEgNNAU׿͟/l۶-ȍӶmz1玗M(~ Uߦn$"]}B`!7몷_{yoo?Yz]4N+7|{/|G>NNl> k?Pv!d-]YmELpz:"Z:DB=sk6h6lLۥձP\VN6 UUr|;1v\Mٛm#({cq?tqJIvVr'.Rn:`5_u])]v|*ϻ</ r])'ɗ?}\xا3xp, Z2x*T "'a&J!0jzolrqrc*~SjBryme]ٷ^'Hl2u0k_QuC!>mtUg?{~xG^t>tZn{_O~1 t6ġIޔq}xr=N65^RDxn8 A_P j\.9f鯈e}G]Hҝʨc#5\۶dҩjǼ`By 6]ѣGOX>,꽒F[i#RXoO]ץ<9N{k_]tѯnw{ Kܝ.oKoo|+_SJ-)3;~X͇nwhW<3Au]X."VU%vDǞ.`*u][pcUUEz??~ħSqMJiyСO~SzK_w}#9,fcm[3+fGSJiJm&,fH;`Tڶf7j&j)]5mi&}B4mn\~^U7M{ͫz,_/m~UDrd!X,r?1Lr3ì95+\.|>)hZ1PjRjBu1Nb<[g={I91g\${m7)}ʞнJIji{\pyS |ߎ=^,_ҍ;y5W/!TuIȫBcBy S[&4:jhR:N*PmD$Vc*DRJaZEU 14Mh&؀-&ڠ*U1FZөlll*P|RJX,:I4M3nd:惏r2cw,]e: XOUU2$ 6M#" HNhmw2 ".sIDrZp?A0 !v`&-ʂC?7Op)Vc]ded2Ѫt\BQW6L&\VJު ۶ )(ko^m"ڶbq2DU"j*6Mi.f%/yɅ{>Q{9gc>p-|oԥ^z+_ʫjFfU.5YȏM_j96M}m4o߾R H⍶%۞Vޞ[yr 9 3yb/bUU1 ebɍ<ʒ#q/`<z^iRѿ"VUUS^d2I!&×@uԮhPh}n=?$DrĂvTu] "LTX'cwbħ?=}̅^׽[Uuu/>|rn/o}Mf ,tfYWU\F''W'IBUU㙨X!x9۷z ^pɟ .<?LNuэo/_^nX EμP1e{{?#(]Di=if3a=)  T6[l'窪R\.U"Q$>]< 461(YE4 cls {|I2 P>,;;@/^!g$1ߺM$;iZulzgHcW8K&xF|aE?ObpKgBʗFr?.jӷm(k /}9.O'}߽0mo1<3 FD:,`Nm%YXbIBٙ'^bXȦÇo[n馛nnz[rW\!FYP?m;lq mRqm<е+N*WU%"u]׍m6) {8ȏ<=Q"vMr.?x9t^GC==Gm;7wbubޭ.|/2TՕ,cm]׫ZO~ڶM&IjFWڶ=l4"EB?B )h+~,mPUUĖr)oG!`y %&q_0RdʠHˀlII e/ʼngmy³ bʁɋ+ ,յ;l&)%ؐj5 J \= oxbτ+iG< Q׵۷OBp*=6H+҉da⌷sS1ٖnXhJ)f3zaz>Nv4 R _(-r4!@eka4 o~ G6l^8:\uUo|d2Д ŔbՔ\.ME$-M!?sև` .Vr[$s;؉ɨMiN2AZdk9︐,MvK# v:0P5M#u]K2e.l|G~j[CȮ7ԖA&u eD$jM۶"Ķ򣿴'2l"e"鏶SyÄ6 6^_b;"]"e]Pv_;""hE ]7!;[ xa>sPs+"띈|,8~=BЮgݷovػhжmEmLpԟr"D~7֕m+?FyNbWU妫 Wr./M8^"|RyE?,O~v~ ; tNl+k:nCp"@۠Lc/SU@1 6ىu'#BXs 4w $0HAxhXU<()#VKAuDM1ۊXfU>u#2Wdbb:҂a;2?8^oVXIz5_նcbiVe KXVi+V+j8z /b C xsS| fǩ1nau7f%[ѧg&-; f\2G%M D5, {V)%%XQd 'g>R'BXb;EC$rDߕuš\:tH>mM}'}r[>iZEcײ Xط P Oز";jz{R՟]~=.-e@Xܿ=y/-lMmj jw\||:JgTU.oP't¹C$=o>Q`#Jg'H~V#t֙hf~{nll TA:e$,@(6M9CجUQ%d> @xZL)s*ExP=X]G%$t|-į:NRs:X׹3]Q6rq <͔&Xlc(Ry <#.OdѣGu66!o"+|yՅb4(; &?$2Z6RpĄRRnO"9+jf#p- /Q]aQcx04(t۶rpɠt:ŤFDD(@\,x.j-p[G͆& m>ѱȆ7+q\jV"(B2mvv;CRME6 OV+f.rȃ)(q]Bcs2lml)dVrx'p?{O&Ea[1ctTz9R*]a҄mšL [1g38k1]8#x z0tobBEJ(.OO<Щjm'bJLVpz'ÚtI*$yADJwػC e;)dy}sP ȧms{E'1l]cu]=Xi,ďk"}F&\3}LbINv I/ٞs&َX<ok; aP=)!+'!{n Ԟ"C+ޱgH pdu1jvDF^MCh)uD}\"Y %D0>̓ucむ Pa5vcp̷jke~!Ds=% KQ]7ESRm (}E; O{ᡈL:yzAUHXC_`ƾc4 y1'1tpRd |+#gx~ڥRaq8$_FCbu BL,H(cRIzCet0~`W>4GcLe=emllw"=4nX N+)7K&PJ"39Hg? tt;d-||Ȼ< f2EO4IL2bVUԙr Ѡ-[xrDd@s=L&J>Qh2/0RI+GTo'6y0l۶acc#O,NޛtMtBHS4MPky)" S[(xoPMX*a?X(?kex.f31jҮ͞U~Bmv.ѺC[JQE0%e|N)!>HҿI`' wLniamD:E;#ΦʴG9.Fe-""Є=Aj$`Jn&;!ЍvHaq)δ̶2.(/E./m=ORI[H))YgmHj=梿k }al lF1*~hXԂ$ŦBq}mvd3QKDK1FL&lgNԕ@;`v0[JI9rx'0O3>I:hjs$K[Dp߰ x")ig>B_C$E6(fP&IޙgSU;0F/m\!`3Eýy)׏O|8tҷW.|q8nZyuyC.C,l?]wj+"Cĩb%JJ50Gm>>|dMCٟ؃bJ,'<($`xm51u IDAT~TNnKRul8Ćxo'eJpZ:)x6'&OGi+R?8}""1=U\.o^#>0Kv'v>dZ[$䶞j v&z>_8NA "JOXyh}b7fr_J u -Hb0wQbd/ԡ FlHv@dESD6kLdxCiGBXd0Lz ,k#3F>ARA()0 |^yqe7(T߲^cqpH)ຓm3ܾr-۲/m*wRJ2BHePsË-GjmfpgٯoxT>MNXjD! \DXTrNFrP>ŏܶ-Ӡ̅ˤl8 {^sl`<Z!P "2h'aiˉyydX|[#wm~N^|yփcaZM# UxƂKʼ{x΋_;藹~X ٥򎯃%_ ./b l7灇LLOwۿE?/{n\߾K'N rLm_s^!QWfb>+!<љL&:ԞcS6j[H?p$9\0MDNL($ރW a0#Ա%(luVMRlT>SyG]Vrd2(  3  }"/&mߝjtIMbHu]t:UzW8L2ً2?Xx̅ tD#Ab*^~DU9QedD،\'ik$Yzr 2ܩ!"w(叶m:$r|#;s%$ KxoYNHw\sl^in|Z9bЦiN 1-"mڨ+b0LBJ)sáZ X,rF{ uҢ \(1[lHak?cT#p6\_=i>b\lS ~`R!+nCWvVg:_˝ov•=_aazI2U’<y_UUFtf%6XbMhwj9!6 oOK^G9 ZDy< vD5ϗ~~q/1 Q˲-q=hn3./ dDb,wc/:  cI33߽߅'=?'-{5_]^GqR?y=y/vwO`Npk* ~0rWlr|`O5RhZ(ѾhDŽ-(CF,nS}n^O$ B$R01nJ)SѣGcVLB4_ D_._z@.?c6uFK.yq_.k۔vqt:]أl)FH^ ![MӰ++~e6'aᜀklKo/O9<~*=4Oڛآ* c;RMۨx,@_io'w<1d#r;|쿼K|5{aX]^/zF],_ '8f&0y%صR\.Y6 N't]dQ@|X +x psؘx $` DtB{.Qm%D ]NuO:Ů.|-OdXu? cpO]%G1;j  ;&DƷYd2Pae0rRYE[ ;Za }nȾͿ H9jGSCOEq at(G6 '?vm˻D}va}& 2î+ =(0 Q W^KxçMoG}|4d [!w v ^ (K^U#b178Bv}r9LQѿ 3wd#F܇p˟\W3Xf>e!Z~Y?%?gˍ\wdȩr@ Ukc92H1<9mM$?~_J婿\9 ͛*;e𑽎㫟^.W/_/\$n^G1wAM,2T?B$z}<-AV 'Vc*&;YJ,+YErbOuLxBjJOMjlq?1*U"2OlJiI<,ˁ[ Trs`OQPUWo31wqE|=‹jjWutN) AGrK1}Ecd,FoD/RYʓR:>t_r՚p9O\%CdMΔqq؜u˦ܙ( 2-*(,m8` =h'u+6pzQu޾/]p" .]!lXeH߇o3 ,ir@fr{þ0KL*ڵȚDD(˒Ig{^NW;{'=WL&wpA/"v )%xE-vg2!}G87e>~F~m*/Z !~}-/qv8pU(*UD|SrBRN#N=zwaTsFJI:Di RP u ؂ǀY)x(ZLY(KS!"kW"2*=&;"n- ^2̪wwQ A؃%|Wyi ƞ-tDnD{;(o;awwcxiXȁc Esb.!}+ȽqY~/ܠ"*)G_I 79ڦ;2_ߑ@ޕgbی$aT.lo7(B}興8$~C{Aj <[^ƶ;ǩ&uEք/O8aH8TVR 6u}̅$8p-4ު/bЊ*/Ա&m۞>Uw~v2 H}wp8( \ Կ"öD6+1K%gy&/8ɱkc xУK[ϓ]ཎt;'Mn`-"Cϼ-p8NoHsc6>;õh<[O]Ip8mޅ p[(]ڌkx>~k:*"rQUN}]73Wq:a/+rG:nC vS蘵sy<ɏ;)/yk$W#{>Tuo+/x;3J+pɰup||+W0vtZСÇovxwp6[RJyapZɎf:'w?+OxH3ut䋟Bgo?x^G8c"ÓJykZp8p8ƶ Vw0%y3 ?$_ss=ʵ\-{ ڢ]2 (}H:p8pMnuK7m7>A/y<ߵPUȻKjOp ey`S] N:p8-l:v 0~^p"'clc׻M*W=&k,-)C ywp8p8N`ȚeB*Tv1;#(?̧ɿ埗}w;kRO?O]gp8C2W v]cԈ:p8pz6Z6(TU4MUU5߼Bž3˽=\1|/Y<8`xXuD$/]98p8p$M _&ph#x< Oɣ#{+ɵ|p8 ZXwp8p6vEX[ՔV #;rED5|߻gqp8GѮ4l*@Omp8p8v ;B_]:ZwW1rWU:'g| _jw~y,vړ88; ]ץR&wݔ&?p8p8Nc/L&NZm[cԺUUm[I""N 'K_ g{zݪr{qWOUS!feW "Bj 9p8p8v;BZMz&$5 !.UU]դD=ܳw=鱻Q|=?7iOp8UM!cLUUiUU|P$[:p8p*MI25ZU@fDo1j ް G=|?nfK|߿ureWN BL&Zuٿm[/^U*^p8p8 ju][ Mh1omVU !$I".)xvs_xF7I^/!BBHf4.@䪪'`s8p8.v䐵!_UN&ZEDWUmۤIDnޟz<ϕ}tőC{]=w8; Ab)VU2,*oF IDAT᧬9p8ctRcbݪ:xp;ssO7k? ].ox/GNǮBDK)c5HYp8p8Q0DUeZI۶b'U mFөR4AUC۶R]$Oysdz`NwIogk'jJ"u]5> ;(p8p8cW2RtZj1sLi~,C}x Ѡ"QE*҇$H˦p+O}/|؉c&UUR%~鲒zpzMmnT-ZNصMwa42>x7{c **AewdM@G˗KIG}[e/ x`PPE!c HLu\.mۜr9P\>(kDXeHgYI)IqP޻JQgVUmrL] \]2pNzTUUiuIzUUI۶p8p8Ǯ`GmjƃFiJށ2~dퟷ28oJ)dMLLZd*fRYDVqDa 5&^/HW (ޭxcˀX{J"߉T%A L1ǁq9:VJq:+yjemLٌA~tŽmjÁ؅ 75nJ)uvJ*ap8p8%xDQ՞BH,}A\4e]߆h%AfB*@Y y ‡kad,?DVw+2o,*ɺ7yY\cJ2ń";F21iby.$ǀaJTw+bL ݙDު|w97˶Q+<[eH_VR׵LB c$R/ˍVCL'tB*]qOAU7p8p8v ;F=zTgY`_!`>.jNYi׃pH twUFR#\@=ߦ΂D*3R|g gUڇ,@DBuȾW]DXŪD1y0^</+Wզ+}禔r> ~PD#e8x'439b>:%S9YY )+2珥v-߉z`77fY* >5.eZ693Lyl \ee>VUX`27 ˅B2[BO7q0 ŮA{N)9p8p8cWc|>d'=ph}}و7Uz7IAA[AF -I?5)ȤTPe#IR%1:&EYZ޿\KIXW/eR2 m,ݜt(Kv]P1~. ba♉AH?eUU$>sre2#2RJY 1o[]ײo>v]7Y>pc2ǵbWUU~>EJ,r1L>&rO˱c/u܇w.i77[0.>z3jn_t\j۶AlMH۶cp8p8cWo6+x Q|S1T@6&j$w6?D$RJ*դL+] L w˃8R}22mcw3K%, LxXQ74A$d{j(9%<,t._.W{r gyq; e96 _(FZ/">߲X9,>,6纮+4X8!s@/^կ8zJp8p8c7ÇkR׵CJI[;_`rAO#o[] ̠d:kc&1t:̈́"L81x0x<+ +)831|,E9 ?ĸΊ`V|PZxmf@2ZJg< k&J%)IUgnD$?(cW._+3mV=s9 ط1,cwV]M&Y,e=F\s]+ J\a6Ҏ\,vJrQQVUUt:A7mW.F r;r3p8p8NviVYU+ujv `,e J^B5_.,Ba ^T]t]%T 00TW&xRヰBظƮH<S[YlG]$K5nz1Z. >Ve1קޔJj D,#&bEG&Fދ:PV<8 VwsNy_C.\) \\f1FeLu-p`iP|'W>h=]p8p8'BH@jM5[3xL$/ԅPLA!H#eU%EzDء\ 8dr~Z`H !r%VxI>c{. ["6] c b7|޲*u'ƮƔ[~t{0/߃ SbaBq_,rz J1)(c~`6-7M&"Bց wcE~ox?]ƺoⅎ'v8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8p8[Bi-IENDB`lektor-lektor-52c4448/example/content/plugin-use/000077500000000000000000000000001466050247100217715ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/plugin-use/contents.lr000066400000000000000000000005521466050247100241670ustar00rootroot00000000000000_model: page --- title: Plugin Use --- body: Here the lektor-markdown-highlighter is being used for a very serious Python snippet. ```python from sys import exit def say_hi(): print("Hello World!") # Engage in conversation def ignore_everything(): exit(0) # Successful exit if being_looked_at is True: say_hi() else: ignore_everything() ``` lektor-lektor-52c4448/example/content/project-categories/000077500000000000000000000000001466050247100234725ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-1/000077500000000000000000000000001466050247100254455ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-1/contents.lr000066400000000000000000000000361466050247100276400ustar00rootroot00000000000000name: Category 1: Single Fish lektor-lektor-52c4448/example/content/project-categories/category-2/000077500000000000000000000000001466050247100254465ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-2/contents.lr000066400000000000000000000000401466050247100276340ustar00rootroot00000000000000name: Category 2: Pairs of Fish lektor-lektor-52c4448/example/content/project-categories/category-3/000077500000000000000000000000001466050247100254475ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-3/contents.lr000066400000000000000000000000331466050247100276370ustar00rootroot00000000000000name: Category 3: Red Fish lektor-lektor-52c4448/example/content/project-categories/category-4/000077500000000000000000000000001466050247100254505ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-4/contents.lr000066400000000000000000000000341466050247100276410ustar00rootroot00000000000000name: Category 4: Blue Fish lektor-lektor-52c4448/example/content/project-categories/category-5/000077500000000000000000000000001466050247100254515ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/project-categories/category-5/contents.lr000066400000000000000000000000321466050247100276400ustar00rootroot00000000000000name: Category 5: Cthulhu lektor-lektor-52c4448/example/content/project-categories/contents.lr000066400000000000000000000000721466050247100256650ustar00rootroot00000000000000_model: project-categories --- _slug: projects/categories lektor-lektor-52c4448/example/content/projects/000077500000000000000000000000001466050247100215325ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/projects/contents.lr000066400000000000000000000001651466050247100237300ustar00rootroot00000000000000_model: projects --- title: Projects --- body: This is a list of the projects: * Project 1 * Project 2 * Project 3 lektor-lektor-52c4448/example/content/projects/project-a/000077500000000000000000000000001466050247100234165ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/projects/project-a/contents.lr000066400000000000000000000001631466050247100256120ustar00rootroot00000000000000name: Project A --- date: 2017-09-13 --- description: Project A description --- categories: category-1, category-2 lektor-lektor-52c4448/example/content/projects/project-b/000077500000000000000000000000001466050247100234175ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/projects/project-b/contents.lr000066400000000000000000000001771466050247100256200ustar00rootroot00000000000000name: Project B --- categories: category-3, category-4, category-2 --- date: 2017-09-19 --- description: Project B description lektor-lektor-52c4448/example/content/projects/project-c/000077500000000000000000000000001466050247100234205ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/projects/project-c/contents.lr000066400000000000000000000001141466050247100256100ustar00rootroot00000000000000name: Project C --- date: 2017-09-18 --- description: Project C description lektor-lektor-52c4448/example/content/types/000077500000000000000000000000001466050247100210455ustar00rootroot00000000000000lektor-lektor-52c4448/example/content/types/contents.lr000066400000000000000000000013431466050247100232420ustar00rootroot00000000000000body: This page is an example of every built-in field type Lektor has. --- title: Types --- boolean1: yes --- date: 2018-07-04 --- datetime: 2018-07-04 05:02:22 EST --- checkboxes: choice1, choice3 --- float: 2.71828 --- floataddon: 3.14 --- int: 1729 --- intaddon: 137 --- flow: #### text #### text: Text from text only flow block. #### text_and_html #### text: Text from text_and_html flow block. ---- html: html from text_and_html flow block. --- html:

--- markdown: - kg - m - s - A - K - mol - cd --- sort_key: 2 --- strings: string1 string2 string3 --- text: Here is some sample text. lektor-lektor-52c4448/example/flowblocks/000077500000000000000000000000001466050247100203745ustar00rootroot00000000000000lektor-lektor-52c4448/example/flowblocks/text.ini000066400000000000000000000001321466050247100220550ustar00rootroot00000000000000[block] name = Text Block button_label = Text [fields.text] label = Text type = markdown lektor-lektor-52c4448/example/flowblocks/text_and_html.ini000066400000000000000000000002201466050247100237210ustar00rootroot00000000000000[block] name = Text and HTML Block button_label = Text+HTML [fields.text] label = Text type = markdown [fields.html] label = HTML type = html lektor-lektor-52c4448/example/models/000077500000000000000000000000001466050247100175125ustar00rootroot00000000000000lektor-lektor-52c4448/example/models/attachment.ini000066400000000000000000000002121466050247100223360ustar00rootroot00000000000000[model] name = Attachment label = Attachment [fields.description] label = {{ site.get('types') }} type = string default = {{ this.body }}lektor-lektor-52c4448/example/models/blog-post.ini000066400000000000000000000005741466050247100221270ustar00rootroot00000000000000[model] name = Blog Post label = {{ this.title }} hidden = yes [fields.title] label = Title type = string size = large [fields.author] label = Author type = string width = 1/2 [fields.twitter_handle] label = Twitter Handle type = string width = 1/4 addon_label = @ [fields.pub_date] label = Publication date type = date width = 1/4 [fields.body] label = Body type = markdown lektor-lektor-52c4448/example/models/blog.ini000066400000000000000000000002751466050247100211420ustar00rootroot00000000000000[model] name = Blog label = Blog hidden = yes [fields.title] label = Title type = string [children] model = blog-post order_by = -pub_date, title [pagination] enabled = yes per_page = 3 lektor-lektor-52c4448/example/models/page.ini000066400000000000000000000002711466050247100211270ustar00rootroot00000000000000[model] name = Page label = {{ this.title }} [fields.title] label = Title type = string [fields.alt_note] label = Alt Note type = markdown [fields.body] label = Body type = markdown lektor-lektor-52c4448/example/models/project-categories.ini000066400000000000000000000002171466050247100240040ustar00rootroot00000000000000[model] name = Project Categories label = Project Categories hidden = yes protected = yes [children] model = project-category order_by = name lektor-lektor-52c4448/example/models/project-category.ini000066400000000000000000000003071466050247100234740ustar00rootroot00000000000000[model] name = Project Category label = {{ this.name }} hidden = yes [children] replaced_with = site.query('/projects').filter(F.categories.contains(this)) [fields.name] label = Name type = string lektor-lektor-52c4448/example/models/project.ini000066400000000000000000000004551466050247100216650ustar00rootroot00000000000000[model] name = Project label = {{ this.name }} hidden = yes [fields.name] label = Name type = string [fields.date] label = Date type = date [fields.description] label = Description type = markdown [fields.categories] label = Categories type = checkboxes source = site.query('/project-categories') lektor-lektor-52c4448/example/models/projects.ini000066400000000000000000000001711466050247100220430ustar00rootroot00000000000000[model] name = Projects label = Projects hidden = yes protected = yes [children] model = project order_by = -date, name lektor-lektor-52c4448/example/models/types.ini000066400000000000000000000023401466050247100213560ustar00rootroot00000000000000[model] name = Field Types label = {{ this.title }} inherits = page [fields.title] label = Title type = string [fields.boolean1] type = boolean checkbox_label = If true, then some sample text will render. default = false [fields.boolean2] type = boolean checkbox_label = If true, then some sample text will render. default = false [fields.checkboxes] label = Checkboxes type = checkboxes choices = choice1, choice2, choice3, choice4 choice_labels = choice_label1, choice_label2, choice_label3, choice_label4 [fields.date] label = Date type = date [fields.datetime] label = Datetime type = datetime [fields.float] label = Float type = float [fields.floataddon] label = FloatAddon type = float description = Percentage mark added addon_label = % [fields.flow] label = Flow type = flow flow_blocks = text_and_html, text [fields.int] label = Int type = integer [fields.intaddon] label = IntAddon type = integer description = px mark added addon_label = px [fields.html] label = HTML type = html [fields.markdown] label = Markdown type = markdown [fields.sort_key] label = Sort_Key type = sort_key [fields.markdown] label = Markdown type = markdown [fields.strings] label = Strings type = strings [fields.text] label = Text type = text lektor-lektor-52c4448/example/templates/000077500000000000000000000000001466050247100202255ustar00rootroot00000000000000lektor-lektor-52c4448/example/templates/blocks/000077500000000000000000000000001466050247100215025ustar00rootroot00000000000000lektor-lektor-52c4448/example/templates/blocks/text.html000066400000000000000000000000371466050247100233540ustar00rootroot00000000000000{# intentionally left blank #} lektor-lektor-52c4448/example/templates/blocks/text_and_html.html000066400000000000000000000001341466050247100252200ustar00rootroot00000000000000{{ this.html }} - color applied in block's template file. lektor-lektor-52c4448/example/templates/blog-post.html000066400000000000000000000003011466050247100230130ustar00rootroot00000000000000{% extends "layout.html" %} {% from "macros/blog.html" import render_blog_post %} {% block title %}{{ this.title }}{% endblock %} {% block body %} {{ render_blog_post(this) }} {% endblock %} lektor-lektor-52c4448/example/templates/blog.html000066400000000000000000000006111466050247100220340ustar00rootroot00000000000000{% extends "layout.html" %} {% from "macros/blog.html" import render_blog_post %} {% from "macros/pagination.html" import render_pagination %} {% block title %}{{ this.title }}{% endblock %} {% block body %} {% for child in this.pagination.items %} {{ render_blog_post(child, from_index=true, blog_post=false) }} {% endfor %} {{ render_pagination(this.pagination) }} {% endblock %} lektor-lektor-52c4448/example/templates/layout.html000066400000000000000000000043651466050247100224400ustar00rootroot00000000000000 {% block title %}Welcome{% endblock %} — Example

Example

{% if this._path == '/' %}
    Available alts: {% for alt in get_alts() %}
  • {{ alt }}
  • {% endfor %}
{{ this.alt_note }} {% endif %} {% block body %}{% endblock %}
Recursive tree navigation of this example site:
© Copyright 2015 by Armin Ronacher.
lektor-lektor-52c4448/example/templates/macros/000077500000000000000000000000001466050247100215115ustar00rootroot00000000000000lektor-lektor-52c4448/example/templates/macros/blog.html000066400000000000000000000030711466050247100233230ustar00rootroot00000000000000{% macro render_blog_post(post, from_index=false, blog_post=true) %}
{% if from_index %}

{{ post.title }}

{% else %}

{{ post.title }}

{% endif %}

written by {% if post.twitter_handle %} {{ post.author or post.twitter_handle }} {% else %} {{ post.author }} {% endif %} on {{ post.pub_date }}

{{ post.body }}
{# display nav links to other blog posts, but hide on parent blog page #} {% if blog_post %} {{ render_blog_sibling_nav(post) }} {% endif %} {% endmacro %} {% macro render_blog_sibling_nav(post, from_index=false) %} {% from "macros/pagination.html" import render_pagination %}

{% set siblings = post.get_siblings() %} {# prev/next are swapped since blog children ordered_by is negative {# nav logic for previous page #} {% if siblings.next_page %} {% set prev = siblings.next_page %} {% endif %} {# nav logic for next page #} {% if siblings.prev_page %} {% set next = siblings.prev_page %} {% endif %} {# previous / nex nav links #} {% if prev %} « Previous {% endif %} {% if prev %} | {% endif %} {{ post.title }} {% if next %} | {% endif %} {% if next %} Next » {% endif %}
{% endmacro %} lektor-lektor-52c4448/example/templates/macros/pagination.html000066400000000000000000000007331466050247100245330ustar00rootroot00000000000000{% macro render_pagination(pagination) %} {% endmacro %} lektor-lektor-52c4448/example/templates/macros/projects.html000066400000000000000000000007311466050247100242310ustar00rootroot00000000000000{% macro render_category_nav(active=none) %} {% endmacro %} {% macro render_project_list(projects) %} {% endmacro %} lektor-lektor-52c4448/example/templates/page.html000066400000000000000000000002321466050247100220240ustar00rootroot00000000000000{% extends "layout.html" %} {% block title %}{{ this.title }}{% endblock %} {% block body %}

{{ this.title }}

{{ this.body }} {% endblock %} lektor-lektor-52c4448/example/templates/project-categories.html000066400000000000000000000003621466050247100247050ustar00rootroot00000000000000{% extends "layout.html" %} {% from "macros/projects.html" import render_category_nav %} {% block title %}Project Categories{% endblock %} {% block body %}

Project Categories

{{ render_category_nav(active=none) }} {% endblock %} lektor-lektor-52c4448/example/templates/project-category.html000066400000000000000000000005251466050247100243760ustar00rootroot00000000000000{% extends "layout.html" %} {% from "macros/projects.html" import render_category_nav, render_project_list %} {% block title %}Project Category {{ this.name }}{% endblock %} {% block body %}

Project Category {{ this.name }}

{{ render_category_nav(active=this._id) }} {{ render_project_list(this.children) }} {% endblock %} lektor-lektor-52c4448/example/templates/project.html000066400000000000000000000002761466050247100225660ustar00rootroot00000000000000{% extends "layout.html" %} {% block title %}{{ this.name }}{% endblock %} {% block body %}

{{ this.name }}

{{ this.description }}
{% endblock %} lektor-lektor-52c4448/example/templates/projects.html000066400000000000000000000004401466050247100227420ustar00rootroot00000000000000{% extends "layout.html" %} {% from "macros/projects.html" import render_project_list, render_category_nav %} {% block title %}Projects{% endblock %} {% block body %}

Projects

{{ render_category_nav(active=none) }} {{ render_project_list(this.children) }} {% endblock %} lektor-lektor-52c4448/example/templates/types.html000066400000000000000000000036701466050247100222650ustar00rootroot00000000000000{% extends "page.html" %} {% block body %} {{ super() }} {# super duper! #}

The title of this page is a string field. ^^

{# bool1 #}

booleans:
{% if this.boolean1 %} Boolean1 caused True text! {% else %} Boolean1 caused False text! {% endif %} Boolean1 = {{ this.boolean1 }} {# bool2 #}
{% if this.boolean2 %} Boolean2 caused True text! {% else %} Boolean2 caused False text! {% endif %} Boolean2 = {{ this.boolean2 }}

{# checkboxes #}

Of the checkboxes 1-4, boxes {% for box in this.checkboxes %} {{ "and " if loop.last }} {{ box }}{{ ", " if not loop.last }} {% endfor %} are checked.

{# dates #}

A date: {{ this.date }}
A datetime formatted three ways:

  • {{ this.datetime }}
  • {{ this.datetime.strftime('%Y-%m-%d %H:%M') }}
  • {{ this.datetime.strftime('%m/%d/%Y %H:%M') }}

{# numbers #}

A float: {{ this.float }} and {{ this.floataddon }}
A int: {{ this.int }} and {{ this.intaddon }}

{# flow blocks #}

These flow blocks are called manually from the main template for this page:
{% for blk in this.flow.blocks %} {{ blk.text }} {% endfor %} and all 'html' blocks with the additional templates in templates/blocks/:
{{ this.flow }}

Raw HTML:
{{ this.html }}

markdown: {{ this.markdown }}

strings: {{ this.strings }}
{% for string in this.strings %} {{ "and " if loop.last }} {{ string }}{{ ", " if not loop.last }} {% endfor %}

Unformatted text: {{ this.text }}

sort_key: {{ this.sort_key }}

{% endblock body %} lektor-lektor-52c4448/example/themes/000077500000000000000000000000001466050247100175145ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/README.md000066400000000000000000000007261466050247100210000ustar00rootroot00000000000000# About this theme in the example project There are several ways to add themes to a project, including using submodules. This theme is a copy of the lektor-theme-nix at ff71ea6cd2dd8ff83268891f9cdd60b344517064 and is not a repository or submodule. It is not intended to change often, and is a simple copy to keep cloning lektor very simple - without someone having to know how to manage submodules, or have to follow additional instruction steps to populate the theme. lektor-lektor-52c4448/example/themes/lektor-theme-nix/000077500000000000000000000000001466050247100227105ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/LICENSE.md000066400000000000000000000021371466050247100243170ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Matúš Námešný Copyright (c) 2017 Rafael Laverde Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. lektor-lektor-52c4448/example/themes/lektor-theme-nix/README.md000066400000000000000000000024731466050247100241750ustar00rootroot00000000000000# Nix Nix is a simple, minimal theme for [Lektor](https://www.getlektor.com/) based in [Nix hugo theme](https://github.com/LordMathis/hugo-theme-nix) # Configuration Create a `404.html/contents.lr` content file pointing to 404.html, using a none model [see Lektor docs](https://www.getlektor.com/docs/guides/error-pages) Create a `contents.lr` content file pointing to index.html, using a none model Add lektor-disqus-comments plugin an configure it https://github.com/lektor/lektor-disqus-comments#lektor-disqus-comments Add params in the `.lektorproject file` ```ini [theme_settings] githubID = "your_github" gitlabId = "your_gitlab" twitterID = "your_twitter" codepenID = "your_codepen" linkedInID = "your_linkedin" googleplusID = "your_googleplus" facebookID = "your_facebook" instagramID = "your_instagram" telegramID = "your_telegram" name = "your_name" headerusername = "username" headerhostname = "hostname" email = "your_email" about = "info_about_you" profilepicture = "profile_picture_asset_url" googleanalytics = "your_google_analytics_id" slackURL = "https://join.slack.com/..." comments = "yes" ``` Add your profile picture in the assets folder and set the path in `profilepicture` (e.g. `img/myprofilepicture.png`) ## License Nix is licensed under the [MIT License](LICENSE.md) lektor-lektor-52c4448/example/themes/lektor-theme-nix/assets/000077500000000000000000000000001466050247100242125ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/assets/static/000077500000000000000000000000001466050247100255015ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/assets/static/css/000077500000000000000000000000001466050247100262715ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/assets/static/css/nix.css000066400000000000000000000033701466050247100276040ustar00rootroot00000000000000* { margin: 0; } html { height: 100%; } body { background-color: #fdfdfd; height: 100%; font-family: 'Open Sans', sans-serif; font-size: 1.5em; padding-top: 70px; } #green-terminal { color: #00ff00; } .font-header { font-family: 'Inconsolata', monospace; } .font-paragraph { font-family: 'Open Sans', sans-serif; } nav { font-size: 1.5em; } h1,h2,h3,h4,h5,h6 { font-family: 'Concert One', cursive; } #user-name { font-size: 5em; } .user-description { border-style: solid; border-width: 5px; border-color: #292929; margin-top: 20px; margin-bottom: 1em; padding: 15px; font-size: initial; } .user-profile { padding: 0; } .user-picture { border-style: solid; border-width: 5px; border-color: #292929; margin-top: 20px; } i { margin: 10px 20px 10px 20px; color: #333; } .post-link { float: left; } .post-date { float: right; } .post-header { background-color: #F5F5F5; overflow: hidden; padding: 0 5px 0 5px; } .post-summary, .post-content{ clear: both; } .post-summary { padding: 0 5px 0 5px; } .post-list-footer { padding-bottom: 5px; } #post-list { list-style: none; padding-left: 0; } .post-list-item { margin-top: 20px; border-style: solid; border-width: 0 0 3px 0; border-color: #292929; } .post-comments { padding-top: 10px; border-top-style: solid; } .wrapper { min-height: 100%; margin: 0 auto -50px; } .footer { background-color: #F5F5F5; display: block; width: 100%; padding: 10px; height: 50px; } .push { height: 50px; } .col-centered{ float: none; margin: 0 auto; } div p { text-align: justify; } td { margin: 0 20px 0 20px; } lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/000077500000000000000000000000001466050247100241735ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/blog-post.ini000066400000000000000000000005741466050247100266100ustar00rootroot00000000000000[model] name = Blog Post label = {{ this.title }} hidden = yes [fields.title] label = Title type = string size = large [fields.author] label = Author type = string width = 1/2 [fields.twitter_handle] label = Twitter Handle type = string width = 1/4 addon_label = @ [fields.pub_date] label = Publication date type = date width = 1/4 [fields.body] label = Body type = markdown lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/blog.ini000066400000000000000000000002761466050247100256240ustar00rootroot00000000000000[model] name = Blog label = Blog hidden = yes [fields.title] label = Title type = string [children] model = blog-post order_by = -pub_date, title [pagination] enabled = yes per_page = 10 lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/page.ini000066400000000000000000000002051466050247100256050ustar00rootroot00000000000000[model] name = Page label = {{ this.title }} [fields.title] label = Title type = string [fields.body] label = Body type = markdown lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/showcase-item.ini000066400000000000000000000005021466050247100274410ustar00rootroot00000000000000[model] name = Site label = {{ this.name }} hidden = yes [fields.name] label = Name type = string size = large [fields.url] label = URL type = url width = 1/2 [fields.cover_image] label = Cover Image type = select source = record.attachments.images width = 1/2 [fields.description] label = Description type = markdown lektor-lektor-52c4448/example/themes/lektor-theme-nix/models/showcase.ini000066400000000000000000000002241466050247100265060ustar00rootroot00000000000000[model] name = Showcase label = Showcase hidden = yes [fields.title] label = Title type = string [children] model = showcase-item order_by = name lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/000077500000000000000000000000001466050247100247065ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/404.html000066400000000000000000000012211466050247100260770ustar00rootroot00000000000000 {{ config.PROJECT.name }} {% include "partials/head.html" %} {% include "partials/header.html" %}
{% if this.title %}

{{ this.title }}

{% else %}

Uhm... WHAT?

{% endif %}
{% if this.body %} {{ this.body }} {% else %}

Looks like you're lost. This page doesn't exist.

{% endif %}
{% include "partials/footer.html" %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/blog-post.html000066400000000000000000000010251466050247100275000ustar00rootroot00000000000000{% extends "layout.html" %} {% block content %} {% with post = this %}

{{ post.title }}

{{ post.body }}
{% if config.THEME_SETTINGS.comments == 'yes' %}
{{ render_disqus_comments() }}
{% endif %} {% endwith %} {% endblock content %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/blog.html000066400000000000000000000016371466050247100265260ustar00rootroot00000000000000{% from "macros/pagination.html" import render_pagination %} {% extends "layout.html" %} {% block content %}

{{ this.title }}

    {% for post in this.pagination.items %}
  • {{ post.title }}

    {{ post.summary }}

    Read More
  • {% endfor %}
{{ render_pagination(this.pagination) }} {% endblock content %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/index.html000066400000000000000000000016141466050247100267050ustar00rootroot00000000000000{% extends "layout.html" %} {% block content %} {% if config.THEME_SETTINGS.profilepicture %}
profile-picture
{% endif %}
{% include "partials/social.html" %}

{{ config.THEME_SETTINGS.about }}

{% endblock content %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/layout.html000066400000000000000000000005761466050247100271210ustar00rootroot00000000000000 {{ this.title }} · {{ config.PROJECT.name }} {% include "partials/head.html" %} {% include "partials/header.html" %}
{% block content %} {% endblock%}
{% include "partials/footer.html" %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/macros/000077500000000000000000000000001466050247100261725ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/macros/pagination.html000066400000000000000000000023651466050247100312170ustar00rootroot00000000000000{% macro render_pagination(pagination) %} {% if pagination.pages > 1 %}
    {% with first = pagination.for_page(0) %}
  • {% endwith %}
  • {% for page in this.pagination.pages %}
  • {{ page }}
  • {% endfor %}
  • {% with last = pagination.for_page(pagination.pages) %}
  • {% endwith %}
{% endif %} {% endmacro %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/page.html000066400000000000000000000002571466050247100265140ustar00rootroot00000000000000{% extends "layout.html" %} {% block content %}

{{ this.title }}

{{ this.body }}
{% endblock content %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/partials/000077500000000000000000000000001466050247100265255ustar00rootroot00000000000000lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/partials/footer.html000066400000000000000000000005261466050247100307140ustar00rootroot00000000000000

Copyright © 2017 {{ config.THEME_SETTINGS.name }} - Powered by Lektor and Nix theme.

lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/partials/head.html000066400000000000000000000034161466050247100303200ustar00rootroot00000000000000 {% if config.THEME_SETTINGS.googleanalytics %} {% endif %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/partials/header.html000066400000000000000000000023601466050247100306440ustar00rootroot00000000000000
lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/partials/social.html000066400000000000000000000041571466050247100306740ustar00rootroot00000000000000{% if config.THEME_SETTINGS.slackURL %} {% endif %} {% if config.THEME_SETTINGS.twitterID %} {% endif %} {% if config.THEME_SETTINGS.googleplusID %} {% endif %} {% if config.THEME_SETTINGS.facebookID %} {% endif %} {% if config.THEME_SETTINGS.githubID %} {% endif %} {% if config.THEME_SETTINGS.gitlabId %} {% endif %} {% if config.THEME_SETTINGS.codepenID %} {% endif %} {% if config.THEME_SETTINGS.linkedInID %} {% endif %} {% if config.THEME_SETTINGS.instagramID %} {% endif %} {% if config.THEME_SETTINGS.telegramID %} {% endif %} {% if config.THEME_SETTINGS.email %} {% endif %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/showcase-item.html000066400000000000000000000002651466050247100303470ustar00rootroot00000000000000{% extends "layout.html" %} {% block content %}

{{ this.name }}

{{ this.description }}
{% endblock content %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/templates/showcase.html000066400000000000000000000012701466050247100274100ustar00rootroot00000000000000{% extends "layout.html" %} {% block content %}

Talks

{{ this.intro }}

{% endblock %} lektor-lektor-52c4448/example/themes/lektor-theme-nix/theme.ini000066400000000000000000000010311466050247100245060ustar00rootroot00000000000000[theme] name = Nix license = MIT licenselink = https://github.com/rlaverde/lektor-theme-nix/blob/master/LICENSE.md description = Simple, minimal theme for Lektor homepage = https://github.com/rlaverde/lektor-theme-nix tags = simple, minimal, unix, terminal, blog features = blog lektor_required_version = 3.1 [author] name = rlaverde homepage = http://rlaverde.github.io/ [original] author = Matúš Námešný homepage = https://namesny.com repo = https://github.com/LordMathis/hugo-theme-nix [packages] lektor-disqus-comments = 0.2 lektor-lektor-52c4448/frontend/000077500000000000000000000000001466050247100164135ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/.eslintrc.js000066400000000000000000000006611466050247100206550ustar00rootroot00000000000000module.exports = { env: { browser: true, es2021: true, }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:react/recommended", "plugin:react-hooks/recommended", ], parser: "@typescript-eslint/parser", settings: { react: { version: "detect", }, }, plugins: ["@typescript-eslint", "react"], rules: { "react/button-has-type": "error", }, }; lektor-lektor-52c4448/frontend/babel-require.js000066400000000000000000000001501466050247100214640ustar00rootroot00000000000000// eslint-disable-next-line require("@babel/register")({ extensions: [".js", ".jsx", ".ts", ".tsx"] }); lektor-lektor-52c4448/frontend/js/000077500000000000000000000000001466050247100170275ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/components/000077500000000000000000000000001466050247100212145ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/components/AdminLink.tsx000066400000000000000000000017261466050247100236300ustar00rootroot00000000000000import React, { forwardRef, memo, ForwardedRef, ReactNode } from "react"; import { NavLink } from "react-router-dom"; import { PageName } from "../context/page-context"; import { RecordPathDetails, useRecord } from "../context/record-context"; import { adminPath } from "./use-go-to-admin-page"; function AdminLink( { page, path, alt, children, ...otherProps }: RecordPathDetails & { page: PageName; children: ReactNode; onClick?: React.MouseEventHandler; title?: string; }, ref: ForwardedRef ): JSX.Element { const current = useRecord(); const recordMatches = path === current.path && alt === current.alt; return ( recordMatches && match != null} {...otherProps} ref={ref} > {children} ); } export default memo(forwardRef(AdminLink)); lektor-lektor-52c4448/frontend/js/components/AdminLinkWithHotkey.tsx000066400000000000000000000027721466050247100256520ustar00rootroot00000000000000import React, { MutableRefObject, ReactNode, useCallback, useEffect, useRef, } from "react"; import { PageName } from "../context/page-context"; import { RecordPathDetails } from "../context/record-context"; import { getKey, KeyboardShortcut, keyboardShortcutHandler } from "../utils"; import AdminLink from "./AdminLink"; /** * React hook to add a global keyboard shortcut for the given * key for the lifetime of the component. */ function useKeyboardShortcut( key: KeyboardShortcut, action: (ev: KeyboardEvent) => void ): void { useEffect(() => { const handler = keyboardShortcutHandler(key, action); window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [key, action]); } /** * Add a global keyboard shortcut for the given key and simulate * a click on the ref'ed element. */ function useKeyboardShortcutRef( key: KeyboardShortcut ): MutableRefObject { const el = useRef(null); const handler = useCallback(() => { el.current?.click(); }, []); useKeyboardShortcut(key, handler); return el; } export default function AdminLinkWithHotkey({ children, shortcut, ...linkProps }: RecordPathDetails & { page: PageName; children: ReactNode; shortcut: KeyboardShortcut; }): JSX.Element { const el = useKeyboardShortcutRef(shortcut); return ( {children} ); } lektor-lektor-52c4448/frontend/js/components/DialogSlot.tsx000066400000000000000000000027471466050247100240270ustar00rootroot00000000000000import React, { useCallback, useEffect, useState } from "react"; import FindFiles from "../dialogs/find-files/FindFiles"; import Publish from "../dialogs/Publish"; import Refresh from "../dialogs/Refresh"; import { LektorEvents, subscribe, unsubscribe } from "../events"; type DialogDetails = LektorEvents["lektor-dialog"]; type DialogState = (DialogDetails & { preventNavigation?: boolean }) | null; export default function DialogSlot(): JSX.Element | null { const [dialog, setDialog] = useState(null); const dismiss = useCallback( () => setDialog((c) => (c?.preventNavigation ? c : null)), [] ); const prevent = useCallback( (preventNavigation: boolean) => setDialog((d) => (d ? { ...d, preventNavigation } : null)), [] ); useEffect(() => { const handler = ({ detail }: CustomEvent) => { // Only change dialog if there is no dialog yet. setDialog((current) => current ?? detail); }; subscribe("lektor-dialog", handler); return () => unsubscribe("lektor-dialog", handler); }, []); if (!dialog) { return null; } if (dialog.type === "find-files") { return ; } else if (dialog.type === "refresh") { return ; } else if (dialog.type === "publish") { return ; } const exhaustiveCheck: never = dialog.type; throw new Error(exhaustiveCheck); } lektor-lektor-52c4448/frontend/js/components/ErrorDialog.tsx000066400000000000000000000022451466050247100241700ustar00rootroot00000000000000import React, { useCallback, useEffect, useState } from "react"; import SlideDialog from "./SlideDialog"; import { trans, TranslationEntry } from "../i18n"; import { LektorEvents, subscribe, unsubscribe } from "../events"; /** * Listen to events and show an error dialog (potentially on top of an open * dialog). */ export default function ErrorDialog(): JSX.Element | null { const [error, setError] = useState<{ code: string } | null>(null); const dismiss = useCallback(() => setError(null), []); useEffect(() => { const handler = ({ detail }: CustomEvent) => setError(detail); subscribe("lektor-error", handler); return () => unsubscribe("lektor-error", handler); }, []); if (!error) { return null; } return (

{trans("ERROR_OCURRED")} {": "} {trans(("ERROR_" + error.code) as TranslationEntry)}

); } lektor-lektor-52c4448/frontend/js/components/ServerStatus.tsx000066400000000000000000000022401466050247100244240ustar00rootroot00000000000000import React, { useEffect, useState } from "react"; import { get } from "../fetch"; import { trans } from "../i18n"; type State = { serverIsUp: boolean; projectId: string | null }; export default function ServerStatus(): JSX.Element | null { const [state, setState] = useState({ serverIsUp: true, projectId: null, }); useEffect(() => { const onInterval = () => { get("/ping", null).then( ({ project_id }) => { setState(({ projectId }) => projectId === null ? { projectId: project_id, serverIsUp: true } : { projectId, serverIsUp: projectId === project_id } ); }, () => { setState((s) => ({ ...s, serverIsUp: false })); } ); }; const id = window.setInterval(onInterval, 2000); return () => window.clearInterval(id); }, []); if (state.serverIsUp) { return null; } return (

{trans("ERROR_SERVER_UNAVAILABLE")}

{trans("ERROR_SERVER_UNAVAILABLE_MESSAGE")}

); } lektor-lektor-52c4448/frontend/js/components/SlideDialog.tsx000066400000000000000000000022751466050247100241420ustar00rootroot00000000000000import React, { ReactNode, useEffect } from "react"; import { trans } from "../i18n"; export default function SlideDialog({ title, hasCloseButton, dismiss, children, }: { title: string; hasCloseButton: boolean; dismiss: () => void; children: ReactNode; }): JSX.Element { useEffect(() => { const handler = (ev: KeyboardEvent) => { if (ev.key === "Escape") { ev.preventDefault(); dismiss(); } }; window.addEventListener("keydown", handler); return () => window.removeEventListener("keydown", handler); }, [dismiss]); useEffect(() => { window.scrollTo(0, 0); }, []); return (
{hasCloseButton && ( { ev.preventDefault(); dismiss(); }} > {trans("CLOSE")} )}

{title}

{children}
); } lektor-lektor-52c4448/frontend/js/components/ToggleGroup.test.tsx000066400000000000000000000021301466050247100251640ustar00rootroot00000000000000import ToggleGroup from "./ToggleGroup"; import React from "react"; import { render } from "react-dom"; import ReactTestUtils from "react-dom/test-utils"; import { JSDOM } from "jsdom"; import { ok } from "assert"; const jsdom = new JSDOM(``); // @ts-expect-error Assigning jsdom.window to window fails global.window = jsdom.window; const document = window.document; const renderToggle = () => { document.body.innerHTML = ""; const container = document.createElement("div"); document.body.appendChild(container); render(
Rick Astley rulz
, container ); return container; }; describe("ToggleGroup", () => { it("renders a closed toggle group", () => { const container = renderToggle(); ok(container.innerHTML.includes("closed")); }); it("renders an open toggle group when toggled", () => { const container = renderToggle(); const el = document.querySelector(".toggle-group h4"); el && ReactTestUtils.Simulate.click(el); ok(!container.innerHTML.includes("closed")); }); }); lektor-lektor-52c4448/frontend/js/components/ToggleGroup.tsx000066400000000000000000000012711466050247100242130ustar00rootroot00000000000000import React, { MouseEvent, useCallback, useState } from "react"; export default function ToggleGroup({ className = "", groupTitle, children, }: { className?: string; groupTitle: string; children: React.ReactNode; }) { const [open, setOpen] = useState(false); const toggle = useCallback((event: MouseEvent) => { event.preventDefault(); setOpen((v) => !v); }, []); return (

{groupTitle}

{children}
); } lektor-lektor-52c4448/frontend/js/components/types.ts000066400000000000000000000015021466050247100227260ustar00rootroot00000000000000import { RecordPath, RecordAlternative } from "../context/record-context"; import type { Translatable } from "../i18n"; export type Alternative = { alt: RecordAlternative; is_primary: boolean; primary_overlay: boolean; name_i18n: Translatable; exists: boolean; }; export type RecordChild = { id: string; path: RecordPath; label: string; label_i18n: Translatable; visible: boolean; }; export type RecordAttachment = { id: string; path: RecordPath; type: string; }; // Returned by /recordinfo export type RecordInfo = { id: string; path: RecordPath; label_i18n?: Translatable; exists: boolean; is_attachment: boolean; attachments: RecordAttachment[]; children: RecordChild[]; alts: Alternative[]; can_have_children: boolean; can_have_attachments: boolean; can_be_deleted: boolean; }; lektor-lektor-52c4448/frontend/js/components/use-go-to-admin-page.ts000066400000000000000000000020221466050247100253770ustar00rootroot00000000000000import { useCallback } from "react"; import { useHistory } from "react-router-dom"; import { PageName } from "../context/page-context"; import { RecordAlternative, RecordPath } from "../context/record-context"; /** * Compute an admin path. * @param page - e.g. edit or preview * @param path - fs path to the record * @param alt - the alternative to use. * @returns */ export function adminPath( page: PageName, path: RecordPath, alt: RecordAlternative ): string { const params = new URLSearchParams({ path }); if (alt !== "_primary") { params.set("alt", alt); } return `${$LEKTOR_CONFIG.admin_root}/${page}?${params}`; } /** * Use a function to change the admin page. * @returns A function to navigate to an admin page for a given view * and page fs path and alt. */ export function useGoToAdminPage() { const history = useHistory(); return useCallback( (name: PageName, path: RecordPath, alt: RecordAlternative) => { history.push(adminPath(name, path, alt)); }, [history] ); } lektor-lektor-52c4448/frontend/js/context/000077500000000000000000000000001466050247100205135ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/context/page-context.ts000066400000000000000000000006471466050247100234700ustar00rootroot00000000000000import { createContext } from "react"; export const PAGE_NAMES = [ "edit", "delete", "preview", "add-child", "upload", ] as const; export type PageName = typeof PAGE_NAMES[number]; export function isPageName(p: unknown): p is PageName { return PAGE_NAMES.includes(p as PageName); } /** The currently rendered page of the Lektor admin interface. */ export const PageContext = createContext("edit"); lektor-lektor-52c4448/frontend/js/context/record-context.ts000066400000000000000000000016611466050247100240270ustar00rootroot00000000000000import { createContext, useContext } from "react"; /** Path to a Lektor record. */ export type RecordPath = `/${string}`; /** Alternative of a Lektor record. */ export type RecordAlternative = string; /** Details about the path to a Lektor record. */ export type RecordPathDetails = { /** Path of the current record (Lektor db path). */ path: RecordPath; /** The alternative of the record. */ alt: RecordAlternative; }; export const RecordContext = createContext({ path: "/", alt: "_primary", }); /** The current record. */ export function useRecord(): RecordPathDetails { return useContext(RecordContext); } /** The alternative of the currently active record. */ export function useRecordAlt(): string { const record = useRecord(); return record.alt; } /** The path of the currently active record. */ export function useRecordPath(): RecordPath { const record = useRecord(); return record.path; } lektor-lektor-52c4448/frontend/js/dialogs/000077500000000000000000000000001466050247100204515ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/dialogs/Publish.tsx000066400000000000000000000100571466050247100226220ustar00rootroot00000000000000import React, { ChangeEvent, useCallback, useEffect, useRef, useState, } from "react"; import SlideDialog from "../components/SlideDialog"; import { apiUrl, get, post } from "../fetch"; import { trans, trans_fallback } from "../i18n"; import { showErrorDialog } from "../error-dialog"; export interface Server { id: string; short_target: string; name: string; name_i18n: Partial>; } /** * Render a {serverOptions}
); } type PublishState = "IDLE" | "BUILDING" | "PUBLISH" | "DONE"; function BuildLog({ log }: { log: string[] }) { const buildLog = useRef(null); useEffect(() => { const node = buildLog.current; if (node) { node.scrollTop = node.scrollHeight; } }, [log]); return (
      {log.join("\n")}
    
); } function Publish({ dismiss, preventNavigation, }: { dismiss: () => void; preventNavigation: (b: boolean) => void; }): JSX.Element { const [servers, setServers] = useState([]); const [activeTarget, setActiveTarget] = useState(""); const [log, setLog] = useState([]); const [state, setState] = useState("IDLE"); useEffect(() => { get("/servers", null).then(({ servers }) => { setServers(servers); setActiveTarget(servers.length ? servers[0].id : ""); }, showErrorDialog); }, []); const onPublish = useCallback(() => { setLog([]); setState("BUILDING"); preventNavigation(true); post("/build", null).then(() => { setState("PUBLISH"); const eventSource = new EventSource( apiUrl("/publish", { server: activeTarget }) ); eventSource.addEventListener("message", (event) => { const data = JSON.parse(event.data); if (data === null) { setState("DONE"); preventNavigation(false); eventSource.close(); } else { setLog((log) => log.concat(data.msg)); } }); }, showErrorDialog); }, [activeTarget, preventNavigation]); const isSafeToPublish = state === "IDLE" || state === "DONE"; return (

{trans("PUBLISH_NOTE")}

{" "}

{state !== "IDLE" ? ( <>

{state !== "DONE" ? trans("CURRENTLY_PUBLISHING") : trans("PUBLISH_DONE")}

{trans("STATE") + ": " + trans(`PUBLISH_STATE_${state}`)}
) : null}
); } export default Publish; lektor-lektor-52c4448/frontend/js/dialogs/Refresh.tsx000066400000000000000000000032411466050247100226070ustar00rootroot00000000000000import React, { useCallback, useEffect, useState } from "react"; import SlideDialog from "../components/SlideDialog"; import { post } from "../fetch"; import { trans } from "../i18n"; import { showErrorDialog } from "../error-dialog"; export default function Refresh({ dismiss, preventNavigation, }: { dismiss: () => void; preventNavigation: (v: boolean) => void; }): JSX.Element { const [state, setState] = useState<"IDLE" | "DONE" | "CLEANING">("IDLE"); const isSafeToNavigate = state === "IDLE" || state === "DONE"; const refresh = useCallback(() => { setState("CLEANING"); post("/clean", null).then(() => { setState("DONE"); }, showErrorDialog); }, []); useEffect( () => preventNavigation(!isSafeToNavigate), [preventNavigation, isSafeToNavigate] ); return (

{trans("REFRESH_BUILD_NOTE")}

{" "}

{state !== "IDLE" && (

{state !== "DONE" ? trans("CURRENTLY_REFRESHING_BUILD") : trans("REFRESHING_BUILD_DONE")}

)}
); } lektor-lektor-52c4448/frontend/js/dialogs/find-files/000077500000000000000000000000001466050247100224715ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/dialogs/find-files/FindFiles.tsx000066400000000000000000000055001466050247100250740ustar00rootroot00000000000000import React, { KeyboardEvent, useCallback, useContext, useEffect, useState, } from "react"; import { RecordPath, useRecordAlt } from "../../context/record-context"; import SlideDialog from "../../components/SlideDialog"; import { post } from "../../fetch"; import { getCurrentLanguge, trans } from "../../i18n"; import { showErrorDialog } from "../../error-dialog"; import ResultRow from "./ResultRow"; import { useGoToAdminPage } from "../../components/use-go-to-admin-page"; import { PageContext } from "../../context/page-context"; export type SearchResult = { parents: { title: string }[]; path: RecordPath; title: string; }; function FindFiles({ dismiss }: { dismiss: () => void }): JSX.Element { const alt = useRecordAlt(); const page = useContext(PageContext); const [query, setQuery] = useState(""); const [results, setResults] = useState([]); const [selected, setSelected] = useState(-1); const goToAdminPage = useGoToAdminPage(); const target = page === "preview" ? "preview" : "edit"; useEffect(() => { if (!query) { setResults([]); setSelected(-1); return; } let ignore = false; post("/find", { q: query, alt, lang: getCurrentLanguge() }).then( ({ results }) => { if (!ignore) { setResults(results); setSelected((selected) => Math.min(selected, results.length - 1)); } }, showErrorDialog ); return () => { ignore = true; }; }, [alt, query]); const onInputKey = useCallback( (event: KeyboardEvent) => { if (event.key === "ArrowDown") { event.preventDefault(); setSelected((selected) => (selected + 1) % results.length); } else if (event.key === "ArrowUp") { event.preventDefault(); setSelected( (selected) => (selected - 1 + results.length) % results.length ); } else if (event.key === "Enter") { const item = results[selected]; if (item) { dismiss(); goToAdminPage(target, item.path, alt); } } }, [alt, dismiss, goToAdminPage, results, selected, target] ); return ( setQuery(ev.target.value)} onKeyDown={onInputKey} placeholder={trans("FIND_FILES_PLACEHOLDER")} />
    {results.map((result, idx) => ( ))}
); } export default FindFiles; lektor-lektor-52c4448/frontend/js/dialogs/find-files/ResultRow.tsx000066400000000000000000000015201466050247100251750ustar00rootroot00000000000000import React from "react"; import AdminLink from "../../components/AdminLink"; import { PageName } from "../../context/page-context"; import { RecordAlternative } from "../../context/record-context"; import { SearchResult } from "./FindFiles"; /** * A page in the result list in the find files dialog. */ export default function ResultRow({ result, isActive, dismiss, alt, target, }: { result: SearchResult; isActive: boolean; dismiss: () => void; alt: RecordAlternative; target: PageName; }): JSX.Element { return (
  • {result.parents.map((item) => ( {item.title} ))} {result.title}
  • ); } lektor-lektor-52c4448/frontend/js/error-dialog.ts000066400000000000000000000004131466050247100217630ustar00rootroot00000000000000import { dispatch } from "./events"; import { FetchError } from "./fetch"; export function showErrorDialog(error: unknown): void { if (error instanceof FetchError) { dispatch("lektor-error", error); } else { console.error("unknown error:", error); } } lektor-lektor-52c4448/frontend/js/events.ts000066400000000000000000000015651466050247100207120ustar00rootroot00000000000000export type LektorEvents = { "lektor-attachments-changed": string; "lektor-dialog": { type: "find-files" | "refresh" | "publish" }; "lektor-error": { code: string }; }; /** Dispatch one of the custom events. */ export function dispatch( type: T, detail: LektorEvents[T] ): void { document.dispatchEvent(new CustomEvent(type, { detail })); } /** Subscribe to one of Lektor's custom events. */ export function subscribe( type: T, handler: (ev: CustomEvent) => void ): void { document.addEventListener(type, handler as EventListener); } /** Subscribe from one of Lektor's custom events. */ export function unsubscribe( type: T, handler: (ev: CustomEvent) => void ): void { document.removeEventListener(type, handler as EventListener); } lektor-lektor-52c4448/frontend/js/fetch.ts000066400000000000000000000115001466050247100204650ustar00rootroot00000000000000import { RecordInfo } from "./components/types"; import { RecordAlternative, RecordPath, RecordPathDetails, } from "./context/record-context"; import { SearchResult } from "./dialogs/find-files/FindFiles"; import { Server } from "./dialogs/Publish"; import { RecordPathInfoSegment } from "./header/BreadCrumbs"; import { NewRecordInfo } from "./views/add-child-page/types"; import { RawRecord } from "./views/edit/EditPage"; export class FetchError extends Error { constructor(readonly code: string) { super(); } } /** * Handle a JSON response - throw on HTTP error. */ function handleJSON(response: Response) { if (!response.ok) { throw new FetchError("REQUEST_FAILED"); } return response.json(); } const credentials = "same-origin"; const headers = { "Content-Type": "application/json" }; /** * Helper to execute one of the fetch requests to the Lektor admin API. */ function fetchJSON( input: string, method: "GET" | "POST" | "PUT", json?: unknown ): Promise { const init: RequestInit = json === undefined ? { credentials, method } : { credentials, method, headers, body: JSON.stringify(json) }; return fetch(input, init).then(handleJSON); } /** Required URL parameters for GET API endpoints. */ type GetAPIParams = { "/matchurl": { url_path: string }; "/newattachment": { path: RecordPath }; "/newrecord": { path: RecordPath; alt?: RecordAlternative }; "/pathinfo": { path: RecordPath }; "/ping": null; "/previewinfo": RecordPathDetails; "/rawrecord": RecordPathDetails; "/recordinfo": { path: RecordPath }; "/servers": null; }; /** Type of the returned JSON for GET API endpoints. */ type GetAPIReturns = { "/matchurl": RecordPathDetails & { exists: boolean }; "/newattachment": { label: string; can_upload: boolean }; "/newrecord": NewRecordInfo; "/pathinfo": { segments: RecordPathInfoSegment[] }; "/ping": { project_id: string }; "/previewinfo": { url: string | null }; "/rawrecord": RawRecord; "/recordinfo": RecordInfo; "/servers": { servers: Server[] }; }; /** * Required URL parameters for POST API endpoints. * Currently one endpoint (newrecord) has the data sent as JSON which isn't typed yet. */ type PostAPIParams = { "/browsefs": RecordPathDetails; "/build": null; "/clean": null; "/deleterecord": RecordPathDetails & { delete_master: "1" | "0" }; "/find": { q: string; alt: RecordAlternative; lang: string }; "/newrecord": null; // it's all in the JSON request. }; /** Type of the returned JSON for POST API endpoints. `unknown` in case that it isn't used */ type PostAPIReturns = { "/browsefs": { okay: boolean }; "/build": unknown; "/clean": unknown; "/deleterecord": unknown; "/find": { results: SearchResult[] }; "/newrecord": { valid_id: boolean; exists: boolean; path: RecordPath }; }; /** * Required JSON data for PUT API endpoints. */ type PutAPIData = { "/rawrecord": RecordPathDetails & { data: Record }; }; /** Type of the returned JSON for PUT API endpoints. `unknown` in case that it isn't used */ type PutAPIReturns = { "/rawrecord": unknown; }; /** * URL to one of Lektor's API endpoints with query string. * @param endpoint - the API endpoint * @param params - possible URL parameters to include in the query string * @returns the absolute API URL with query string. */ export function apiUrl( endpoint: string, params?: Record | null ): string { const url = `${$LEKTOR_CONFIG.admin_root}/api${endpoint}`; if (params) { const urlParams = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { urlParams.set(key, value); }); return `${url}?${urlParams.toString()}`; } return url; } /** * Load data from the JSON API. * @param endpoint - The API endpoint to get. * @param params - URL params to set. * @param json - Possible JSON body of the request. */ export function get( endpoint: T, params: GetAPIParams[T] ): Promise { const url = apiUrl(endpoint, params); return fetchJSON(url, "GET") as Promise; } /** * Execute a POST api request. * @param endpoint - The API endpoint to POST to. * @param params - URL params to set. * @param json - Possible JSON body of the request. */ export function post( endpoint: T, params: PostAPIParams[T], json?: unknown ): Promise { const url = apiUrl(endpoint, params); return fetchJSON(url, "POST", json) as Promise; } /** * Execute a PUT api request. * @param endpoint - The API endpoint to PUT to. * @param json - Possible JSON body of the request. */ export function put( endpoint: T, json: PutAPIData[T] ): Promise { const url = apiUrl(endpoint); return fetchJSON(url, "PUT", json); } lektor-lektor-52c4448/frontend/js/globals.d.ts000066400000000000000000000001361466050247100212440ustar00rootroot00000000000000declare const $LEKTOR_CONFIG: { admin_root: string; site_root: string; lang: string; }; lektor-lektor-52c4448/frontend/js/header/000077500000000000000000000000001466050247100202575ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/header/BreadCrumbs.tsx000066400000000000000000000046521466050247100232170ustar00rootroot00000000000000import React, { useContext, useEffect, useState } from "react"; import { useRecord } from "../context/record-context"; import { get } from "../fetch"; import { trans, trans_fallback } from "../i18n"; import { showErrorDialog } from "../error-dialog"; import AdminLink from "../components/AdminLink"; import { RecordAlternative, RecordPath } from "../context/record-context"; import { PageContext } from "../context/page-context"; export interface RecordPathInfoSegment { id: string; path: RecordPath; label: string; label_i18n?: Record; exists: boolean; can_have_children: boolean; } function Crumb({ alt, item, targetPage, }: { alt: RecordAlternative; item: RecordPathInfoSegment; targetPage: "preview" | "edit"; }) { const { path, exists } = item; const label = exists ? trans_fallback(item.label_i18n, item.label) : item.id; const className = exists ? "breadcrumb-item" : "breadcrumb-item missing-record-crumb"; return (
  • {label}
  • ); } function AddNewPage({ alt, item, }: { alt: RecordAlternative; item: RecordPathInfoSegment; }) { return item.can_have_children ? (
  • +
  • ) : null; } function BreadCrumbs(): JSX.Element { const page = useContext(PageContext); const { path, alt } = useRecord(); const [segments, setSegments] = useState( null ); useEffect(() => { let ignore = false; get("/pathinfo", { path }).then((resp) => { if (!ignore) { setSegments(resp.segments); } }, showErrorDialog); return () => { ignore = true; }; }, [path]); if (!segments) { return (
    • {trans("BACK_TO_OVERVIEW")}
    ); } const target = page === "preview" ? "preview" : "edit"; const lastItem = segments[segments.length - 1]; return (
      {segments.map((item) => ( ))}
    ); } export default BreadCrumbs; lektor-lektor-52c4448/frontend/js/header/GlobalActions.tsx000066400000000000000000000040331466050247100235400ustar00rootroot00000000000000import React, { useCallback, useEffect } from "react"; import { useRecord } from "../context/record-context"; import { getCanonicalUrl, keyboardShortcutHandler } from "../utils"; import { get } from "../fetch"; import { trans } from "../i18n"; import { showErrorDialog } from "../error-dialog"; import { dispatch } from "../events"; const findFiles = () => dispatch("lektor-dialog", { type: "find-files" }); const refresh = () => dispatch("lektor-dialog", { type: "refresh" }); const publish = () => dispatch("lektor-dialog", { type: "publish" }); const onKeyPress = keyboardShortcutHandler( { key: "Control+g", mac: "Meta+g", preventDefault: true }, findFiles ); export default function GlobalActions(): JSX.Element { const record = useRecord(); useEffect(() => { window.addEventListener("keydown", onKeyPress); return () => window.removeEventListener("keydown", onKeyPress); }, []); const returnToWebsite = useCallback(() => { get("/previewinfo", record).then(({ url }) => { window.location.href = url === null ? getCanonicalUrl("/") : getCanonicalUrl(url); }, showErrorDialog); }, [record]); return (
    ); } lektor-lektor-52c4448/frontend/js/header/Header.tsx000066400000000000000000000014361466050247100222130ustar00rootroot00000000000000import React from "react"; import BreadCrumbs from "./BreadCrumbs"; import GlobalActions from "./GlobalActions"; export default function Header({ sidebarIsActive, toggleSidebar, }: { sidebarIsActive: boolean; toggleSidebar: () => void; }): JSX.Element { return (
    ); } lektor-lektor-52c4448/frontend/js/i18n.test.ts000066400000000000000000000007331466050247100211370ustar00rootroot00000000000000import { promises } from "fs"; import { strictEqual } from "assert"; import { translations } from "./i18n"; import { join } from "path"; const { readdir } = promises; it("i18n: imports translations for all languages", () => { return readdir(join(__dirname, "..", "..", "lektor", "translations")).then( (allTranslations) => strictEqual( allTranslations.filter((s) => s.endsWith(".json")).length, Object.keys(translations).length ) ); }); lektor-lektor-52c4448/frontend/js/i18n.tsx000066400000000000000000000046351466050247100203560ustar00rootroot00000000000000import ca from "../../lektor/translations/ca.json"; import de from "../../lektor/translations/de.json"; import en from "../../lektor/translations/en.json"; import es from "../../lektor/translations/es.json"; import fr from "../../lektor/translations/fr.json"; import it from "../../lektor/translations/it.json"; import ja from "../../lektor/translations/ja.json"; import ko from "../../lektor/translations/ko.json"; import nl from "../../lektor/translations/nl.json"; import pl from "../../lektor/translations/pl.json"; import pt from "../../lektor/translations/pt.json"; import ru from "../../lektor/translations/ru.json"; import zh from "../../lektor/translations/zh.json"; type LektorTranslations = typeof en; export type TranslationEntry = keyof LektorTranslations; export const translations: Record> = { ca, de, en, es, fr, it, ja, ko, nl, pl, pt, ru, zh, }; let currentLanguage = "en"; let currentTranslations = translations[currentLanguage] ?? {}; export function setCurrentLanguage(lang: string): void { currentLanguage = lang; currentTranslations = translations[currentLanguage]; } export function getCurrentLanguge(): string { return currentLanguage; } export type Translatable = Partial>; /** * Get translation for a key. * @param key - The translation key. */ export function trans(key: TranslationEntry): string { return currentTranslations[key] ?? key; } /** * Get translation from an object of translations * @param translation_object - The object containing translations. */ export function trans_obj(translation_object: Translatable): string { return translation_object[currentLanguage] ?? translation_object.en ?? ""; } /** * Get translation for a key with a fallback. * @param translation_object - The translation key * @param fallback - A fallback to use if the translation is missing. */ export function trans_fallback( translation_object: Translatable | undefined, fallback: string ): string { if (!translation_object) { return fallback; } return trans_obj(translation_object) || fallback; } /** * Get translation for a key with a `%s` replacement. * @param key - The translation key * @param replacement - replacement for `%s`. */ export function trans_format( key: TranslationEntry, replacement: string ): string { const translation = trans(key); return translation.replace("%s", replacement); } lektor-lektor-52c4448/frontend/js/main.tsx000066400000000000000000000030041466050247100205100ustar00rootroot00000000000000import React, { useMemo } from "react"; import ReactDOM from "react-dom"; import { BrowserRouter, Redirect, useLocation, useRouteMatch, } from "react-router-dom"; import { setCurrentLanguage } from "./i18n"; import { RecordContext, RecordPathDetails } from "./context/record-context"; import App from "./views/App"; import { adminPath } from "./components/use-go-to-admin-page"; import "font-awesome/css/font-awesome.css"; import "../scss/main.scss"; import { PageContext, PageName, isPageName } from "./context/page-context"; import { trimSlashes } from "./utils"; function Page({ page }: { page: PageName }) { const { search } = useLocation(); const record = useMemo((): RecordPathDetails => { const params = new URLSearchParams(search); return { path: `/${trimSlashes(params.get("path") ?? "/")}`, alt: params.get("alt") ?? "_primary", }; }, [search]); return ( ); } function Main() { const root = $LEKTOR_CONFIG.admin_root; const page = useRouteMatch<{ page: string }>(`${root}/:page`)?.params.page; if (!isPageName(page)) { return ; } return ; } const dash = document.getElementById("dash"); if (dash) { setCurrentLanguage($LEKTOR_CONFIG.lang); ReactDOM.render(
    , dash ); } lektor-lektor-52c4448/frontend/js/sidebar/000077500000000000000000000000001466050247100204405ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/sidebar/Alternatives.tsx000066400000000000000000000021531466050247100236420ustar00rootroot00000000000000import React, { memo, useContext } from "react"; import { Alternative } from "../components/types"; import { trans, trans_obj } from "../i18n"; import AdminLink from "../components/AdminLink"; import { PageContext } from "../context/page-context"; import { useRecordPath } from "../context/record-context"; function Alternatives({ alts }: { alts: Alternative[] }) { const page = useContext(PageContext); const path = useRecordPath(); if (alts.length < 2) { return null; } const items = alts.map((item) => { let title = trans_obj(item.name_i18n); if (item.is_primary) { title += ` (${trans("PRIMARY_ALT")})`; } else if (item.primary_overlay) { title += ` (${trans("PRIMARY_OVERLAY")})`; } const className = item.exists ? "alt" : "alt alt-missing"; return (
  • {title}
  • ); }); return ( <>

    {trans("ALTS")}

      {items}
    ); } export default memo(Alternatives); lektor-lektor-52c4448/frontend/js/sidebar/AttachmentActions.tsx000066400000000000000000000017001466050247100246070ustar00rootroot00000000000000import React, { memo } from "react"; import AdminLink from "../components/AdminLink"; import { useRecordAlt } from "../context/record-context"; import { RecordInfo } from "../components/types"; import { trans } from "../i18n"; function AttachmentActions({ recordInfo }: { recordInfo: RecordInfo }) { const alt = useRecordAlt(); const attachments = recordInfo.attachments; return ( <>

    {trans("ATTACHMENTS")}

      {attachments.length > 0 ? ( attachments.map((atch) => { return (
    • {atch.id} ({atch.type})
    • ); }) ) : (
    • {trans("NO_ATTACHMENTS")}
    • )}
    ); } export default memo(AttachmentActions); lektor-lektor-52c4448/frontend/js/sidebar/ChildActions.tsx000066400000000000000000000025001466050247100235410ustar00rootroot00000000000000import React from "react"; import { trans, trans_obj } from "../i18n"; import { useRecordAlt } from "../context/record-context"; import { RecordChild } from "../components/types"; import ChildPagination from "./ChildPagination"; import { CHILDREN_PER_PAGE } from "./constants"; import AdminLink from "../components/AdminLink"; export default function ChildActions({ targetPage, allChildren, page, setPage, }: { targetPage: "preview" | "edit"; allChildren: RecordChild[]; page: number; setPage: (n: number) => void; }): JSX.Element { const alt = useRecordAlt(); const shownChildren = allChildren.slice( (page - 1) * CHILDREN_PER_PAGE, page * CHILDREN_PER_PAGE ); return ( <>

    {trans("CHILD_PAGES")}

      {shownChildren.length > 0 ? ( shownChildren.map((child) => (
    • {trans_obj(child.label_i18n)}
    • )) ) : (
    • {trans("NO_CHILD_PAGES")}
    • )}
    ); } lektor-lektor-52c4448/frontend/js/sidebar/ChildPagination.tsx000066400000000000000000000020021466050247100242270ustar00rootroot00000000000000import React from "react"; import { CHILDREN_PER_PAGE } from "./constants"; /** Pagination buttons for the list of children in the sidebar. */ export default function ChildPagination({ page, numberOfChildren, setPage, }: { page: number; numberOfChildren: number; setPage: (n: number) => void; }): JSX.Element | null { const pages = Math.ceil(numberOfChildren / CHILDREN_PER_PAGE); if (pages <= 1) { return null; } return (
  • {page > 1 ? ( { ev.preventDefault(); setPage(page - 1); }} > « ) : ( « )} {page + " / " + pages} {page < pages ? ( { ev.preventDefault(); setPage(page + 1); }} > » ) : ( » )}
  • ); } lektor-lektor-52c4448/frontend/js/sidebar/ChildPosCache.test.ts000066400000000000000000000010221466050247100244120ustar00rootroot00000000000000import { strictEqual } from "assert"; import { ChildPosCache } from "./Sidebar"; it("sidebar: child page position cache", () => { const cache = new ChildPosCache(); strictEqual(cache.getPosition("test", 50), 1); cache.rememberPosition("test", 4); strictEqual(cache.getPosition("test", 50), 4); cache.rememberPosition("test", 2); strictEqual(cache.getPosition("test", 50), 2); cache.rememberPosition("test2", 3); strictEqual(cache.getPosition("test", 50), 2); strictEqual(cache.getPosition("test2", 50), 3); }); lektor-lektor-52c4448/frontend/js/sidebar/PageActions.tsx000066400000000000000000000053401466050247100233770ustar00rootroot00000000000000import React, { MouseEvent, memo, useCallback } from "react"; import { useRecord } from "../context/record-context"; import { RecordInfo } from "../components/types"; import { trans } from "../i18n"; import { getPlatform } from "../utils"; import { post } from "../fetch"; import { showErrorDialog } from "../error-dialog"; import AdminLinkWithHotkey from "../components/AdminLinkWithHotkey"; import AdminLink from "../components/AdminLink"; const getBrowseButtonTitle = () => { const platform = getPlatform(); if (platform === "mac") { return trans("BROWSE_FS_MAC"); } else if (platform === "windows") { return trans("BROWSE_FS_WINDOWS"); } else { return trans("BROWSE_FS"); } }; function BrowseFSLink() { const record = useRecord(); const fsOpen = useCallback( (ev: MouseEvent) => { ev.preventDefault(); post("/browsefs", record).then(({ okay }) => { if (!okay) { alert(trans("ERROR_CANNOT_BROWSE_FS")); } }, showErrorDialog); }, [record] ); return ( {getBrowseButtonTitle()} ); } const editKey = { key: "Control+e", mac: "Meta+e", preventDefault: true }; function PageActions({ recordInfo }: { recordInfo: RecordInfo }) { const { path, alt } = useRecord(); return ( <>

    {recordInfo.is_attachment ? trans("ATTACHMENT_ACTIONS") : trans("PAGE_ACTIONS")}

    • {recordInfo.is_attachment ? trans("EDIT_METADATA") : trans("EDIT")}
    • {recordInfo.can_be_deleted && (
    • {trans("DELETE")}
    • )}
    • {trans("PREVIEW")}
    • {recordInfo.exists && (
    • )} {recordInfo.can_have_children && (
    • {trans("ADD_CHILD_PAGE")}
    • )} {recordInfo.can_have_attachments && (
    • {trans("ADD_ATTACHMENT")}
    • )}
    ); } export default memo(PageActions); lektor-lektor-52c4448/frontend/js/sidebar/Sidebar.tsx000066400000000000000000000065301466050247100225550ustar00rootroot00000000000000import React, { useContext, useEffect, useReducer, useState } from "react"; import { get } from "../fetch"; import { trans_obj } from "../i18n"; import { showErrorDialog } from "../error-dialog"; import { Alternative, RecordInfo } from "../components/types"; import PageActions from "./PageActions"; import Alternatives from "./Alternatives"; import AttachmentActions from "./AttachmentActions"; import { CHILDREN_PER_PAGE } from "./constants"; import ChildActions from "./ChildActions"; import { subscribe, unsubscribe } from "../events"; import { PageContext } from "../context/page-context"; import { useRecordPath } from "../context/record-context"; /** * Keep a cache of the page number in the list of subpages that we are currently * on. Only keeps this page number in memory for the last five records. */ export class ChildPosCache { private memo: [record: string, page: number][]; constructor() { this.memo = []; } /** Remember the page for a record. */ rememberPosition(record: string, page: number): void { // remove current value this.memo = this.memo.filter(([r]) => r !== record); this.memo.unshift([record, page]); if (this.memo.length > 5) { this.memo.length = 5; } } getPosition(record: string, childCount: number): number { const page = this.memo.find(([r]) => r === record)?.[1]; return page ? Math.min(page, Math.ceil(childCount / CHILDREN_PER_PAGE)) : 1; } } const compareAlternatives = (a: Alternative, b: Alternative) => { const nameA = (a.is_primary ? "A" : "B") + trans_obj(a.name_i18n); const nameB = (b.is_primary ? "A" : "B") + trans_obj(b.name_i18n); return nameA === nameB ? 0 : nameA < nameB ? -1 : 1; }; function Sidebar(): JSX.Element | null { const page = useContext(PageContext); const path = useRecordPath(); const [recordInfo, setRecordInfo] = useState(null); const [childrenPage, setChildrenPage] = useState(1); const [childPosCache] = useState(() => new ChildPosCache()); const [updateForced, forceUpdate] = useReducer((x) => x + 1, 0); useEffect(() => { const handler = ({ detail }: CustomEvent) => { if (detail === path) { forceUpdate(); } }; subscribe("lektor-attachments-changed", handler); return () => unsubscribe("lektor-attachments-changed", handler); }, [path]); useEffect(() => { let ignore = false; get("/recordinfo", { path }).then((resp) => { if (!ignore) { setRecordInfo({ ...resp, alts: resp.alts.sort(compareAlternatives) }); setChildrenPage(childPosCache.getPosition(path, resp.children.length)); } }, showErrorDialog); return () => { ignore = true; }; }, [path, childPosCache, updateForced]); if (!recordInfo) { return null; } return ( <> {recordInfo.can_have_children && ( { childPosCache.rememberPosition(path, page); setChildrenPage(page); }} /> )} {recordInfo.can_have_attachments && ( )} ); } export default Sidebar; lektor-lektor-52c4448/frontend/js/sidebar/constants.ts000066400000000000000000000001331466050247100230210ustar00rootroot00000000000000/** The number of children to show in the sidebar. */ export const CHILDREN_PER_PAGE = 15; lektor-lektor-52c4448/frontend/js/slugify.test.ts000066400000000000000000000006401466050247100220370ustar00rootroot00000000000000import { strictEqual } from "assert"; import { slugify } from "./slugify"; describe("slugs", () => { it("slugify strings", () => { strictEqual(slugify("asdf asdf"), "asdf-asdf"); strictEqual(slugify("äasdf asdf"), "aeasdf-asdf"); strictEqual(slugify("<3"), "love"); strictEqual(slugify("€"), "euro"); strictEqual(slugify("&"), "and"); strictEqual(slugify("..€.."), "euro"); }); }); lektor-lektor-52c4448/frontend/js/slugify.ts000066400000000000000000000134231466050247100210640ustar00rootroot00000000000000// https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/media/js/urlify.js const charmap: Record = { // latin À: "A", Á: "A", Â: "A", Ã: "A", Ä: "Ae", Å: "A", Æ: "AE", Ç: "C", È: "E", É: "E", Ê: "E", Ë: "E", Ì: "I", Í: "I", Î: "I", Ï: "I", Ð: "D", Ñ: "N", Ò: "O", Ó: "O", Ô: "O", Õ: "O", Ö: "Oe", Ő: "O", Ø: "O", Ù: "U", Ú: "U", Û: "U", Ü: "Ue", Ű: "U", Ý: "Y", Þ: "TH", ß: "ss", à: "a", á: "a", â: "a", ã: "a", ä: "ae", å: "a", æ: "ae", ç: "c", è: "e", é: "e", ê: "e", ë: "e", ì: "i", í: "i", î: "i", ï: "i", ð: "d", ñ: "n", ò: "o", ó: "o", ô: "o", õ: "o", ö: "oe", ő: "o", ø: "o", ù: "u", ú: "u", û: "u", ü: "ue", ű: "u", ý: "y", þ: "th", ÿ: "y", ẞ: "SS", // greek α: "a", β: "b", γ: "g", δ: "d", ε: "e", ζ: "z", η: "h", θ: "8", ι: "i", κ: "k", λ: "l", μ: "m", ν: "n", ξ: "3", ο: "o", π: "p", ρ: "r", σ: "s", τ: "t", υ: "y", φ: "f", χ: "x", ψ: "ps", ω: "w", ά: "a", έ: "e", ί: "i", ό: "o", ύ: "y", ή: "h", ώ: "w", ς: "s", ϊ: "i", ΰ: "y", ϋ: "y", ΐ: "i", Α: "A", Β: "B", Γ: "G", Δ: "D", Ε: "E", Ζ: "Z", Η: "H", Θ: "8", Ι: "I", Κ: "K", Λ: "L", Μ: "M", Ν: "N", Ξ: "3", Ο: "O", Π: "P", Ρ: "R", Σ: "S", Τ: "T", Υ: "Y", Φ: "F", Χ: "X", Ψ: "PS", Ω: "W", Ά: "A", Έ: "E", Ί: "I", Ό: "O", Ύ: "Y", Ή: "H", Ώ: "W", Ϊ: "I", Ϋ: "Y", // turkish ş: "s", Ş: "S", ı: "i", İ: "I", ğ: "g", Ğ: "G", // russian а: "a", б: "b", в: "v", г: "g", д: "d", е: "e", ё: "yo", ж: "zh", з: "z", и: "i", й: "j", к: "k", л: "l", м: "m", н: "n", о: "o", п: "p", р: "r", с: "s", т: "t", у: "u", ф: "f", х: "h", ц: "c", ч: "ch", ш: "sh", щ: "sh", ъ: "u", ы: "y", ь: "", э: "e", ю: "yu", я: "ya", А: "A", Б: "B", В: "V", Г: "G", Д: "D", Е: "E", Ё: "Yo", Ж: "Zh", З: "Z", И: "I", Й: "J", К: "K", Л: "L", М: "M", Н: "N", О: "O", П: "P", Р: "R", С: "S", Т: "T", У: "U", Ф: "F", Х: "H", Ц: "C", Ч: "Ch", Ш: "Sh", Щ: "Sh", Ъ: "U", Ы: "Y", Ь: "", Э: "E", Ю: "Yu", Я: "Ya", // ukranian Є: "Ye", І: "I", Ї: "Yi", Ґ: "G", є: "ye", і: "i", ї: "yi", ґ: "g", // czech č: "c", ď: "d", ě: "e", ň: "n", ř: "r", š: "s", ť: "t", ů: "u", ž: "z", Č: "C", Ď: "D", Ě: "E", Ň: "N", Ř: "R", Š: "S", Ť: "T", Ů: "U", Ž: "Z", // polish ą: "a", ć: "c", ę: "e", ł: "l", ń: "n", ś: "s", ź: "z", ż: "z", Ą: "A", Ć: "C", Ę: "E", Ł: "L", Ń: "N", Ś: "S", Ź: "Z", Ż: "Z", // latvian ā: "a", ē: "e", ģ: "g", ī: "i", ķ: "k", ļ: "l", ņ: "n", ū: "u", Ā: "A", Ē: "E", Ģ: "G", Ī: "I", Ķ: "K", Ļ: "L", Ņ: "N", Ū: "U", // lithuanian ė: "e", į: "i", ų: "u", Ė: "E", Į: "I", Ų: "U", // romanian ț: "t", Ț: "T", ţ: "t", Ţ: "T", ș: "s", Ș: "S", ă: "a", Ă: "A", // currency "€": "euro", "₢": "cruzeiro", "₣": "french franc", "£": "pound", "₤": "lira", "₥": "mill", "₦": "naira", "₧": "peseta", "₨": "rupee", "₩": "won", "₪": "new shequel", "₫": "dong", "₭": "kip", "₮": "tugrik", "₯": "drachma", "₰": "penny", "₱": "peso", "₲": "guarani", "₳": "austral", "₴": "hryvnia", "₵": "cedi", "¢": "cent", "¥": "yen", 元: "yuan", 円: "yen", "﷼": "rial", "₠": "ecu", "¤": "currency", "฿": "baht", $: "dollar", "₹": "indian rupee", // symbols "©": "(c)", œ: "oe", Œ: "OE", "∑": "sum", "®": "(r)", "†": "+", "“": '"', "”": '"', "‘": "'", "’": "'", "∂": "d", ƒ: "f", "™": "tm", "℠": "sm", "…": "...", "˚": "o", º: "o", ª: "a", "•": "*", "∆": "delta", "∞": "infinity", "♥": "love", "&": "and", "|": "or", "<": "less", ">": "greater", "=": "equals", }; const multicharmap: Record = { "<3": "love", "&&": "and", "||": "or", "w/": "with", }; interface SlugifyOptions { replacement: string; remove: string | RegExp; charmap: Record; multicharmap: Record; } const pretty: SlugifyOptions = { replacement: "-", remove: /[.]/g, charmap, multicharmap, }; export function slugify(rawString: string): string { const string = rawString.toString(); const opts = pretty; const lengths: number[] = []; Object.keys(opts.multicharmap).forEach((key) => { const len = key.length; if (!lengths.includes(len)) { lengths.push(len); } }); let result = ""; for (let char, i = 0, l = string.length; i < l; i++) { char = string[i]; if ( !lengths.some((len) => { const str = string.substr(i, len); if (opts.multicharmap[str]) { i += len - 1; char = opts.multicharmap[str]; return true; } else { return false; } }) ) { if (opts.charmap[char]) { char = opts.charmap[char]; } } char = char.replace(/[^\w\s\-._~]/g, ""); // allowed if (opts.remove) { // add flavour: char = char.replace(opts.remove, ""); } result += char; } // trim leading/trailing spaces: result = result.replace(/^\s+|\s+$/g, ""); // convert spaces: result = result.replace(/[-\s]+/g, opts.replacement); // remove trailing separator: return result.replace(opts.replacement + "$", ""); } lektor-lektor-52c4448/frontend/js/userLabel.tsx000066400000000000000000000012271466050247100215070ustar00rootroot00000000000000import React from "react"; import { trans_obj, Translatable } from "./i18n"; /** * Formats a user label appropriately */ export function formatUserLabel( inputConfig: Translatable | string ): JSX.Element { const label = typeof inputConfig === "string" ? inputConfig : trans_obj(inputConfig); if (!label) { return ; } const iconData = label.match(/^\[\[\s*(.*?)\s*(;\s*(.*?))?\s*\]\]$/); if (iconData) { let className = "fa fa-" + iconData[1]; if ((iconData[3] || "").match(/90|180|270/)) { className += " fa-rotate-" + iconData[3]; } return ; } return {label}; } lektor-lektor-52c4448/frontend/js/utils.test.ts000066400000000000000000000032231466050247100215150ustar00rootroot00000000000000import { ok, strictEqual } from "assert"; import { isValidUrl, trimLeadingSlashes, trimTrailingSlashes, trimSlashes, trimColons, } from "./utils"; describe("Utils", () => { it("check URL validity", () => { ok(isValidUrl("http://example.com")); ok(isValidUrl("https://example.com")); // ok(!isValidUrl("https:file")); // ignoring this case in favor of more generic regex ok(!isValidUrl("https:/example.com")); ok(isValidUrl("ftp://example.com")); ok(isValidUrl("ftps://example.com")); ok(!isValidUrl("ftps:/example.com")); ok(isValidUrl("mailto:user@example.com")); ok(isValidUrl("mailto:anythingreally")); ok(!isValidUrl("mailto:with spaces")); ok(isValidUrl("z39.50r://example.com:8001/database?45")); ok(isValidUrl("svn+ssh://example.com")); ok(isValidUrl("feed:example.com/rss")); ok(isValidUrl("webcal:example.com/calendar")); ok(isValidUrl("ms-help://section/path/file.htm")); ok(!isValidUrl("anyscheme:/oneslash")); }); it("trim strings of slashes and colons", () => { strictEqual(trimLeadingSlashes("///asdf"), "asdf"); strictEqual(trimLeadingSlashes("asdf///"), "asdf///"); strictEqual(trimLeadingSlashes(""), ""); strictEqual(trimTrailingSlashes("///asdf"), "///asdf"); strictEqual(trimTrailingSlashes("asdf///"), "asdf"); strictEqual(trimTrailingSlashes(""), ""); strictEqual(trimSlashes("///asdf///"), "asdf"); strictEqual(trimSlashes("asdf///"), "asdf"); strictEqual(trimSlashes("///asdf"), "asdf"); strictEqual(trimSlashes(""), ""); strictEqual(trimColons(":asdf:"), "asdf"); strictEqual(trimColons(":asdf:asdf:"), "asdf:asdf"); }); }); lektor-lektor-52c4448/frontend/js/utils.ts000066400000000000000000000046351466050247100205470ustar00rootroot00000000000000import { RecordPath } from "./context/record-context"; export function isValidUrl(url: string): boolean { return !!url.match(/^([a-z0-9+.-]+):(\/\/)?[^/]\S+$/); } /** * Trim leading slashes from a string. */ export function trimLeadingSlashes(string: string): string { const match = /^\/*(.*?)$/.exec(string); return match ? match[1] : ""; } /** * Trim trailing slashes from a string. */ export function trimTrailingSlashes(string: string): string { const match = /^(.*?)\/*$/.exec(string); return match ? match[1] : ""; } /** * Trim both leading and trailing slashes from a string. */ export function trimSlashes(string: string): string { const match = /^\/*(.*?)\/*$/.exec(string); return match ? match[1] : ""; } /** * Trim both leading and trailing colons from a string. */ export function trimColons(string: string): string { const match = /^:*(.*?):*$/.exec(string); return match ? match[1] : ""; } export function getCanonicalUrl(localPath: string): string { const base = trimTrailingSlashes($LEKTOR_CONFIG.site_root); return `${base}/${trimLeadingSlashes(localPath)}`; } export function getPlatform(): "windows" | "mac" | "linux" | "other" { if (navigator.appVersion.indexOf("Win") !== -1) { return "windows"; } else if (navigator.appVersion.indexOf("Mac") !== -1) { return "mac"; } else if ( navigator.appVersion.indexOf("X11") !== -1 || navigator.appVersion.indexOf("Linux") !== -1 ) { return "linux"; } return "other"; } export interface KeyboardShortcut { key: string; mac?: string; preventDefault?: boolean; } export function getKey(shortcut: KeyboardShortcut): string { return getPlatform() === "mac" && shortcut.mac ? shortcut.mac : shortcut.key; } export function keyboardShortcutHandler( shortcut: KeyboardShortcut, action: (ev: KeyboardEvent) => void ): (ev: KeyboardEvent) => void { const key = getKey(shortcut); return (ev) => { let eventKey = ev.key; if (ev.altKey) { eventKey = `Alt+${eventKey}`; } if (ev.ctrlKey) { eventKey = `Control+${eventKey}`; } if (ev.metaKey) { eventKey = `Meta+${eventKey}`; } if (eventKey === key) { if (shortcut.preventDefault) { ev.preventDefault(); } action(ev); } }; } export function getParentPath(path: RecordPath): RecordPath | null { const match = /^(\/.*)\/[^/]*$/.exec(path); return match ? (match[1] as RecordPath) : null; } lektor-lektor-52c4448/frontend/js/views/000077500000000000000000000000001466050247100201645ustar00rootroot00000000000000lektor-lektor-52c4448/frontend/js/views/AddAttachmentPage.tsx000066400000000000000000000053661466050247100242340ustar00rootroot00000000000000import React, { ChangeEvent, useCallback, useEffect, useRef, useState, } from "react"; import { useRecordPath } from "../context/record-context"; import { apiUrl, get } from "../fetch"; import { trans, trans_format } from "../i18n"; import { showErrorDialog } from "../error-dialog"; import { dispatch } from "../events"; type NewAttachmentInfo = { label: string; can_upload: boolean; }; function AddAttachmentPage(): JSX.Element | null { const path = useRecordPath(); const [newAttachmentInfo, setNewAttachmentInfo] = useState(null); const fileInput = useRef(null); const [currentFiles, setCurrentFiles] = useState([]); const [isUploading, setIsUploading] = useState(false); const [currentProgress, setCurrentProgress] = useState(0); useEffect(() => { let ignore = false; get("/newattachment", { path }).then((resp) => { if (!ignore) { setNewAttachmentInfo(resp); } }, showErrorDialog); return () => { ignore = true; }; }, [path]); const onFileSelected = useCallback( ({ target }: ChangeEvent) => { if (isUploading || !target.files) { return; } const files = Array.prototype.slice.call(target.files, 0); setCurrentFiles(files); setIsUploading(true); const formData = new FormData(); formData.append("path", path); files.forEach((file) => { formData.append("file", file, file.name); }); const xhr = new XMLHttpRequest(); xhr.open("POST", apiUrl("/newattachment")); xhr.onprogress = (event) => { setCurrentProgress(Math.round((event.loaded * 100) / event.total)); }; xhr.onload = () => { setIsUploading(false); setCurrentProgress(100); dispatch("lektor-attachments-changed", path); }; xhr.send(formData); }, [path, isUploading] ); if (!newAttachmentInfo) { return null; } return ( <>

    {trans_format("ADD_ATTACHMENT_TO", newAttachmentInfo.label)}

    {trans("ADD_ATTACHMENT_NOTE")}

      {currentFiles.map((file) => (
    • {file.name} ({file.type})
    • ))}

    {trans("PROGRESS")}: {currentProgress}%

    ); } export default AddAttachmentPage; lektor-lektor-52c4448/frontend/js/views/App.tsx000066400000000000000000000030271466050247100214460ustar00rootroot00000000000000import React, { useContext, useReducer } from "react"; import Header from "../header/Header"; import Sidebar from "../sidebar/Sidebar"; import DialogSlot from "../components/DialogSlot"; import ServerStatus from "../components/ServerStatus"; import ErrorDialog from "../components/ErrorDialog"; import EditPage from "./edit/EditPage"; import DeletePage from "./delete/DeletePage"; import PreviewPage from "./PreviewPage"; import AddChildPage from "./add-child-page/AddChildPage"; import AddAttachmentPage from "./AddAttachmentPage"; import { PageContext } from "../context/page-context"; const mainComponentForPage = { edit: EditPage, delete: DeletePage, preview: PreviewPage, "add-child": AddChildPage, upload: AddAttachmentPage, } as const; export default function App() { const page = useContext(PageContext); const [sidebarIsActive, toggleSidebar] = useReducer((v) => !v, false); const MainComponent = mainComponentForPage[page]; return ( <>
    ); } lektor-lektor-52c4448/frontend/js/views/PreviewPage.tsx000066400000000000000000000061441466050247100231470ustar00rootroot00000000000000import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { trimLeadingSlashes, trimTrailingSlashes } from "../utils"; import { get } from "../fetch"; import { useRecord } from "../context/record-context"; import { showErrorDialog } from "../error-dialog"; import { useGoToAdminPage } from "../components/use-go-to-admin-page"; function getSiteRootUrl() { const site_root = `/${trimLeadingSlashes($LEKTOR_CONFIG.site_root)}`; const absRootUrl = new URL(site_root, document.baseURI).href; return trimTrailingSlashes(absRootUrl); } function usePreviewUrl(siteRootUrl: string): string { const { path, alt } = useRecord(); const [previewUrl, setPreviewUrl] = useState("about:blank"); useEffect(() => { let ignore = false; get("/previewinfo", { path, alt }).then(({ url }) => { if (!ignore) { setPreviewUrl(url ? siteRootUrl + url : "about:blank"); } }, showErrorDialog); return () => { ignore = true; }; }, [path, alt, siteRootUrl]); return previewUrl; } export default function PreviewPage(): JSX.Element { const siteRootUrl = useMemo(getSiteRootUrl, []); const previewUrl = usePreviewUrl(siteRootUrl); const iframe = useRef(null); const goToAdminPage = useGoToAdminPage(); useEffect(() => { const location = iframe.current?.contentWindow?.location; if (location && location.href !== previewUrl) { location.replace(previewUrl); } }, [previewUrl]); const onLoad = useCallback(() => { const contentWindow = iframe.current?.contentWindow; if (contentWindow) { // Note that cross-origin security restrictions prevent us // from being able to inspect or manipulate pages from // other origins, so this will only work when the iframe is // previewing one of our pages. try { // Pass keydown events on to parent window // This is an attempt to ensure that hotkeys like Ctl-e work // even when the iframe has the focus. contentWindow.addEventListener("keydown", (ev: KeyboardEvent) => { const clone = new KeyboardEvent(ev.type, ev); window.dispatchEvent(clone) || ev.preventDefault(); }); const href = contentWindow.location?.href; if (href && href !== previewUrl) { // Iframe has been navigated to a new page (e.g. user clicked link) if (href.startsWith(`${siteRootUrl}/`)) { // Attempt to move Admin UI to new page const url_path = href.substring(siteRootUrl.length); get("/matchurl", { url_path }).then(({ exists, alt, path }) => { if (exists) { goToAdminPage("preview", path, alt); } }, showErrorDialog); } } } catch (e) { if (e instanceof DOMException && e.name === "SecurityError") { // Ignore exceptions having to do with cross-origin restrictions } else { throw e; } } } }, [goToAdminPage, previewUrl, siteRootUrl]); return