├── .document ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── ruby.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-version ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── docker-compose.yml ├── exe └── image_scraper ├── image_scraper.gemspec ├── lib ├── image_scraper.rb └── image_scraper │ ├── client.rb │ ├── railtie.rb │ ├── util.rb │ └── version.rb ├── sig └── image_scraper.rbs └── spec ├── cassettes └── ImageScraper_Client │ ├── _image_urls │ ├── handles_url_with_unescaped_spaces.yml │ ├── scrapes_absolute_paths.yml │ └── scrapes_relative_paths.yml │ ├── _page_images │ ├── handldes_image_urls_that_include_square_brackets.yml │ └── handles_unescaped_urls.yml │ ├── _stylesheet_images │ ├── handles_404s.yml │ ├── handles_stylesheet_image_with_a_relative_url.yml │ └── scrapes_stylesheet_images.yml │ ├── _stylesheets │ └── lists_relative_path_stylesheets.yml │ └── foo │ └── something.yml ├── image_scraper ├── client_spec.rb └── util_spec.rb ├── image_scraper_spec.rb ├── spec_helper.rb └── support ├── extra_whitespace.html ├── relative_image_url.css ├── relative_image_url.html ├── space in url.html ├── stylesheet_test.html ├── stylesheet_unescaped_image.html └── unescaped_image.css /.document: -------------------------------------------------------------------------------- 1 | lib/**/*.rb 2 | bin/* 3 | - 4 | features/**/*.feature 5 | LICENSE.txt 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "bundler" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: [master] 5 | jobs: 6 | release: 7 | runs-on: ubuntu-latest 8 | env: 9 | GEM_NAME: image_scraper 10 | steps: 11 | - uses: google-github-actions/release-please-action@v3 12 | id: release 13 | with: 14 | bump-minor-pre-major: true 15 | package-name: image_scraper 16 | release-type: ruby 17 | version-file: "lib/image_scraper/version.rb" 18 | - uses: actions/checkout@v3 19 | - name: install ruby 20 | if: "${{ steps.release.outputs.release_created }}" 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | bundler-cache: true 24 | - name: bundle 25 | if: "${{ steps.release.outputs.release_created }}" 26 | run: | 27 | bundle config unset --local deployment 28 | bundle 29 | - name: publish gem 30 | if: "${{ steps.release.outputs.release_created }}" 31 | uses: dawidd6/action-publish-gem@v1 32 | with: 33 | api_key: "${{secrets.RUBYGEMS_API_KEY}}" 34 | github_token: "${{secrets.GITHUB_TOKEN}}" 35 | -------------------------------------------------------------------------------- /.github/workflows/ruby.yml: -------------------------------------------------------------------------------- 1 | name: ruby 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | build-and-run-tests: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | bundler-cache: true 19 | - name: Run tests 20 | run: bundle exec rspec spec 21 | - name: Run rubocop 22 | run: bundle exec rubocop 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # rspec failure tracking 2 | .rspec_status 3 | 4 | /.bundle/ 5 | /.yardoc 6 | /Gemfile*lock 7 | /_yardoc/ 8 | /coverage/ 9 | /doc/ 10 | /pkg/ 11 | /spec/reports/ 12 | /tmp/ 13 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | 3 | AllCops: 4 | SuggestExtensions: false 5 | NewCops: enable 6 | 7 | require: 8 | - rubocop-rspec 9 | 10 | Layout/SpaceAroundMethodCallOperator: 11 | Enabled: true 12 | Lint/RaiseException: 13 | Enabled: True 14 | Lint/StructNewOverride: 15 | Enabled: True 16 | Style/ExponentialNotation: 17 | Enabled: True 18 | Style/HashEachMethods: 19 | Enabled: True 20 | Style/HashTransformKeys: 21 | Enabled: True 22 | Style/HashTransformValues: 23 | Enabled: True 24 | Layout/EmptyLinesAroundAttributeAccessor: 25 | Enabled: True 26 | Lint/DeprecatedOpenSSLConstant: 27 | Enabled: True 28 | Lint/MixedRegexpCaptureTypes: 29 | Enabled: True 30 | Style/RedundantRegexpCharacterClass: 31 | Enabled: False 32 | Style/RedundantRegexpEscape: 33 | Enabled: False 34 | Style/SlicingWithRange: 35 | Enabled: True 36 | Style/AccessorGrouping: 37 | Enabled: True 38 | Style/BisectedAttrAccessor: 39 | Enabled: True 40 | Style/RedundantAssignment: 41 | Enabled: True 42 | Style/RedundantFetchBlock: 43 | Enabled: True 44 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2021-12-21 21:23:53 UTC using RuboCop version 1.23.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Configuration parameters: Include. 11 | # Include: **/*.gemspec 12 | Gemspec/RequiredRubyVersion: 13 | Exclude: 14 | - 'image_scraper.gemspec' 15 | 16 | # Offense count: 1 17 | # Cop supports --auto-correct. 18 | # Configuration parameters: EnforcedStyle. 19 | # SupportedStyles: empty_lines, no_empty_lines 20 | Layout/EmptyLinesAroundBlockBody: 21 | Exclude: 22 | - 'spec/image_scraper/util_spec.rb' 23 | 24 | # Offense count: 1 25 | Lint/DuplicateRescueException: 26 | Exclude: 27 | - 'lib/image_scraper/client.rb' 28 | 29 | # Offense count: 1 30 | # Cop supports --auto-correct. 31 | Lint/OrderedMagicComments: 32 | Exclude: 33 | - 'image_scraper.gemspec' 34 | 35 | # Offense count: 2 36 | # Configuration parameters: IgnoredMethods, CountRepeatedAttributes. 37 | Metrics/AbcSize: 38 | Max: 19 39 | 40 | # Offense count: 5 41 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 42 | # IgnoredMethods: refine 43 | Metrics/BlockLength: 44 | Max: 120 45 | 46 | # Offense count: 5 47 | # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods. 48 | Metrics/MethodLength: 49 | Max: 18 50 | 51 | # Offense count: 2 52 | RSpec/AnyInstance: 53 | Exclude: 54 | - 'spec/image_scraper/client_spec.rb' 55 | 56 | # Offense count: 6 57 | # Configuration parameters: Max. 58 | RSpec/ExampleLength: 59 | Exclude: 60 | - 'spec/image_scraper/client_spec.rb' 61 | - 'spec/image_scraper/util_spec.rb' 62 | 63 | # Offense count: 4 64 | RSpec/MultipleExpectations: 65 | Max: 5 66 | 67 | # Offense count: 1 68 | Security/Open: 69 | Exclude: 70 | - 'lib/image_scraper/client.rb' 71 | 72 | # Offense count: 1 73 | # Cop supports --auto-correct. 74 | Style/CaseLikeIf: 75 | Exclude: 76 | - 'lib/image_scraper/client.rb' 77 | 78 | # Offense count: 2 79 | # Configuration parameters: AllowedConstants. 80 | Style/Documentation: 81 | Exclude: 82 | - 'spec/**/*' 83 | - 'test/**/*' 84 | - 'lib/image_scraper/client.rb' 85 | - 'lib/image_scraper/util.rb' 86 | 87 | # Offense count: 1 88 | # Cop supports --auto-correct. 89 | # Configuration parameters: EnforcedStyle. 90 | # SupportedStyles: always, always_true, never 91 | Style/FrozenStringLiteralComment: 92 | Exclude: 93 | - 'Guardfile' 94 | 95 | # Offense count: 1 96 | # Cop supports --auto-correct. 97 | Style/IfUnlessModifier: 98 | Exclude: 99 | - 'image_scraper.gemspec' 100 | 101 | # Offense count: 1 102 | # Cop supports --auto-correct. 103 | # Configuration parameters: PreferredDelimiters. 104 | Style/PercentLiteralDelimiters: 105 | Exclude: 106 | - 'Guardfile' 107 | 108 | # Offense count: 8 109 | # Cop supports --auto-correct. 110 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 111 | # SupportedStyles: single_quotes, double_quotes 112 | Style/StringLiterals: 113 | Exclude: 114 | - 'Guardfile' 115 | - 'spec/image_scraper/client_spec.rb' 116 | 117 | # Offense count: 2 118 | # Cop supports --auto-correct. 119 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 120 | # URISchemes: http, https 121 | Layout/LineLength: 122 | Max: 142 123 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 3.2.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [Unreleased] 2 | 3 | ## [0.1.14](https://github.com/charlotte-ruby/image_scraper/compare/v0.1.13...v0.1.14) (2022-12-09) 4 | 5 | 6 | ### Bug Fixes 7 | 8 | * testing release please ([c722746](https://github.com/charlotte-ruby/image_scraper/commit/c72274671065e8329f879b9640a39fc5652bbbd7)) 9 | 10 | ## [0.1.13](https://github.com/charlotte-ruby/image_scraper/compare/v0.1.12...v0.1.13) (2022-11-25) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * bump to gems. bump ruby to 3.1.3 ([7ee3877](https://github.com/charlotte-ruby/image_scraper/commit/7ee38775bbc0e9684fe512c698a9caa1ecb3c07b)) 16 | 17 | ## [0.1.12](https://github.com/charlotte-ruby/image_scraper/compare/v0.1.11...v0.1.12) (2022-11-25) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * trying again lol ([9f55af5](https://github.com/charlotte-ruby/image_scraper/commit/9f55af55bb34d61a4a6dbf141998212229bf6ac9)) 23 | 24 | ## [0.1.11](https://github.com/charlotte-ruby/image_scraper/compare/v0.1.10...v0.1.11) (2022-11-25) 25 | 26 | 27 | ### Bug Fixes 28 | 29 | * bump version ([936f850](https://github.com/charlotte-ruby/image_scraper/commit/936f850b0f8f3d87607d28a5f3a4a088975b2ada)) 30 | 31 | ### [0.1.10](https://www.github.com/charlotte-ruby/image_scraper/compare/v0.1.7...v0.1.10) (2022-04-17) 32 | 33 | ### Bug Fixes 34 | 35 | * bump ruby ([3519010](https://github.com/charlotte-ruby/image_scraper/commit/351901036ed4b4b9432814ce05bcd5c67ae0c332)) 36 | 37 | ## [0.1.9] - 2022-02-21 38 | 39 | - deprecate jeweler in favor of bundler and rake tasks 40 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. 8 | 9 | ## Our Standards 10 | 11 | Examples of behavior that contributes to a positive environment for our community include: 12 | 13 | * Demonstrating empathy and kindness toward other people 14 | * Being respectful of differing opinions, viewpoints, and experiences 15 | * Giving and gracefully accepting constructive feedback 16 | * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience 17 | * Focusing on what is best not just for us as individuals, but for the overall community 18 | 19 | Examples of unacceptable behavior include: 20 | 21 | * The use of sexualized language or imagery, and sexual attention or 22 | advances of any kind 23 | * Trolling, insulting or derogatory comments, and personal or political attacks 24 | * Public or private harassment 25 | * Publishing others' private information, such as a physical or email 26 | address, without their explicit permission 27 | * Other conduct which could reasonably be considered inappropriate in a 28 | professional setting 29 | 30 | ## Enforcement Responsibilities 31 | 32 | Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful. 33 | 34 | Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at matt@invalid8.com. All complaints will be reviewed and investigated promptly and fairly. 43 | 44 | All community leaders are obligated to respect the privacy and security of the reporter of any incident. 45 | 46 | ## Enforcement Guidelines 47 | 48 | Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct: 49 | 50 | ### 1. Correction 51 | 52 | **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community. 53 | 54 | **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested. 55 | 56 | ### 2. Warning 57 | 58 | **Community Impact**: A violation through a single incident or series of actions. 59 | 60 | **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban. 61 | 62 | ### 3. Temporary Ban 63 | 64 | **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior. 65 | 66 | **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban. 67 | 68 | ### 4. Permanent Ban 69 | 70 | **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals. 71 | 72 | **Consequence**: A permanent ban from any sort of public interaction within the community. 73 | 74 | ## Attribution 75 | 76 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.0, 77 | available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 78 | 79 | Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder](https://github.com/mozilla/diversity). 80 | 81 | [homepage]: https://www.contributor-covenant.org 82 | 83 | For answers to common questions about this code of conduct, see the FAQ at 84 | https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. 85 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:3.2-alpine 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apk update && \ 6 | apk add gcc gcompat git \ 7 | libxml2-dev libxslt-dev \ 8 | make musl-dev 9 | 10 | RUN mkdir -p lib/image_scraper 11 | COPY lib/image_scraper/version.rb ./lib/image_scraper/version.rb 12 | COPY .ruby-version image_scraper.gemspec Gemfile Gemfile.lock ./ 13 | 14 | RUN bundle install 15 | COPY . . 16 | 17 | CMD ["bundle", "exec", "rspec"] 18 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'http://rubygems.org' 4 | 5 | ruby File.read('.ruby-version').chomp 6 | 7 | gemspec 8 | 9 | gem 'rake', '~> 13.0' 10 | gem 'rspec', '~> 3.4' 11 | gem 'rubocop', '~> 1.21' 12 | 13 | group :development do 14 | gem 'bundler', '~> 2.3' 15 | gem 'guard-rspec', require: false 16 | gem 'pry' 17 | gem 'rubocop-rspec', require: false 18 | gem 'test-unit' 19 | gem 'vcr', '~> 6.0' 20 | gem 'webmock' 21 | end 22 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exist?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # NOTE: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separately) 25 | # * 'just' rspec: 'rspec' 26 | 27 | guard :rspec, cmd: "bundle exec rspec" do 28 | require "guard/rspec/dsl" 29 | dsl = Guard::RSpec::Dsl.new(self) 30 | 31 | # Feel free to open issues for suggestions and improvements 32 | 33 | # RSpec files 34 | rspec = dsl.rspec 35 | watch(rspec.spec_helper) { rspec.spec_dir } 36 | watch(rspec.spec_support) { rspec.spec_dir } 37 | watch(rspec.spec_files) 38 | 39 | # Ruby files 40 | ruby = dsl.ruby 41 | dsl.watch_spec_files_for(ruby.lib_files) 42 | 43 | # Rails files 44 | rails = dsl.rails(view_extensions: %w(erb haml slim)) 45 | dsl.watch_spec_files_for(rails.app_files) 46 | dsl.watch_spec_files_for(rails.views) 47 | 48 | watch(rails.controllers) do |m| 49 | [ 50 | rspec.spec.call("routing/#{m[1]}_routing"), 51 | rspec.spec.call("controllers/#{m[1]}_controller"), 52 | rspec.spec.call("acceptance/#{m[1]}") 53 | ] 54 | end 55 | 56 | # Rails config changes 57 | watch(rails.spec_helper) { rspec.spec_dir } 58 | watch(rails.routes) { "#{rspec.spec_dir}/routing" } 59 | watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" } 60 | 61 | # Capybara features specs 62 | watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") } 63 | watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") } 64 | 65 | # Turnip features and steps 66 | watch(%r{^spec/acceptance/(.+)\.feature$}) 67 | watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m| 68 | Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance" 69 | end 70 | end 71 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 John McAliley 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ImageScraper 2 | [![ruby](https://github.com/charlotte-ruby/image_scraper/actions/workflows/ruby.yml/badge.svg)](https://github.com/charlotte-ruby/image_scraper/actions/workflows/ruby.yml) 3 | 4 | Simple utility that pulls image URLs from web page 5 | ## Installation 6 | 7 | Install in your application's Gemfile or as a standalone gem: 8 | 9 | ```ruby 10 | gem 'image_scraper' 11 | ``` 12 | 13 | And then execute: 14 | 15 | ``` 16 | $ bundle install 17 | ``` 18 | 19 | Standalone install: 20 | 21 | ``` 22 | $ gem install image_scraper 23 | ``` 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | options = { 29 | convert_to_absolute_url: true, 30 | include_css_images: true # convert any relative images to absolute urls. 31 | include_css_data_images: true # convert any data images (data:image/gif;base64....) 32 | } 33 | 34 | image_scraper = ImageScraper::Client.new("http://www.rubygems.org", options) 35 | image_scraper.image_urls 36 | 37 | # => ["https://rubygems.org/assets/github_icon.png"", "https://rubygems.org/sponsors.png"] 38 | ``` 39 | 40 | ### CLI 41 | 42 | ``` 43 | $ image_scraper https://unsplash.com | head -n 2 44 | https://images.unsplash.com/photo-1471897488648 45 | https://images.unsplash.com/photo-1590073242678 46 | ``` 47 | 48 | ## Development 49 | 50 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 51 | 52 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`. Releases are done via Github Actions. 53 | 54 | If you prefer to use docker: 55 | 56 | ``` 57 | docker-compose build 58 | docker-compose run app 59 | ``` 60 | 61 | Once inside the container, run the tests and you'll see output similar to this: 62 | 63 | ``` 64 | /usr/src/app # bundle exec rspec 65 | ........................ 66 | 67 | Finished in 0.54303 seconds (files took 0.95976 seconds to load) 68 | 24 examples, 0 failures 69 | ``` 70 | 71 | ## Contributing 72 | 73 | Bug reports and pull requests are welcome on GitHub at https://github.com/charlotte-ruby/image_scraper. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/charlotte-ruby/image_scraper/blob/master/CODE_OF_CONDUCT.md). 74 | 75 | - Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet 76 | - Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it 77 | - Fork the project 78 | - Start a feature/bugfix branch 79 | - Commit and push until you are happy with your contribution 80 | - Make sure to add tests for it. This is important so I don't break it in a future version unintentionally. 81 | - Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it. 82 | 83 | ## Copyright 84 | 85 | Copyright (c) 2011 John McAliley. See LICENSE.txt for 86 | further details. 87 | 88 | ## Code of Conduct 89 | 90 | Everyone interacting in the ImageScraper project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/charlotte-ruby/image_scraper/blob/master/CODE_OF_CONDUCT.md). 91 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/gem_tasks' 4 | require 'rspec/core/rake_task' 5 | 6 | RSpec::Core::RakeTask.new(:spec) 7 | 8 | require 'rubocop/rake_task' 9 | 10 | RuboCop::RakeTask.new 11 | 12 | task default: %i[spec rubocop] 13 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'bundler/setup' 5 | require 'image_scraper' 6 | 7 | # You can add fixtures and/or initialization code here to make experimenting 8 | # with your gem easier. You can also use a different console, if you like. 9 | 10 | # (If you use this, don't forget to add pry to your Gemfile!) 11 | # require "pry" 12 | # Pry.start 13 | 14 | require 'irb' 15 | IRB.start(__FILE__) 16 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | app: 4 | build: . 5 | command: /bin/sh 6 | volumes: 7 | - .:/usr/src/app 8 | -------------------------------------------------------------------------------- /exe/image_scraper: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | 4 | require 'image_scraper' 5 | 6 | url = ARGV[0].to_s 7 | 8 | unless url.length.positive? 9 | puts 'usage: image_scraper ' 10 | exit 1 11 | end 12 | 13 | image_scraper = ImageScraper::Client.new(url) 14 | scraped_urls = image_scraper.image_urls 15 | 16 | puts scraped_urls * "\n" if scraped_urls.count.positive? 17 | -------------------------------------------------------------------------------- /image_scraper.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'lib/image_scraper/version' 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = 'image_scraper' 7 | spec.version = ImageScraper::VERSION 8 | spec.authors = ['John McAliley', 'Matt McMahand'] 9 | spec.email = ['john.mcaliley@gmail.com', 'matt@invalid8.com'] 10 | spec.summary = 'Simple utility to pull image urls from web page' 11 | spec.description = spec.summary 12 | spec.homepage = 'http://github.com/charlotte-ruby/image_scraper' 13 | spec.license = 'MIT' 14 | spec.required_ruby_version = '>= 2.6.0' 15 | 16 | spec.metadata['homepage_uri'] = spec.homepage 17 | spec.metadata['source_code_uri'] = spec.homepage 18 | spec.metadata['changelog_uri'] = File.join(spec.homepage, 'blob/master/CHANGELOG.md') 19 | 20 | spec.metadata['rubygems_mfa_required'] = 'true' 21 | 22 | begin 23 | files = (result = `git ls-files -z`.split "\0").empty? ? Dir['**/*'] : result 24 | rescue StandardError 25 | files = Dir['**/*'] 26 | end 27 | 28 | # Specify which files should be added to the gem when it is released. 29 | spec.files = files.grep_v(/^\A(?:(?:test|spec|features|.git|sig))/) 30 | 31 | spec.bindir = 'exe' 32 | spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) } 33 | spec.require_paths = ['lib'] 34 | 35 | spec.add_dependency 'css_parser', '~> 1.11' 36 | spec.add_dependency 'nokogiri', '~> 1.13' 37 | end 38 | -------------------------------------------------------------------------------- /lib/image_scraper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'open-uri' 4 | require 'nokogiri' 5 | 6 | require_relative 'image_scraper/client' 7 | require_relative 'image_scraper/railtie' if defined?(Rails::Railtie) 8 | require_relative 'image_scraper/util' 9 | require_relative 'image_scraper/version' 10 | 11 | module ImageScraper 12 | class Error < StandardError; end 13 | end 14 | -------------------------------------------------------------------------------- /lib/image_scraper/client.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'net/http' 4 | require 'uri' 5 | require 'cgi' 6 | 7 | module ImageScraper 8 | class Client 9 | USER_AGENT = 'Mozilla/5.0 (Macintosh)' 10 | 11 | attr_accessor :url, :convert_to_absolute_url, :include_css_images, :include_css_data_images, :doc 12 | attr_reader :uri, :error 13 | 14 | def initialize(url, options = {}) 15 | defaults = { convert_to_absolute_url: true, include_css_images: true, include_css_data_images: false } 16 | options.merge!(defaults) 17 | 18 | @url = url 19 | @uri = Util.convert_to_uri(url) 20 | 21 | @convert_to_absolute_url = options[:convert_to_absolute_url] 22 | @include_css_images = options[:include_css_images] 23 | @include_css_data_images = options[:include_css_data_images] 24 | 25 | begin 26 | html = fetch(@uri) 27 | rescue StandardError => e 28 | @error = e 29 | html = nil 30 | end 31 | 32 | @doc = html ? Nokogiri::HTML(html, nil, 'UTF-8') : nil 33 | end 34 | 35 | def fetch(url, limit = 10) 36 | raise ArgumentError, 'HTTP redirect too deep' if limit.zero? 37 | 38 | uri = Util.convert_to_uri(url) 39 | 40 | return false unless uri 41 | 42 | result = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.port == 443) do |http| 43 | request = Net::HTTP::Get.new(uri, { 'User-Agent' => USER_AGENT }) 44 | response = http.request request 45 | 46 | case response 47 | when Net::HTTPSuccess then response 48 | when Net::HTTPRedirection then fetch(response['location'], limit - 1) 49 | else 50 | response.error! 51 | end 52 | end 53 | 54 | if result.is_a? Net::HTTPOK 55 | result.body 56 | elsif result.is_a? String 57 | result 58 | end 59 | end 60 | 61 | def image_urls 62 | images = page_images 63 | images += stylesheet_images if include_css_images 64 | images.sort.uniq 65 | end 66 | 67 | def cleanup_src_value(text) 68 | text.to_s.strip! 69 | text.gsub!(' ', '%20') 70 | 71 | # escape characters that CGI::escape doesn't get 72 | text.gsub(/([{}|\^\[\]\@`])/) { |s| s } 73 | end 74 | 75 | def page_images 76 | return [] if doc.to_s.empty? 77 | 78 | doc.xpath('//img').collect do |e| 79 | src = cleanup_src_value(e['src']) 80 | next if src.empty? 81 | 82 | if convert_to_absolute_url 83 | Util.absolute_url(@uri.to_s, src) 84 | else 85 | src 86 | end 87 | end.compact 88 | end 89 | 90 | def fetch_css(url) 91 | begin 92 | file = URI.open(url) 93 | rescue StandardError 94 | return '' 95 | end 96 | 97 | begin 98 | css = file.string 99 | rescue StandardError 100 | css = File.read(file) 101 | rescue StandardError 102 | return '' 103 | end 104 | 105 | css.unpack('C*').pack('U*') 106 | end 107 | 108 | def stylesheet_images 109 | images = [] 110 | 111 | stylesheets.each do |stylesheet| 112 | css = fetch_css(stylesheet) 113 | 114 | next unless css.to_s.length.positive? 115 | 116 | images += css.scan(/url\((.*?)\)/).collect do |image_url| 117 | image_url = Util.cleanup_url(image_url[0]) 118 | image_url = image_url.gsub(/([{}|\^\[\]\@`])/) { |s| CGI.escape(s) } # escape characters that URI.escape doesn't get 119 | if image_url.include?('data:image') && @include_css_data_images 120 | image_url 121 | else 122 | @convert_to_absolute_url ? Util.absolute_url(stylesheet, image_url) : image_url 123 | end 124 | end 125 | end 126 | images.compact 127 | end 128 | 129 | def stylesheets 130 | return [] if doc.to_s.empty? 131 | 132 | doc.xpath('//link[@rel="stylesheet"]').collect do |stylesheet| 133 | Util.absolute_url(@uri.to_s, Util.cleanup_url(stylesheet['href'])) 134 | end.compact 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /lib/image_scraper/railtie.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ImageScraper 4 | class Railtie < Rails::Railtie 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/image_scraper/util.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ImageScraper 4 | module Util 5 | def self.absolute_url(url, asset = nil) 6 | # TODO: - what happens when an index redirect occurs? 7 | # Example: 'http://example.com/about' specified as url 8 | # 'style.css' specified as asset 9 | # url redirects to 'http://example.com/about/' 10 | # and serves http://example.com/about/index.html 11 | # which then links to the relative asset path 'style.css' 12 | # based on original url (http://example.com/about), 13 | # self.absolute_url gives 14 | # 'http://example.com/style.css 15 | # but should get: 16 | # 'http://example.com/about/style.css 17 | 18 | URI.parse(url).merge(URI.parse(asset.to_s)).to_s 19 | rescue StandardError 20 | nil 21 | end 22 | 23 | def self.convert_to_uri(url) 24 | if url.is_a?(URI::HTTP) 25 | url 26 | else 27 | url = url.strip 28 | url = "http://#{url}" unless url.include?('://') 29 | url = url.gsub(' ', '%20') if url.include?(' ') 30 | 31 | begin 32 | URI.parse(url) 33 | rescue URI::InvalidURIError 34 | nil 35 | end 36 | end 37 | end 38 | 39 | def self.domain(url) 40 | uri = URI.parse(url) 41 | "#{uri.scheme}://#{uri.host}" 42 | rescue StandardError 43 | print('domain error') 44 | nil 45 | end 46 | 47 | def self.path(url) 48 | URI.parse(url).path 49 | rescue StandardError 50 | nil 51 | end 52 | 53 | def self.strip_backslashes(image_url) 54 | image_url.gsub('\\', '') 55 | end 56 | 57 | def self.strip_quotes(image_url) 58 | image_url.gsub("'", '').gsub('"', '') 59 | end 60 | 61 | def self.chomp(image_url) 62 | image_url.gsub(/\s/, '') 63 | end 64 | 65 | def self.cleanup_url(image_url) 66 | ImageScraper::Util.chomp( 67 | ImageScraper::Util.strip_quotes( 68 | ImageScraper::Util.strip_backslashes(image_url || '') 69 | ) 70 | ) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /lib/image_scraper/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module ImageScraper 4 | VERSION = '0.1.14' 5 | end 6 | -------------------------------------------------------------------------------- /sig/image_scraper.rbs: -------------------------------------------------------------------------------- 1 | module ImageScraper 2 | VERSION: String 3 | # See the writing guide of rbs: https://github.com/ruby/rbs#guides 4 | end 5 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_image_urls/handles_url_with_unescaped_spaces.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://raw.github.com/syoder/image_scraper/stylesheet_fix/test/resources/space%20in%20url.html 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - raw.github.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Connection: 24 | - keep-alive 25 | Content-Length: 26 | - '0' 27 | Location: 28 | - https://raw.githubusercontent.com/syoder/image_scraper/stylesheet_fix/test/resources/space%20in%20url.html 29 | Accept-Ranges: 30 | - bytes 31 | Date: 32 | - Sat, 11 Jul 2020 02:48:32 GMT 33 | Via: 34 | - 1.1 varnish 35 | Age: 36 | - '0' 37 | X-Served-By: 38 | - cache-fty21351-FTY 39 | X-Cache: 40 | - MISS 41 | X-Cache-Hits: 42 | - '0' 43 | Vary: 44 | - Accept-Encoding 45 | X-Fastly-Request-Id: 46 | - 1938ad6b24faae4219882b22499a0884f3fcfac5 47 | body: 48 | encoding: UTF-8 49 | string: '' 50 | recorded_at: Sat, 11 Jul 2020 02:48:32 GMT 51 | - request: 52 | method: get 53 | uri: https://raw.githubusercontent.com/syoder/image_scraper/stylesheet_fix/test/resources/space%20in%20url.html 54 | body: 55 | encoding: US-ASCII 56 | string: '' 57 | headers: 58 | User-Agent: 59 | - Mozilla/5.0 (Macintosh) 60 | Accept-Encoding: 61 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 62 | Accept: 63 | - "*/*" 64 | Host: 65 | - raw.githubusercontent.com 66 | response: 67 | status: 68 | code: 200 69 | message: OK 70 | headers: 71 | Connection: 72 | - keep-alive 73 | Content-Length: 74 | - '66' 75 | Cache-Control: 76 | - max-age=300 77 | Content-Security-Policy: 78 | - default-src 'none'; style-src 'unsafe-inline'; sandbox 79 | Content-Type: 80 | - text/plain; charset=utf-8 81 | Etag: 82 | - W/"97a67c7d07a8aa3a39a54347e00f2eb4a7b9a18080d07f38fa16538b9187a65a" 83 | Strict-Transport-Security: 84 | - max-age=31536000 85 | X-Content-Type-Options: 86 | - nosniff 87 | X-Frame-Options: 88 | - deny 89 | X-Xss-Protection: 90 | - 1; mode=block 91 | Via: 92 | - 1.1 varnish 93 | - 1.1 varnish (Varnish/6.0) 94 | X-Github-Request-Id: 95 | - B330:6FFF:8A3AC:A38C4:5F09287F 96 | Accept-Ranges: 97 | - bytes 98 | Date: 99 | - Sat, 11 Jul 2020 02:48:32 GMT 100 | X-Served-By: 101 | - cache-fty21323-FTY 102 | X-Cache: 103 | - MISS, MISS 104 | X-Cache-Hits: 105 | - 0, 0 106 | X-Timer: 107 | - S1594435712.380265,VS0,VE118 108 | Vary: 109 | - Authorization,Accept-Encoding 110 | Access-Control-Allow-Origin: 111 | - "*" 112 | X-Fastly-Request-Id: 113 | - 3ce7f01ccb996f373840a16c0c3b1756eba30c9a 114 | Expires: 115 | - Sat, 11 Jul 2020 02:53:32 GMT 116 | Source-Age: 117 | - '0' 118 | body: 119 | encoding: ASCII-8BIT 120 | string: | 121 | 122 | 123 | 124 | 125 | 126 | recorded_at: Sat, 11 Jul 2020 02:48:32 GMT 127 | recorded_with: VCR 6.0.0 128 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_page_images/handldes_image_urls_that_include_square_brackets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://google.com/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - google.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Location: 24 | - http://www.google.com/ 25 | Content-Type: 26 | - text/html; charset=UTF-8 27 | Date: 28 | - Sat, 11 Jul 2020 03:19:08 GMT 29 | Expires: 30 | - Mon, 10 Aug 2020 03:19:08 GMT 31 | Cache-Control: 32 | - public, max-age=2592000 33 | Server: 34 | - gws 35 | Content-Length: 36 | - '219' 37 | X-Xss-Protection: 38 | - '0' 39 | X-Frame-Options: 40 | - SAMEORIGIN 41 | body: 42 | encoding: UTF-8 43 | string: "\n301 44 | Moved\n

301 Moved

\nThe document has moved\nhere.\r\n\r\n" 46 | recorded_at: Sat, 11 Jul 2020 03:19:08 GMT 47 | - request: 48 | method: get 49 | uri: http://www.google.com/ 50 | body: 51 | encoding: US-ASCII 52 | string: '' 53 | headers: 54 | User-Agent: 55 | - Mozilla/5.0 (Macintosh) 56 | Accept-Encoding: 57 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 58 | Accept: 59 | - "*/*" 60 | Host: 61 | - www.google.com 62 | response: 63 | status: 64 | code: 200 65 | message: OK 66 | headers: 67 | Date: 68 | - Sat, 11 Jul 2020 03:19:08 GMT 69 | Expires: 70 | - "-1" 71 | Cache-Control: 72 | - private, max-age=0 73 | Content-Type: 74 | - text/html; charset=UTF-8 75 | P3p: 76 | - CP="This is not a P3P policy! See g.co/p3phelp for more info." 77 | Server: 78 | - gws 79 | Content-Length: 80 | - '15676' 81 | X-Xss-Protection: 82 | - '0' 83 | X-Frame-Options: 84 | - SAMEORIGIN 85 | Set-Cookie: 86 | - 1P_JAR=2020-07-11-03; expires=Mon, 10-Aug-2020 03:19:08 GMT; path=/; domain=.google.com; 87 | Secure 88 | - NID=204=NKuzVtzV2dKPsOc7_z9bjNoIRhrN5kM9LQVZji5cqnUzPnDf6oMK-ritO8eMSwcgEJfqzbjCFUynwrJg1XMrJTjn_8t3hvFXqZhqvCCVAgBSQiN1wFE7ZP2JGvXkvahwYVo6QkfpnbwfRK3uMGL2Wk5tD4KbnopvxHAQ9qCHrGA; 89 | expires=Sun, 10-Jan-2021 03:19:08 GMT; path=/; domain=.google.com; HttpOnly 90 | body: 91 | encoding: ASCII-8BIT 92 | string: !binary |- 93 | PCFkb2N0eXBlIGh0bWw+PGh0bWwgaXRlbXNjb3BlPSIiIGl0ZW10eXBlPSJodHRwOi8vc2NoZW1hLm9yZy9XZWJQYWdlIiBsYW5nPSJlbiI+PGhlYWQ+PG1ldGEgY29udGVudD0iU2VhcmNoIHRoZSB3b3JsZCdzIGluZm9ybWF0aW9uLCBpbmNsdWRpbmcgd2VicGFnZXMsIGltYWdlcywgdmlkZW9zIGFuZCBtb3JlLiBHb29nbGUgaGFzIG1hbnkgc3BlY2lhbCBmZWF0dXJlcyB0byBoZWxwIHlvdSBmaW5kIGV4YWN0bHkgd2hhdCB5b3UncmUgbG9va2luZyBmb3IuIiBuYW1lPSJkZXNjcmlwdGlvbiI+PG1ldGEgY29udGVudD0ibm9vZHAiIG5hbWU9InJvYm90cyI+PG1ldGEgY29udGVudD0idGV4dC9odG1sOyBjaGFyc2V0PVVURi04IiBodHRwLWVxdWl2PSJDb250ZW50LVR5cGUiPjxtZXRhIGNvbnRlbnQ9Ii9pbWFnZXMvYnJhbmRpbmcvZ29vZ2xlZy8xeC9nb29nbGVnX3N0YW5kYXJkX2NvbG9yXzEyOGRwLnBuZyIgaXRlbXByb3A9ImltYWdlIj48dGl0bGU+R29vZ2xlPC90aXRsZT48c2NyaXB0IG5vbmNlPSJaMHJ3dHhNTnRNdGo1R2xJdnlyZDJnPT0iPihmdW5jdGlvbigpe3dpbmRvdy5nb29nbGU9e2tFSTonckM4SlhfVC1CY0hsX1FiX19yam9DQScsa0VYUEk6JzAsMjAyMTIzLDMsNCwzMiwxMTUxNTg1LDU2NjIsNzMwLDIyNCw1MTA1LDIwNiwzMjA0LDEwLDEyMjYsMzY0LDE0OTksNjExLDkzLDExMywzODMsMjQ2LDUsMTE4NCwxNzAsNDA3LDk2LDE0NSwxOTQsNDg2LDUsMzYxLDIxODQsMjIxLDEzLDgwLDEwNiwxMTYsMyw1MTksNTMxLDkxLDQ5LDEzMSwyLDEwLDI3NSwxMDAsMzc5LDExLDExMjEyNTEsMTE5Nzc1NCwzOTUsMzI5MTE4LDEyOTQsMTIzODMsNDg1NSwzMjY5MSwxNTI0OCw4NjEsMjg2OTAsOTE4OCw4Mzg0LDQ4NTgsMTM2MiwyODMsOTAwNywzMDIyLDQ3NDYsNywxMTAyNiwxODA4LDQwMjAsOTc4LDQ3ODgsMSwzMTQ0LDUyOTUsMjY3NiwyOTgsODczLDM3LDExNzgsMjM4NCwzMzc4LDM2NDUsMTE0MiwxMDE2NCwzMjIyLDQ1MTYsMjc3OCw5MjEsMjI3NSwxMCwyNzk0LDE1OTMsMTI3OSwyMjEyLDUzMCwxNDksNTYxLDU0Miw4NDAsNTE3LDE0NjYsNTcsMTU3LDQxMDAsMzEyLDExMzUsMSwzLDIwNjMsNjA2LDE4MzksMTg2LDE3NzUsMTQzLDM3NywxOTQ3LDEwMzAsMSwxMTk4LDkzLDMyOCwxMjg0LDI0LDI5MTksMjI0Nyw0NzMsMTMzOSwxNzk1LDMyMTksMTk4OSw4NTcsNiw2MDY4LDYyODYsNDQ1NSw2NDEsMjQ0OSwyNDU5LDEyMjYsMTc0MiwzNjU0LDEyNzUsMTA4LDM0MDgsOTA3LDIsOTQxLDI2MTQsMjM5OCw3NDY5LDEsODM5LDEzMzcsNjY2LDQzMiwzLDM0NiwxMjAwLDg2NSwxLDM3MiwzNTQ1LDcwNiwxNDksMTg5LDMzMTMsNTAyLDEsOTU3LDEwMjgsMTU4LDEsMjA5MywxOTkyLDE5OTcsODMsMTAxOCw3Myw1NzAsNCwxNTI4LDE3LDQxNyw4NjEsMTAwOSwyMjQsMTAwMywxNSwyNjUsODc0LDQwNSwxMjU5LDU4MCwyNCwxNzQsNjksMTU3MCwyNzksMzMxLDQxLDQ2Miw0MCwyNDIsNTYsOTE3LDEwLDQyLDIxNCw3NjIsNDQwLDk2Myw0NjEsNDYwLDEyMCw3NjEsNDM4LDIzLDE1LDE5OCwxMjMyLDUzOCwyMjk3LDI5MiwxOTczLDg2LDMsMTQ5Myw4NzcsOTEwLDE0MjYsNjksOTQsMjExLDEzNjIsOTQ4LDEzMDUsMjA5LDEwMDksMiwyODYsOTQwLDcsMyw4MzcsNTcsNTk5LDk4MSw0MzMsNTU0LDIsMTc3LDYwLDYsMzIsMjE1LDEwNiw5MSwyMzUsMSwyLDgyLDEwOCw5MSwxNzEsNjEwLDE1NTYsNDQsNzIsMjk4LDIwMCwyLDQwMiwxOTMsMTYwLDEsNzU2LDY3LDE3NSwzNSw2LDI4MSw2ODksOTcsMjc2LDU3LDMxLDE3NywxNjksMTE0LDYxOCw1NTIsMTIyLDEwNiwyNTQsMTA1LDEyNiwzLDQwNywxNDY5LDEsNzMyLDM0NiwxNCwzMzcsOTIsMiwxNywzNzAsNTIzLDEsNTc5MTY1NywzMzc3LDU5OTczMjIsMjgwMTIxNyw1NDksMzMzLDQ0NCwxLDIsODAsMSw5MDAsODk2LDEsOSwyLDI1NTEsMSw3NDgsMTQxLDU5LDczNiw1NjMsMSw0MjY1LDEsMSwxLDEsMTM3LDEsODc5LDksMzA1LDI4NzQsMTQ3LDEwNSwyLDE2LDEwLDEwLDEzMywyLDQsMjM5NjExMDAnLGtCTDonaE92Xyd9O2dvb2dsZS5zbj0nd2ViaHAnO2dvb2dsZS5rSEw9J2VuJzt9KSgpOyhmdW5jdGlvbigpe2dvb2dsZS5sYz1bXTtnb29nbGUubGk9MDtnb29nbGUuZ2V0RUk9ZnVuY3Rpb24oYSl7Zm9yKHZhciBjO2EmJighYS5nZXRBdHRyaWJ1dGV8fCEoYz1hLmdldEF0dHJpYnV0ZSgiZWlkIikpKTspYT1hLnBhcmVudE5vZGU7cmV0dXJuIGN8fGdvb2dsZS5rRUl9O2dvb2dsZS5nZXRMRUk9ZnVuY3Rpb24oYSl7Zm9yKHZhciBjPW51bGw7YSYmKCFhLmdldEF0dHJpYnV0ZXx8IShjPWEuZ2V0QXR0cmlidXRlKCJsZWlkIikpKTspYT1hLnBhcmVudE5vZGU7cmV0dXJuIGN9O2dvb2dsZS5tbD1mdW5jdGlvbigpe3JldHVybiBudWxsfTtnb29nbGUudGltZT1mdW5jdGlvbigpe3JldHVybiBEYXRlLm5vdygpfTtnb29nbGUubG9nPWZ1bmN0aW9uKGEsYyxiLGQsZyl7aWYoYj1nb29nbGUubG9nVXJsKGEsYyxiLGQsZykpe2E9bmV3IEltYWdlO3ZhciBlPWdvb2dsZS5sYyxmPWdvb2dsZS5saTtlW2ZdPWE7YS5vbmVycm9yPWEub25sb2FkPWEub25hYm9ydD1mdW5jdGlvbigpe2RlbGV0ZSBlW2ZdfTtnb29nbGUudmVsJiZnb29nbGUudmVsLmx1JiZnb29nbGUudmVsLmx1KGIpO2Euc3JjPWI7Z29vZ2xlLmxpPWYrMX19O2dvb2dsZS5sb2dVcmw9ZnVuY3Rpb24oYSxjLGIsZCxnKXt2YXIgZT0iIixmPWdvb2dsZS5sc3x8IiI7Ynx8LTEhPWMuc2VhcmNoKCImZWk9Iil8fChlPSImZWk9Iitnb29nbGUuZ2V0RUkoZCksLTE9PWMuc2VhcmNoKCImbGVpPSIpJiYoZD1nb29nbGUuZ2V0TEVJKGQpKSYmKGUrPSImbGVpPSIrZCkpO2Q9IiI7IWImJmdvb2dsZS5jc2hpZCYmLTE9PWMuc2VhcmNoKCImY3NoaWQ9IikmJiJzbGgiIT1hJiYoZD0iJmNzaGlkPSIrZ29vZ2xlLmNzaGlkKTtiPWJ8fCIvIisoZ3x8Imdlbl8yMDQiKSsiP2F0eXA9aSZjdD0iK2ErIiZjYWQ9IitjK2UrZisiJnp4PSIrZ29vZ2xlLnRpbWUoKStkOy9eaHR0cDovaS50ZXN0KGIpJiYiaHR0cHM6Ij09d2luZG93LmxvY2F0aW9uLnByb3RvY29sJiYoZ29vZ2xlLm1sKEVycm9yKCJhIiksITEse3NyYzpiLGdsbW06MX0pLGI9IiIpO3JldHVybiBifTt9KS5jYWxsKHRoaXMpOyhmdW5jdGlvbigpe2dvb2dsZS55PXt9O2dvb2dsZS54PWZ1bmN0aW9uKGEsYil7aWYoYSl2YXIgYz1hLmlkO2Vsc2V7ZG8gYz1NYXRoLnJhbmRvbSgpO3doaWxlKGdvb2dsZS55W2NdKX1nb29nbGUueVtjXT1bYSxiXTtyZXR1cm4hMX07Z29vZ2xlLmxtPVtdO2dvb2dsZS5wbG09ZnVuY3Rpb24oYSl7Z29vZ2xlLmxtLnB1c2guYXBwbHkoZ29vZ2xlLmxtLGEpfTtnb29nbGUubHE9W107Z29vZ2xlLmxvYWQ9ZnVuY3Rpb24oYSxiLGMpe2dvb2dsZS5scS5wdXNoKFtbYV0sYixjXSl9O2dvb2dsZS5sb2FkQWxsPWZ1bmN0aW9uKGEsYil7Z29vZ2xlLmxxLnB1c2goW2EsYl0pfTt9KS5jYWxsKHRoaXMpO2dvb2dsZS5mPXt9OyhmdW5jdGlvbigpewpkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuYWRkRXZlbnRMaXN0ZW5lcigic3VibWl0IixmdW5jdGlvbihiKXt2YXIgYTtpZihhPWIudGFyZ2V0KXt2YXIgYz1hLmdldEF0dHJpYnV0ZSgiZGF0YS1zdWJtaXRmYWxzZSIpO2E9IjEiPT1jfHwicSI9PWMmJiFhLmVsZW1lbnRzLnEudmFsdWU/ITA6ITF9ZWxzZSBhPSExO2EmJihiLnByZXZlbnREZWZhdWx0KCksYi5zdG9wUHJvcGFnYXRpb24oKSl9LCEwKTtkb2N1bWVudC5kb2N1bWVudEVsZW1lbnQuYWRkRXZlbnRMaXN0ZW5lcigiY2xpY2siLGZ1bmN0aW9uKGIpe3ZhciBhO2E6e2ZvcihhPWIudGFyZ2V0O2EmJmEhPWRvY3VtZW50LmRvY3VtZW50RWxlbWVudDthPWEucGFyZW50RWxlbWVudClpZigiQSI9PWEudGFnTmFtZSl7YT0iMSI9PWEuZ2V0QXR0cmlidXRlKCJkYXRhLW5vaHJlZiIpO2JyZWFrIGF9YT0hMX1hJiZiLnByZXZlbnREZWZhdWx0KCl9LCEwKTt9KS5jYWxsKHRoaXMpOwp2YXIgYT13aW5kb3cubG9jYXRpb24sYj1hLmhyZWYuaW5kZXhPZigiIyIpO2lmKDA8PWIpe3ZhciBjPWEuaHJlZi5zdWJzdHJpbmcoYisxKTsvKF58JilxPS8udGVzdChjKSYmLTE9PWMuaW5kZXhPZigiIyIpJiZhLnJlcGxhY2UoIi9zZWFyY2g/IitjLnJlcGxhY2UoLyhefCYpZnA9W14mXSovZywiIikrIiZjYWQ9aCIpfTs8L3NjcmlwdD48c3R5bGU+I2die2ZvbnQ6MTNweC8yN3B4IEFyaWFsLHNhbnMtc2VyaWY7aGVpZ2h0OjMwcHh9I2dieiwjZ2Jne3Bvc2l0aW9uOmFic29sdXRlO3doaXRlLXNwYWNlOm5vd3JhcDt0b3A6MDtoZWlnaHQ6MzBweDt6LWluZGV4OjEwMDB9I2dientsZWZ0OjA7cGFkZGluZy1sZWZ0OjRweH0jZ2Jne3JpZ2h0OjA7cGFkZGluZy1yaWdodDo1cHh9I2dic3tiYWNrZ3JvdW5kOnRyYW5zcGFyZW50O3Bvc2l0aW9uOmFic29sdXRlO3RvcDotOTk5cHg7dmlzaWJpbGl0eTpoaWRkZW47ei1pbmRleDo5OTg7cmlnaHQ6MH0uZ2J0byAjZ2Jze2JhY2tncm91bmQ6I2ZmZn0jZ2J4MywjZ2J4NHtiYWNrZ3JvdW5kLWNvbG9yOiMyZDJkMmQ7YmFja2dyb3VuZC1pbWFnZTpub25lO19iYWNrZ3JvdW5kLWltYWdlOm5vbmU7YmFja2dyb3VuZC1wb3NpdGlvbjowIC0xMzhweDtiYWNrZ3JvdW5kLXJlcGVhdDpyZXBlYXQteDtib3JkZXItYm90dG9tOjFweCBzb2xpZCAjMDAwO2ZvbnQtc2l6ZToyNHB4O2hlaWdodDoyOXB4O19oZWlnaHQ6MzBweDtvcGFjaXR5OjE7ZmlsdGVyOmFscGhhKG9wYWNpdHk9MTAwKTtwb3NpdGlvbjphYnNvbHV0ZTt0b3A6MDt3aWR0aDoxMDAlO3otaW5kZXg6OTkwfSNnYngze2xlZnQ6MH0jZ2J4NHtyaWdodDowfSNnYmJ7cG9zaXRpb246cmVsYXRpdmV9I2diYnd7bGVmdDowO3Bvc2l0aW9uOmFic29sdXRlO3RvcDozMHB4O3dpZHRoOjEwMCV9LmdidGNie3Bvc2l0aW9uOmFic29sdXRlO3Zpc2liaWxpdHk6aGlkZGVufSNnYnogLmdidGNie3JpZ2h0OjB9I2diZyAuZ2J0Y2J7bGVmdDowfS5nYnh4e2Rpc3BsYXk6bm9uZSAhaW1wb3J0YW50fS5nYnhve29wYWNpdHk6MCAhaW1wb3J0YW50O2ZpbHRlcjphbHBoYShvcGFjaXR5PTApICFpbXBvcnRhbnR9LmdibXtwb3NpdGlvbjphYnNvbHV0ZTt6LWluZGV4Ojk5OTt0b3A6LTk5OXB4O3Zpc2liaWxpdHk6aGlkZGVuO3RleHQtYWxpZ246bGVmdDtib3JkZXI6MXB4IHNvbGlkICNiZWJlYmU7YmFja2dyb3VuZDojZmZmOy1tb3otYm94LXNoYWRvdzotMXB4IDFweCAxcHggcmdiYSgwLDAsMCwuMik7LXdlYmtpdC1ib3gtc2hhZG93OjAgMnB4IDRweCByZ2JhKDAsMCwwLC4yKTtib3gtc2hhZG93OjAgMnB4IDRweCByZ2JhKDAsMCwwLC4yKX0uZ2JydGwgLmdibXstbW96LWJveC1zaGFkb3c6MXB4IDFweCAxcHggcmdiYSgwLDAsMCwuMil9LmdidG8gLmdibSwuZ2J0byAjZ2Jze3RvcDoyOXB4O3Zpc2liaWxpdHk6dmlzaWJsZX0jZ2J6IC5nYm17bGVmdDowfSNnYmcgLmdibXtyaWdodDowfS5nYnhtc3tiYWNrZ3JvdW5kLWNvbG9yOiNjY2M7ZGlzcGxheTpibG9jaztwb3NpdGlvbjphYnNvbHV0ZTt6LWluZGV4OjE7dG9wOi0xcHg7bGVmdDotMnB4O3JpZ2h0Oi0ycHg7Ym90dG9tOi0ycHg7b3BhY2l0eTouNDstbW96LWJvcmRlci1yYWRpdXM6M3B4O2ZpbHRlcjpwcm9naWQ6RFhJbWFnZVRyYW5zZm9ybS5NaWNyb3NvZnQuQmx1cihwaXhlbHJhZGl1cz01KTsqb3BhY2l0eToxOyp0b3A6LTJweDsqbGVmdDotNXB4OypyaWdodDo1cHg7KmJvdHRvbTo0cHg7LW1zLWZpbHRlcjoicHJvZ2lkOkRYSW1hZ2VUcmFuc2Zvcm0uTWljcm9zb2Z0LkJsdXIocGl4ZWxyYWRpdXM9NSkiO29wYWNpdHk6MVwwLzt0b3A6LTRweFwwLztsZWZ0Oi02cHhcMC87cmlnaHQ6NXB4XDAvO2JvdHRvbTo0cHhcMC99LmdibWF7cG9zaXRpb246cmVsYXRpdmU7dG9wOi0xcHg7Ym9yZGVyLXN0eWxlOnNvbGlkIGRhc2hlZCBkYXNoZWQ7Ym9yZGVyLWNvbG9yOnRyYW5zcGFyZW50O2JvcmRlci10b3AtY29sb3I6I2MwYzBjMDtkaXNwbGF5Oi1tb3otaW5saW5lLWJveDtkaXNwbGF5OmlubGluZS1ibG9jaztmb250LXNpemU6MDtoZWlnaHQ6MDtsaW5lLWhlaWdodDowO3dpZHRoOjA7Ym9yZGVyLXdpZHRoOjNweCAzcHggMDtwYWRkaW5nLXRvcDoxcHg7bGVmdDo0cHh9I2dienRtczEsI2diaTRtMSwjZ2JpNHMsI2diaTR0e3pvb206MX0uZ2J0YywuZ2JtYywuZ2JtY2N7ZGlzcGxheTpibG9jaztsaXN0LXN0eWxlOm5vbmU7bWFyZ2luOjA7cGFkZGluZzowfS5nYm1je2JhY2tncm91bmQ6I2ZmZjtwYWRkaW5nOjEwcHggMDtwb3NpdGlvbjpyZWxhdGl2ZTt6LWluZGV4OjI7em9vbToxfS5nYnR7cG9zaXRpb246cmVsYXRpdmU7ZGlzcGxheTotbW96LWlubGluZS1ib3g7ZGlzcGxheTppbmxpbmUtYmxvY2s7bGluZS1oZWlnaHQ6MjdweDtwYWRkaW5nOjA7dmVydGljYWwtYWxpZ246dG9wfS5nYnR7KmRpc3BsYXk6aW5saW5lfS5nYnRve2JveC1zaGFkb3c6MCAycHggNHB4IHJnYmEoMCwwLDAsLjIpOy1tb3otYm94LXNoYWRvdzowIDJweCA0cHggcmdiYSgwLDAsMCwuMik7LXdlYmtpdC1ib3gtc2hhZG93OjAgMnB4IDRweCByZ2JhKDAsMCwwLC4yKX0uZ2J6dCwuZ2JndHtjdXJzb3I6cG9pbnRlcjtkaXNwbGF5OmJsb2NrO3RleHQtZGVjb3JhdGlvbjpub25lICFpbXBvcnRhbnR9c3BhbiNnYmc2LHNwYW4jZ2JnNHtjdXJzb3I6ZGVmYXVsdH0uZ2J0c3tib3JkZXItbGVmdDoxcHggc29saWQgdHJhbnNwYXJlbnQ7Ym9yZGVyLXJpZ2h0OjFweCBzb2xpZCB0cmFuc3BhcmVudDtkaXNwbGF5OmJsb2NrOypkaXNwbGF5OmlubGluZS1ibG9jaztwYWRkaW5nOjAgNXB4O3Bvc2l0aW9uOnJlbGF0aXZlO3otaW5kZXg6MTAwMH0uZ2J0c3sqZGlzcGxheTppbmxpbmV9LmdienQgLmdidHN7ZGlzcGxheTppbmxpbmU7em9vbToxfS5nYnRvIC5nYnRze2JhY2tncm91bmQ6I2ZmZjtib3JkZXItY29sb3I6I2JlYmViZTtjb2xvcjojMzZjO3BhZGRpbmctYm90dG9tOjFweDtwYWRkaW5nLXRvcDoycHh9LmdiejBsIC5nYnRze2NvbG9yOiNmZmY7Zm9udC13ZWlnaHQ6Ym9sZH0uZ2J0c2F7cGFkZGluZy1yaWdodDo5cHh9I2dieiAuZ2J6dCwjZ2J6IC5nYmd0LCNnYmcgLmdiZ3R7Y29sb3I6I2NjYyFpbXBvcnRhbnR9LmdidGIye2Rpc3BsYXk6YmxvY2s7Ym9yZGVyLXRvcDoycHggc29saWQgdHJhbnNwYXJlbnR9LmdidG8gLmdienQgLmdidGIyLC5nYnRvIC5nYmd0IC5nYnRiMntib3JkZXItdG9wLXdpZHRoOjB9LmdidGIgLmdidHN7YmFja2dyb3VuZDp1cmwoaHR0cHM6Ly9zc2wuZ3N0YXRpYy5jb20vZ2IvaW1hZ2VzL2JfOGQ1YWZjMDkucG5nKTtfYmFja2dyb3VuZDp1cmwoaHR0cHM6Ly9zc2wuZ3N0YXRpYy5jb20vZ2IvaW1hZ2VzL2I4XzM2MTVkNjRkLnBuZyk7YmFja2dyb3VuZC1wb3NpdGlvbjotMjdweCAtMjJweDtib3JkZXI6MDtmb250LXNpemU6MDtwYWRkaW5nOjI5cHggMCAwOypwYWRkaW5nOjI3cHggMCAwO3dpZHRoOjFweH0uZ2J6dDpob3ZlciwuZ2J6dDpmb2N1cywuZ2JndC1odnIsLmdiZ3Q6Zm9jdXN7YmFja2dyb3VuZC1jb2xvcjojNGM0YzRjO2JhY2tncm91bmQtaW1hZ2U6bm9uZTtfYmFja2dyb3VuZC1pbWFnZTpub25lO2JhY2tncm91bmQtcG9zaXRpb246MCAtMTAycHg7YmFja2dyb3VuZC1yZXBlYXQ6cmVwZWF0LXg7b3V0bGluZTpub25lO3RleHQtZGVjb3JhdGlvbjpub25lICFpbXBvcnRhbnR9LmdicGRqcyAuZ2J0byAuZ2Jte21pbi13aWR0aDo5OSV9LmdiejBsIC5nYnRiMntib3JkZXItdG9wLWNvbG9yOiNkZDRiMzkhaW1wb3J0YW50fSNnYmk0cywjZ2JpNHMxe2ZvbnQtd2VpZ2h0OmJvbGR9I2diZzYuZ2JndC1odnIsI2diZzYuZ2JndDpmb2N1c3tiYWNrZ3JvdW5kLWNvbG9yOnRyYW5zcGFyZW50O2JhY2tncm91bmQtaW1hZ2U6bm9uZX0uZ2JnNGF7Zm9udC1zaXplOjA7bGluZS1oZWlnaHQ6MH0uZ2JnNGEgLmdidHN7cGFkZGluZzoyN3B4IDVweCAwOypwYWRkaW5nOjI1cHggNXB4IDB9LmdidG8gLmdiZzRhIC5nYnRze3BhZGRpbmc6MjlweCA1cHggMXB4OypwYWRkaW5nOjI3cHggNXB4IDFweH0jZ2JpNGksI2diaTRpZHtsZWZ0OjVweDtib3JkZXI6MDtoZWlnaHQ6MjRweDtwb3NpdGlvbjphYnNvbHV0ZTt0b3A6MXB4O3dpZHRoOjI0cHh9LmdidG8gI2diaTRpLC5nYnRvICNnYmk0aWR7dG9wOjNweH0uZ2JpNHB7ZGlzcGxheTpibG9jazt3aWR0aDoyNHB4fSNnYmk0aWR7YmFja2dyb3VuZC1wb3NpdGlvbjotNDRweCAtMTAxcHh9I2dibXBpZHtiYWNrZ3JvdW5kLXBvc2l0aW9uOjAgMH0jZ2JtcGksI2dibXBpZHtib3JkZXI6bm9uZTtkaXNwbGF5OmlubGluZS1ibG9jaztoZWlnaHQ6NDhweDt3aWR0aDo0OHB4fSNnYm1waXd7ZGlzcGxheTppbmxpbmUtYmxvY2s7bGluZS1oZWlnaHQ6OXB4O3BhZGRpbmctbGVmdDoyMHB4O21hcmdpbi10b3A6MTBweDtwb3NpdGlvbjpyZWxhdGl2ZX0jZ2JtcGksI2dibXBpZCwjZ2JtcGl3eypkaXNwbGF5OmlubGluZX0jZ2JnNXtmb250LXNpemU6MH0jZ2JnczV7cGFkZGluZzo1cHggIWltcG9ydGFudH0uZ2J0byAjZ2JnczV7cGFkZGluZzo3cHggNXB4IDZweCAhaW1wb3J0YW50fSNnYmk1e2JhY2tncm91bmQ6dXJsKGh0dHBzOi8vc3NsLmdzdGF0aWMuY29tL2diL2ltYWdlcy9iXzhkNWFmYzA5LnBuZyk7X2JhY2tncm91bmQ6dXJsKGh0dHBzOi8vc3NsLmdzdGF0aWMuY29tL2diL2ltYWdlcy9iOF8zNjE1ZDY0ZC5wbmcpO2JhY2tncm91bmQtcG9zaXRpb246MCAwO2Rpc3BsYXk6YmxvY2s7Zm9udC1zaXplOjA7aGVpZ2h0OjE3cHg7d2lkdGg6MTZweH0uZ2J0byAjZ2JpNXtiYWNrZ3JvdW5kLXBvc2l0aW9uOi02cHggLTIycHh9LmdibiAuZ2JtdCwuZ2JuIC5nYm10OnZpc2l0ZWQsLmdibmQgLmdibXQsLmdibmQgLmdibXQ6dmlzaXRlZHtjb2xvcjojZGQ4ZTI3ICFpbXBvcnRhbnR9LmdiZiAuZ2JtdCwuZ2JmIC5nYm10OnZpc2l0ZWR7Y29sb3I6IzkwMCAhaW1wb3J0YW50fS5nYm10LC5nYm1sMSwuZ2JtbGIsLmdibXQ6dmlzaXRlZCwuZ2JtbDE6dmlzaXRlZCwuZ2JtbGI6dmlzaXRlZHtjb2xvcjojMzZjICFpbXBvcnRhbnQ7dGV4dC1kZWNvcmF0aW9uOm5vbmUgIWltcG9ydGFudH0uZ2JtdCwuZ2JtdDp2aXNpdGVke2Rpc3BsYXk6YmxvY2t9LmdibWwxLC5nYm1sYiwuZ2JtbDE6dmlzaXRlZCwuZ2JtbGI6dmlzaXRlZHtkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW46MCAxMHB4fS5nYm1sMSwuZ2JtbGIsLmdibWwxOnZpc2l0ZWQsLmdibWxiOnZpc2l0ZWR7KmRpc3BsYXk6aW5saW5lfS5nYm1sMSwuZ2JtbDE6dmlzaXRlZHtwYWRkaW5nOjAgMTBweH0uZ2JtbDEtaHZyLC5nYm1sMTpmb2N1c3tvdXRsaW5lOm5vbmU7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZSAhaW1wb3J0YW50fSNnYnBtIC5nYm1sMXtkaXNwbGF5OmlubGluZTttYXJnaW46MDtwYWRkaW5nOjA7d2hpdGUtc3BhY2U6bm93cmFwfS5nYm1sYiwuZ2JtbGI6dmlzaXRlZHtsaW5lLWhlaWdodDoyN3B4fS5nYm1sYi1odnIsLmdibWxiOmZvY3Vze291dGxpbmU6bm9uZTt0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lICFpbXBvcnRhbnR9LmdibWxid3tjb2xvcjojY2NjO21hcmdpbjowIDEwcHh9LmdibXR7cGFkZGluZzowIDIwcHh9LmdibXQ6aG92ZXIsLmdibXQ6Zm9jdXN7YmFja2dyb3VuZDojZWVlO2N1cnNvcjpwb2ludGVyO291dGxpbmU6MCBzb2xpZCBibGFjazt0ZXh0LWRlY29yYXRpb246bm9uZSAhaW1wb3J0YW50fS5nYm0wbCwuZ2JtMGw6dmlzaXRlZHtjb2xvcjojMDAwICFpbXBvcnRhbnQ7Zm9udC13ZWlnaHQ6Ym9sZH0uZ2JtaHtib3JkZXItdG9wOjFweCBzb2xpZCAjYmViZWJlO2ZvbnQtc2l6ZTowO21hcmdpbjoxMHB4IDB9I2diZDQgLmdibWN7YmFja2dyb3VuZDojZjVmNWY1O3BhZGRpbmctdG9wOjB9I2diZDQgLmdic2JpYzo6LXdlYmtpdC1zY3JvbGxiYXItdHJhY2s6dmVydGljYWx7YmFja2dyb3VuZC1jb2xvcjojZjVmNWY1O21hcmdpbi10b3A6MnB4fSNnYm1wZHZ7YmFja2dyb3VuZDojZmZmO2JvcmRlci1ib3R0b206MXB4IHNvbGlkICNiZWJlYmU7LW1vei1ib3gtc2hhZG93OjAgMnB4IDRweCByZ2JhKDAsMCwwLC4xMik7LW8tYm94LXNoYWRvdzowIDJweCA0cHggcmdiYSgwLDAsMCwuMTIpOy13ZWJraXQtYm94LXNoYWRvdzowIDJweCA0cHggcmdiYSgwLDAsMCwuMTIpO2JveC1zaGFkb3c6MCAycHggNHB4IHJnYmEoMCwwLDAsLjEyKTtwb3NpdGlvbjpyZWxhdGl2ZTt6LWluZGV4OjF9I2diZDQgLmdibWh7bWFyZ2luOjB9LmdibXRje3BhZGRpbmc6MDttYXJnaW46MDtsaW5lLWhlaWdodDoyN3B4fS5HQk1DQzpsYXN0LWNoaWxkOmFmdGVyLCNHQk1QQUw6bGFzdC1jaGlsZDphZnRlcntjb250ZW50OidcMEFcMEEnO3doaXRlLXNwYWNlOnByZTtwb3NpdGlvbjphYnNvbHV0ZX0jZ2JtcHN7Knpvb206MX0jZ2JkNCAuZ2JwYywjZ2JtcGFzIC5nYm10e2xpbmUtaGVpZ2h0OjE3cHh9I2diZDQgLmdicGdzIC5nYm10Y3tsaW5lLWhlaWdodDoyN3B4fSNnYmQ0IC5nYm10Y3tib3JkZXItYm90dG9tOjFweCBzb2xpZCAjYmViZWJlfSNnYmQ0IC5nYnBje2Rpc3BsYXk6aW5saW5lLWJsb2NrO21hcmdpbjoxNnB4IDAgMTBweDtwYWRkaW5nLXJpZ2h0OjUwcHg7dmVydGljYWwtYWxpZ246dG9wfSNnYmQ0IC5nYnBjeypkaXNwbGF5OmlubGluZX0uZ2JwYyAuZ2JwcywuZ2JwYyAuZ2JwczJ7ZGlzcGxheTpibG9jazttYXJnaW46MCAyMHB4fSNnYm1wbHAuZ2Jwc3ttYXJnaW46MCAxMHB4fS5nYnBjIC5nYnBze2NvbG9yOiMwMDA7Zm9udC13ZWlnaHQ6Ym9sZH0uZ2JwYyAuZ2JwZHttYXJnaW4tYm90dG9tOjVweH0uZ2JwZCAuZ2JtdCwuZ2JwZCAuZ2Jwc3tjb2xvcjojNjY2ICFpbXBvcnRhbnR9LmdicGQgLmdibXR7b3BhY2l0eTouNDtmaWx0ZXI6YWxwaGEob3BhY2l0eT00MCl9LmdicHMye2NvbG9yOiM2NjY7ZGlzcGxheTpibG9ja30uZ2JwMHtkaXNwbGF5Om5vbmV9LmdicDAgLmdicHMye2ZvbnQtd2VpZ2h0OmJvbGR9I2diZDQgLmdibWNje21hcmdpbi10b3A6NXB4fS5nYnBtY3tiYWNrZ3JvdW5kOiNmZWY5ZGJ9LmdicG1jIC5nYnBtdGN7cGFkZGluZzoxMHB4IDIwcHh9I2dicG17Ym9yZGVyOjA7KmJvcmRlci1jb2xsYXBzZTpjb2xsYXBzZTtib3JkZXItc3BhY2luZzowO21hcmdpbjowO3doaXRlLXNwYWNlOm5vcm1hbH0jZ2JwbSAuZ2JwbXRje2JvcmRlci10b3A6bm9uZTtjb2xvcjojMDAwICFpbXBvcnRhbnQ7Zm9udDoxMXB4IEFyaWFsLHNhbnMtc2VyaWZ9I2dicG1zeyp3aGl0ZS1zcGFjZTpub3dyYXB9LmdicG1zMntmb250LXdlaWdodDpib2xkO3doaXRlLXNwYWNlOm5vd3JhcH0jZ2JtcGFseypib3JkZXItY29sbGFwc2U6Y29sbGFwc2U7Ym9yZGVyLXNwYWNpbmc6MDtib3JkZXI6MDttYXJnaW46MDt3aGl0ZS1zcGFjZTpub3dyYXA7d2lkdGg6MTAwJX0uZ2JtcGFsYSwuZ2JtcGFsYntmb250OjEzcHggQXJpYWwsc2Fucy1zZXJpZjtsaW5lLWhlaWdodDoyN3B4O3BhZGRpbmc6MTBweCAyMHB4IDA7d2hpdGUtc3BhY2U6bm93cmFwfS5nYm1wYWxhe3BhZGRpbmctbGVmdDowO3RleHQtYWxpZ246bGVmdH0uZ2JtcGFsYntwYWRkaW5nLXJpZ2h0OjA7dGV4dC1hbGlnbjpyaWdodH0jZ2JtcGFzYiAuZ2Jwc3tjb2xvcjojMDAwfSNnYm1wYWwgLmdicWZiYnttYXJnaW46MCAyMHB4fS5nYnAwIC5nYnBzeypkaXNwbGF5OmlubGluZX1hLmdiaWJhe21hcmdpbjo4cHggMjBweCAxMHB4fS5nYm1waWF3e2Rpc3BsYXk6aW5saW5lLWJsb2NrO3BhZGRpbmctcmlnaHQ6MTBweDttYXJnaW4tYm90dG9tOjZweDttYXJnaW4tdG9wOjEwcHh9LmdieHZ7dmlzaWJpbGl0eTpoaWRkZW59LmdibXBpYWF7ZGlzcGxheTpibG9jazttYXJnaW4tdG9wOjEwcHh9LmdibXBpYXtib3JkZXI6bm9uZTtkaXNwbGF5OmJsb2NrO2hlaWdodDo0OHB4O3dpZHRoOjQ4cHh9LmdibXBud3tkaXNwbGF5OmlubGluZS1ibG9jaztoZWlnaHQ6YXV0bzttYXJnaW46MTBweCAwO3ZlcnRpY2FsLWFsaWduOnRvcH0KLmdicWZiLC5nYnFmYmEsLmdicWZiYnstbW96LWJvcmRlci1yYWRpdXM6MnB4Oy13ZWJraXQtYm9yZGVyLXJhZGl1czoycHg7Ym9yZGVyLXJhZGl1czoycHg7Y3Vyc29yOmRlZmF1bHQgIWltcG9ydGFudDtkaXNwbGF5OmlubGluZS1ibG9jaztmb250LXdlaWdodDpib2xkO2hlaWdodDoyOXB4O2xpbmUtaGVpZ2h0OjI5cHg7bWluLXdpZHRoOjU0cHg7Km1pbi13aWR0aDo3MHB4O3BhZGRpbmc6MCA4cHg7dGV4dC1hbGlnbjpjZW50ZXI7dGV4dC1kZWNvcmF0aW9uOm5vbmUgIWltcG9ydGFudDstbW96LXVzZXItc2VsZWN0Om5vbmU7LXdlYmtpdC11c2VyLXNlbGVjdDpub25lfS5nYnFmYjpmb2N1cywuZ2JxZmJhOmZvY3VzLC5nYnFmYmI6Zm9jdXN7Ym9yZGVyOjFweCBzb2xpZCAjNGQ5MGZlOy1tb3otYm94LXNoYWRvdzppbnNldCAwIDAgMCAxcHggcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjUpOy13ZWJraXQtYm94LXNoYWRvdzppbnNldCAwIDAgMCAxcHggcmdiYSgyNTUsIDI1NSwgMjU1LCAwLjUpO2JveC1zaGFkb3c6aW5zZXQgMCAwIDAgMXB4IHJnYmEoMjU1LCAyNTUsIDI1NSwgMC41KTtvdXRsaW5lOm5vbmV9LmdicWZiLWh2cjpmb2N1cywuZ2JxZmJhLWh2cjpmb2N1cywuZ2JxZmJiLWh2cjpmb2N1c3std2Via2l0LWJveC1zaGFkb3c6aW5zZXQgMCAwIDAgMXB4ICNmZmYsMCAxcHggMXB4IHJnYmEoMCwwLDAsLjEpOy1tb3otYm94LXNoYWRvdzppbnNldCAwIDAgMCAxcHggI2ZmZiwwIDFweCAxcHggcmdiYSgwLDAsMCwuMSk7Ym94LXNoYWRvdzppbnNldCAwIDAgMCAxcHggI2ZmZiwwIDFweCAxcHggcmdiYSgwLDAsMCwuMSl9LmdicWZiLW5vLWZvY3VzOmZvY3Vze2JvcmRlcjoxcHggc29saWQgIzMwNzllZDstbW96LWJveC1zaGFkb3c6bm9uZTstd2Via2l0LWJveC1zaGFkb3c6bm9uZTtib3gtc2hhZG93Om5vbmV9LmdicWZiLWh2ciwuZ2JxZmJhLWh2ciwuZ2JxZmJiLWh2cnstd2Via2l0LWJveC1zaGFkb3c6MCAxcHggMXB4IHJnYmEoMCwwLDAsLjEpOy1tb3otYm94LXNoYWRvdzowIDFweCAxcHggcmdiYSgwLDAsMCwuMSk7Ym94LXNoYWRvdzowIDFweCAxcHggcmdiYSgwLDAsMCwuMSl9LmdicWZiOjotbW96LWZvY3VzLWlubmVyLC5nYnFmYmE6Oi1tb3otZm9jdXMtaW5uZXIsLmdicWZiYjo6LW1vei1mb2N1cy1pbm5lcntib3JkZXI6MH0uZ2JxZmJhLC5nYnFmYmJ7Ym9yZGVyOjFweCBzb2xpZCAjZGNkY2RjO2JvcmRlci1jb2xvcjpyZ2JhKDAsMCwwLC4xKTtjb2xvcjojNDQ0ICFpbXBvcnRhbnQ7Zm9udC1zaXplOjExcHh9LmdicWZie2JhY2tncm91bmQtY29sb3I6IzRkOTBmZTtiYWNrZ3JvdW5kLWltYWdlOi13ZWJraXQtZ3JhZGllbnQobGluZWFyLGxlZnQgdG9wLGxlZnQgYm90dG9tLGZyb20oIzRkOTBmZSksdG8oIzQ3ODdlZCkpO2JhY2tncm91bmQtaW1hZ2U6LXdlYmtpdC1saW5lYXItZ3JhZGllbnQodG9wLCM0ZDkwZmUsIzQ3ODdlZCk7YmFja2dyb3VuZC1pbWFnZTotbW96LWxpbmVhci1ncmFkaWVudCh0b3AsIzRkOTBmZSwjNDc4N2VkKTtiYWNrZ3JvdW5kLWltYWdlOi1tcy1saW5lYXItZ3JhZGllbnQodG9wLCM0ZDkwZmUsIzQ3ODdlZCk7YmFja2dyb3VuZC1pbWFnZTotby1saW5lYXItZ3JhZGllbnQodG9wLCM0ZDkwZmUsIzQ3ODdlZCk7YmFja2dyb3VuZC1pbWFnZTpsaW5lYXItZ3JhZGllbnQodG9wLCM0ZDkwZmUsIzQ3ODdlZCk7ZmlsdGVyOnByb2dpZDpEWEltYWdlVHJhbnNmb3JtLk1pY3Jvc29mdC5ncmFkaWVudChzdGFydENvbG9yU3RyPScjNGQ5MGZlJyxFbmRDb2xvclN0cj0nIzQ3ODdlZCcpO2JvcmRlcjoxcHggc29saWQgIzMwNzllZDtjb2xvcjojZmZmIWltcG9ydGFudDttYXJnaW46MCAwfS5nYnFmYi1odnJ7Ym9yZGVyLWNvbG9yOiMyZjViYjd9LmdicWZiLWh2cjpmb2N1c3tib3JkZXItY29sb3I6IzJmNWJiN30uZ2JxZmItaHZyLC5nYnFmYi1odnI6Zm9jdXN7YmFja2dyb3VuZC1jb2xvcjojMzU3YWU4O2JhY2tncm91bmQtaW1hZ2U6LXdlYmtpdC1ncmFkaWVudChsaW5lYXIsbGVmdCB0b3AsbGVmdCBib3R0b20sZnJvbSgjNGQ5MGZlKSx0bygjMzU3YWU4KSk7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWxpbmVhci1ncmFkaWVudCh0b3AsIzRkOTBmZSwjMzU3YWU4KTtiYWNrZ3JvdW5kLWltYWdlOi1tb3otbGluZWFyLWdyYWRpZW50KHRvcCwjNGQ5MGZlLCMzNTdhZTgpO2JhY2tncm91bmQtaW1hZ2U6LW1zLWxpbmVhci1ncmFkaWVudCh0b3AsIzRkOTBmZSwjMzU3YWU4KTtiYWNrZ3JvdW5kLWltYWdlOi1vLWxpbmVhci1ncmFkaWVudCh0b3AsIzRkOTBmZSwjMzU3YWU4KTtiYWNrZ3JvdW5kLWltYWdlOmxpbmVhci1ncmFkaWVudCh0b3AsIzRkOTBmZSwjMzU3YWU4KX0uZ2JxZmI6YWN0aXZle2JhY2tncm91bmQtY29sb3I6aW5oZXJpdDstd2Via2l0LWJveC1zaGFkb3c6aW5zZXQgMCAxcHggMnB4IHJnYmEoMCwgMCwgMCwgMC4zKTstbW96LWJveC1zaGFkb3c6aW5zZXQgMCAxcHggMnB4IHJnYmEoMCwgMCwgMCwgMC4zKTtib3gtc2hhZG93Omluc2V0IDAgMXB4IDJweCByZ2JhKDAsIDAsIDAsIDAuMyl9LmdicWZiYXtiYWNrZ3JvdW5kLWNvbG9yOiNmNWY1ZjU7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWdyYWRpZW50KGxpbmVhcixsZWZ0IHRvcCxsZWZ0IGJvdHRvbSxmcm9tKCNmNWY1ZjUpLHRvKCNmMWYxZjEpKTtiYWNrZ3JvdW5kLWltYWdlOi13ZWJraXQtbGluZWFyLWdyYWRpZW50KHRvcCwjZjVmNWY1LCNmMWYxZjEpO2JhY2tncm91bmQtaW1hZ2U6LW1vei1saW5lYXItZ3JhZGllbnQodG9wLCNmNWY1ZjUsI2YxZjFmMSk7YmFja2dyb3VuZC1pbWFnZTotbXMtbGluZWFyLWdyYWRpZW50KHRvcCwjZjVmNWY1LCNmMWYxZjEpO2JhY2tncm91bmQtaW1hZ2U6LW8tbGluZWFyLWdyYWRpZW50KHRvcCwjZjVmNWY1LCNmMWYxZjEpO2JhY2tncm91bmQtaW1hZ2U6bGluZWFyLWdyYWRpZW50KHRvcCwjZjVmNWY1LCNmMWYxZjEpO2ZpbHRlcjpwcm9naWQ6RFhJbWFnZVRyYW5zZm9ybS5NaWNyb3NvZnQuZ3JhZGllbnQoc3RhcnRDb2xvclN0cj0nI2Y1ZjVmNScsRW5kQ29sb3JTdHI9JyNmMWYxZjEnKX0uZ2JxZmJhLWh2ciwuZ2JxZmJhLWh2cjphY3RpdmV7YmFja2dyb3VuZC1jb2xvcjojZjhmOGY4O2JhY2tncm91bmQtaW1hZ2U6LXdlYmtpdC1ncmFkaWVudChsaW5lYXIsbGVmdCB0b3AsbGVmdCBib3R0b20sZnJvbSgjZjhmOGY4KSx0bygjZjFmMWYxKSk7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWxpbmVhci1ncmFkaWVudCh0b3AsI2Y4ZjhmOCwjZjFmMWYxKTtiYWNrZ3JvdW5kLWltYWdlOi1tb3otbGluZWFyLWdyYWRpZW50KHRvcCwjZjhmOGY4LCNmMWYxZjEpO2JhY2tncm91bmQtaW1hZ2U6LW1zLWxpbmVhci1ncmFkaWVudCh0b3AsI2Y4ZjhmOCwjZjFmMWYxKTtiYWNrZ3JvdW5kLWltYWdlOi1vLWxpbmVhci1ncmFkaWVudCh0b3AsI2Y4ZjhmOCwjZjFmMWYxKTtiYWNrZ3JvdW5kLWltYWdlOmxpbmVhci1ncmFkaWVudCh0b3AsI2Y4ZjhmOCwjZjFmMWYxKTtmaWx0ZXI6cHJvZ2lkOkRYSW1hZ2VUcmFuc2Zvcm0uTWljcm9zb2Z0LmdyYWRpZW50KHN0YXJ0Q29sb3JTdHI9JyNmOGY4ZjgnLEVuZENvbG9yU3RyPScjZjFmMWYxJyl9LmdicWZiYntiYWNrZ3JvdW5kLWNvbG9yOiNmZmY7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWdyYWRpZW50KGxpbmVhcixsZWZ0IHRvcCxsZWZ0IGJvdHRvbSxmcm9tKCNmZmYpLHRvKCNmYmZiZmIpKTtiYWNrZ3JvdW5kLWltYWdlOi13ZWJraXQtbGluZWFyLWdyYWRpZW50KHRvcCwjZmZmLCNmYmZiZmIpO2JhY2tncm91bmQtaW1hZ2U6LW1vei1saW5lYXItZ3JhZGllbnQodG9wLCNmZmYsI2ZiZmJmYik7YmFja2dyb3VuZC1pbWFnZTotbXMtbGluZWFyLWdyYWRpZW50KHRvcCwjZmZmLCNmYmZiZmIpO2JhY2tncm91bmQtaW1hZ2U6LW8tbGluZWFyLWdyYWRpZW50KHRvcCwjZmZmLCNmYmZiZmIpO2JhY2tncm91bmQtaW1hZ2U6bGluZWFyLWdyYWRpZW50KHRvcCwjZmZmLCNmYmZiZmIpO2ZpbHRlcjpwcm9naWQ6RFhJbWFnZVRyYW5zZm9ybS5NaWNyb3NvZnQuZ3JhZGllbnQoc3RhcnRDb2xvclN0cj0nI2ZmZmZmZicsRW5kQ29sb3JTdHI9JyNmYmZiZmInKX0uZ2JxZmJiLWh2ciwuZ2JxZmJiLWh2cjphY3RpdmV7YmFja2dyb3VuZC1jb2xvcjojZmZmO2JhY2tncm91bmQtaW1hZ2U6LXdlYmtpdC1ncmFkaWVudChsaW5lYXIsbGVmdCB0b3AsbGVmdCBib3R0b20sZnJvbSgjZmZmKSx0bygjZjhmOGY4KSk7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWxpbmVhci1ncmFkaWVudCh0b3AsI2ZmZiwjZjhmOGY4KTtiYWNrZ3JvdW5kLWltYWdlOi1tb3otbGluZWFyLWdyYWRpZW50KHRvcCwjZmZmLCNmOGY4ZjgpO2JhY2tncm91bmQtaW1hZ2U6LW1zLWxpbmVhci1ncmFkaWVudCh0b3AsI2ZmZiwjZjhmOGY4KTtiYWNrZ3JvdW5kLWltYWdlOi1vLWxpbmVhci1ncmFkaWVudCh0b3AsI2ZmZiwjZjhmOGY4KTtiYWNrZ3JvdW5kLWltYWdlOmxpbmVhci1ncmFkaWVudCh0b3AsI2ZmZiwjZjhmOGY4KTtmaWx0ZXI6cHJvZ2lkOkRYSW1hZ2VUcmFuc2Zvcm0uTWljcm9zb2Z0LmdyYWRpZW50KHN0YXJ0Q29sb3JTdHI9JyNmZmZmZmYnLEVuZENvbG9yU3RyPScjZjhmOGY4Jyl9LmdicWZiYS1odnIsLmdicWZiYS1odnI6YWN0aXZlLC5nYnFmYmItaHZyLC5nYnFmYmItaHZyOmFjdGl2ZXtib3JkZXItY29sb3I6I2M2YzZjNjstd2Via2l0LWJveC1zaGFkb3c6MCAxcHggMXB4IHJnYmEoMCwwLDAsLjEpOy1tb3otYm94LXNoYWRvdzowIDFweCAxcHggcmdiYSgwLDAsMCwuMSk7Ym94LXNoYWRvdzowIDFweCAxcHggcmdiYSgwLDAsMCwuMSk7Y29sb3I6IzIyMiAhaW1wb3J0YW50fS5nYnFmYmE6YWN0aXZlLC5nYnFmYmI6YWN0aXZley13ZWJraXQtYm94LXNoYWRvdzppbnNldCAwIDFweCAycHggcmdiYSgwLDAsMCwuMSk7LW1vei1ib3gtc2hhZG93Omluc2V0IDAgMXB4IDJweCByZ2JhKDAsMCwwLC4xKTtib3gtc2hhZG93Omluc2V0IDAgMXB4IDJweCByZ2JhKDAsMCwwLC4xKX0KI2dibXBhc3ttYXgtaGVpZ2h0OjIyMHB4fSNnYm1te21heC1oZWlnaHQ6NTMwcHh9Lmdic2J7LXdlYmtpdC1ib3gtc2l6aW5nOmJvcmRlci1ib3g7ZGlzcGxheTpibG9jaztwb3NpdGlvbjpyZWxhdGl2ZTsqem9vbToxfS5nYnNiaWN7b3ZlcmZsb3c6YXV0b30uZ2JzYmlzIC5nYnNidCwuZ2JzYmlzIC5nYnNiYnstd2Via2l0LW1hc2stYm94LWltYWdlOi13ZWJraXQtZ3JhZGllbnQobGluZWFyLGxlZnQgdG9wLHJpZ2h0IHRvcCxjb2xvci1zdG9wKDAscmdiYSgwLDAsMCwuMSkpLGNvbG9yLXN0b3AoLjUscmdiYSgwLDAsMCwuOCkpLGNvbG9yLXN0b3AoMSxyZ2JhKDAsMCwwLC4xKSkpO2xlZnQ6MDttYXJnaW4tcmlnaHQ6MDtvcGFjaXR5OjA7cG9zaXRpb246YWJzb2x1dGU7d2lkdGg6MTAwJX0uZ2JzYiAuZ2JzYnQ6YWZ0ZXIsLmdic2IgLmdic2JiOmFmdGVye2NvbnRlbnQ6IiI7ZGlzcGxheTpibG9jaztoZWlnaHQ6MDtsZWZ0OjA7cG9zaXRpb246YWJzb2x1dGU7d2lkdGg6MTAwJX0uZ2JzYmlzIC5nYnNidHtiYWNrZ3JvdW5kOi13ZWJraXQtZ3JhZGllbnQobGluZWFyLGxlZnQgdG9wLGxlZnQgYm90dG9tLGZyb20ocmdiYSgwLDAsMCwuMikpLHRvKHJnYmEoMCwwLDAsMCkpKTtiYWNrZ3JvdW5kLWltYWdlOi13ZWJraXQtbGluZWFyLWdyYWRpZW50KHRvcCxyZ2JhKDAsMCwwLC4yKSxyZ2JhKDAsMCwwLDApKTtiYWNrZ3JvdW5kLWltYWdlOi1tb3otbGluZWFyLWdyYWRpZW50KHRvcCxyZ2JhKDAsMCwwLC4yKSxyZ2JhKDAsMCwwLDApKTtiYWNrZ3JvdW5kLWltYWdlOi1tcy1saW5lYXItZ3JhZGllbnQodG9wLHJnYmEoMCwwLDAsLjIpLHJnYmEoMCwwLDAsMCkpO2JhY2tncm91bmQtaW1hZ2U6LW8tbGluZWFyLWdyYWRpZW50KHRvcCxyZ2JhKDAsMCwwLC4yKSxyZ2JhKDAsMCwwLDApKTtiYWNrZ3JvdW5kLWltYWdlOmxpbmVhci1ncmFkaWVudCh0b3AscmdiYSgwLDAsMCwuMikscmdiYSgwLDAsMCwwKSk7aGVpZ2h0OjZweDt0b3A6MH0uZ2JzYiAuZ2JzYnQ6YWZ0ZXJ7Ym9yZGVyLXRvcDoxcHggc29saWQgI2ViZWJlYjtib3JkZXItY29sb3I6cmdiYSgwLDAsMCwuMyk7dG9wOjB9Lmdic2IgLmdic2Jiey13ZWJraXQtbWFzay1ib3gtaW1hZ2U6LXdlYmtpdC1ncmFkaWVudChsaW5lYXIsbGVmdCB0b3AscmlnaHQgdG9wLGNvbG9yLXN0b3AoMCxyZ2JhKDAsMCwwLC4xKSksY29sb3Itc3RvcCguNSxyZ2JhKDAsMCwwLC44KSksY29sb3Itc3RvcCgxLHJnYmEoMCwwLDAsLjEpKSk7YmFja2dyb3VuZDotd2Via2l0LWdyYWRpZW50KGxpbmVhcixsZWZ0IGJvdHRvbSxsZWZ0IHRvcCxmcm9tKHJnYmEoMCwwLDAsLjIpKSx0byhyZ2JhKDAsMCwwLDApKSk7YmFja2dyb3VuZC1pbWFnZTotd2Via2l0LWxpbmVhci1ncmFkaWVudChib3R0b20scmdiYSgwLDAsMCwuMikscmdiYSgwLDAsMCwwKSk7YmFja2dyb3VuZC1pbWFnZTotbW96LWxpbmVhci1ncmFkaWVudChib3R0b20scmdiYSgwLDAsMCwuMikscmdiYSgwLDAsMCwwKSk7YmFja2dyb3VuZC1pbWFnZTotbXMtbGluZWFyLWdyYWRpZW50KGJvdHRvbSxyZ2JhKDAsMCwwLC4yKSxyZ2JhKDAsMCwwLDApKTtiYWNrZ3JvdW5kLWltYWdlOi1vLWxpbmVhci1ncmFkaWVudChib3R0b20scmdiYSgwLDAsMCwuMikscmdiYSgwLDAsMCwwKSk7YmFja2dyb3VuZC1pbWFnZTpsaW5lYXItZ3JhZGllbnQoYm90dG9tLHJnYmEoMCwwLDAsLjIpLHJnYmEoMCwwLDAsMCkpO2JvdHRvbTowO2hlaWdodDo0cHh9Lmdic2IgLmdic2JiOmFmdGVye2JvcmRlci1ib3R0b206MXB4IHNvbGlkICNlYmViZWI7Ym9yZGVyLWNvbG9yOnJnYmEoMCwwLDAsLjMpO2JvdHRvbTowfQo8L3N0eWxlPjxzdHlsZT5ib2R5LHRkLGEscCwuaHtmb250LWZhbWlseTphcmlhbCxzYW5zLXNlcmlmfWJvZHl7bWFyZ2luOjA7b3ZlcmZsb3cteTpzY3JvbGx9I2dvZ3twYWRkaW5nOjNweCA4cHggMH10ZHtsaW5lLWhlaWdodDouOGVtfS5nYWNfbSB0ZHtsaW5lLWhlaWdodDoxN3B4fWZvcm17bWFyZ2luLWJvdHRvbToyMHB4fS5oe2NvbG9yOiMzNmN9LnF7Y29sb3I6IzAwY30udHMgdGR7cGFkZGluZzowfS50c3tib3JkZXItY29sbGFwc2U6Y29sbGFwc2V9ZW17Zm9udC13ZWlnaHQ6Ym9sZDtmb250LXN0eWxlOm5vcm1hbH0ubHN0e2hlaWdodDoyNXB4O3dpZHRoOjQ5NnB4fS5nc2ZpLC5sc3R7Zm9udDoxOHB4IGFyaWFsLHNhbnMtc2VyaWZ9LmdzZnN7Zm9udDoxN3B4IGFyaWFsLHNhbnMtc2VyaWZ9LmRze2Rpc3BsYXk6aW5saW5lLWJveDtkaXNwbGF5OmlubGluZS1ibG9jazttYXJnaW46M3B4IDAgNHB4O21hcmdpbi1sZWZ0OjRweH1pbnB1dHtmb250LWZhbWlseTppbmhlcml0fWJvZHl7YmFja2dyb3VuZDojZmZmO2NvbG9yOiMwMDB9YXtjb2xvcjojMTFjO3RleHQtZGVjb3JhdGlvbjpub25lfWE6aG92ZXIsYTphY3RpdmV7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZX0uZmwgYXtjb2xvcjojMzZjfWE6dmlzaXRlZHtjb2xvcjojNTUxYThifS5zYmxje3BhZGRpbmctdG9wOjVweH0uc2JsYyBhe2Rpc3BsYXk6YmxvY2s7bWFyZ2luOjJweCAwO21hcmdpbi1sZWZ0OjEzcHg7Zm9udC1zaXplOjExcHh9LmxzYmJ7YmFja2dyb3VuZDojZWVlO2JvcmRlcjpzb2xpZCAxcHg7Ym9yZGVyLWNvbG9yOiNjY2MgIzk5OSAjOTk5ICNjY2M7aGVpZ2h0OjMwcHh9LmxzYmJ7ZGlzcGxheTpibG9ja30jZmxsIGF7ZGlzcGxheTppbmxpbmUtYmxvY2s7bWFyZ2luOjAgMTJweH0ubHNie2JhY2tncm91bmQ6dXJsKC9pbWFnZXMvbmF2X2xvZ28yMjkucG5nKSAwIC0yNjFweCByZXBlYXQteDtib3JkZXI6bm9uZTtjb2xvcjojMDAwO2N1cnNvcjpwb2ludGVyO2hlaWdodDozMHB4O21hcmdpbjowO291dGxpbmU6MDtmb250OjE1cHggYXJpYWwsc2Fucy1zZXJpZjt2ZXJ0aWNhbC1hbGlnbjp0b3B9LmxzYjphY3RpdmV7YmFja2dyb3VuZDojY2NjfS5sc3Q6Zm9jdXN7b3V0bGluZTpub25lfTwvc3R5bGU+PHNjcmlwdCBub25jZT0iWjByd3R4TU50TXRqNUdsSXZ5cmQyZz09Ij4oZnVuY3Rpb24oKXt0cnl7LyoKCiBDb3B5cmlnaHQgVGhlIENsb3N1cmUgTGlicmFyeSBBdXRob3JzLgogU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjAKKi8KdmFyIGU9dGhpc3x8c2VsZjt2YXIgYWE9ZnVuY3Rpb24oYSxiLGMsZCl7ZD1kfHx7fTtkLl9zbj1bImNmZyIsYixjXS5qb2luKCIuIik7d2luZG93LmdiYXIubG9nZ2VyLm1sKGEsZCl9O3ZhciBnPXdpbmRvdy5nYmFyPXdpbmRvdy5nYmFyfHx7fSxoPXdpbmRvdy5nYmFyLmk9d2luZG93LmdiYXIuaXx8e30sYmE7ZnVuY3Rpb24gX3R2bihhLGIpe2E9cGFyc2VJbnQoYSwxMCk7cmV0dXJuIGlzTmFOKGEpP2I6YX1mdW5jdGlvbiBfdHZmKGEsYil7YT1wYXJzZUZsb2F0KGEpO3JldHVybiBpc05hTihhKT9iOmF9ZnVuY3Rpb24gX3R2dihhKXtyZXR1cm4hIWF9ZnVuY3Rpb24gcChhLGIsYyl7KGN8fGcpW2FdPWJ9Zy5idj17bjpfdHZuKCIyIiwwKSxyOiIiLGY6Ii42Ni40MS4iLGU6IjEzMDAxMDIsMzcwMDMwMiwzNzAwNjk3IixtOl90dm4oIjEiLDEpfTsKZnVuY3Rpb24gY2EoYSxiLGMpe3ZhciBkPSJvbiIrYjtpZihhLmFkZEV2ZW50TGlzdGVuZXIpYS5hZGRFdmVudExpc3RlbmVyKGIsYywhMSk7ZWxzZSBpZihhLmF0dGFjaEV2ZW50KWEuYXR0YWNoRXZlbnQoZCxjKTtlbHNle3ZhciBmPWFbZF07YVtkXT1mdW5jdGlvbigpe3ZhciBrPWYuYXBwbHkodGhpcyxhcmd1bWVudHMpLG09Yy5hcHBseSh0aGlzLGFyZ3VtZW50cyk7cmV0dXJuIHZvaWQgMD09az9tOnZvaWQgMD09bT9rOm0mJmt9fX12YXIgZGE9ZnVuY3Rpb24oYSl7cmV0dXJuIGZ1bmN0aW9uKCl7cmV0dXJuIGcuYnYubT09YX19LGVhPWRhKDEpLGZhPWRhKDIpO3AoInNiIixlYSk7cCgia24iLGZhKTtoLmE9X3R2djtoLmI9X3R2ZjtoLmM9X3R2bjtoLmk9YWE7dmFyIHI9d2luZG93LmdiYXIuaS5pO3ZhciB0PWZ1bmN0aW9uKCl7fSxoYT1mdW5jdGlvbigpe30sa2E9ZnVuY3Rpb24oYSl7dmFyIGI9bmV3IEltYWdlLGM9aWE7Yi5vbmVycm9yPWIub25sb2FkPWIub25hYm9ydD1mdW5jdGlvbigpe3RyeXtkZWxldGUgamFbY119Y2F0Y2goZCl7fX07amFbY109YjtiLnNyYz1hO2lhPWMrMX0samE9W10saWE9MDtwKCJsb2dnZXIiLHtpbDpoYSxtbDp0LGxvZzprYX0pO3ZhciB1PXdpbmRvdy5nYmFyLmxvZ2dlcjt2YXIgdj17fSxsYT17fSx3PVtdLG1hPWguYigiMC4xIiwuMSksbmE9aC5hKCIxIiwhMCksb2E9ZnVuY3Rpb24oYSxiKXt3LnB1c2goW2EsYl0pfSxwYT1mdW5jdGlvbihhLGIpe3ZbYV09Yn0scWE9ZnVuY3Rpb24oYSl7cmV0dXJuIGEgaW4gdn0seD17fSxBPWZ1bmN0aW9uKGEsYil7eFthXXx8KHhbYV09W10pO3hbYV0ucHVzaChiKX0sQj1mdW5jdGlvbihhKXtBKCJtIixhKX0scmE9ZnVuY3Rpb24oYSxiKXt2YXIgYz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJzY3JpcHQiKTtjLnNyYz1hO2MuYXN5bmM9bmE7TWF0aC5yYW5kb20oKTxtYSYmKGMub25lcnJvcj1mdW5jdGlvbigpe2Mub25lcnJvcj1udWxsO3QoRXJyb3IoIkJ1bmRsZSBsb2FkIGZhaWxlZDogbmFtZT0iKyhifHwiVU5LIikrIiB1cmw9IithKSl9KTsoZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoInhqc2MiKXx8ZG9jdW1lbnQuZ2V0RWxlbWVudHNCeVRhZ05hbWUoImJvZHkiKVswXXx8CmRvY3VtZW50LmdldEVsZW1lbnRzQnlUYWdOYW1lKCJoZWFkIilbMF0pLmFwcGVuZENoaWxkKGMpfSxEPWZ1bmN0aW9uKGEpe2Zvcih2YXIgYj0wLGM7KGM9d1tiXSkmJmNbMF0hPWE7KytiKTshY3x8Y1sxXS5sfHxjWzFdLnN8fChjWzFdLnM9ITAsc2EoMixhKSxjWzFdLnVybCYmcmEoY1sxXS51cmwsYSksY1sxXS5saWJzJiZDJiZDKGNbMV0ubGlicykpfSx0YT1mdW5jdGlvbihhKXtBKCJnYyIsYSl9LHVhPW51bGwsdmE9ZnVuY3Rpb24oYSl7dWE9YX0sc2E9ZnVuY3Rpb24oYSxiLGMpe2lmKHVhKXthPXt0OmEsYjpifTtpZihjKWZvcih2YXIgZCBpbiBjKWFbZF09Y1tkXTt0cnl7dWEoYSl9Y2F0Y2goZil7fX19O3AoIm1kYyIsdik7cCgibWRpIixsYSk7cCgiYm5jIix3KTtwKCJxR0MiLHRhKTtwKCJxbSIsQik7cCgicWQiLHgpO3AoImxiIixEKTtwKCJtY2YiLHBhKTtwKCJiY2YiLG9hKTtwKCJhcSIsQSk7cCgibWRkIiwiIik7CnAoImhhcyIscWEpO3AoInRyaCIsdmEpO3AoInRldiIsc2EpO2lmKGguYSgibTsvXy9zY3MvYWJjLXN0YXRpYy9fL2pzL2s9Z2FwaS5nYXBpLmVuLnl5aEJ5WWVNVEFjLk8vYW09QUFZL2Q9MS9jdD16Z21zL3JzPUFIcE9vby1PNDcwRVFkWi00dHBXcHBweVRRbWVPRVV2LWcvbT1fX2ZlYXR1cmVzX18iKSl7dmFyIEY9ZnVuY3Rpb24oYSxiKXtyZXR1cm4gd2E/YXx8YjpifSx4YT1oLmEoIjEiKSx5YT1oLmEoIiIpLHphPWguYSgiIiksd2E9aC5hKCIiKSxBYT13aW5kb3cuZ2FwaT1GKHdpbmRvdy5nYXBpLHt9KSxCYT1mdW5jdGlvbihhLGIpe3ZhciBjPWZ1bmN0aW9uKCl7Zy5kZ2woYSxiKX07eGE/QihjKTooQSgiZ2wiLGMpLEQoImdsIikpfSxDYT17fSxEYT1mdW5jdGlvbihhKXthPWEuc3BsaXQoIjoiKTtmb3IodmFyIGI7KGI9YS5wb3AoKSkmJkNhW2JdOyk7cmV0dXJuIWJ9LEM9ZnVuY3Rpb24oYSl7ZnVuY3Rpb24gYigpe2Zvcih2YXIgYz1hLnNwbGl0KCI6IiksZD0wLGY7Zj1jW2RdOysrZClDYVtmXT0xO2ZvcihjPTA7ZD13W2NdOysrYylkPWRbMV0sKGY9ZC5saWJzKSYmIWQubCYmZC5pJiYKRGEoZikmJmQuaSgpfWcuZGdsKGEsYil9LEc9d2luZG93Ll9fX2pzbD1GKHdpbmRvdy5fX19qc2wse30pO0cuaD1GKEcuaCwibTsvXy9zY3MvYWJjLXN0YXRpYy9fL2pzL2s9Z2FwaS5nYXBpLmVuLnl5aEJ5WWVNVEFjLk8vYW09QUFZL2Q9MS9jdD16Z21zL3JzPUFIcE9vby1PNDcwRVFkWi00dHBXcHBweVRRbWVPRVV2LWcvbT1fX2ZlYXR1cmVzX18iKTtHLm1zPUYoRy5tcywiaHR0cHM6Ly9hcGlzLmdvb2dsZS5jb20iKTtHLm09RihHLm0sIiIpO0cubD1GKEcubCxbXSk7Ry5kcG89RihHLmRwbywiIik7eGF8fHcucHVzaChbImdsIix7dXJsOiIvL3NzbC5nc3RhdGljLmNvbS9nYi9qcy9hYmMvZ2xtX2U3YmIzOWE3ZTFhMjQ1ODFmZjRmOGQxOTk2NzhiMWI5LmpzIn1dKTt2YXIgRWE9e3B1OnlhLHNoOiIiLHNpOnphLGhsOiJlbiJ9O3YuZ2w9RWE7d2E/QWEubG9hZHx8cCgibG9hZCIsQmEsQWEpOnAoImxvYWQiLEJhLEFhKTtwKCJkZ2wiLEJhKTtwKCJhZ2wiLERhKTtoLm89eGF9O3ZhciBGYT1oLmIoIjAuMSIsLjAwMSksR2E9MDsKZnVuY3Rpb24gX21sVG9rZW4oYSxiKXt0cnl7aWYoMT5HYSl7R2ErKzt2YXIgYz1hO2I9Ynx8e307dmFyIGQ9ZW5jb2RlVVJJQ29tcG9uZW50LGY9WyIvL3d3dy5nb29nbGUuY29tL2dlbl8yMDQ/YXR5cD1pJnp4PSIsKG5ldyBEYXRlKS5nZXRUaW1lKCksIiZqZXhwaWQ9IixkKCIyODgzNCIpLCImc3JjcGc9IixkKCJwcm9wPTEiKSwiJmpzcj0iLE1hdGgucm91bmQoMS9GYSksIiZvZ2V2PSIsZCgickM4Slg3bVNCLWFhX1FhMW5ZR0FDdyIpLCImb2dmPSIsZy5idi5mLCImb2dycD0iLGQoIiIpLCImb2d2PSIsZCgiMzE5NzAzNzQ0LjAiKSwiJm9nZ3Y9IitkKCJlc19wbHVzb25lX2djXzIwMjAwNjAxLjBfcDAiKSwiJm9nZD0iLGQoImNvbSIpLCImb2djPSIsZCgiVVNBIiksIiZvZ2w9IixkKCJlbiIpXTtiLl9zbiYmKGIuX3NuPQoib2cuIitiLl9zbik7Zm9yKHZhciBrIGluIGIpZi5wdXNoKCImIiksZi5wdXNoKGQoaykpLGYucHVzaCgiPSIpLGYucHVzaChkKGJba10pKTtmLnB1c2goIiZlbXNnPSIpO2YucHVzaChkKGMubmFtZSsiOiIrYy5tZXNzYWdlKSk7dmFyIG09Zi5qb2luKCIiKTtIYShtKSYmKG09bS5zdWJzdHIoMCwyRTMpKTt2YXIgbj1tO3ZhciBsPXdpbmRvdy5nYmFyLmxvZ2dlci5fYWVtKGEsbik7a2EobCl9fWNhdGNoKHEpe319dmFyIEhhPWZ1bmN0aW9uKGEpe3JldHVybiAyRTM8PWEubGVuZ3RofSxJYT1mdW5jdGlvbihhLGIpe3JldHVybiBifTtmdW5jdGlvbiBKYShhKXt0PWE7cCgiX2l0bCIsSGEsdSk7cCgiX2FlbSIsSWEsdSk7cCgibWwiLHQsdSk7YT17fTt2LmVyPWF9aC5hKCIiKT9KYShmdW5jdGlvbihhKXt0aHJvdyBhO30pOmguYSgiMSIpJiZNYXRoLnJhbmRvbSgpPEZhJiZKYShfbWxUb2tlbik7dmFyIF9FPSJsZWZ0IixLYT1oLmEoIiIpLEo9ZnVuY3Rpb24oYSxiKXt2YXIgYz1hLmNsYXNzTmFtZTtIKGEsYil8fChhLmNsYXNzTmFtZSs9KCIiIT1jPyIgIjoiIikrYil9LEs9ZnVuY3Rpb24oYSxiKXt2YXIgYz1hLmNsYXNzTmFtZTtiPW5ldyBSZWdFeHAoIlxccz9cXGIiK2IrIlxcYiIpO2MmJmMubWF0Y2goYikmJihhLmNsYXNzTmFtZT1jLnJlcGxhY2UoYiwiIikpfSxIPWZ1bmN0aW9uKGEsYil7Yj1uZXcgUmVnRXhwKCJcXGIiK2IrIlxcYiIpO2E9YS5jbGFzc05hbWU7cmV0dXJuISghYXx8IWEubWF0Y2goYikpfSxMYT1mdW5jdGlvbihhLGIpe0goYSxiKT9LKGEsYik6SihhLGIpfSxNYT1mdW5jdGlvbihhLGIpe2FbYl09ZnVuY3Rpb24oYyl7dmFyIGQ9YXJndW1lbnRzO2cucW0oZnVuY3Rpb24oKXthW2JdLmFwcGx5KHRoaXMsZCl9KX19LE5hPWZ1bmN0aW9uKCl7cmV0dXJuIjEifSwKT2E9ZnVuY3Rpb24oYSl7YT1bIi8vd3d3LmdzdGF0aWMuY29tIiwiL29nL18vanMvZD0xL2s9Iiwib2cub2cyLmVuX1VTLmI1eEVtcjhQUWx3Lk8iLCIvcnQ9ai9tPSIsYSwiL3JzPSIsIkFBMllyVHVWLXIzcWctNFFNbGY2YTQ5S1NFLTVNSmY3YnciXTtLYSYmYS5wdXNoKCI/aG9zdD13d3cuZ3N0YXRpYy5jb20mYnVzdD1vZy5vZzIuZW5fVVMubUVHSXA2cEc5SDQuRFUiKTthPWEuam9pbigiIik7cmEoYSl9O3AoImNhIixKKTtwKCJjciIsSyk7cCgiY2MiLEgpO2guaz1KO2gubD1LO2gubT1IO2gubj1MYTtoLnA9T2E7aC5xPU1hO2gucj1OYTt2YXIgUGE9WyJnYl83MSIsImdiXzE1NSJdLFFhO2Z1bmN0aW9uIFJhKGEpe1FhPWF9ZnVuY3Rpb24gU2EoYSl7dmFyIGI9UWEmJiFhLmhyZWYubWF0Y2goLy4qXC9hY2NvdW50c1wvQ2xlYXJTSURbP10vKSYmZW5jb2RlVVJJQ29tcG9uZW50KFFhKCkpO2ImJihhLmhyZWY9YS5ocmVmLnJlcGxhY2UoLyhbPyZdY29udGludWU9KVteJl0qLywiJDEiK2IpKX1mdW5jdGlvbiBUYShhKXt3aW5kb3cuZ0FwcGxpY2F0aW9uJiYoYS5ocmVmPXdpbmRvdy5nQXBwbGljYXRpb24uZ2V0VGFiVXJsKGEuaHJlZikpfWZ1bmN0aW9uIFVhKGEpe3RyeXt2YXIgYj0oZG9jdW1lbnQuZm9ybXNbMF0ucXx8IiIpLnZhbHVlO2ImJihhLmhyZWY9YS5ocmVmLnJlcGxhY2UoLyhbPyZdKXE9W14mXSp8JC8sZnVuY3Rpb24oYyxkKXtyZXR1cm4oZHx8IiYiKSsicT0iK2VuY29kZVVSSUNvbXBvbmVudChiKX0pKX1jYXRjaChjKXtyKGMsInNiIiwicHEiKX19CnZhciBWYT1mdW5jdGlvbigpe2Zvcih2YXIgYT1bXSxiPTAsYztjPVBhW2JdOysrYikoYz1kb2N1bWVudC5nZXRFbGVtZW50QnlJZChjKSkmJmEucHVzaChjKTtyZXR1cm4gYX0sV2E9ZnVuY3Rpb24oKXt2YXIgYT1WYSgpO3JldHVybiAwPGEubGVuZ3RoP2FbMF06bnVsbH0sWGE9ZnVuY3Rpb24oKXtyZXR1cm4gZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImdiXzcwIil9LEw9e30sTT17fSxZYT17fSxOPXt9LE89dm9pZCAwLGNiPWZ1bmN0aW9uKGEsYil7dHJ5e3ZhciBjPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJnYiIpO0ooYywiZ2JwZGpzIik7UCgpO1phKGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJnYiIpKSYmSihjLCJnYnJ0bCIpO2lmKGImJmIuZ2V0QXR0cmlidXRlKXt2YXIgZD1iLmdldEF0dHJpYnV0ZSgiYXJpYS1vd25zIik7aWYoZC5sZW5ndGgpe3ZhciBmPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKGQpO2lmKGYpe3ZhciBrPWIucGFyZW50Tm9kZTtpZihPPT1kKU89dm9pZCAwLApLKGssImdidG8iKTtlbHNle2lmKE8pe3ZhciBtPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKE8pO2lmKG0mJm0uZ2V0QXR0cmlidXRlKXt2YXIgbj1tLmdldEF0dHJpYnV0ZSgiYXJpYS1vd25lciIpO2lmKG4ubGVuZ3RoKXt2YXIgbD1kb2N1bWVudC5nZXRFbGVtZW50QnlJZChuKTtsJiZsLnBhcmVudE5vZGUmJksobC5wYXJlbnROb2RlLCJnYnRvIil9fX0kYShmKSYmYWIoZik7Tz1kO0ooaywiZ2J0byIpfX19fUIoZnVuY3Rpb24oKXtnLnRnKGEsYiwhMCl9KTtiYihhKX1jYXRjaChxKXtyKHEsInNiIiwidGciKX19LGRiPWZ1bmN0aW9uKGEpe0IoZnVuY3Rpb24oKXtnLmNsb3NlKGEpfSl9LGViPWZ1bmN0aW9uKGEpe0IoZnVuY3Rpb24oKXtnLnJkZChhKX0pfSxaYT1mdW5jdGlvbihhKXt2YXIgYixjPWRvY3VtZW50LmRlZmF1bHRWaWV3O2MmJmMuZ2V0Q29tcHV0ZWRTdHlsZT8oYT1jLmdldENvbXB1dGVkU3R5bGUoYSwiIikpJiYoYj1hLmRpcmVjdGlvbik6Yj1hLmN1cnJlbnRTdHlsZT8KYS5jdXJyZW50U3R5bGUuZGlyZWN0aW9uOmEuc3R5bGUuZGlyZWN0aW9uO3JldHVybiJydGwiPT1ifSxnYj1mdW5jdGlvbihhLGIsYyl7aWYoYSl0cnl7dmFyIGQ9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImdiZDUiKTtpZihkKXt2YXIgZj1kLmZpcnN0Q2hpbGQsaz1mLmZpcnN0Q2hpbGQsbT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJsaSIpO20uY2xhc3NOYW1lPWIrIiBnYm10YyI7bS5pZD1jO2EuY2xhc3NOYW1lPSJnYm10IjttLmFwcGVuZENoaWxkKGEpO2lmKGsuaGFzQ2hpbGROb2RlcygpKXtjPVtbImdia2MiXSxbImdiZiIsImdiZSIsImdibiJdLFsiZ2JrcCJdLFsiZ2JuZCJdXTtkPTA7dmFyIG49ay5jaGlsZE5vZGVzLmxlbmd0aDtmPSExO2Zvcih2YXIgbD0tMSxxPTAsRTtFPWNbcV07cSsrKXtmb3IodmFyIFU9MCxJO0k9RVtVXTtVKyspe2Zvcig7ZDxuJiZIKGsuY2hpbGROb2Rlc1tkXSxJKTspZCsrO2lmKEk9PWIpe2suaW5zZXJ0QmVmb3JlKG0say5jaGlsZE5vZGVzW2RdfHwKbnVsbCk7Zj0hMDticmVha319aWYoZil7aWYoZCsxPGsuY2hpbGROb2Rlcy5sZW5ndGgpe3ZhciBWPWsuY2hpbGROb2Rlc1tkKzFdO0goVi5maXJzdENoaWxkLCJnYm1oIil8fGZiKFYsRSl8fChsPWQrMSl9ZWxzZSBpZigwPD1kLTEpe3ZhciBXPWsuY2hpbGROb2Rlc1tkLTFdO0goVy5maXJzdENoaWxkLCJnYm1oIil8fGZiKFcsRSl8fChsPWQpfWJyZWFrfTA8ZCYmZCsxPG4mJmQrK31pZigwPD1sKXt2YXIgeT1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJsaSIpLHo9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiZGl2Iik7eS5jbGFzc05hbWU9ImdibXRjIjt6LmNsYXNzTmFtZT0iZ2JtdCBnYm1oIjt5LmFwcGVuZENoaWxkKHopO2suaW5zZXJ0QmVmb3JlKHksay5jaGlsZE5vZGVzW2xdKX1nLmFkZEhvdmVyJiZnLmFkZEhvdmVyKGEpfWVsc2Ugay5hcHBlbmRDaGlsZChtKX19Y2F0Y2goRWIpe3IoRWIsInNiIiwiYWwiKX19LGZiPWZ1bmN0aW9uKGEsYil7Zm9yKHZhciBjPWIubGVuZ3RoLApkPTA7ZDxjO2QrKylpZihIKGEsYltkXSkpcmV0dXJuITA7cmV0dXJuITF9LGhiPWZ1bmN0aW9uKGEsYixjKXtnYihhLGIsYyl9LGliPWZ1bmN0aW9uKGEsYil7Z2IoYSwiZ2JlIixiKX0samI9ZnVuY3Rpb24oKXtCKGZ1bmN0aW9uKCl7Zy5wY20mJmcucGNtKCl9KX0sa2I9ZnVuY3Rpb24oKXtCKGZ1bmN0aW9uKCl7Zy5wY2EmJmcucGNhKCl9KX0sbGI9ZnVuY3Rpb24oYSxiLGMsZCxmLGssbSxuLGwscSl7QihmdW5jdGlvbigpe2cucGFhJiZnLnBhYShhLGIsYyxkLGYsayxtLG4sbCxxKX0pfSxtYj1mdW5jdGlvbihhLGIpe0xbYV18fChMW2FdPVtdKTtMW2FdLnB1c2goYil9LG5iPWZ1bmN0aW9uKGEsYil7TVthXXx8KE1bYV09W10pO01bYV0ucHVzaChiKX0sb2I9ZnVuY3Rpb24oYSxiKXtZYVthXT1ifSxwYj1mdW5jdGlvbihhLGIpe05bYV18fChOW2FdPVtdKTtOW2FdLnB1c2goYil9LGJiPWZ1bmN0aW9uKGEpe2EucHJldmVudERlZmF1bHQmJmEucHJldmVudERlZmF1bHQoKTthLnJldHVyblZhbHVlPQohMTthLmNhbmNlbEJ1YmJsZT0hMH0scWI9bnVsbCxhYj1mdW5jdGlvbihhLGIpe1AoKTtpZihhKXtyYihhLCJPcGVuaW5nJmhlbGxpcDsiKTtRKGEsITApO2I9InVuZGVmaW5lZCIhPXR5cGVvZiBiP2I6MUU0O3ZhciBjPWZ1bmN0aW9uKCl7c2IoYSl9O3FiPXdpbmRvdy5zZXRUaW1lb3V0KGMsYil9fSx0Yj1mdW5jdGlvbihhKXtQKCk7YSYmKFEoYSwhMSkscmIoYSwiIikpfSxzYj1mdW5jdGlvbihhKXt0cnl7UCgpO3ZhciBiPWF8fGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKE8pO2ImJihyYihiLCJUaGlzIHNlcnZpY2UgaXMgY3VycmVudGx5IHVuYXZhaWxhYmxlLiUxJHNQbGVhc2UgdHJ5IGFnYWluIGxhdGVyLiIsIiUxJHMiKSxRKGIsITApKX1jYXRjaChjKXtyKGMsInNiIiwic2RoZSIpfX0scmI9ZnVuY3Rpb24oYSxiLGMpe2lmKGEmJmIpe3ZhciBkPSRhKGEpO2lmKGQpe2lmKGMpe2QudGV4dENvbnRlbnQ9IiI7Yj1iLnNwbGl0KGMpO2M9MDtmb3IodmFyIGY7Zj1iW2NdO2MrKyl7dmFyIGs9ZG9jdW1lbnQuY3JlYXRlRWxlbWVudCgiZGl2Iik7CmsuaW5uZXJIVE1MPWY7ZC5hcHBlbmRDaGlsZChrKX19ZWxzZSBkLmlubmVySFRNTD1iO1EoYSwhMCl9fX0sUT1mdW5jdGlvbihhLGIpeyhiPXZvaWQgMCE9PWI/YjohMCk/SihhLCJnYm1zZ28iKTpLKGEsImdibXNnbyIpfSwkYT1mdW5jdGlvbihhKXtmb3IodmFyIGI9MCxjO2M9YS5jaGlsZE5vZGVzW2JdO2IrKylpZihIKGMsImdibXNnIikpcmV0dXJuIGN9LFA9ZnVuY3Rpb24oKXtxYiYmd2luZG93LmNsZWFyVGltZW91dChxYil9LHViPWZ1bmN0aW9uKGEpe3ZhciBiPSJpbm5lciIrYTthPSJvZmZzZXQiK2E7cmV0dXJuIHdpbmRvd1tiXT93aW5kb3dbYl06ZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50JiZkb2N1bWVudC5kb2N1bWVudEVsZW1lbnRbYV0/ZG9jdW1lbnQuZG9jdW1lbnRFbGVtZW50W2FdOjB9LHZiPWZ1bmN0aW9uKCl7cmV0dXJuITF9LHdiPWZ1bmN0aW9uKCl7cmV0dXJuISFPfTtwKCJzbyIsV2EpO3AoInNvcyIsVmEpO3AoInNpIixYYSk7cCgidGciLGNiKTsKcCgiY2xvc2UiLGRiKTtwKCJyZGQiLGViKTtwKCJhZGRMaW5rIixoYik7cCgiYWRkRXh0cmFMaW5rIixpYik7cCgicGNtIixqYik7cCgicGNhIixrYik7cCgicGFhIixsYik7cCgiZGRsZCIsYWIpO3AoImRkcmQiLHRiKTtwKCJkZGVyciIsc2IpO3AoInJ0bCIsWmEpO3AoIm9wIix3Yik7cCgiYmgiLEwpO3AoImFiaCIsbWIpO3AoImRoIixNKTtwKCJhZGgiLG5iKTtwKCJjaCIsTik7cCgiYWNoIixwYik7cCgiZWgiLFlhKTtwKCJhZWgiLG9iKTtiYT1oLmEoIiIpP1RhOlVhO3AoInFzIixiYSk7cCgic2V0Q29udGludWVDYiIsUmEpO3AoInBjIixTYSk7cCgiYnN5Iix2Yik7aC5kPWJiO2guaj11Yjt2YXIgeGI9e307di5iYXNlPXhiO3cucHVzaChbIm0iLHt1cmw6Ii8vc3NsLmdzdGF0aWMuY29tL2diL2pzL3NlbV82OGNjMDViZGNhZDIyMmMzNWYzZmNlYmRiN2VhZDYwZC5qcyJ9XSk7Zy5zZz17YzoiMSJ9O3AoIndnIix7cmc6e319KTt2YXIgeWI9e3RpdzpoLmMoIjE1MDAwIiwwKSx0aWU6aC5jKCIzMDAwMCIsMCl9O3Yud2c9eWI7dmFyIHpiPXt0aGk6aC5jKCIxMDAwMCIsMCksdGhwOmguYygiMTgwMDAwIiwwKSx0aG86aC5jKCI1MDAwIiwwKSx0ZXQ6aC5iKCIwLjUiLDApfTt2LndtPXpiO2lmKGguYSgiMSIpKXt2YXIgQWI9aC5hKCIiKTt3LnB1c2goWyJnYyIse2F1dG86QWIsdXJsOiIvL3NzbC5nc3RhdGljLmNvbS9nYi9qcy9hYmMvZ2NpXzkxZjMwNzU1ZDZhNmI3ODdkY2MyYTQwNjJlNmU5ODI0LmpzIixsaWJzOiJnb29nbGVhcGlzLmNsaWVudDpwbHVzb25lOmdhcGkuaWZyYW1lcyJ9XSk7dmFyIEJiPXt2ZXJzaW9uOiJnY2lfOTFmMzA3NTVkNmE2Yjc4N2RjYzJhNDA2MmU2ZTk4MjQuanMiLGluZGV4OiIiLGxhbmc6ImVuIn07di5nYz1CYjt2YXIgQ2I9ZnVuY3Rpb24oYSl7d2luZG93Lmdvb2dsZWFwaXMmJndpbmRvdy5pZnJhbWVzP2EmJmEoKTooYSYmdGEoYSksRCgiZ2MiKSl9O3AoImxHQyIsQ2IpO2guYSgiMSIpJiZwKCJsUFdGIixDYil9O3dpbmRvdy5fX1BWVD0iIjtpZihoLmEoIjEiKSYmaC5hKCIxIikpe3ZhciBEYj1mdW5jdGlvbihhKXtDYihmdW5jdGlvbigpe0EoInB3IixhKTtEKCJwdyIpfSl9O3AoImxQVyIsRGIpO3cucHVzaChbInB3Iix7dXJsOiIvL3NzbC5nc3RhdGljLmNvbS9nYi9qcy9hYmMvcHdtXzQ1ZjczZTRkZjA3YTBlMzg4YjBmYTFmM2QzMGU3MjgwLmpzIn1dKTt2YXIgRmI9W10sR2I9ZnVuY3Rpb24oYSl7RmJbMF09YX0sSGI9ZnVuY3Rpb24oYSxiKXtiPWJ8fHt9O2IuX3NuPSJwdyI7dChhLGIpfSxJYj17c2lnbmVkOkZiLGVsb2c6SGIsYmFzZToiaHR0cHM6Ly9wbHVzb25lLmdvb2dsZS5jb20vdS8wIixsb2FkVGltZToobmV3IERhdGUpLmdldFRpbWUoKX07di5wdz1JYjt2YXIgSmI9ZnVuY3Rpb24oYSxiKXt2YXIgYz1iLnNwbGl0KCIuIik7Yj1mdW5jdGlvbigpe3ZhciBtPWFyZ3VtZW50czthKGZ1bmN0aW9uKCl7Zm9yKHZhciBuPWcsbD0wLHE9Yy5sZW5ndGgtMTtsPHE7KytsKW49bltjW2xdXTtuW2NbbF1dLmFwcGx5KG4sbSl9KX07Zm9yKHZhciBkPWcsZj0wLGs9Yy5sZW5ndGgtMTtmPAprOysrZilkPWRbY1tmXV09ZFtjW2ZdXXx8e307cmV0dXJuIGRbY1tmXV09Yn07SmIoRGIsInB3LmNsayIpO0piKERiLCJwdy5odnIiKTtwKCJzdSIsR2IsZy5wdyl9O3ZhciBLYj1bMSwyLDMsNCw1LDYsOSwxMCwxMSwxMywxNCwyOCwyOSwzMCwzNCwzNSwzNywzOCwzOSw0MCw0MSw0Miw0Myw0OCw0OSw1MDBdO3ZhciBMYj1oLmIoIjAuMDAxIiwxRS00KSxNYj1oLmIoIjEiLDEpLE5iPSExLE9iPSExO2lmKGguYSgiMSIpKXt2YXIgUGI9TWF0aC5yYW5kb20oKTtQYjxMYiYmKE5iPSEwKTtQYjxNYiYmKE9iPSEwKX12YXIgUj1udWxsOwpmdW5jdGlvbiBRYihhLGIpe3ZhciBjPUxiLGQ9TmI7dmFyIGY9YTtpZighUil7Uj17fTtmb3IodmFyIGs9MDtrPEtiLmxlbmd0aDtrKyspe3ZhciBtPUtiW2tdO1JbbV09ITB9fWlmKGY9ISFSW2ZdKWM9TWIsZD1PYjtpZihkKXtkPWVuY29kZVVSSUNvbXBvbmVudDtpZihnLnJwKXt2YXIgbj1nLnJwKCk7bj0iLTEiIT1uP246IiJ9ZWxzZSBuPSIiO2Y9KG5ldyBEYXRlKS5nZXRUaW1lKCk7az1kKCIyODgzNCIpO209ZCgickM4Slg3bVNCLWFhX1FhMW5ZR0FDdyIpO3ZhciBsPWcuYnYuZixxPWQoIjEiKTtuPWQobik7Yz1NYXRoLnJvdW5kKDEvYyk7dmFyIEU9ZCgiMzE5NzAzNzQ0LjAiKSxVPSImb2dndj0iK2QoImVzX3BsdXNvbmVfZ2NfMjAyMDA2MDEuMF9wMCIpLEk9ZCgiY29tIiksVj1kKCJlbiIpLFc9CmQoIlVTQSIpO3ZhciB5PTA7aC5hKCIiKSYmKHl8PTEpO2guYSgiIikmJih5fD0yKTtoLmEoIiIpJiYoeXw9NCk7YT1bIi8vd3d3Lmdvb2dsZS5jb20vZ2VuXzIwND9hdHlwPWkmeng9IixmLCImb2dlPSIsYSwiJm9nZXg9IixrLCImb2dldj0iLG0sIiZvZ2Y9IixsLCImb2dwPSIscSwiJm9ncnA9IixuLCImb2dzcj0iLGMsIiZvZ3Y9IixFLFUsIiZvZ2Q9IixJLCImb2dsPSIsViwiJm9nYz0iLFcsIiZvZ3VzPSIseV07aWYoYil7Im9ndyJpbiBiJiYoYS5wdXNoKCImb2d3PSIrYi5vZ3cpLGRlbGV0ZSBiLm9ndyk7Zj1bXTtmb3IoeiBpbiBiKTAhPWYubGVuZ3RoJiZmLnB1c2goIiwiKSxmLnB1c2goUmIoeikpLGYucHVzaCgiLiIpLGYucHVzaChSYihiW3pdKSk7dmFyIHo9Zi5qb2luKCIiKTsiIiE9eiYmKGEucHVzaCgiJm9nYWQ9IiksYS5wdXNoKGQoeikpKX1rYShhLmpvaW4oIiIpKX19CmZ1bmN0aW9uIFJiKGEpeyJudW1iZXIiPT10eXBlb2YgYSYmKGErPSIiKTtyZXR1cm4ic3RyaW5nIj09dHlwZW9mIGE/YS5yZXBsYWNlKCIuIiwiJTJFIikucmVwbGFjZSgiLCIsIiUyQyIpOmF9aGE9UWI7cCgiaWwiLGhhLHUpO3ZhciBTYj17fTt2LmlsPVNiO3ZhciBUYj1mdW5jdGlvbihhLGIsYyxkLGYsayxtLG4sbCxxKXtCKGZ1bmN0aW9uKCl7Zy5wYWEoYSxiLGMsZCxmLGssbSxuLGwscSl9KX0sVWI9ZnVuY3Rpb24oKXtCKGZ1bmN0aW9uKCl7Zy5wcm0oKX0pfSxWYj1mdW5jdGlvbihhKXtCKGZ1bmN0aW9uKCl7Zy5zcG4oYSl9KX0sV2I9ZnVuY3Rpb24oYSl7QihmdW5jdGlvbigpe2cuc3BzKGEpfSl9LFhiPWZ1bmN0aW9uKGEpe0IoZnVuY3Rpb24oKXtnLnNwcChhKX0pfSxZYj17IjI3IjoiaHR0cHM6Ly9saDMuZ29vZ2xldXNlcmNvbnRlbnQuY29tL29ndy9kZWZhdWx0LXVzZXI9czI0IiwiMjciOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vb2d3L2RlZmF1bHQtdXNlcj1zMjQiLCIyNyI6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9vZ3cvZGVmYXVsdC11c2VyPXMyNCJ9LFpiPWZ1bmN0aW9uKGEpe3JldHVybihhPVliW2FdKXx8Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9vZ3cvZGVmYXVsdC11c2VyPXMyNCJ9LAokYj1mdW5jdGlvbigpe0IoZnVuY3Rpb24oKXtnLnNwZCgpfSl9O3AoInNwbiIsVmIpO3AoInNwcCIsWGIpO3AoInNwcyIsV2IpO3AoInNwZCIsJGIpO3AoInBhYSIsVGIpO3AoInBybSIsVWIpO21iKCJnYmQ0IixVYik7CmlmKGguYSgiIikpe3ZhciBhYz17ZDpoLmEoIiIpLGU6IiIsc2FudzpoLmEoIiIpLHA6Imh0dHBzOi8vbGgzLmdvb2dsZXVzZXJjb250ZW50LmNvbS9vZ3cvZGVmYXVsdC11c2VyPXM5NiIsY3A6IjEiLHhwOmguYSgiMSIpLG1nOiIlMSRzIChkZWxlZ2F0ZWQpIixtZDoiJTEkcyAoZGVmYXVsdCkiLG1oOiIyMjAiLHM6IjEiLHBwOlpiLHBwbDpoLmEoIiIpLHBwYTpoLmEoIiIpLApwcG06Ikdvb2dsZSsgcGFnZSJ9O3YucHJmPWFjfTt2YXIgUyxiYyxULGNjLFg9MCxkYz1mdW5jdGlvbihhLGIsYyl7aWYoYS5pbmRleE9mKXJldHVybiBhLmluZGV4T2YoYixjKTtpZihBcnJheS5pbmRleE9mKXJldHVybiBBcnJheS5pbmRleE9mKGEsYixjKTtmb3IoYz1udWxsPT1jPzA6MD5jP01hdGgubWF4KDAsYS5sZW5ndGgrYyk6YztjPGEubGVuZ3RoO2MrKylpZihjIGluIGEmJmFbY109PT1iKXJldHVybiBjO3JldHVybi0xfSxZPWZ1bmN0aW9uKGEsYil7cmV0dXJuLTE9PWRjKGEsWCk/KHIoRXJyb3IoWCsiXyIrYiksInVwIiwiY2FhIiksITEpOiEwfSxmYz1mdW5jdGlvbihhLGIpe1koWzEsMl0sInIiKSYmKFNbYV09U1thXXx8W10sU1thXS5wdXNoKGIpLDI9PVgmJndpbmRvdy5zZXRUaW1lb3V0KGZ1bmN0aW9uKCl7YihlYyhhKSl9LDApKX0sZ2M9ZnVuY3Rpb24oYSxiLGMpe2lmKFkoWzFdLCJuYXAiKSYmYyl7Zm9yKHZhciBkPTA7ZDxjLmxlbmd0aDtkKyspYmNbY1tkXV09ITA7Zy51cC5zcGwoYSxiLCJuYXAiLGMpfX0saGM9CmZ1bmN0aW9uKGEsYixjKXtpZihZKFsxXSwiYW9wIikmJmMpe2lmKFQpZm9yKHZhciBkIGluIFQpVFtkXT1UW2RdJiYtMSE9ZGMoYyxkKTtlbHNlIGZvcihUPXt9LGQ9MDtkPGMubGVuZ3RoO2QrKylUW2NbZF1dPSEwO2cudXAuc3BsKGEsYiwiYW9wIixjKX19LGljPWZ1bmN0aW9uKCl7dHJ5e2lmKFg9MiwhY2Mpe2NjPSEwO2Zvcih2YXIgYSBpbiBTKWZvcih2YXIgYj1TW2FdLGM9MDtjPGIubGVuZ3RoO2MrKyl0cnl7YltjXShlYyhhKSl9Y2F0Y2goZCl7cihkLCJ1cCIsInRwIil9fX1jYXRjaChkKXtyKGQsInVwIiwibXRwIil9fSxlYz1mdW5jdGlvbihhKXtpZihZKFsyXSwic3NwIikpe3ZhciBiPSFiY1thXTtUJiYoYj1iJiYhIVRbYV0pO3JldHVybiBifX07Y2M9ITE7Uz17fTtiYz17fTtUPW51bGw7WD0xOwp2YXIgamM9ZnVuY3Rpb24oYSl7dmFyIGI9ITE7dHJ5e2I9YS5jb29raWUmJmEuY29va2llLm1hdGNoKCJQUkVGIil9Y2F0Y2goYyl7fXJldHVybiFifSxrYz1mdW5jdGlvbigpe3RyeXtyZXR1cm4hIWUubG9jYWxTdG9yYWdlJiYib2JqZWN0Ij09dHlwZW9mIGUubG9jYWxTdG9yYWdlfWNhdGNoKGEpe3JldHVybiExfX0sbGM9ZnVuY3Rpb24oYSl7cmV0dXJuIGEmJmEuc3R5bGUmJmEuc3R5bGUuYmVoYXZpb3ImJiJ1bmRlZmluZWQiIT10eXBlb2YgYS5sb2FkfSxtYz1mdW5jdGlvbihhLGIsYyxkKXt0cnl7amMoZG9jdW1lbnQpfHwoZHx8KGI9Im9nLXVwLSIrYiksa2MoKT9lLmxvY2FsU3RvcmFnZS5zZXRJdGVtKGIsYyk6bGMoYSkmJihhLnNldEF0dHJpYnV0ZShiLGMpLGEuc2F2ZShhLmlkKSkpfWNhdGNoKGYpe2YuY29kZSE9RE9NRXhjZXB0aW9uLlFVT1RBX0VYQ0VFREVEX0VSUiYmcihmLCJ1cCIsInNwZCIpfX0sbmM9ZnVuY3Rpb24oYSxiLGMpe3RyeXtpZihqYyhkb2N1bWVudCkpcmV0dXJuIiI7CmN8fChiPSJvZy11cC0iK2IpO2lmKGtjKCkpcmV0dXJuIGUubG9jYWxTdG9yYWdlLmdldEl0ZW0oYik7aWYobGMoYSkpcmV0dXJuIGEubG9hZChhLmlkKSxhLmdldEF0dHJpYnV0ZShiKX1jYXRjaChkKXtkLmNvZGUhPURPTUV4Y2VwdGlvbi5RVU9UQV9FWENFRURFRF9FUlImJnIoZCwidXAiLCJncGQiKX1yZXR1cm4iIn0sb2M9ZnVuY3Rpb24oYSxiLGMpe2EuYWRkRXZlbnRMaXN0ZW5lcj9hLmFkZEV2ZW50TGlzdGVuZXIoYixjLCExKTphLmF0dGFjaEV2ZW50JiZhLmF0dGFjaEV2ZW50KCJvbiIrYixjKX0scGM9ZnVuY3Rpb24oYSl7Zm9yKHZhciBiPTAsYztjPWFbYl07YisrKXt2YXIgZD1nLnVwO2M9YyBpbiBkJiZkW2NdO2lmKCFjKXJldHVybiExfXJldHVybiEwfSxxYz1mdW5jdGlvbihhLGIpe3RyeXtpZihqYyhhKSlyZXR1cm4tMTt2YXIgYz1hLmNvb2tpZS5tYXRjaCgvT0dQQz0oW147XSopLyk7aWYoYyYmY1sxXSl7dmFyIGQ9Y1sxXS5tYXRjaChuZXcgUmVnRXhwKCJcXGIiKwpiKyItKFswLTldKyk6IikpO2lmKGQmJmRbMV0pcmV0dXJuIHBhcnNlSW50KGRbMV0sMTApfX1jYXRjaChmKXtmLmNvZGUhPURPTUV4Y2VwdGlvbi5RVU9UQV9FWENFRURFRF9FUlImJnIoZiwidXAiLCJnY2MiKX1yZXR1cm4tMX07cCgidXAiLHtyOmZjLG5hcDpnYyxhb3A6aGMsdHA6aWMsc3NwOmVjLHNwZDptYyxncGQ6bmMsYWVoOm9jLGFhbDpwYyxnY2M6cWN9KTt2YXIgWj1mdW5jdGlvbihhLGIpe2FbYl09ZnVuY3Rpb24oYyl7dmFyIGQ9YXJndW1lbnRzO2cucW0oZnVuY3Rpb24oKXthW2JdLmFwcGx5KHRoaXMsZCl9KX19O1ooZy51cCwic2wiKTtaKGcudXAsInNpIik7WihnLnVwLCJzcGwiKTtaKGcudXAsImRwYyIpO1ooZy51cCwiaWljIik7Zy5tY2YoInVwIix7c3A6aC5iKCIwLjAxIiwxKSx0bGQ6ImNvbSIscHJpZDoiMSJ9KTtmdW5jdGlvbiByYygpe2Z1bmN0aW9uIGEoKXtmb3IodmFyIGw7KGw9a1ttKytdKSYmIm0iIT1sWzBdJiYhbFsxXS5hdXRvOyk7bCYmKHNhKDIsbFswXSksbFsxXS51cmwmJnJhKGxbMV0udXJsLGxbMF0pLGxbMV0ubGlicyYmQyYmQyhsWzFdLmxpYnMpKTttPGsubGVuZ3RoJiZzZXRUaW1lb3V0KGEsMCl9ZnVuY3Rpb24gYigpezA8Zi0tP3NldFRpbWVvdXQoYiwwKTphKCl9dmFyIGM9aC5hKCIxIiksZD1oLmEoIiIpLGY9MyxrPXcsbT0wLG49d2luZG93LmdiYXJPblJlYWR5O2lmKG4pdHJ5e24oKX1jYXRjaChsKXtyKGwsIm1sIiwib3IiKX1kP3AoImxkYiIsYSk6Yz9jYSh3aW5kb3csImxvYWQiLGIpOmIoKX1wKCJyZGwiLHJjKTt9Y2F0Y2goZSl7d2luZG93LmdiYXImJmdiYXIubG9nZ2VyJiZnYmFyLmxvZ2dlci5tbChlLHsiX3NuIjoiY2ZnLmluaXQifSk7fX0pKCk7CihmdW5jdGlvbigpe3RyeXsvKgoKIENvcHlyaWdodCBUaGUgQ2xvc3VyZSBMaWJyYXJ5IEF1dGhvcnMuCiBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMAoqLwp2YXIgYT13aW5kb3cuZ2JhcjthLm1jZigicG0iLHtwOiIifSk7fWNhdGNoKGUpe3dpbmRvdy5nYmFyJiZnYmFyLmxvZ2dlciYmZ2Jhci5sb2dnZXIubWwoZSx7Il9zbiI6ImNmZy5pbml0In0pO319KSgpOwooZnVuY3Rpb24oKXt0cnl7LyoKCiBDb3B5cmlnaHQgVGhlIENsb3N1cmUgTGlicmFyeSBBdXRob3JzLgogU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjAKKi8KdmFyIGE9d2luZG93LmdiYXI7YS5tY2YoIm1tIix7czoiMSJ9KTt9Y2F0Y2goZSl7d2luZG93LmdiYXImJmdiYXIubG9nZ2VyJiZnYmFyLmxvZ2dlci5tbChlLHsiX3NuIjoiY2ZnLmluaXQifSk7fX0pKCk7CihmdW5jdGlvbigpe3RyeXsvKgoKIENvcHlyaWdodCBUaGUgQ2xvc3VyZSBMaWJyYXJ5IEF1dGhvcnMuCiBTUERYLUxpY2Vuc2UtSWRlbnRpZmllcjogQXBhY2hlLTIuMAoqLwp2YXIgZD13aW5kb3cuZ2Jhci5pLmk7dmFyIGU9d2luZG93LmdiYXI7dmFyIGY9ZS5pO3ZhciBnPWYuYygiMSIsMCksaD0vXGJnYm10XGIvLGs9ZnVuY3Rpb24oYSl7dHJ5e3ZhciBiPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJnYl8iK2cpLGM9ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoImdiXyIrYSk7YiYmZi5sKGIsaC50ZXN0KGIuY2xhc3NOYW1lKT8iZ2JtMGwiOiJnYnowbCIpO2MmJmYuayhjLGgudGVzdChjLmNsYXNzTmFtZSk/ImdibTBsIjoiZ2J6MGwiKX1jYXRjaChsKXtkKGwsInNqIiwic3NwIil9Zz1hfSxtPWUucXMsbj1mdW5jdGlvbihhKXt2YXIgYj1hLmhyZWY7dmFyIGM9d2luZG93LmxvY2F0aW9uLmhyZWYubWF0Y2goLy4qPzpcL1wvW15cL10qLylbMF07Yz1uZXcgUmVnRXhwKCJeIitjKyIvc2VhcmNoXFw/Iik7KGI9Yy50ZXN0KGIpKSYmIS8oXnxcXD98JillaT0vLnRlc3QoYS5ocmVmKSYmKGI9d2luZG93Lmdvb2dsZSkmJmIua0VYUEkmJihhLmhyZWYrPSImZWk9IitiLmtFSSl9LHA9ZnVuY3Rpb24oYSl7bShhKTsKbihhKX0scT1mdW5jdGlvbigpe2lmKHdpbmRvdy5nb29nbGUmJndpbmRvdy5nb29nbGUuc24pe3ZhciBhPS8uKmhwJC87cmV0dXJuIGEudGVzdCh3aW5kb3cuZ29vZ2xlLnNuKT8iIjoiMSJ9cmV0dXJuIi0xIn07ZS5ycD1xO2Uuc2xwPWs7ZS5xcz1wO2UucXNpPW47fWNhdGNoKGUpe3dpbmRvdy5nYmFyJiZnYmFyLmxvZ2dlciYmZ2Jhci5sb2dnZXIubWwoZSx7Il9zbiI6ImNmZy5pbml0In0pO319KSgpOwooZnVuY3Rpb24oKXt0cnl7LyoKCiBDb3B5cmlnaHQgVGhlIENsb3N1cmUgTGlicmFyeSBBdXRob3JzLgogU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjAKKi8KdmFyIGE9dGhpc3x8c2VsZjt2YXIgYj13aW5kb3cuZ2Jhcjt2YXIgYz1iLmk7dmFyIGQ9Yy5hLGU9Yy5jLGY9e2N0eToiVVNBIixjdjoiMzE5NzAzNzQ0IixkYmc6ZCgiIiksZWN2OiIwIixlaTplKCJyQzhKWDdtU0ItYWFfUWExbllHQUN3IiksZWxlOmQoIjEiKSxlc3I6ZSgiMC4xIiksZXZ0czpbIm1vdXNlZG93biIsInRvdWNoc3RhcnQiLCJ0b3VjaG1vdmUiLCJ3aGVlbCIsImtleWRvd24iXSxnYmw6ImVzX3BsdXNvbmVfZ2NfMjAyMDA2MDEuMF9wMCIsaGQ6ImNvbSIsaGw6ImVuIixpcnA6ZCgiIikscGlkOmUoIjEiKSwKc25pZDplKCIyODgzNCIpLHRvOmUoIjMwMDAwMCIpLHU6ZSgiIiksdmY6Ii42Ni40MS4ifSxnPWYsaD1bImJuZGNmZyJdLGs9YTtoWzBdaW4ga3x8InVuZGVmaW5lZCI9PXR5cGVvZiBrLmV4ZWNTY3JpcHR8fGsuZXhlY1NjcmlwdCgidmFyICIraFswXSk7Zm9yKHZhciBsO2gubGVuZ3RoJiYobD1oLnNoaWZ0KCkpOyloLmxlbmd0aHx8dm9pZCAwPT09Zz9rPWtbbF0mJmtbbF0hPT1PYmplY3QucHJvdG90eXBlW2xdP2tbbF06a1tsXT17fTprW2xdPWc7fWNhdGNoKGUpe3dpbmRvdy5nYmFyJiZnYmFyLmxvZ2dlciYmZ2Jhci5sb2dnZXIubWwoZSx7Il9zbiI6ImNmZy5pbml0In0pO319KSgpOwooZnVuY3Rpb24oKXt0cnl7LyoKCiBDb3B5cmlnaHQgVGhlIENsb3N1cmUgTGlicmFyeSBBdXRob3JzLgogU1BEWC1MaWNlbnNlLUlkZW50aWZpZXI6IEFwYWNoZS0yLjAKKi8Kd2luZG93LmdiYXIucmRsKCk7fWNhdGNoKGUpe3dpbmRvdy5nYmFyJiZnYmFyLmxvZ2dlciYmZ2Jhci5sb2dnZXIubWwoZSx7Il9zbiI6ImNmZy5pbml0In0pO319KSgpOwo8L3NjcmlwdD48L2hlYWQ+PGJvZHkgYmdjb2xvcj0iI2ZmZiI+PHNjcmlwdCBub25jZT0iWjByd3R4TU50TXRqNUdsSXZ5cmQyZz09Ij4oZnVuY3Rpb24oKXt2YXIgc3JjPScvaW1hZ2VzL25hdl9sb2dvMjI5LnBuZyc7dmFyIGllc2c9ZmFsc2U7ZG9jdW1lbnQuYm9keS5vbmxvYWQgPSBmdW5jdGlvbigpe3dpbmRvdy5uICYmIHdpbmRvdy5uKCk7aWYgKGRvY3VtZW50LmltYWdlcyl7bmV3IEltYWdlKCkuc3JjPXNyYzt9CmlmICghaWVzZyl7ZG9jdW1lbnQuZiYmZG9jdW1lbnQuZi5xLmZvY3VzKCk7ZG9jdW1lbnQuZ2JxZiYmZG9jdW1lbnQuZ2JxZi5xLmZvY3VzKCk7fQp9Cn0pKCk7PC9zY3JpcHQ+PGRpdiBpZD0ibW5nYiI+PGRpdiBpZD1nYj48c2NyaXB0PndpbmRvdy5nYmFyJiZnYmFyLmVsaSYmZ2Jhci5lbGkoKTwvc2NyaXB0PjxkaXYgaWQ9Z2J3PjxkaXYgaWQ9Z2J6PjxzcGFuIGNsYXNzPWdidGNiPjwvc3Bhbj48b2wgaWQ9Z2J6YyBjbGFzcz1nYnRjPjxsaSBjbGFzcz1nYnQ+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0OjF9KTsgY2xhc3M9ImdienQgZ2J6MGwgZ2JwMSIgaWQ9Z2JfMSBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL3dlYmhwP3RhYj13dyI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+U2VhcmNoPC9zcGFuPjwvYT48L2xpPjxsaSBjbGFzcz1nYnQ+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0OjJ9KTsgY2xhc3M9Z2J6dCBpZD1nYl8yIGhyZWY9Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbS9pbWdocD9obD1lbiZ0YWI9d2kiPjxzcGFuIGNsYXNzPWdidGIyPjwvc3Bhbj48c3BhbiBjbGFzcz1nYnRzPkltYWdlczwvc3Bhbj48L2E+PC9saT48bGkgY2xhc3M9Z2J0PjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDo4fSk7IGNsYXNzPWdienQgaWQ9Z2JfOCBocmVmPSJodHRwOi8vbWFwcy5nb29nbGUuY29tL21hcHM/aGw9ZW4mdGFiPXdsIj48c3BhbiBjbGFzcz1nYnRiMj48L3NwYW4+PHNwYW4gY2xhc3M9Z2J0cz5NYXBzPC9zcGFuPjwvYT48L2xpPjxsaSBjbGFzcz1nYnQ+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0Ojc4fSk7IGNsYXNzPWdienQgaWQ9Z2JfNzggaHJlZj0iaHR0cHM6Ly9wbGF5Lmdvb2dsZS5jb20vP2hsPWVuJnRhYj13OCI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+UGxheTwvc3Bhbj48L2E+PC9saT48bGkgY2xhc3M9Z2J0PjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDozNn0pOyBjbGFzcz1nYnp0IGlkPWdiXzM2IGhyZWY9Imh0dHA6Ly93d3cueW91dHViZS5jb20vP2dsPVVTJnRhYj13MSI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+WW91VHViZTwvc3Bhbj48L2E+PC9saT48bGkgY2xhc3M9Z2J0PjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDo1fSk7IGNsYXNzPWdienQgaWQ9Z2JfNSBocmVmPSJodHRwOi8vbmV3cy5nb29nbGUuY29tL253c2hwP2hsPWVuJnRhYj13biI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+TmV3czwvc3Bhbj48L2E+PC9saT48bGkgY2xhc3M9Z2J0PjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDoyM30pOyBjbGFzcz1nYnp0IGlkPWdiXzIzIGhyZWY9Imh0dHBzOi8vbWFpbC5nb29nbGUuY29tL21haWwvP3RhYj13bSI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+R21haWw8L3NwYW4+PC9hPjwvbGk+PGxpIGNsYXNzPWdidD48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6NDl9KTsgY2xhc3M9Z2J6dCBpZD1nYl80OSBocmVmPSJodHRwczovL2RyaXZlLmdvb2dsZS5jb20vP3RhYj13byI+PHNwYW4gY2xhc3M9Z2J0YjI+PC9zcGFuPjxzcGFuIGNsYXNzPWdidHM+RHJpdmU8L3NwYW4+PC9hPjwvbGk+PGxpIGNsYXNzPWdidD48YSBjbGFzcz1nYmd0IGlkPWdienRtIGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vaW50bC9lbi9hYm91dC9wcm9kdWN0cz90YWI9d2giIG9uY2xpY2s9ImdiYXIudGcoZXZlbnQsdGhpcykiIGFyaWEtaGFzcG9wdXA9dHJ1ZSBhcmlhLW93bnM9Z2JkPjxzcGFuIGNsYXNzPWdidGIyPjwvc3Bhbj48c3BhbiBpZD1nYnp0bXMgY2xhc3M9ImdidHMgZ2J0c2EiPjxzcGFuIGlkPWdienRtczE+TW9yZTwvc3Bhbj48c3BhbiBjbGFzcz1nYm1hPjwvc3Bhbj48L3NwYW4+PC9hPjxkaXYgY2xhc3M9Z2JtIGlkPWdiZCBhcmlhLW93bmVyPWdienRtPjxkaXYgaWQ9Z2JtbWIgY2xhc3M9ImdibWMgZ2JzYiBnYnNiaXMiPjxvbCBpZD1nYm1tIGNsYXNzPSJnYm1jYyBnYnNiaWMiPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6MjR9KTsgY2xhc3M9Z2JtdCBpZD1nYl8yNCBocmVmPSJodHRwczovL3d3dy5nb29nbGUuY29tL2NhbGVuZGFyP3RhYj13YyI+Q2FsZW5kYXI8L2E+PC9saT48bGkgY2xhc3M9Z2JtdGM+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0OjUxfSk7IGNsYXNzPWdibXQgaWQ9Z2JfNTEgaHJlZj0iaHR0cDovL3RyYW5zbGF0ZS5nb29nbGUuY29tLz9obD1lbiZ0YWI9d1QiPlRyYW5zbGF0ZTwvYT48L2xpPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6MTd9KTsgY2xhc3M9Z2JtdCBpZD1nYl8xNyBocmVmPSJodHRwOi8vd3d3Lmdvb2dsZS5jb20vbW9iaWxlLz9obD1lbiZ0YWI9d0QiPk1vYmlsZTwvYT48L2xpPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6MTB9KTsgY2xhc3M9Z2JtdCBpZD1nYl8xMCBocmVmPSJodHRwczovL2Jvb2tzLmdvb2dsZS5jb20vYmtzaHA/aGw9ZW4mdGFiPXdwIj5Cb29rczwvYT48L2xpPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6Nn0pOyBjbGFzcz1nYm10IGlkPWdiXzYgaHJlZj0iaHR0cHM6Ly93d3cuZ29vZ2xlLmNvbS9zaG9wcGluZz9obD1lbiZzb3VyY2U9b2cmdGFiPXdmIj5TaG9wcGluZzwvYT48L2xpPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6MzB9KTsgY2xhc3M9Z2JtdCBpZD1nYl8zMCBocmVmPSJodHRwOi8vd3d3LmJsb2dnZXIuY29tLz90YWI9d2oiPkJsb2dnZXI8L2E+PC9saT48bGkgY2xhc3M9Z2JtdGM+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0OjI3fSk7IGNsYXNzPWdibXQgaWQ9Z2JfMjcgaHJlZj0iaHR0cDovL3d3dy5nb29nbGUuY29tL2ZpbmFuY2U/dGFiPXdlIj5GaW5hbmNlPC9hPjwvbGk+PGxpIGNsYXNzPWdibXRjPjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDozMX0pOyBjbGFzcz1nYm10IGlkPWdiXzMxIGhyZWY9Imh0dHBzOi8vcGhvdG9zLmdvb2dsZS5jb20vP3RhYj13cSZwYWdlSWQ9bm9uZSI+UGhvdG9zPC9hPjwvbGk+PGxpIGNsYXNzPWdibXRjPjxhIG9uY2xpY2s9Z2Jhci5sb2dnZXIuaWwoMSx7dDoxMn0pOyBjbGFzcz1nYm10IGlkPWdiXzEyIGhyZWY9Imh0dHA6Ly92aWRlby5nb29nbGUuY29tLz9obD1lbiZ0YWI9d3YiPlZpZGVvczwvYT48L2xpPjxsaSBjbGFzcz1nYm10Yz48YSBvbmNsaWNrPWdiYXIubG9nZ2VyLmlsKDEse3Q6MjV9KTsgY2xhc3M9Z2JtdCBpZD1nYl8yNSBocmVmPSJodHRwczovL2RvY3MuZ29vZ2xlLmNvbS9kb2N1bWVudC8/dXNwPWRvY3NfYWxjIj5Eb2NzPC9hPjwvbGk+PGxpIGNsYXNzPWdibXRjPjxkaXYgY2xhc3M9ImdibXQgZ2JtaCI+PC9kaXY+PC9saT48bGkgY2xhc3M9Z2JtdGM+PGEgb25jbGljaz1nYmFyLmxvZ2dlci5pbCgxLHt0OjY2fSk7IGhyZWY9Imh0dHBzOi8vd3d3Lmdvb2dsZS5jb20vaW50bC9lbi9hYm91dC9wcm9kdWN0cz90YWI9d2giIGNsYXNzPWdibXQ+RXZlbiBtb3JlICZyYXF1bzs8L2E+PC9saT48L29sPjxkaXYgY2xhc3M9Z2JzYnQ+PC9kaXY+PGRpdiBjbGFzcz1nYnNiYj48L2Rpdj48L2Rpdj48L2Rpdj48L2xpPjwvb2w+PC9kaXY+PGRpdiBpZD1nYmc+PGgyIGNsYXNzPWdieHg+QWNjb3VudCBPcHRpb25zPC9oMj48c3BhbiBjbGFzcz1nYnRjYj48L3NwYW4+PG9sIGNsYXNzPWdidGM+PGxpIGNsYXNzPWdidD48YSB0YXJnZXQ9X3RvcCBocmVmPSJodHRwczovL2FjY291bnRzLmdvb2dsZS5jb20vU2VydmljZUxvZ2luP2hsPWVuJnBhc3NpdmU9dHJ1ZSZjb250aW51ZT1odHRwOi8vd3d3Lmdvb2dsZS5jb20vIiBvbmNsaWNrPSJnYmFyLmxvZ2dlci5pbCg5LHtsOidpJ30pIiBpZD1nYl83MCBjbGFzcz1nYmd0PjxzcGFuIGNsYXNzPWdidGIyPjwvc3Bhbj48c3BhbiBpZD1nYmdzNCBjbGFzcz1nYnRzPjxzcGFuIGlkPWdiaTRzMT5TaWduIGluPC9zcGFuPjwvc3Bhbj48L2E+PC9saT48bGkgY2xhc3M9ImdidCBnYnRiIj48c3BhbiBjbGFzcz1nYnRzPjwvc3Bhbj48L2xpPjxsaSBjbGFzcz1nYnQ+PGEgY2xhc3M9Z2JndCBpZD1nYmc1IGhyZWY9Imh0dHA6Ly93d3cuZ29vZ2xlLmNvbS9wcmVmZXJlbmNlcz9obD1lbiIgdGl0bGU9Ik9wdGlvbnMiIG9uY2xpY2s9ImdiYXIudGcoZXZlbnQsdGhpcykiIGFyaWEtaGFzcG9wdXA9dHJ1ZSBhcmlhLW93bnM9Z2JkNT48c3BhbiBjbGFzcz1nYnRiMj48L3NwYW4+PHNwYW4gaWQ9Z2JnczUgY2xhc3M9Z2J0cz48c3BhbiBpZD1nYmk1Pjwvc3Bhbj48L3NwYW4+PC9hPjxkaXYgY2xhc3M9Z2JtIGlkPWdiZDUgYXJpYS1vd25lcj1nYmc1PjxkaXYgY2xhc3M9Z2JtYz48b2wgaWQ9Z2JvbSBjbGFzcz1nYm1jYz48bGkgY2xhc3M9Imdia2MgZ2JtdGMiPjxhICBjbGFzcz1nYm10IGhyZWY9Ii9wcmVmZXJlbmNlcz9obD1lbiI+U2VhcmNoIHNldHRpbmdzPC9hPjwvbGk+PGxpIGNsYXNzPWdibXRjPjxkaXYgY2xhc3M9ImdibXQgZ2JtaCI+PC9kaXY+PC9saT48bGkgY2xhc3M9Imdia3AgZ2JtdGMiPjxhIGNsYXNzPWdibXQgaHJlZj0iaHR0cDovL3d3dy5nb29nbGUuY29tL2hpc3Rvcnkvb3B0b3V0P2hsPWVuIj5XZWIgSGlzdG9yeTwvYT48L2xpPjwvb2w+PC9kaXY+PC9kaXY+PC9saT48L29sPjwvZGl2PjwvZGl2PjxkaXYgaWQ9Z2J4Mz48L2Rpdj48ZGl2IGlkPWdieDQ+PC9kaXY+PHNjcmlwdD53aW5kb3cuZ2JhciYmZ2Jhci5lbHAmJmdiYXIuZWxwKCk8L3NjcmlwdD48L2Rpdj48L2Rpdj48Y2VudGVyPjxiciBjbGVhcj0iYWxsIiBpZD0ibGdwZCI+PGRpdiBpZD0ibGdhIj48aW1nIGFsdD0iR29vZ2xlIiBoZWlnaHQ9IjkyIiBzcmM9Ii9pbWFnZXMvYnJhbmRpbmcvZ29vZ2xlbG9nby8xeC9nb29nbGVsb2dvX3doaXRlX2JhY2tncm91bmRfY29sb3JfMjcyeDkyZHAucG5nIiBzdHlsZT0icGFkZGluZzoyOHB4IDAgMTRweCIgd2lkdGg9IjI3MiIgaWQ9ImhwbG9nbyI+PGJyPjxicj48L2Rpdj48Zm9ybSBhY3Rpb249Ii9zZWFyY2giIG5hbWU9ImYiPjx0YWJsZSBjZWxscGFkZGluZz0iMCIgY2VsbHNwYWNpbmc9IjAiPjx0ciB2YWxpZ249InRvcCI+PHRkIHdpZHRoPSIyNSUiPiZuYnNwOzwvdGQ+PHRkIGFsaWduPSJjZW50ZXIiIG5vd3JhcD0iIj48aW5wdXQgdmFsdWU9ImVuIiBuYW1lPSJobCIgdHlwZT0iaGlkZGVuIj48aW5wdXQgbmFtZT0ic291cmNlIiB0eXBlPSJoaWRkZW4iIHZhbHVlPSJocCI+PGlucHV0IG5hbWU9ImJpdyIgdHlwZT0iaGlkZGVuIj48aW5wdXQgbmFtZT0iYmloIiB0eXBlPSJoaWRkZW4iPjxkaXYgY2xhc3M9ImRzIiBzdHlsZT0iaGVpZ2h0OjMycHg7bWFyZ2luOjRweCAwIj48aW5wdXQgY2xhc3M9ImxzdCIgc3R5bGU9Im1hcmdpbjowO3BhZGRpbmc6NXB4IDhweCAwIDZweDt2ZXJ0aWNhbC1hbGlnbjp0b3A7Y29sb3I6IzAwMCIgYXV0b2NvbXBsZXRlPSJvZmYiIHZhbHVlPSIiIHRpdGxlPSJHb29nbGUgU2VhcmNoIiBtYXhsZW5ndGg9IjIwNDgiIG5hbWU9InEiIHNpemU9IjU3Ij48L2Rpdj48YnIgc3R5bGU9ImxpbmUtaGVpZ2h0OjAiPjxzcGFuIGNsYXNzPSJkcyI+PHNwYW4gY2xhc3M9ImxzYmIiPjxpbnB1dCBjbGFzcz0ibHNiIiB2YWx1ZT0iR29vZ2xlIFNlYXJjaCIgbmFtZT0iYnRuRyIgdHlwZT0ic3VibWl0Ij48L3NwYW4+PC9zcGFuPjxzcGFuIGNsYXNzPSJkcyI+PHNwYW4gY2xhc3M9ImxzYmIiPjxpbnB1dCBjbGFzcz0ibHNiIiBpZD0idHN1aWQxIiB2YWx1ZT0iSSdtIEZlZWxpbmcgTHVja3kiIG5hbWU9ImJ0bkkiIHR5cGU9InN1Ym1pdCI+PHNjcmlwdCBub25jZT0iWjByd3R4TU50TXRqNUdsSXZ5cmQyZz09Ij4oZnVuY3Rpb24oKXt2YXIgaWQ9J3RzdWlkMSc7ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoaWQpLm9uY2xpY2sgPSBmdW5jdGlvbigpe2lmICh0aGlzLmZvcm0ucS52YWx1ZSl7dGhpcy5jaGVja2VkID0gMTtpZiAodGhpcy5mb3JtLmlmbHNpZyl0aGlzLmZvcm0uaWZsc2lnLmRpc2FibGVkID0gZmFsc2U7fQplbHNlIHRvcC5sb2NhdGlvbj0nL2Rvb2RsZXMvJzt9O30pKCk7PC9zY3JpcHQ+PGlucHV0IHZhbHVlPSJBSU5GQ2JZQUFBQUFYd2s5dkFZa3VSN0I1NktxaFNYVmkwU3FWbUlrcVB3ZCIgbmFtZT0iaWZsc2lnIiB0eXBlPSJoaWRkZW4iPjwvc3Bhbj48L3NwYW4+PC90ZD48dGQgY2xhc3M9ImZsIHNibGMiIGFsaWduPSJsZWZ0IiBub3dyYXA9IiIgd2lkdGg9IjI1JSI+PGEgaHJlZj0iL2FkdmFuY2VkX3NlYXJjaD9obD1lbiZhbXA7YXV0aHVzZXI9MCI+QWR2YW5jZWQgc2VhcmNoPC9hPjwvdGQ+PC90cj48L3RhYmxlPjxpbnB1dCBpZD0iZ2J2IiBuYW1lPSJnYnYiIHR5cGU9ImhpZGRlbiIgdmFsdWU9IjEiPjxzY3JpcHQgbm9uY2U9Ilowcnd0eE1OdE10ajVHbEl2eXJkMmc9PSI+KGZ1bmN0aW9uKCl7dmFyIGEsYj0iMSI7aWYoZG9jdW1lbnQmJmRvY3VtZW50LmdldEVsZW1lbnRCeUlkKWlmKCJ1bmRlZmluZWQiIT10eXBlb2YgWE1MSHR0cFJlcXVlc3QpYj0iMiI7ZWxzZSBpZigidW5kZWZpbmVkIiE9dHlwZW9mIEFjdGl2ZVhPYmplY3Qpe3ZhciBjLGQsZT1bIk1TWE1MMi5YTUxIVFRQLjYuMCIsIk1TWE1MMi5YTUxIVFRQLjMuMCIsIk1TWE1MMi5YTUxIVFRQIiwiTWljcm9zb2Z0LlhNTEhUVFAiXTtmb3IoYz0wO2Q9ZVtjKytdOyl0cnl7bmV3IEFjdGl2ZVhPYmplY3QoZCksYj0iMiJ9Y2F0Y2goaCl7fX1hPWI7aWYoIjIiPT1hJiYtMT09bG9jYXRpb24uc2VhcmNoLmluZGV4T2YoIiZnYnY9MiIpKXt2YXIgZj1nb29nbGUuZ2J2dSxnPWRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCJnYnYiKTtnJiYoZy52YWx1ZT1hKTtmJiZ3aW5kb3cuc2V0VGltZW91dChmdW5jdGlvbigpe2xvY2F0aW9uLmhyZWY9Zn0sMCl9O30pLmNhbGwodGhpcyk7PC9zY3JpcHQ+PC9mb3JtPjxkaXYgaWQ9ImdhY19zY29udCI+PC9kaXY+PGRpdiBzdHlsZT0iZm9udC1zaXplOjgzJTttaW4taGVpZ2h0OjMuNWVtIj48YnI+PC9kaXY+PHNwYW4gaWQ9ImZvb3RlciI+PGRpdiBzdHlsZT0iZm9udC1zaXplOjEwcHQiPjxkaXYgc3R5bGU9Im1hcmdpbjoxOXB4IGF1dG87dGV4dC1hbGlnbjpjZW50ZXIiIGlkPSJmbGwiPjxhIGhyZWY9Ii9pbnRsL2VuL2Fkcy8iPkFkdmVydGlzaW5nwqBQcm9ncmFtczwvYT48YSBocmVmPSIvc2VydmljZXMvIj5CdXNpbmVzcyBTb2x1dGlvbnM8L2E+PGEgaHJlZj0iL2ludGwvZW4vYWJvdXQuaHRtbCI+QWJvdXQgR29vZ2xlPC9hPjwvZGl2PjwvZGl2PjxwIHN0eWxlPSJmb250LXNpemU6OHB0O2NvbG9yOiM3Njc2NzYiPiZjb3B5OyAyMDIwIC0gPGEgaHJlZj0iL2ludGwvZW4vcG9saWNpZXMvcHJpdmFjeS8iPlByaXZhY3k8L2E+IC0gPGEgaHJlZj0iL2ludGwvZW4vcG9saWNpZXMvdGVybXMvIj5UZXJtczwvYT48L3A+PC9zcGFuPjwvY2VudGVyPjxzY3JpcHQgbm9uY2U9Ilowcnd0eE1OdE10ajVHbEl2eXJkMmc9PSI+KGZ1bmN0aW9uKCl7d2luZG93Lmdvb2dsZS5jZG89e2hlaWdodDowLHdpZHRoOjB9OyhmdW5jdGlvbigpe3ZhciBhPXdpbmRvdy5pbm5lcldpZHRoLGI9d2luZG93LmlubmVySGVpZ2h0O2lmKCFhfHwhYil7dmFyIGM9d2luZG93LmRvY3VtZW50LGQ9IkNTUzFDb21wYXQiPT1jLmNvbXBhdE1vZGU/Yy5kb2N1bWVudEVsZW1lbnQ6Yy5ib2R5O2E9ZC5jbGllbnRXaWR0aDtiPWQuY2xpZW50SGVpZ2h0fWEmJmImJihhIT1nb29nbGUuY2RvLndpZHRofHxiIT1nb29nbGUuY2RvLmhlaWdodCkmJmdvb2dsZS5sb2coIiIsIiIsIi9jbGllbnRfMjA0PyZhdHlwPWkmYml3PSIrYSsiJmJpaD0iK2IrIiZlaT0iK2dvb2dsZS5rRUkpO30pLmNhbGwodGhpcyk7fSkoKTsoZnVuY3Rpb24oKXt2YXIgdT0nL3hqcy9fL2pzL2tceDNkeGpzLmhwLmVuX1VTLjdkd3UxSmpYc1dVLk8vbVx4M2RzYl9oZSxkL2FtXHgzZEFFLXdPUS9kXHgzZDEvcnNceDNkQUNUOTBvRlV0X20yeW9TOEpLVXhwUk9ucjd1ZXZlOFNpQSc7CnNldFRpbWVvdXQoZnVuY3Rpb24oKXt2YXIgYj1kb2N1bWVudDt2YXIgYT0iU0NSSVBUIjsiYXBwbGljYXRpb24veGh0bWwreG1sIj09PWIuY29udGVudFR5cGUmJihhPWEudG9Mb3dlckNhc2UoKSk7YT1iLmNyZWF0ZUVsZW1lbnQoYSk7YS5zcmM9dTtnb29nbGUudGltZXJzJiZnb29nbGUudGltZXJzLmxvYWQmJmdvb2dsZS50aWNrJiZnb29nbGUudGljaygibG9hZCIsInhqc2xzIik7ZG9jdW1lbnQuYm9keS5hcHBlbmRDaGlsZChhKX0sMCk7fSkoKTsoZnVuY3Rpb24oKXt3aW5kb3cuZ29vZ2xlLnhqc3U9Jy94anMvXy9qcy9rXHgzZHhqcy5ocC5lbl9VUy43ZHd1MUpqWHNXVS5PL21ceDNkc2JfaGUsZC9hbVx4M2RBRS13T1EvZFx4M2QxL3JzXHgzZEFDVDkwb0ZVdF9tMnlvUzhKS1V4cFJPbnI3dWV2ZThTaUEnO30pKCk7ZnVuY3Rpb24gX0R1bXBFeGNlcHRpb24oZSl7dGhyb3cgZTt9CmZ1bmN0aW9uIF9GX2luc3RhbGxDc3MoYyl7fQooZnVuY3Rpb24oKXtnb29nbGUuamw9e2R3OmZhbHNlLGVtOltdLGVtdzpmYWxzZSxsbHM6J2RlZmF1bHQnLHBkdDowLHNuZXQ6dHJ1ZSx1d3A6dHJ1ZX07fSkoKTsoZnVuY3Rpb24oKXt2YXIgcG1jPSd7XHgyMmRceDIyOnt9LFx4MjJzYl9oZVx4MjI6e1x4MjJhZ2VuXHgyMjpmYWxzZSxceDIyY2dlblx4MjI6ZmFsc2UsXHgyMmNsaWVudFx4MjI6XHgyMmhlaXJsb29tLWhwXHgyMixceDIyZGhceDIyOnRydWUsXHgyMmRocXRceDIyOnRydWUsXHgyMmRzXHgyMjpceDIyXHgyMixceDIyZmZxbFx4MjI6XHgyMmVuXHgyMixceDIyZmxceDIyOnRydWUsXHgyMmhvc3RceDIyOlx4MjJnb29nbGUuY29tXHgyMixceDIyaXNiaFx4MjI6MjgsXHgyMmpzb25wXHgyMjp0cnVlLFx4MjJtc2dzXHgyMjp7XHgyMmNpYmxceDIyOlx4MjJDbGVhciBTZWFyY2hceDIyLFx4MjJkeW1ceDIyOlx4MjJEaWQgeW91IG1lYW46XHgyMixceDIybGNreVx4MjI6XHgyMklcXHUwMDI2IzM5O20gRmVlbGluZyBMdWNreVx4MjIsXHgyMmxtbFx4MjI6XHgyMkxlYXJuIG1vcmVceDIyLFx4MjJvc2t0XHgyMjpceDIySW5wdXQgdG9vbHNceDIyLFx4MjJwc3JjXHgyMjpceDIyVGhpcyBzZWFyY2ggd2FzIHJlbW92ZWQgZnJvbSB5b3VyIFxcdTAwM0NhIGhyZWZceDNkXFxceDIyL2hpc3RvcnlcXFx4MjJcXHUwMDNFV2ViIEhpc3RvcnlcXHUwMDNDL2FcXHUwMDNFXHgyMixceDIycHNybFx4MjI6XHgyMlJlbW92ZVx4MjIsXHgyMnNiaXRceDIyOlx4MjJTZWFyY2ggYnkgaW1hZ2VceDIyLFx4MjJzcmNoXHgyMjpceDIyR29vZ2xlIFNlYXJjaFx4MjJ9LFx4MjJvdnJceDIyOnt9LFx4MjJwcVx4MjI6XHgyMlx4MjIsXHgyMnJlZnBkXHgyMjp0cnVlLFx4MjJyZnNceDIyOltdLFx4MjJzYnBsXHgyMjoxNixceDIyc2Jwclx4MjI6MTYsXHgyMnNjZFx4MjI6MTAsXHgyMnN0b2tceDIyOlx4MjI0b3BaY1pwa0Z0d3NqR0hmUXVwd3JxbG5XckFceDIyLFx4MjJ1aGRlXHgyMjpmYWxzZX19Jztnb29nbGUucG1jPUpTT04ucGFyc2UocG1jKTt9KSgpOzwvc2NyaXB0PiAgICAgICAgPC9ib2R5PjwvaHRtbD4= 94 | recorded_at: Sat, 11 Jul 2020 03:19:08 GMT 95 | recorded_with: VCR 6.0.0 96 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_page_images/handles_unescaped_urls.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://test.com/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - test.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Server: 24 | - nginx/1.16.1 25 | Date: 26 | - Sat, 11 Jul 2020 03:19:00 GMT 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Keep-Alive: 34 | - timeout=20 35 | X-Dis-Request-Id: 36 | - a2838aa187355b40c449c1d4341cc5d8 37 | Location: 38 | - http://www.test.com/ 39 | body: 40 | encoding: UTF-8 41 | string: "301 Moved Permanently

301 42 | Moved Permanently

Object moved to here.


DOSarrest 43 | Internet Security
\n" 44 | recorded_at: Sat, 11 Jul 2020 03:19:00 GMT 45 | - request: 46 | method: get 47 | uri: http://www.test.com/ 48 | body: 49 | encoding: US-ASCII 50 | string: '' 51 | headers: 52 | User-Agent: 53 | - Mozilla/5.0 (Macintosh) 54 | Accept-Encoding: 55 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 56 | Accept: 57 | - "*/*" 58 | Host: 59 | - www.test.com 60 | response: 61 | status: 62 | code: 301 63 | message: Moved Permanently 64 | headers: 65 | Server: 66 | - nginx/1.16.1 67 | Date: 68 | - Sat, 11 Jul 2020 03:19:00 GMT 69 | Content-Type: 70 | - text/html; charset=UTF-8 71 | Transfer-Encoding: 72 | - chunked 73 | Connection: 74 | - keep-alive 75 | Keep-Alive: 76 | - timeout=20 77 | X-Dis-Request-Id: 78 | - 8b09747e816f16714b25596e80293886 79 | Location: 80 | - https://www.test.com/ 81 | body: 82 | encoding: UTF-8 83 | string: "301 Moved Permanently

301 84 | Moved Permanently

Object moved to here.


DOSarrest 85 | Internet Security
\n" 86 | recorded_at: Sat, 11 Jul 2020 03:19:01 GMT 87 | - request: 88 | method: get 89 | uri: https://www.test.com/ 90 | body: 91 | encoding: US-ASCII 92 | string: '' 93 | headers: 94 | User-Agent: 95 | - Mozilla/5.0 (Macintosh) 96 | Accept-Encoding: 97 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 98 | Accept: 99 | - "*/*" 100 | Host: 101 | - www.test.com 102 | response: 103 | status: 104 | code: 200 105 | message: OK 106 | headers: 107 | Server: 108 | - nginx/1.16.1 109 | Date: 110 | - Sat, 11 Jul 2020 03:19:01 GMT 111 | Content-Type: 112 | - text/html 113 | Transfer-Encoding: 114 | - chunked 115 | Connection: 116 | - keep-alive 117 | Keep-Alive: 118 | - timeout=20 119 | X-Dis-Request-Id: 120 | - ab80d220c4e964a267b7edb970c710d0 121 | P3p: 122 | - CP="NON DSP COR ADMa OUR IND UNI COM NAV INT" 123 | Cache-Control: 124 | - no-cache 125 | body: 126 | encoding: ASCII-8BIT 127 | string: "\n\n\n\n\n\n\n\n\n\n\n" 146 | recorded_at: Sat, 11 Jul 2020 03:19:01 GMT 147 | recorded_with: VCR 6.0.0 148 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_stylesheet_images/handles_404s.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://google.com/does_not_exist.css 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | Accept-Encoding: 11 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 12 | Accept: 13 | - "*/*" 14 | User-Agent: 15 | - Ruby 16 | response: 17 | status: 18 | code: 404 19 | message: Not Found 20 | headers: 21 | Content-Type: 22 | - text/html; charset=UTF-8 23 | Referrer-Policy: 24 | - no-referrer 25 | Content-Length: 26 | - '1579' 27 | Date: 28 | - Sat, 11 Jul 2020 02:47:46 GMT 29 | body: 30 | encoding: ASCII-8BIT 31 | string: !binary |- 32 | PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ZW4+CiAgPG1ldGEgY2hhcnNldD11dGYtOD4KICA8bWV0YSBuYW1lPXZpZXdwb3J0IGNvbnRlbnQ9ImluaXRpYWwtc2NhbGU9MSwgbWluaW11bS1zY2FsZT0xLCB3aWR0aD1kZXZpY2Utd2lkdGgiPgogIDx0aXRsZT5FcnJvciA0MDQgKE5vdCBGb3VuZCkhITE8L3RpdGxlPgogIDxzdHlsZT4KICAgICp7bWFyZ2luOjA7cGFkZGluZzowfWh0bWwsY29kZXtmb250OjE1cHgvMjJweCBhcmlhbCxzYW5zLXNlcmlmfWh0bWx7YmFja2dyb3VuZDojZmZmO2NvbG9yOiMyMjI7cGFkZGluZzoxNXB4fWJvZHl7bWFyZ2luOjclIGF1dG8gMDttYXgtd2lkdGg6MzkwcHg7bWluLWhlaWdodDoxODBweDtwYWRkaW5nOjMwcHggMCAxNXB4fSogPiBib2R5e2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2Vycm9ycy9yb2JvdC5wbmcpIDEwMCUgNXB4IG5vLXJlcGVhdDtwYWRkaW5nLXJpZ2h0OjIwNXB4fXB7bWFyZ2luOjExcHggMCAyMnB4O292ZXJmbG93OmhpZGRlbn1pbnN7Y29sb3I6Izc3Nzt0ZXh0LWRlY29yYXRpb246bm9uZX1hIGltZ3tib3JkZXI6MH1AbWVkaWEgc2NyZWVuIGFuZCAobWF4LXdpZHRoOjc3MnB4KXtib2R5e2JhY2tncm91bmQ6bm9uZTttYXJnaW4tdG9wOjA7bWF4LXdpZHRoOm5vbmU7cGFkZGluZy1yaWdodDowfX0jbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzF4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7bWFyZ2luLWxlZnQ6LTVweH1AbWVkaWEgb25seSBzY3JlZW4gYW5kIChtaW4tcmVzb2x1dGlvbjoxOTJkcGkpeyNsb2dve2JhY2tncm91bmQ6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIG5vLXJlcGVhdCAwJSAwJS8xMDAlIDEwMCU7LW1vei1ib3JkZXItaW1hZ2U6dXJsKC8vd3d3Lmdvb2dsZS5jb20vaW1hZ2VzL2JyYW5kaW5nL2dvb2dsZWxvZ28vMngvZ29vZ2xlbG9nb19jb2xvcl8xNTB4NTRkcC5wbmcpIDB9fUBtZWRpYSBvbmx5IHNjcmVlbiBhbmQgKC13ZWJraXQtbWluLWRldmljZS1waXhlbC1yYXRpbzoyKXsjbG9nb3tiYWNrZ3JvdW5kOnVybCgvL3d3dy5nb29nbGUuY29tL2ltYWdlcy9icmFuZGluZy9nb29nbGVsb2dvLzJ4L2dvb2dsZWxvZ29fY29sb3JfMTUweDU0ZHAucG5nKSBuby1yZXBlYXQ7LXdlYmtpdC1iYWNrZ3JvdW5kLXNpemU6MTAwJSAxMDAlfX0jbG9nb3tkaXNwbGF5OmlubGluZS1ibG9jaztoZWlnaHQ6NTRweDt3aWR0aDoxNTBweH0KICA8L3N0eWxlPgogIDxhIGhyZWY9Ly93d3cuZ29vZ2xlLmNvbS8+PHNwYW4gaWQ9bG9nbyBhcmlhLWxhYmVsPUdvb2dsZT48L3NwYW4+PC9hPgogIDxwPjxiPjQwNC48L2I+IDxpbnM+VGhhdOKAmXMgYW4gZXJyb3IuPC9pbnM+CiAgPHA+VGhlIHJlcXVlc3RlZCBVUkwgPGNvZGU+L2RvZXNfbm90X2V4aXN0LmNzczwvY29kZT4gd2FzIG5vdCBmb3VuZCBvbiB0aGlzIHNlcnZlci4gIDxpbnM+VGhhdOKAmXMgYWxsIHdlIGtub3cuPC9pbnM+Cg== 33 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 34 | recorded_with: VCR 6.0.0 35 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_stylesheet_images/handles_stylesheet_image_with_a_relative_url.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://raw.github.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.html 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - raw.github.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Connection: 24 | - keep-alive 25 | Content-Length: 26 | - '0' 27 | Location: 28 | - https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.html 29 | Accept-Ranges: 30 | - bytes 31 | Date: 32 | - Sat, 11 Jul 2020 02:47:46 GMT 33 | Via: 34 | - 1.1 varnish 35 | Age: 36 | - '0' 37 | X-Served-By: 38 | - cache-fty21334-FTY 39 | X-Cache: 40 | - MISS 41 | X-Cache-Hits: 42 | - '0' 43 | Vary: 44 | - Accept-Encoding 45 | X-Fastly-Request-Id: 46 | - 42defefcb229163cf23f39d4a0e7bb09dc2346c7 47 | body: 48 | encoding: UTF-8 49 | string: '' 50 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 51 | - request: 52 | method: get 53 | uri: https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.html 54 | body: 55 | encoding: US-ASCII 56 | string: '' 57 | headers: 58 | User-Agent: 59 | - Mozilla/5.0 (Macintosh) 60 | Accept-Encoding: 61 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 62 | Accept: 63 | - "*/*" 64 | Host: 65 | - raw.githubusercontent.com 66 | response: 67 | status: 68 | code: 200 69 | message: OK 70 | headers: 71 | Connection: 72 | - keep-alive 73 | Content-Length: 74 | - '361' 75 | Cache-Control: 76 | - max-age=300 77 | Content-Security-Policy: 78 | - default-src 'none'; style-src 'unsafe-inline'; sandbox 79 | Content-Type: 80 | - text/plain; charset=utf-8 81 | Etag: 82 | - W/"5d863c870c1499165ae27fc405308b04c53be69e32f5d1a80e1e1265c1165454" 83 | Strict-Transport-Security: 84 | - max-age=31536000 85 | X-Content-Type-Options: 86 | - nosniff 87 | X-Frame-Options: 88 | - deny 89 | X-Xss-Protection: 90 | - 1; mode=block 91 | Via: 92 | - 1.1 varnish 93 | - 1.1 varnish (Varnish/6.0) 94 | X-Github-Request-Id: 95 | - 8CE0:4149:2B2888:32CDBC:5F092851 96 | Accept-Ranges: 97 | - bytes 98 | Date: 99 | - Sat, 11 Jul 2020 02:47:47 GMT 100 | X-Served-By: 101 | - cache-fty21353-FTY 102 | X-Cache: 103 | - MISS, MISS 104 | X-Cache-Hits: 105 | - 0, 0 106 | X-Timer: 107 | - S1594435667.903284,VS0,VE105 108 | Vary: 109 | - Authorization,Accept-Encoding 110 | Access-Control-Allow-Origin: 111 | - "*" 112 | X-Fastly-Request-Id: 113 | - 53c0ddf688d229bb9907820b623a2f696ff20874 114 | Expires: 115 | - Sat, 11 Jul 2020 02:52:47 GMT 116 | Source-Age: 117 | - '0' 118 | body: 119 | encoding: ASCII-8BIT 120 | string: | 121 | 122 | 123 | 124 | 125 | 126 | stylesheet_test 127 | 128 | 129 | 137 | 138 | 139 | 140 | 141 | recorded_at: Sat, 11 Jul 2020 02:47:47 GMT 142 | - request: 143 | method: get 144 | uri: https://raw.github.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.css 145 | body: 146 | encoding: US-ASCII 147 | string: '' 148 | headers: 149 | Accept-Encoding: 150 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 151 | Accept: 152 | - "*/*" 153 | User-Agent: 154 | - Ruby 155 | response: 156 | status: 157 | code: 301 158 | message: Moved Permanently 159 | headers: 160 | Connection: 161 | - keep-alive 162 | Content-Length: 163 | - '0' 164 | Location: 165 | - https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.css 166 | Accept-Ranges: 167 | - bytes 168 | Date: 169 | - Sat, 11 Jul 2020 02:47:47 GMT 170 | Via: 171 | - 1.1 varnish 172 | Age: 173 | - '0' 174 | X-Served-By: 175 | - cache-fty21382-FTY 176 | X-Cache: 177 | - MISS 178 | X-Cache-Hits: 179 | - '0' 180 | Vary: 181 | - Accept-Encoding 182 | X-Fastly-Request-Id: 183 | - c4d0c44089a9cd9f259ba5dfba01b82886c5bdd3 184 | body: 185 | encoding: UTF-8 186 | string: '' 187 | recorded_at: Sat, 11 Jul 2020 02:47:47 GMT 188 | - request: 189 | method: get 190 | uri: https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/relative_image_url.css 191 | body: 192 | encoding: US-ASCII 193 | string: '' 194 | headers: 195 | Accept-Encoding: 196 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 197 | Accept: 198 | - "*/*" 199 | User-Agent: 200 | - Ruby 201 | response: 202 | status: 203 | code: 200 204 | message: OK 205 | headers: 206 | Connection: 207 | - keep-alive 208 | Content-Length: 209 | - '75' 210 | Cache-Control: 211 | - max-age=300 212 | Content-Security-Policy: 213 | - default-src 'none'; style-src 'unsafe-inline'; sandbox 214 | Content-Type: 215 | - text/plain; charset=utf-8 216 | Etag: 217 | - W/"2c5ae7c2bb86ae5983019ef6e4fdd989bb35f94f20f3fd73e86ed5d9a953c9d1" 218 | Strict-Transport-Security: 219 | - max-age=31536000 220 | X-Content-Type-Options: 221 | - nosniff 222 | X-Frame-Options: 223 | - deny 224 | X-Xss-Protection: 225 | - 1; mode=block 226 | Via: 227 | - 1.1 varnish 228 | - 1.1 varnish (Varnish/6.0) 229 | X-Github-Request-Id: 230 | - A436:5FF4:137A64:1747FA:5F092852 231 | Accept-Ranges: 232 | - bytes 233 | Date: 234 | - Sat, 11 Jul 2020 02:47:47 GMT 235 | X-Served-By: 236 | - cache-fty21326-FTY 237 | X-Cache: 238 | - MISS, MISS 239 | X-Cache-Hits: 240 | - 0, 0 241 | X-Timer: 242 | - S1594435667.266851,VS0,VE115 243 | Vary: 244 | - Authorization,Accept-Encoding 245 | Access-Control-Allow-Origin: 246 | - "*" 247 | X-Fastly-Request-Id: 248 | - d8d1068470ba9ef99c8926302b7cfd66c55e3673 249 | Expires: 250 | - Sat, 11 Jul 2020 02:52:47 GMT 251 | Source-Age: 252 | - '0' 253 | body: 254 | encoding: ASCII-8BIT 255 | string: | 256 | .test { 257 | background-image: url('../images/some_image.png') 258 | } 259 | recorded_at: Sat, 11 Jul 2020 02:47:47 GMT 260 | recorded_with: VCR 6.0.0 261 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_stylesheet_images/scrapes_stylesheet_images.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: https://raw.github.com/charlotte-ruby/image_scraper/master/spec/support/stylesheet_unescaped_image.html 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - raw.github.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Connection: 24 | - keep-alive 25 | Content-Length: 26 | - '0' 27 | Location: 28 | - https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/stylesheet_unescaped_image.html 29 | Accept-Ranges: 30 | - bytes 31 | Date: 32 | - Sat, 11 Jul 2020 02:47:46 GMT 33 | Via: 34 | - 1.1 varnish 35 | Age: 36 | - '0' 37 | X-Served-By: 38 | - cache-fty21330-FTY 39 | X-Cache: 40 | - MISS 41 | X-Cache-Hits: 42 | - '0' 43 | Vary: 44 | - Accept-Encoding 45 | X-Fastly-Request-Id: 46 | - 67cf4e16a67c4ff9dc5bc784d87f623f506e97b6 47 | body: 48 | encoding: UTF-8 49 | string: '' 50 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 51 | - request: 52 | method: get 53 | uri: https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/stylesheet_unescaped_image.html 54 | body: 55 | encoding: US-ASCII 56 | string: '' 57 | headers: 58 | User-Agent: 59 | - Mozilla/5.0 (Macintosh) 60 | Accept-Encoding: 61 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 62 | Accept: 63 | - "*/*" 64 | Host: 65 | - raw.githubusercontent.com 66 | response: 67 | status: 68 | code: 200 69 | message: OK 70 | headers: 71 | Connection: 72 | - keep-alive 73 | Content-Length: 74 | - '393' 75 | Cache-Control: 76 | - max-age=300 77 | Content-Security-Policy: 78 | - default-src 'none'; style-src 'unsafe-inline'; sandbox 79 | Content-Type: 80 | - text/plain; charset=utf-8 81 | Etag: 82 | - W/"92596301aa24cd3a793a9a17d1d618bcc5b1e7072ce62d94abbf449f3f44e523" 83 | Strict-Transport-Security: 84 | - max-age=31536000 85 | X-Content-Type-Options: 86 | - nosniff 87 | X-Frame-Options: 88 | - deny 89 | X-Xss-Protection: 90 | - 1; mode=block 91 | Via: 92 | - 1.1 varnish 93 | - 1.1 varnish (Varnish/6.0) 94 | X-Github-Request-Id: 95 | - B256:5FF4:137A5F:1747CE:5F09284B 96 | Accept-Ranges: 97 | - bytes 98 | Date: 99 | - Sat, 11 Jul 2020 02:47:46 GMT 100 | X-Served-By: 101 | - cache-fty21365-FTY 102 | X-Cache: 103 | - MISS, MISS 104 | X-Cache-Hits: 105 | - 0, 0 106 | X-Timer: 107 | - S1594435666.161183,VS0,VE114 108 | Vary: 109 | - Authorization,Accept-Encoding 110 | Access-Control-Allow-Origin: 111 | - "*" 112 | X-Fastly-Request-Id: 113 | - b43101a4b597ca7c347a93cff934973b3d17990c 114 | Expires: 115 | - Sat, 11 Jul 2020 02:52:46 GMT 116 | Source-Age: 117 | - '0' 118 | body: 119 | encoding: ASCII-8BIT 120 | string: | 121 | z 122 | 123 | 124 | 125 | 126 | stylesheet_test 127 | 128 | 129 | 137 | 138 | 139 | 140 | 141 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 142 | - request: 143 | method: get 144 | uri: https://raw.github.com/charlotte-ruby/image_scraper/master/spec/support/unescaped_image.css 145 | body: 146 | encoding: US-ASCII 147 | string: '' 148 | headers: 149 | Accept-Encoding: 150 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 151 | Accept: 152 | - "*/*" 153 | User-Agent: 154 | - Ruby 155 | response: 156 | status: 157 | code: 301 158 | message: Moved Permanently 159 | headers: 160 | Connection: 161 | - keep-alive 162 | Content-Length: 163 | - '0' 164 | Location: 165 | - https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/unescaped_image.css 166 | Accept-Ranges: 167 | - bytes 168 | Date: 169 | - Sat, 11 Jul 2020 02:47:46 GMT 170 | Via: 171 | - 1.1 varnish 172 | Age: 173 | - '0' 174 | X-Served-By: 175 | - cache-fty21378-FTY 176 | X-Cache: 177 | - MISS 178 | X-Cache-Hits: 179 | - '0' 180 | Vary: 181 | - Accept-Encoding 182 | X-Fastly-Request-Id: 183 | - c798264dfc2e5d7861e0abbe1696d6b1694c6882 184 | body: 185 | encoding: UTF-8 186 | string: '' 187 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 188 | - request: 189 | method: get 190 | uri: https://raw.githubusercontent.com/charlotte-ruby/image_scraper/master/spec/support/unescaped_image.css 191 | body: 192 | encoding: US-ASCII 193 | string: '' 194 | headers: 195 | Accept-Encoding: 196 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 197 | Accept: 198 | - "*/*" 199 | User-Agent: 200 | - Ruby 201 | response: 202 | status: 203 | code: 200 204 | message: OK 205 | headers: 206 | Connection: 207 | - keep-alive 208 | Content-Length: 209 | - '117' 210 | Cache-Control: 211 | - max-age=300 212 | Content-Security-Policy: 213 | - default-src 'none'; style-src 'unsafe-inline'; sandbox 214 | Content-Type: 215 | - text/plain; charset=utf-8 216 | Etag: 217 | - W/"a372d59f8b5e966084cad7f1c3389d218c1056d1a6b0cc1f78ce7766543521cd" 218 | Strict-Transport-Security: 219 | - max-age=31536000 220 | X-Content-Type-Options: 221 | - nosniff 222 | X-Frame-Options: 223 | - deny 224 | X-Xss-Protection: 225 | - 1; mode=block 226 | Via: 227 | - 1.1 varnish 228 | - 1.1 varnish (Varnish/6.0) 229 | X-Github-Request-Id: 230 | - 95FC:6E25:146840:18350B:5F092848 231 | Accept-Ranges: 232 | - bytes 233 | Date: 234 | - Sat, 11 Jul 2020 02:47:46 GMT 235 | X-Served-By: 236 | - cache-fty21349-FTY 237 | X-Cache: 238 | - MISS, MISS 239 | X-Cache-Hits: 240 | - 0, 0 241 | X-Timer: 242 | - S1594435667.501147,VS0,VE115 243 | Vary: 244 | - Authorization,Accept-Encoding 245 | Access-Control-Allow-Origin: 246 | - "*" 247 | X-Fastly-Request-Id: 248 | - 05d7f78a7df9449f89a871c44ebb8caf1b56dfbe 249 | Expires: 250 | - Sat, 11 Jul 2020 02:52:46 GMT 251 | Source-Age: 252 | - '0' 253 | body: 254 | encoding: ASCII-8BIT 255 | string: | 256 | .test { 257 | background-image: url('https://raw.github.com/charlotte-ruby/image_scraper/master/some image.png') 258 | } 259 | recorded_at: Sat, 11 Jul 2020 02:47:46 GMT 260 | recorded_with: VCR 6.0.0 261 | -------------------------------------------------------------------------------- /spec/cassettes/ImageScraper_Client/_stylesheets/lists_relative_path_stylesheets.yml: -------------------------------------------------------------------------------- 1 | --- 2 | http_interactions: 3 | - request: 4 | method: get 5 | uri: http://test.com/ 6 | body: 7 | encoding: US-ASCII 8 | string: '' 9 | headers: 10 | User-Agent: 11 | - Mozilla/5.0 (Macintosh) 12 | Accept-Encoding: 13 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 14 | Accept: 15 | - "*/*" 16 | Host: 17 | - test.com 18 | response: 19 | status: 20 | code: 301 21 | message: Moved Permanently 22 | headers: 23 | Server: 24 | - nginx/1.16.1 25 | Date: 26 | - Sat, 11 Jul 2020 03:17:21 GMT 27 | Content-Type: 28 | - text/html; charset=UTF-8 29 | Transfer-Encoding: 30 | - chunked 31 | Connection: 32 | - keep-alive 33 | Keep-Alive: 34 | - timeout=20 35 | X-Dis-Request-Id: 36 | - 903727ea6a57471f663fbe8dabe1c00f 37 | Location: 38 | - http://www.test.com/ 39 | body: 40 | encoding: UTF-8 41 | string: "301 Moved Permanently

301 42 | Moved Permanently

Object moved to here.


DOSarrest 43 | Internet Security
\n" 44 | recorded_at: Sat, 11 Jul 2020 03:17:21 GMT 45 | - request: 46 | method: get 47 | uri: http://www.test.com/ 48 | body: 49 | encoding: US-ASCII 50 | string: '' 51 | headers: 52 | User-Agent: 53 | - Mozilla/5.0 (Macintosh) 54 | Accept-Encoding: 55 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 56 | Accept: 57 | - "*/*" 58 | Host: 59 | - www.test.com 60 | response: 61 | status: 62 | code: 301 63 | message: Moved Permanently 64 | headers: 65 | Server: 66 | - nginx/1.16.1 67 | Date: 68 | - Sat, 11 Jul 2020 03:17:21 GMT 69 | Content-Type: 70 | - text/html; charset=UTF-8 71 | Transfer-Encoding: 72 | - chunked 73 | Connection: 74 | - keep-alive 75 | Keep-Alive: 76 | - timeout=20 77 | X-Dis-Request-Id: 78 | - b654c84e0b7623155267ef12e5fe8d60 79 | Location: 80 | - https://www.test.com/ 81 | body: 82 | encoding: UTF-8 83 | string: "301 Moved Permanently

301 84 | Moved Permanently

Object moved to here.


DOSarrest 85 | Internet Security
\n" 86 | recorded_at: Sat, 11 Jul 2020 03:17:21 GMT 87 | - request: 88 | method: get 89 | uri: https://www.test.com/ 90 | body: 91 | encoding: US-ASCII 92 | string: '' 93 | headers: 94 | User-Agent: 95 | - Mozilla/5.0 (Macintosh) 96 | Accept-Encoding: 97 | - gzip;q=1.0,deflate;q=0.6,identity;q=0.3 98 | Accept: 99 | - "*/*" 100 | Host: 101 | - www.test.com 102 | response: 103 | status: 104 | code: 200 105 | message: OK 106 | headers: 107 | Server: 108 | - nginx/1.16.1 109 | Date: 110 | - Sat, 11 Jul 2020 03:17:22 GMT 111 | Content-Type: 112 | - text/html 113 | Transfer-Encoding: 114 | - chunked 115 | Connection: 116 | - keep-alive 117 | Keep-Alive: 118 | - timeout=20 119 | X-Dis-Request-Id: 120 | - df0d48077779cd58216d5edc81a47f69 121 | P3p: 122 | - CP="NON DSP COR ADMa OUR IND UNI COM NAV INT" 123 | Cache-Control: 124 | - no-cache 125 | body: 126 | encoding: ASCII-8BIT 127 | string: "\n\n\n\n\n\n\n\n\n\n\n" 146 | recorded_at: Sat, 11 Jul 2020 03:17:22 GMT 147 | recorded_with: VCR 6.0.0 148 | -------------------------------------------------------------------------------- /spec/image_scraper/client_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe ImageScraper::Client, :vcr do 6 | let(:repo_url) { "https://raw.github.com/charlotte-ruby/image_scraper" } 7 | 8 | describe "foo" do 9 | it "something" do 10 | url = "http://www.amazon.com/Planet-Two-Disc-Digital-Combo-Blu-ray/dp/B004LWZW4W/ref=sr_1_1?s=movies-tv&ie=UTF8&qid=1324771542&sr=1-1" 11 | 12 | client = described_class.new(url) 13 | 14 | expect(client.page_images).not_to be_empty 15 | end 16 | end 17 | 18 | describe "#initialize" do 19 | it 'works with invalid URLs' do 20 | allow_any_instance_of(described_class).to receive(:fetch).and_return(nil) 21 | 22 | scraper = described_class.new('bogusurl4444.com') 23 | 24 | expect(scraper.doc).to be_nil 25 | end 26 | 27 | it 'has empty data if URL is invalid' do 28 | allow_any_instance_of(described_class).to receive(:fetch).and_return(nil) 29 | 30 | scraper = described_class.new('bogusurl4444.com') 31 | 32 | expect(scraper.image_urls).to be_empty 33 | expect(scraper.stylesheets).to be_empty 34 | expect(scraper.stylesheet_images).to be_empty 35 | expect(scraper.page_images).to be_empty 36 | end 37 | end 38 | 39 | describe '#image_urls' do 40 | it 'scrapes absolute paths' do 41 | images = [ 42 | 'http://en.wikipedia.org/static/images/poweredby_mediawiki_88x31.png', 43 | 'http://en.wikipedia.org/static/images/wikimedia-button.png', 44 | 'http://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1', 45 | 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-220px-SIPI_Jelly_Beans_4.1.07.tiff.jpg', 46 | 'http://upload.wikimedia.org/wikipedia/en/thumb/5/5c/Symbol_template_class.svg/16px-Symbol_template_class.svg.png' 47 | ] 48 | 49 | url = 'http://en.wikipedia.org/wiki/Standard_test_image' 50 | 51 | client = described_class.new(url, include_css_images: false) 52 | 53 | expect(client.image_urls).to eq(images) 54 | end 55 | 56 | it 'scrapes with whitespace stripped' do 57 | file = 'spec/support/extra_whitespace.html' 58 | 59 | client = described_class.new('') 60 | client.doc = File.open(file) { |f| Nokogiri::HTML(f) } 61 | 62 | images = [ 63 | 'http://g-ecx.images-amazon.com/images/G/01/SIMON/IsaacsonWalter._V164348457_.jpg', 64 | 'http://g-ecx.images-amazon.com/images/G/01/SIMON/IsaacsonWalter.jpg' 65 | ] 66 | 67 | expect(client.image_urls).to eq(images) 68 | end 69 | 70 | it 'scrapes relative paths' do 71 | scraper = described_class.new('http://en.wikipedia.org/wiki/Standard_test_image', 72 | convert_to_absolute_url: false, 73 | include_css_images: false) 74 | 75 | images = [ 76 | 'http://en.wikipedia.org/static/images/poweredby_mediawiki_88x31.png', 77 | 'http://en.wikipedia.org/static/images/wikimedia-button.png', 78 | 'http://en.wikipedia.org/wiki/Special:CentralAutoLogin/start?type=1x1', 79 | 'http://upload.wikimedia.org/wikipedia/commons/thumb/b/b6/SIPI_Jelly_Beans_4.1.07.tiff/lossy-page1-220px-SIPI_Jelly_Beans_4.1.07.tiff.jpg', 80 | 'http://upload.wikimedia.org/wikipedia/en/thumb/5/5c/Symbol_template_class.svg/16px-Symbol_template_class.svg.png' 81 | ] 82 | 83 | expect(scraper.image_urls).to eq(images) 84 | end 85 | 86 | it 'handles url with unescaped spaces' do 87 | url = 'https://raw.github.com/syoder/image_scraper/stylesheet_fix/test/resources/space in url.html' 88 | 89 | scraper = described_class.new(url, include_css_images: false) 90 | 91 | expected_url = 'https://raw.github.com/syoder/image_scraper/stylesheet_fix/test/resources/image1.png' 92 | 93 | expect(scraper.image_urls.length).to eq(1) 94 | expect(scraper.image_urls.first).to eq(expected_url) 95 | end 96 | end 97 | 98 | describe '#stylesheets' do 99 | it 'lists relative path stylesheets' do 100 | file = 'spec/support/stylesheet_test.html' 101 | 102 | client = described_class.new('http://test.com') 103 | client.doc = File.open(file) { |f| Nokogiri::HTML(f) } 104 | 105 | stylesheets = [ 106 | 'http://test.com/css/master.css', 107 | 'http://test.com/css/master2.css' 108 | ] 109 | 110 | expect(client.stylesheets).to eq(stylesheets) 111 | end 112 | 113 | it 'handles stylesheet with an unescaped url' do 114 | scraper = described_class.new('') 115 | scraper.url = 'http://test.com' 116 | scraper.doc = Nokogiri::HTML("") 117 | 118 | expect(scraper.stylesheets).to include('http://test.com/unescapedpath.css') 119 | end 120 | end 121 | 122 | describe '#page_images' do 123 | it 'handles unescaped urls' do 124 | scraper = described_class.new('http://test.com') 125 | scraper.doc = Nokogiri::HTML("") 126 | 127 | expect(scraper.page_images.length).to eq(1) 128 | expect(scraper.page_images).to include('http://test.com/unescaped%20path') 129 | end 130 | 131 | it 'handldes image urls that include square brackets' do 132 | scraper = described_class.new('http://google.com') 133 | scraper.doc = Nokogiri::HTML("") 134 | 135 | expect(scraper.page_images).to be_empty 136 | end 137 | end 138 | 139 | describe '#stylesheet_images' do 140 | it 'scrapes stylesheet images' do 141 | url = "#{repo_url}/master/spec/support/stylesheet_unescaped_image.html" 142 | stylesheet_path = "#{repo_url}/master/someimage.png" 143 | # /charlotte-ruby/image_scraper/master/spec/support/unescaped_image.css 144 | scraper = described_class.new(url, include_css_images: true) 145 | 146 | expect(scraper.stylesheet_images).to include(stylesheet_path) 147 | end 148 | 149 | it 'handles 404s' do 150 | scraper = described_class.new('') 151 | scraper.url = 'http://google.com' 152 | scraper.doc = Nokogiri::HTML("") 153 | 154 | expect(scraper.stylesheet_images).to be_empty 155 | end 156 | 157 | it 'handles stylesheet image with a relative url' do 158 | url = "#{repo_url}/master/spec/support/relative_image_url.html" 159 | image_url = "#{repo_url}/master/spec/images/some_image.png" 160 | 161 | scraper = described_class.new(url, include_css_images: true) 162 | 163 | expect(scraper.stylesheet_images).to include(image_url) 164 | end 165 | end 166 | end 167 | -------------------------------------------------------------------------------- /spec/image_scraper/util_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe ImageScraper::Util do 6 | 7 | describe 'absolute_url' do 8 | it 'parses asset' do 9 | url = 'http://www.test.com' 10 | asset = 'image.gif' 11 | 12 | result = described_class.absolute_url(url, asset) 13 | 14 | expect(result).to eq('http://www.test.com/image.gif') 15 | end 16 | 17 | it 'parses relative asset' do 18 | url = 'http://www.test.com' 19 | asset = 'images/image.gif' 20 | 21 | result = described_class.absolute_url(url, asset) 22 | 23 | expect(result).to eq('http://www.test.com/images/image.gif') 24 | end 25 | 26 | it 'parses absolute asset' do 27 | url = 'http://www.test.com' 28 | asset = '/images/image.gif' 29 | 30 | result = described_class.absolute_url(url, asset) 31 | 32 | expect(result).to eq('http://www.test.com/images/image.gif') 33 | end 34 | 35 | it 'parses root url with no asset' do 36 | result = described_class.absolute_url('http://www.test.com') 37 | 38 | expect(result).to eq('http://www.test.com') 39 | end 40 | 41 | it 'parses url with no asset' do 42 | result = described_class.absolute_url('http://www.test.com/a/test.html') 43 | 44 | expect(result).to eq('http://www.test.com/a/test.html') 45 | end 46 | end 47 | 48 | describe 'strip_quotes' do 49 | it 'parses paths' do 50 | result = described_class.strip_quotes("'/images/test.png'") 51 | 52 | expect(result).to eq('/images/test.png') 53 | end 54 | 55 | it 'parses a full url' do 56 | str = "'http://www.somsite.com/images/test.png'" 57 | 58 | result = described_class.strip_quotes(str) 59 | 60 | expect(result).to eq('http://www.somsite.com/images/test.png') 61 | end 62 | 63 | it 'parses emptyness' do 64 | result = described_class.strip_quotes('') 65 | 66 | expect(result).to be_empty 67 | end 68 | end 69 | 70 | describe 'domain' do 71 | it 'parses the domain of a url' do 72 | u = described_class 73 | 74 | expect(u.domain('http://ug.ly')).to eq('http://ug.ly') 75 | expect(u.domain('http://ug.ly/what')).to eq('http://ug.ly') 76 | expect(u.domain('http://ug.ly/what/is/this/')).to eq('http://ug.ly') 77 | expect(u.domain('http://www.ug.ly/what/is/this/')).to eq('http://www.ug.ly') 78 | expect(u.domain('http://ug.ly/what/is/this.html')).to eq('http://ug.ly') 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /spec/image_scraper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe ImageScraper do 4 | it 'has a version number' do 5 | expect(ImageScraper::VERSION).not_to be_nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'webmock/rspec' 4 | require 'vcr' 5 | require 'image_scraper' 6 | 7 | # FIXME: remove when fixed in vcr 8 | # https://github.com/vcr/vcr/pull/907/files 9 | module VCR 10 | class LibraryHooks 11 | # @private 12 | module WebMock 13 | module_function 14 | 15 | def with_global_hook_disabled(request) 16 | global_hook_disabled_requests << request 17 | 18 | begin 19 | yield 20 | ensure 21 | global_hook_disabled_requests.delete(request) 22 | end 23 | end 24 | 25 | def global_hook_disabled?(request) 26 | requests = Thread.current[:_vcr_webmock_disabled_requests] 27 | requests&.include?(request) 28 | end 29 | 30 | def global_hook_disabled_requests 31 | Thread.current[:_vcr_webmock_disabled_requests] ||= [] 32 | end 33 | end 34 | end 35 | end 36 | 37 | VCR.configure do |c| 38 | c.cassette_library_dir = 'spec/cassettes' 39 | c.hook_into :webmock 40 | c.configure_rspec_metadata! 41 | end 42 | 43 | # This file was generated by the `rspec --init` command. Conventionally, all 44 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 45 | # The generated `.rspec` file contains `--require spec_helper` which will cause 46 | # this file to always be loaded, without a need to explicitly require it in any 47 | # files. 48 | # 49 | # Given that it is always loaded, you are encouraged to keep this file as 50 | # light-weight as possible. Requiring heavyweight dependencies from this file 51 | # will add to the boot time of your test suite on EVERY test run, even for an 52 | # individual file that may not need all of that loaded. Instead, consider making 53 | # a separate helper file that requires the additional dependencies and performs 54 | # the additional setup, and require it from the spec files that actually need 55 | # it. 56 | # 57 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 58 | RSpec.configure do |config| 59 | # rspec-expectations config goes here. You can use an alternate 60 | # assertion/expectation library such as wrong or the stdlib/minitest 61 | # assertions if you prefer. 62 | config.expect_with :rspec do |expectations| 63 | # This option will default to `true` in RSpec 4. It makes the `description` 64 | # and `failure_message` of custom matchers include text for helper methods 65 | # defined using `chain`, e.g.: 66 | # be_bigger_than(2).and_smaller_than(4).description 67 | # # => "be bigger than 2 and smaller than 4" 68 | # ...rather than: 69 | # # => "be bigger than 2" 70 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 71 | end 72 | 73 | # rspec-mocks config goes here. You can use an alternate test double 74 | # library (such as bogus or mocha) by changing the `mock_with` option here. 75 | config.mock_with :rspec do |mocks| 76 | # Prevents you from mocking or stubbing a method that does not exist on 77 | # a real object. This is generally recommended, and will default to 78 | # `true` in RSpec 4. 79 | mocks.verify_partial_doubles = true 80 | end 81 | 82 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 83 | # have no way to turn it off -- the option exists only for backwards 84 | # compatibility in RSpec 3). It causes shared context metadata to be 85 | # inherited by the metadata hash of host groups and examples, rather than 86 | # triggering implicit auto-inclusion in groups with matching metadata. 87 | config.shared_context_metadata_behavior = :apply_to_host_groups 88 | 89 | # The settings below are suggested to provide a good initial experience 90 | # with RSpec, but feel free to customize to your heart's content. 91 | # # This allows you to limit a spec run to individual examples or groups 92 | # # you care about by tagging them with `:focus` metadata. When nothing 93 | # # is tagged with `:focus`, all examples get run. RSpec also provides 94 | # # aliases for `it`, `describe`, and `context` that include `:focus` 95 | # # metadata: `fit`, `fdescribe` and `fcontext`, respectively. 96 | config.filter_run_when_matching :focus 97 | # 98 | # # Allows RSpec to persist some state between runs in order to support 99 | # # the `--only-failures` and `--next-failure` CLI options. We recommend 100 | # # you configure your source control system to ignore this file. 101 | # config.example_status_persistence_file_path = "spec/examples.txt" 102 | # 103 | # # Limits the available syntax to the non-monkey patched syntax that is 104 | # # recommended. For more details, see: 105 | # # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 106 | # # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 107 | # # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 108 | # config.disable_monkey_patching! 109 | # 110 | # # This setting enables warnings. It's recommended, but in some cases may 111 | # # be too noisy due to issues in dependencies. 112 | # config.warnings = true 113 | # 114 | # # Many RSpec users commonly either run the entire suite or an individual 115 | # # file, and it's useful to allow more verbose output when running an 116 | # # individual spec file. 117 | # if config.files_to_run.one? 118 | # # Use the documentation formatter for detailed output, 119 | # # unless a formatter has already been configured 120 | # # (e.g. via a command-line flag). 121 | # config.default_formatter = "doc" 122 | # end 123 | # 124 | # # Print the 10 slowest examples and example groups at the 125 | # # end of the spec run, to help surface which specs are running 126 | # # particularly slow. 127 | # config.profile_examples = 10 128 | # 129 | # # Run specs in random order to surface order dependencies. If you find an 130 | # # order dependency and want to debug it, you can fix the order by providing 131 | # # the seed, which is printed after each run. 132 | # # --seed 1234 133 | # config.order = :random 134 | # 135 | # # Seed global randomization in this process using the `--seed` CLI option. 136 | # # Setting this allows you to use `--seed` to deterministically reproduce 137 | # # test failures related to randomization by passing the same `--seed` value 138 | # # as the one that triggered the failure. 139 | # Kernel.srand config.seed 140 | end 141 | -------------------------------------------------------------------------------- /spec/support/extra_whitespace.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /spec/support/relative_image_url.css: -------------------------------------------------------------------------------- 1 | .test { 2 | background-image: url('../images/some_image.png') 3 | } 4 | -------------------------------------------------------------------------------- /spec/support/relative_image_url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | stylesheet_test 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/support/space in url.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /spec/support/stylesheet_test.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | stylesheet_test 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /spec/support/stylesheet_unescaped_image.html: -------------------------------------------------------------------------------- 1 | z 2 | 3 | 4 | 5 | 6 | stylesheet_test 7 | 8 | 9 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /spec/support/unescaped_image.css: -------------------------------------------------------------------------------- 1 | .test { 2 | background-image: url('https://raw.github.com/charlotte-ruby/image_scraper/master/some image.png') 3 | } 4 | --------------------------------------------------------------------------------